feat: improve entry gate logic and lifecycle management

- Introduced a ref to manage entry lifecycle states, preventing duplicate entry attempts during loading and error handling.
- Updated entry handling to ensure proper state transitions and avoid flickering on success or failure pages.
- Enhanced token validation logic to streamline the entry process and improve user experience.
This commit is contained in:
2026-06-14 23:09:45 +08:00
parent 0b14e77f64
commit 9a42f47ac7

View File

@@ -12,7 +12,14 @@ import {
} from "lucide-react";
import Image from "next/image";
import { useRouter, useSearchParams } from "next/navigation";
import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from "react";
import {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react";
import { useTranslation } from "react-i18next";
import { getPublicCurrencies } from "@/api/currency";
@@ -151,6 +158,8 @@ export function EntryGate() {
const [steps, setSteps] = useState<EntryStep[]>(initialSteps());
const effectiveToken = tokenFromUrl || bearerToken;
/** 防止 token 写入 store / URL 剥离后重复触发进场,避免成功/失败页闪一下 */
const entryLifecycleRef = useRef<"idle" | "running" | "done">("idle");
const updateStep = useCallback((stepId: EntryStepId, status: EntryStepStatus) => {
setSteps((prev) => prev.map((s) => (s.id === stepId ? { ...s, status } : s)));
@@ -163,6 +172,10 @@ export function EntryGate() {
}, [steps]);
const doEntry = useCallback(async () => {
if (entryLifecycleRef.current === "running" || entryLifecycleRef.current === "done") {
return;
}
if (!effectiveToken) {
// 主站 iframetoken 由 MAIN_INIT_TOKEN 稍后到达,勿先闪「授权失败」
if (typeof window !== "undefined" && isInIframe() && !tokenFromUrl) {
@@ -172,11 +185,13 @@ export function EntryGate() {
router.replace("/login");
return;
}
entryLifecycleRef.current = "done";
setPhase("failed");
setFailureDetails([{ code: "NO_TOKEN", detailKey: "errors.noTokenDetail" }]);
return;
}
entryLifecycleRef.current = "running";
setPhase("loading");
setFailureDetails([]);
@@ -215,15 +230,14 @@ export function EntryGate() {
updateStep("hall", "done");
setProfile(me);
setPhase("success");
await sleep(600);
entryLifecycleRef.current = "done";
router.replace("/hall");
return;
} catch (err) {
lastError = err;
if (err instanceof LotteryApiBizError) {
entryLifecycleRef.current = "done";
updateStep("token", "error");
setPhase("failed");
@@ -253,6 +267,7 @@ export function EntryGate() {
}
if (!shouldRetryEntryRequest(err)) {
entryLifecycleRef.current = "done";
updateStep("token", "error");
setPhase("failed");
setFailureDetails([{ code: "NETWORK_ERROR", detailKey: "errors.networkDetail" }]);
@@ -265,6 +280,7 @@ export function EntryGate() {
}
}
entryLifecycleRef.current = "done";
updateStep("token", "error");
setPhase("failed");
setFailureDetails([
@@ -287,6 +303,7 @@ export function EntryGate() {
]);
const handleRetry = useCallback(() => {
entryLifecycleRef.current = "idle";
setPhase("loading");
setFailureDetails([]);
setSteps(initialSteps());
@@ -299,13 +316,20 @@ export function EntryGate() {
stripSearchParamFromBrowserUrl("session");
}, [sessionExpired, clearBearerToken]);
const trimmedEffectiveToken = (effectiveToken ?? "").trim();
const doEntryRef = useRef(doEntry);
doEntryRef.current = doEntry;
useEffect(() => {
if (sessionExpired || waitingForEmbeddedToken) return;
if (entryLifecycleRef.current !== "idle") return;
if (!trimmedEffectiveToken) return;
const tmr = window.setTimeout(() => {
void doEntry();
void doEntryRef.current();
}, 300);
return () => window.clearTimeout(tmr);
}, [doEntry, sessionExpired, waitingForEmbeddedToken]);
}, [sessionExpired, trimmedEffectiveToken, waitingForEmbeddedToken]);
useEffect(() => {
if (sessionExpired) return;
@@ -314,6 +338,8 @@ export function EntryGate() {
const tmr = window.setTimeout(() => {
if (usePlayerSessionStore.getState().bearerToken?.trim()) return;
if (entryLifecycleRef.current !== "idle") return;
entryLifecycleRef.current = "done";
setFailureDetails([{ code: "NO_TOKEN", detailKey: "errors.noTokenDetail" }]);
setPhase("failed");
}, IFRAME_TOKEN_WAIT_MS);