From 9a42f47ac726554ecf1703792d457bb2b66e14f4 Mon Sep 17 00:00:00 2001 From: kang Date: Sun, 14 Jun 2026 23:09:45 +0800 Subject: [PATCH] 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. --- src/features/player/entry-gate.tsx | 38 +++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/src/features/player/entry-gate.tsx b/src/features/player/entry-gate.tsx index a93fed0..8c952b6 100644 --- a/src/features/player/entry-gate.tsx +++ b/src/features/player/entry-gate.tsx @@ -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(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) { // 主站 iframe:token 由 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);