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