"use client"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; import { useEffect, useMemo, useState, useTransition } from "react"; import { emitBillingRefreshSignal } from "@/components/billing-refresh-listener"; import { QRCodeSVG } from "@/components/vendor/qrcode-react"; type ApiResponse = { ok: boolean; data?: { message?: string; trade_no?: string; url?: string; code_url?: string; status?: string; }; error?: { message?: string; }; }; export function BillingPurchaseActions({ kind, itemId, labels, }: { kind: "subscription" | "points"; itemId: number; labels: { alipay: string; wxpay: string; creating: string; tradeNo: string; openLink: string; codeUrl: string; invalid: string; pending: string; success: string; failed: string; paymentStatus: string; qrTitle: string; openConsole: string; openBilling: string; }; }) { const pathname = usePathname(); const router = useRouter(); const [result, setResult] = useState<(ApiResponse["data"] & { provider?: string; kind?: string }) | null>(null); const [error, setError] = useState(null); const [status, setStatus] = useState(""); const [pendingProvider, setPendingProvider] = useState(null); const [isPending, startTransition] = useTransition(); const locale = useMemo(() => pathname.split("/")[1] || "zh", [pathname]); function checkout(provider: "alipay" | "wxpay") { startTransition(async () => { setPendingProvider(provider); setError(null); setResult(null); setStatus(""); try { const response = await fetch("/api/billing/checkout", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ kind, provider, plan_id: kind === "subscription" ? itemId : undefined, package_id: kind === "points" ? itemId : undefined, return_url: typeof window !== "undefined" ? `${window.location.origin}${pathname}` : "", }), }); const payload = (await response.json()) as ApiResponse; if (!response.ok || !payload.ok) { setError(payload.error?.message ?? labels.invalid); return; } setResult({ ...(payload.data ?? null), provider, kind }); setStatus(labels.pending); } catch (requestError) { setError(requestError instanceof Error ? requestError.message : labels.invalid); } finally { setPendingProvider(null); } }); } useEffect(() => { if (!result?.trade_no || !result.provider || !result.kind) { return; } const currentResult = result; const currentKind = currentResult.kind ?? ""; const currentProvider = currentResult.provider ?? ""; if (!currentKind || !currentProvider) { return; } let cancelled = false; let attempts = 0; async function poll() { attempts += 1; try { const query = new URLSearchParams(); query.set("kind", currentKind); query.set("provider", currentProvider); query.set("trade_no", currentResult.trade_no ?? ""); const response = await fetch(`/api/billing/checkout?${query.toString()}`); const payload = (await response.json()) as ApiResponse; if (!response.ok || !payload.ok) { if (!cancelled) { setError(payload.error?.message ?? labels.invalid); } return; } const nextStatus = payload.data?.status ?? ""; if (!cancelled && nextStatus) { setStatus(nextStatus); } if (cancelled) { return; } if (nextStatus === "SUCCESS" || nextStatus === "TRADE_SUCCESS") { setStatus(labels.success); emitBillingRefreshSignal(); window.setTimeout(() => { router.push( `/${locale}/billing/success?kind=${encodeURIComponent(currentKind)}&provider=${encodeURIComponent(currentProvider)}&trade_no=${encodeURIComponent(currentResult.trade_no ?? "")}`, ); }, 1200); return; } if (nextStatus === "FAILED" || nextStatus === "CLOSED" || nextStatus === "TRADE_CLOSED") { setStatus(labels.failed); return; } if (attempts < 20) { window.setTimeout(poll, 3000); } } catch (requestError) { if (!cancelled) { setError(requestError instanceof Error ? requestError.message : labels.invalid); } } } const timer = window.setTimeout(poll, 2000); return () => { cancelled = true; window.clearTimeout(timer); }; }, [labels.failed, labels.invalid, labels.success, locale, result, router]); return (
{error ?
{error}
: null} {result ? (
{result.trade_no ? ( {labels.tradeNo}: {result.trade_no} ) : null} {status ? ( {labels.paymentStatus}: {status} ) : null} {result.url ? ( {labels.openLink} ) : null} {result.code_url ? {labels.codeUrl}: {result.code_url} : null} {result.code_url ? (
{labels.qrTitle}
) : null} {status === labels.success ? (
{labels.openConsole} {labels.openBilling}
) : null}
) : null}
); }