"use client"; import { useEffect, useCallback, type ReactNode } from "react"; import { usePlayerSessionStore } from "@/stores/player-session-store"; import { setPlayerBearerToken } from "@/lib/lottery-auth"; import { isIframeOriginAllowed, loadIframeAllowedOrigins, resolvePostMessageTargetOrigin, } from "@/lib/iframe-origins"; function sanitizeUrlForParent(href: string): string { try { const url = new URL(href); url.searchParams.delete("token"); return `${url.pathname}${url.search}${url.hash}`; } catch { return href; } } function resolveSafeInAppPath(path: string): string | null { if (!path.startsWith("/") || path.startsWith("//")) { return null; } try { const url = new URL(path, window.location.origin); if (url.origin !== window.location.origin) { return null; } return `${url.pathname}${url.search}${url.hash}`; } catch { return null; } } /** * iframe 通信桥接组件 * * 功能: * 1. 监听父窗口(主站)通过 postMessage 发送的 Token * 2. 向父窗口发送心跳和状态通知 * 3. 支持在主站 iframe 内嵌入时的双向通信 */ export function IframeBridge({ children }: { children: ReactNode }): ReactNode { const setBearerToken = usePlayerSessionStore((state) => state.setBearerToken); /** * 向父窗口发送消息 */ const sendToParent = useCallback( (type: string, payload?: Record): void => { if (typeof window === "undefined" || window.parent === window) return; const targetOrigin = resolvePostMessageTargetOrigin(); if (!targetOrigin) return; window.parent.postMessage( { type: `LOTTERY_${type}`, payload, timestamp: Date.now(), source: "lottery-iframe", }, targetOrigin, ); }, [], ); /** * 通知父窗口:已准备就绪 */ const notifyReady = useCallback((): void => { sendToParent("READY", { url: sanitizeUrlForParent(window.location.href), userAgent: navigator.userAgent, }); }, [sendToParent]); /** * 通知父窗口:需要新 Token */ const notifyTokenNeeded = useCallback((): void => { sendToParent("TOKEN_NEEDED", { reason: "token_expired", }); }, [sendToParent]); /** * 通知父窗口:Token 刷新成功 */ const notifyTokenRefreshed = useCallback((): void => { sendToParent("TOKEN_REFRESHED"); }, [sendToParent]); /** * 通知父窗口:发生错误 */ const notifyError = useCallback( (error: string): void => { sendToParent("ERROR", { error }); }, [sendToParent], ); /** * 监听父窗口消息 */ useEffect(() => { if (typeof window === "undefined") return; // 检查是否在 iframe 内 const isInIframe = window.self !== window.top; if (!isInIframe) { console.log("[IframeBridge] Not in iframe, skipping bridge setup"); return; } console.log("[IframeBridge] Setting up iframe communication"); const handleMessage = async (event: MessageEvent): Promise => { if (!isIframeOriginAllowed(event.origin)) { await loadIframeAllowedOrigins(true); if (!isIframeOriginAllowed(event.origin)) { console.warn("[IframeBridge] Rejected message from:", event.origin); return; } } const { data } = event; if (!data || typeof data !== "object") return; console.log("[IframeBridge] Received message:", data.type); switch (data.type) { // 主站发送初始化 Token case "MAIN_INIT_TOKEN": if (data.token) { console.log("[IframeBridge] Received initial token"); setBearerToken(data.token); setPlayerBearerToken(data.token); // 勿再 notifyReady(),否则主站会重复 MAIN_INIT_TOKEN 导致消息刷屏 } break; // 主站刷新 Token case "MAIN_REFRESH_TOKEN": if (data.token) { console.log("[IframeBridge] Received refreshed token"); setBearerToken(data.token); setPlayerBearerToken(data.token); notifyTokenRefreshed(); } break; // 主站通知 Token 即将过期 case "MAIN_TOKEN_EXPIRING": console.log("[IframeBridge] Token expiring soon"); // 可以显示提示或自动刷新 break; // 主站请求当前状态 case "MAIN_REQUEST_STATUS": sendToParent("STATUS_RESPONSE", { isReady: true, currentPath: window.location.pathname, }); break; // 主站导航请求 case "MAIN_NAVIGATE": if (data.path && typeof data.path === "string") { const nextPath = resolveSafeInAppPath(data.path); if (nextPath) { window.history.pushState({}, "", nextPath); } } break; default: break; } }; window.addEventListener("message", handleMessage); // 先加载后台白名单,再发送 READY,避免父站立即回 Token 时被本端误拒。 void loadIframeAllowedOrigins().finally(() => { notifyReady(); }); const originsRefresh = setInterval(() => { void loadIframeAllowedOrigins(true); }, 60_000); // 定期发送心跳 const heartbeat = setInterval(() => { sendToParent("HEARTBEAT", { timestamp: Date.now(), }); }, 30000); // 每 30 秒 return () => { window.removeEventListener("message", handleMessage); clearInterval(originsRefresh); clearInterval(heartbeat); }; }, [notifyReady, notifyTokenRefreshed, sendToParent, setBearerToken]); // 暴露全局方法供调试 useEffect(() => { if (typeof window === "undefined") return; (window as unknown as Record).lotteryIframeBridge = { notifyReady, notifyTokenNeeded, notifyError, sendToParent, }; }, [notifyError, notifyReady, notifyTokenNeeded, sendToParent]); return children; } /** * 检测当前是否在 iframe 内 */ export function isInIframe(): boolean { if (typeof window === "undefined") return false; try { return window.self !== window.top; } catch { return true; // 跨域时无法访问 window.top,说明在 iframe 内 } } /** * 获取父窗口信息 */ export function getParentInfo(): { isInIframe: boolean; referrer: string; } { if (typeof window === "undefined") { return { isInIframe: false, referrer: "" }; } return { isInIframe: isInIframe(), referrer: document.referrer || "", }; }