实现 resolvePostMessageTargetOrigin,优化 iframe 消息通信中的目标来源(origin)解析与校验。 更新 IframeBridge:支持定期刷新允许的来源列表,并优化消息事件管理机制。 重构 usePendingWalletReconcile:优化待对账通知的获取与缓存逻辑,提升性能与用户体验。 增强 NotificationsScreen:新增待对账通知内容,并优化界面展示效果。 更新英文、尼泊尔语与中文语言包,新增待对账通知相关翻译文案。
211 lines
6.1 KiB
TypeScript
211 lines
6.1 KiB
TypeScript
import { useCallback, useEffect, useRef } from "react";
|
||
|
||
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 自动续签 Hook
|
||
*
|
||
* 功能:
|
||
* 1. 监听主站 postMessage 发送的新 Token
|
||
* 2. 自动检测 Token 过期时间并在过期前静默续签
|
||
* 3. 支持手动触发刷新
|
||
* 4. 刷新失败时提示用户返回主站
|
||
*/
|
||
export function useTokenRefresh(): {
|
||
/** 手动触发 Token 刷新 */
|
||
refreshToken: () => Promise<void>;
|
||
/** 当前 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 refreshTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||
const retryCountRef = useRef(0);
|
||
|
||
/**
|
||
* 解析 JWT 的 exp 字段
|
||
* @returns exp 时间戳(秒),解析失败返回 null
|
||
*/
|
||
const parseTokenExp = useCallback((token: string | null): number | null => {
|
||
if (!token) return null;
|
||
|
||
try {
|
||
// JWT 格式:header.payload.signature
|
||
const parts = token.split(".");
|
||
if (parts.length !== 3) return null;
|
||
|
||
// Base64 解码 payload
|
||
const payload = JSON.parse(atob(parts[1]));
|
||
return payload.exp ?? null;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}, []);
|
||
|
||
/**
|
||
* 获取 Token 剩余有效时间
|
||
* @returns 剩余毫秒数,-1 表示未知
|
||
*/
|
||
const getTokenRemainingTime = useCallback((): number => {
|
||
const exp = parseTokenExp(bearerToken);
|
||
if (!exp) return -1;
|
||
|
||
const now = Math.floor(Date.now() / 1000);
|
||
return Math.max(0, (exp - now) * 1000);
|
||
}, [bearerToken, parseTokenExp]);
|
||
|
||
/**
|
||
* 检查 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
|
||
window.parent.postMessage(
|
||
{
|
||
type: "LOTTERY_TOKEN_REFRESH_REQUEST",
|
||
timestamp: Date.now(),
|
||
},
|
||
resolvePostMessageTargetOrigin(),
|
||
);
|
||
}, []);
|
||
|
||
/**
|
||
* 手动触发 Token 刷新
|
||
*/
|
||
const refreshToken = useCallback(async (): Promise<void> => {
|
||
if (retryCountRef.current >= MAX_RETRY) {
|
||
setServerError(true, "Token 刷新失败,请返回主站重新进入");
|
||
return;
|
||
}
|
||
|
||
clearServerError();
|
||
retryCountRef.current++;
|
||
|
||
// 向主站请求新 Token
|
||
requestParentRefresh();
|
||
|
||
// 等待主站响应(通过 postMessage)
|
||
// 实际逻辑在下面的 useEffect 中处理
|
||
}, [clearServerError, requestParentRefresh, setServerError]);
|
||
|
||
/**
|
||
* 监听主站 postMessage 发送的新 Token
|
||
*/
|
||
useEffect(() => {
|
||
if (typeof window === "undefined") return;
|
||
|
||
void loadIframeAllowedOrigins();
|
||
|
||
const handleMessage = async (event: MessageEvent): Promise<void> => {
|
||
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");
|
||
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 = parseTokenExp(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, parseTokenExp, requestParentRefresh]);
|
||
|
||
return {
|
||
refreshToken,
|
||
getTokenRemainingTime,
|
||
isTokenExpiringSoon,
|
||
};
|
||
}
|