Files
gitlab-instance-0a899031_no…/app/lib/usePayStatusPoll.ts
2026-03-10 08:46:58 -05:00

119 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 支付状态轮询:点击支付后轮询,支付成功刷新,超时重置
* 优先使用 sessionStoragepay_pending_order其次 cookie
*/
import { useEffect, useRef, useState } from "react";
const COOKIE_NAME = "nomadvip_pay_order";
const SESSION_KEY_ORDER = "pay_pending_order";
const SESSION_KEY_TS = "pay_pending_ts";
const POLL_INTERVAL = 2000;
const MAX_POLLS = 160; // ~5 分钟
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 getSessionOrder(): [string | null, boolean] {
if (typeof window === "undefined") return [null, false];
const orderId = sessionStorage.getItem(SESSION_KEY_ORDER)?.trim() || null;
const ts = sessionStorage.getItem(SESSION_KEY_TS);
const timestamp = ts ? parseInt(ts, 10) : NaN;
if (!orderId) return [null, false];
if (Number.isNaN(timestamp)) return [orderId, true];
if (Date.now() - timestamp > MAX_AGE_MS) return [orderId, false];
return [orderId, true];
}
function setSessionOrder(orderId: string): void {
if (typeof window === "undefined") return;
sessionStorage.setItem(SESSION_KEY_ORDER, orderId);
sessionStorage.setItem(SESSION_KEY_TS, String(Date.now()));
}
function clearSessionOrder(): void {
if (typeof window === "undefined") return;
sessionStorage.removeItem(SESSION_KEY_ORDER);
sessionStorage.removeItem(SESSION_KEY_TS);
}
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];
}
function getOrderIdToPoll(): [string | null, "session" | "cookie" | null] {
const [sessionOrder, sessionValid] = getSessionOrder();
if (sessionOrder && sessionValid) return [sessionOrder, "session"];
const raw = getCookie(COOKIE_NAME);
const [cookieOrder, cookieValid] = parsePayOrderCookie(raw);
if (cookieOrder && cookieValid) return [cookieOrder, "cookie"];
return [null, null];
}
export function setPayPendingOrder(orderId: string): void {
setSessionOrder(orderId);
}
export function usePayStatusPoll(onPaid: (orderId?: string) => void, onTimeout?: () => void): boolean {
const [polling, setPolling] = useState(false);
const onPaidRef = useRef(onPaid);
const onTimeoutRef = useRef(onTimeout);
onPaidRef.current = onPaid;
onTimeoutRef.current = onTimeout;
useEffect(() => {
const [orderId] = getOrderIdToPoll();
if (!orderId) {
const raw = getCookie(COOKIE_NAME);
if (raw) clearCookie(COOKIE_NAME);
return;
}
setPolling(true);
let count = 0;
const timer = setInterval(async () => {
count++;
if (count > MAX_POLLS) {
clearInterval(timer);
clearSessionOrder();
clearCookie(COOKIE_NAME);
setPolling(false);
onTimeoutRef.current?.();
return;
}
try {
const res = await fetch(`/api/pay/status?order_id=${encodeURIComponent(orderId)}`);
const data = await res.json();
if (data?.ok && data?.paid) {
clearInterval(timer);
clearSessionOrder();
clearCookie(COOKIE_NAME);
setPolling(false);
onPaidRef.current(orderId);
}
} catch {
// 继续轮询
}
}, POLL_INTERVAL);
return () => clearInterval(timer);
}, []);
return polling;
}