import { useEffect, useRef, useState } from "react"; const COOKIE_NAME = "join_pay_order"; const STORAGE_NAME = "join_pay_order"; const POLL_INTERVAL_MS = 1200; const MAX_POLLS = 60; const MAX_AGE_MS = 15 * 60 * 1000; function getCookie(name: string): string | null { if (typeof document === "undefined") return null; const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`)); return match ? decodeURIComponent(match[1]) : null; } function clearCookie(name: string): void { if (typeof document === "undefined") return; document.cookie = `${name}=; path=/; max-age=0`; } function getStorage(name: string): string | null { if (typeof window === "undefined") return null; return window.localStorage.getItem(name); } function removeStorage(name: string): void { if (typeof window === "undefined") return; window.localStorage.removeItem(name); } function clearPendingOrder(): void { clearCookie(COOKIE_NAME); removeStorage(STORAGE_NAME); } function persistPendingOrder(raw: string): void { if (typeof document !== "undefined") { document.cookie = `${COOKIE_NAME}=${encodeURIComponent(raw)}; path=/; max-age=900`; } if (typeof window !== "undefined") { try { window.localStorage.setItem(STORAGE_NAME, raw); } catch {} } } function parsePendingOrder(raw: string | null): [string | null, boolean] { if (!raw?.trim()) return [null, false]; const parts = raw.split("|"); const orderId = parts[0]?.trim() || raw.trim(); const ts = parts[1] ? parseInt(parts[1], 10) : NaN; if (!orderId) return [null, false]; if (Number.isNaN(ts)) return [orderId, false]; if (Date.now() - ts > MAX_AGE_MS) return [orderId, false]; return [orderId, true]; } export function usePayStatusPoll(onPaid: () => void): boolean { const [polling, setPolling] = useState(false); const onPaidRef = useRef(onPaid); onPaidRef.current = onPaid; useEffect(() => { const cookieRaw = getCookie(COOKIE_NAME); const [cookieOrderId, cookieValid] = parsePendingOrder(cookieRaw); const storageRaw = getStorage(STORAGE_NAME); const [storageOrderId, storageValid] = parsePendingOrder(storageRaw); const orderId = cookieValid ? cookieOrderId : storageValid ? storageOrderId : null; const activeRaw = cookieValid ? cookieRaw : storageValid ? storageRaw : null; if (!orderId) { if (cookieRaw || storageRaw) { clearPendingOrder(); } return; } if (activeRaw) { persistPendingOrder(activeRaw); } setPolling(true); let attempts = 0; let stopped = false; let timer: number | undefined; let inFlight = false; const stop = () => { stopped = true; if (timer) window.clearTimeout(timer); }; const scheduleNext = () => { if (stopped) return; timer = window.setTimeout(tick, POLL_INTERVAL_MS); }; const tick = async () => { if (stopped) return; if (inFlight) { scheduleNext(); return; } attempts += 1; if (attempts > MAX_POLLS) { stop(); clearPendingOrder(); setPolling(false); return; } inFlight = true; try { const res = await fetch( `/api/pay/status?order_id=${encodeURIComponent(orderId)}`, { cache: "no-store" } ); const data = await res.json().catch(() => ({})); if (data?.ok && data?.paid) { stop(); clearPendingOrder(); setPolling(false); // 对齐 nomadvip:调用 confirm 写入 PocketBase site_vip(不依赖 payjsapi 异步 notify) try { const confirmRes = await fetch("/api/pay/confirm", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ order_id: orderId }), cache: "no-store", }); if (confirmRes.ok && typeof window !== "undefined") { window.dispatchEvent(new CustomEvent("vip:updated")); } } catch { /* 忽略,不影响跳转 */ } onPaidRef.current(); return; } } catch { // keep polling } finally { inFlight = false; } scheduleNext(); }; const handleResume = () => { if (typeof document !== "undefined" && document.visibilityState === "hidden") { return; } void tick(); }; document.addEventListener("visibilitychange", handleResume); window.addEventListener("pageshow", handleResume); window.addEventListener("focus", handleResume); void tick(); return () => { document.removeEventListener("visibilitychange", handleResume); window.removeEventListener("pageshow", handleResume); window.removeEventListener("focus", handleResume); stop(); }; }, []); return polling; }