import { useEffect, useRef, useState } from "react"; const COOKIE_NAME = "nomadvip_pay_order"; const STORAGE_NAME = "nomadvip_pay_order"; const POLL_INTERVAL_MS = 1200; const MAX_POLLS = 30; 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 parsePayOrderCookie(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: (orderId?: string) => void, onTimeout?: () => void, orderIdFromState?: string | null ): boolean { const [polling, setPolling] = useState(false); const onPaidRef = useRef(onPaid); const onTimeoutRef = useRef(onTimeout); onPaidRef.current = onPaid; onTimeoutRef.current = onTimeout; useEffect(() => { const orderId = orderIdFromState !== undefined ? orderIdFromState?.trim() || null : (() => { const cookieRaw = getCookie(COOKIE_NAME); const [cookieOrderId, cookieValid] = parsePayOrderCookie(cookieRaw); if (cookieValid) { return cookieOrderId; } const storageRaw = getStorage(STORAGE_NAME); const [storageOrderId, storageValid] = parsePayOrderCookie(storageRaw); return storageValid ? storageOrderId : null; })(); if (!orderId) { if ( orderIdFromState === undefined && (getCookie(COOKIE_NAME) || getStorage(STORAGE_NAME)) ) { clearPendingOrder(); } return; } 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 tick = async () => { if (stopped) return; if (inFlight) { scheduleNext(); return; } attempts += 1; if (attempts > MAX_POLLS) { stop(); clearPendingOrder(); setPolling(false); onTimeoutRef.current?.(); 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); onPaidRef.current(orderId); return; } } catch { // ignore and continue polling } finally { inFlight = false; } scheduleNext(); }; const scheduleNext = () => { if (stopped) return; timer = window.setTimeout(tick, POLL_INTERVAL_MS); }; 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(); }; }, [orderIdFromState]); return polling; }