Files
gitlab-instance-0a899031_cn…/app/lib/payment/usePayStatusPoll.ts
2026-03-13 05:51:29 -05:00

166 lines
4.5 KiB
TypeScript

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: (orderId: string) => void): boolean {
const [polling, setPolling] = useState(false);
const onPaidRef = useRef(onPaid);
onPaidRef.current = onPaid;
useEffect(() => {
const urlOrderId =
typeof window !== "undefined"
? new URLSearchParams(window.location.search).get("order_id") || ""
: "";
const cookieRaw = getCookie(COOKIE_NAME);
const [cookieOrderId, cookieValid] = parsePendingOrder(cookieRaw);
const storageRaw = getStorage(STORAGE_NAME);
const [storageOrderId, storageValid] = parsePendingOrder(storageRaw);
let orderId = cookieValid ? cookieOrderId : storageValid ? storageOrderId : null;
let activeRaw = cookieValid ? cookieRaw : storageValid ? storageRaw : null;
if (!orderId && urlOrderId.trim()) {
orderId = urlOrderId.trim();
activeRaw = `${orderId}|${Date.now()}`;
persistPendingOrder(activeRaw);
}
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);
onPaidRef.current(orderId);
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;
}