import { useCallback, useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; import { parseJwtExp } from "@/lib/jwt-payload"; import { usePlayerSessionStore } from "@/stores/player-session-store"; import { useErrorStore } from "@/stores/error-store"; import { isIframeOriginAllowed, loadIframeAllowedOrigins, resolvePostMessageTargetOrigin, } from "@/lib/iframe-origins"; /** Token 过期前警告阈值(毫秒) */ const TOKEN_WARNING_THRESHOLD = 60 * 1000; // 1 分钟 /** 最大重试次数 */ const MAX_RETRY = 3; /** 等待主站回传新 Token 的超时(毫秒) */ const REFRESH_RESPONSE_TIMEOUT_MS = 5_000; /** * Token 自动续签 Hook * * 功能: * 1. 监听主站 postMessage 发送的新 Token * 2. 自动检测 Token 过期时间并在过期前静默续签 * 3. 支持手动触发刷新 * 4. 刷新失败时提示用户返回主站 */ export function useTokenRefresh(): { /** 手动触发 Token 刷新 */ refreshToken: () => Promise; /** 当前 Token 剩余有效时间(毫秒),-1 表示未知 */ getTokenRemainingTime: () => number; /** 是否即将过期 */ isTokenExpiringSoon: () => boolean; } { const bearerToken = usePlayerSessionStore((state) => state.bearerToken); const setBearerToken = usePlayerSessionStore((state) => state.setBearerToken); const setServerError = useErrorStore((state) => state.setServerError); const clearServerError = useErrorStore((state) => state.clearServerError); const { t } = useTranslation("player"); const refreshTimerRef = useRef(null); const retryCountRef = useRef(0); const pendingRefreshRef = useRef(0); /** * 获取 Token 剩余有效时间 * @returns 剩余毫秒数,-1 表示未知 */ const getTokenRemainingTime = useCallback((): number => { const exp = parseJwtExp(bearerToken); if (!exp) return -1; const now = Math.floor(Date.now() / 1000); return Math.max(0, (exp - now) * 1000); }, [bearerToken]); /** * 检查 Token 是否即将过期(1 分钟内) */ const isTokenExpiringSoon = useCallback((): boolean => { const remaining = getTokenRemainingTime(); return remaining > 0 && remaining < TOKEN_WARNING_THRESHOLD; }, [getTokenRemainingTime]); /** * 请求主站刷新 Token * 通过 postMessage 向父窗口(主站)发送刷新请求 */ const requestParentRefresh = useCallback((): void => { if (typeof window === "undefined") return; // 向主站请求新 Token const targetOrigin = resolvePostMessageTargetOrigin(); if (!targetOrigin) return; window.parent.postMessage( { type: "LOTTERY_TOKEN_REFRESH_REQUEST", timestamp: Date.now(), }, targetOrigin, ); }, []); /** * 手动触发 Token 刷新 */ const refreshToken = useCallback(async (): Promise => { if (retryCountRef.current >= MAX_RETRY) { setServerError(true, t("token.refreshFailed")); return; } clearServerError(); const attemptId = Date.now(); pendingRefreshRef.current = attemptId; requestParentRefresh(); window.setTimeout(() => { if (pendingRefreshRef.current !== attemptId) return; retryCountRef.current += 1; if (retryCountRef.current >= MAX_RETRY) { setServerError(true, t("token.refreshFailed")); } }, REFRESH_RESPONSE_TIMEOUT_MS); }, [clearServerError, requestParentRefresh, setServerError, t]); /** * 监听主站 postMessage 发送的新 Token */ useEffect(() => { if (typeof window === "undefined") return; void loadIframeAllowedOrigins(); const handleMessage = async (event: MessageEvent): Promise => { if (!isIframeOriginAllowed(event.origin)) { await loadIframeAllowedOrigins(true); if (!isIframeOriginAllowed(event.origin)) { console.warn("[TokenRefresh] Ignored message from unknown origin:", event.origin); return; } } const { data } = event; if (!data || typeof data !== "object") return; // 处理主站发送的新 Token(兼容 MAIN_REFRESH_TOKEN 与 LOTTERY_TOKEN_REFRESH_RESPONSE) if ( (data.type === "LOTTERY_TOKEN_REFRESH_RESPONSE" || data.type === "MAIN_REFRESH_TOKEN") && data.token ) { console.log("[TokenRefresh] Received new token from parent"); pendingRefreshRef.current = 0; setBearerToken(data.token); retryCountRef.current = 0; } // 处理主站通知 Token 即将过期 if (data.type === "LOTTERY_TOKEN_EXPIRING_WARNING") { console.log("[TokenRefresh] Token expiring warning from parent"); // 可以在这里显示提示或自动刷新 } }; window.addEventListener("message", handleMessage); return () => window.removeEventListener("message", handleMessage); }, [setBearerToken]); /** * 自动刷新逻辑 */ useEffect(() => { if (!bearerToken) { if (refreshTimerRef.current) { clearTimeout(refreshTimerRef.current); refreshTimerRef.current = null; } return; } const exp = parseJwtExp(bearerToken); if (!exp) return; const now = Date.now(); const expMs = exp * 1000; const remaining = expMs - now; // 如果已经过期,立即请求刷新 if (remaining <= 0) { console.warn("[TokenRefresh] Token already expired, requesting refresh"); requestParentRefresh(); return; } // 在过期前 30 秒刷新 const refreshDelay = Math.max(0, remaining - TOKEN_WARNING_THRESHOLD); console.log( `[TokenRefresh] Token expires in ${Math.floor(remaining / 1000)}s, ` + `will refresh in ${Math.floor(refreshDelay / 1000)}s`, ); refreshTimerRef.current = setTimeout(() => { console.log("[TokenRefresh] Auto-refreshing token"); requestParentRefresh(); }, refreshDelay); return () => { if (refreshTimerRef.current) { clearTimeout(refreshTimerRef.current); } }; }, [bearerToken, requestParentRefresh]); return { refreshToken, getTokenRemainingTime, isTokenExpiringSoon, }; }