"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import type { FormEvent } from "react"; import type { Account } from "@/lib/home-types"; import { notifyBalanceChanged, redeemErrorMessage, type BillingPaneTab, type MembershipPlanAlias, type MembershipProduct, } from "@/lib/membership"; import { fetchPaymentPackages } from "@/lib/payment-packages-cache"; export type BillingAccount = { user: { id: string; email: string | null }; credits: number; activeSubscription: Account["activeSubscription"]; }; export type PaymentOrderState = { orderNo: string; payUrl: string | null; qrCode: string | null; status: string; productId: string; }; export type PaymentOrder = { orderNo: string; status: string; credits: number; moneyCents: number; currency: string; productName: string; paidAt: string | null; createdAt: string; }; export type OrdersState = "idle" | "ready" | "unavailable"; function toBillingAccount(account: Account | BillingAccount | null): BillingAccount | null { if (!account) return null; return { user: account.user, credits: account.credits, activeSubscription: account.activeSubscription, }; } export function normalizeOrders(list: readonly unknown[]): PaymentOrder[] { return list.map((raw) => { const item = (raw ?? {}) as Record; const snapshot = (item.product_snapshot ?? item.product ?? {}) as Record; const price = typeof item.price === "number" ? item.price : typeof item.money_cents === "number" ? item.money_cents : typeof item.moneyCents === "number" ? item.moneyCents : 0; return { orderNo: String(item.order_no ?? item.orderNo ?? ""), status: String(item.status ?? "unknown"), credits: typeof item.credits === "number" ? item.credits : 0, moneyCents: price, currency: String(item.currency ?? "CNY"), productName: typeof snapshot.name === "string" ? snapshot.name : typeof item.name === "string" ? item.name : String(item.product_code ?? item.productCode ?? ""), paidAt: item.paid_at ? String(item.paid_at) : item.paidAt ? String(item.paidAt) : null, createdAt: String(item.created_at ?? item.createdAt ?? ""), }; }); } export function useBillingPanel(input: { readonly account: Account; readonly highlightedPlan: MembershipPlanAlias | null; readonly initialTab: BillingPaneTab; }) { const seedAccount = input.account; const [accountError, setAccountError] = useState(""); const [paymentPackages, setPaymentPackages] = useState([]); const [paymentEnabled, setPaymentEnabled] = useState(false); const [packagesError, setPackagesError] = useState(""); const [redeemCode, setRedeemCode] = useState(""); const [redeemError, setRedeemError] = useState(""); const [redeemMessage, setRedeemMessage] = useState(""); const [redeeming, setRedeeming] = useState(false); const [paymentOrder, setPaymentOrder] = useState(null); const [paymentError, setPaymentError] = useState(""); const [payingProductId, setPayingProductId] = useState(null); const [selectedProductId, setSelectedProductId] = useState(null); const [redeemDone, setRedeemDone] = useState(false); const [orders, setOrders] = useState([]); const [ordersState, setOrdersState] = useState("idle"); const [packagesSettled, setPackagesSettled] = useState(false); const highlightedCardRef = useRef(null); const planHighlightScrolled = useRef(false); const ordersRequested = useRef(input.initialTab === "orders"); const activeSubscription = seedAccount.activeSubscription ?? null; const currentProductCode = activeSubscription?.status === "active" ? activeSubscription.productCode : null; const resetPaymentState = useCallback(() => { setPaymentEnabled(false); setPaymentPackages([]); setPaymentOrder(null); setPaymentError(""); setPayingProductId(null); setSelectedProductId(null); }, []); const fetchPackages = useCallback(async () => { try { const payload = await fetchPaymentPackages((next) => { setPaymentEnabled(next.enabled); setPaymentPackages(next.packages); setPackagesError(""); }); setPaymentEnabled(payload.enabled); setPaymentPackages(payload.packages); setPackagesError(""); } catch { setPackagesError("套餐支付暂时不可用,请稍后重试"); resetPaymentState(); } finally { setPackagesSettled(true); } }, [resetPaymentState]); const fetchOrders = useCallback(async () => { ordersRequested.current = true; try { const response = await fetch("/api/payment/orders", { cache: "no-store" }); if (response.status === 401) { window.location.assign("/login"); return; } if (!response.ok) throw new Error("订单记录暂时不可用"); const payload = await response.json().catch(() => null); const list = Array.isArray(payload) ? payload : payload && typeof payload === "object" && Array.isArray(payload.orders) ? payload.orders : []; setOrders(normalizeOrders(list)); setOrdersState("ready"); } catch { setOrders([]); setOrdersState("unavailable"); } }, []); useEffect(() => { void fetchPackages(); }, [fetchPackages]); useEffect(() => { if (!input.highlightedPlan || planHighlightScrolled.current || paymentPackages.length === 0) return; planHighlightScrolled.current = true; const card = highlightedCardRef.current; if (!card) return; window.requestAnimationFrame(() => { card.scrollIntoView({ behavior: "smooth", block: "center" }); card.focus({ preventScroll: true }); }); }, [input.highlightedPlan, paymentPackages.length]); async function redeem(event: FormEvent) { event.preventDefault(); const code = redeemCode.trim(); if (!code || redeeming) return; setRedeeming(true); setRedeemError(""); setRedeemMessage(""); try { const response = await fetch("/api/redeem", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ code }), }); const payload = await response.json().catch(() => null); if (response.status === 401) { window.location.assign("/login"); return; } if (!response.ok) throw new Error(redeemErrorMessage(response.status, payload)); const result = payload as { awardedCredits?: number; credits: number; message?: string }; const awarded = typeof result.awardedCredits === "number" ? result.awardedCredits : result.credits; setRedeemCode(""); setRedeemDone(true); setRedeemMessage(result.message || `本次到账 ${awarded} 点,最新余额 ${result.credits} 点。`); notifyBalanceChanged(result.credits); } catch (caught) { setRedeemError(caught instanceof Error ? caught.message : "兑换失败,请稍后重试"); } finally { setRedeeming(false); } } function closeRedeem() { setRedeemDone(false); setRedeemMessage(""); setRedeemError(""); } async function createPayment(productId: string) { if (payingProductId) return; setSelectedProductId(productId); setPayingProductId(productId); setPaymentError(""); try { const response = await fetch("/api/payment/epay/create", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ productId }), }); const payload = await response.json().catch(() => null); if (response.status === 401) { window.location.assign("/login"); return; } if (!response.ok) throw new Error(payload?.error || "创建支付失败"); if (typeof payload?.orderNo !== "string" || typeof payload?.payUrl !== "string") { throw new Error("创建支付失败"); } setPaymentOrder({ orderNo: payload.orderNo, payUrl: payload.payUrl, qrCode: payload.qrCode ?? null, status: "pending", productId, }); window.open(payload.payUrl, "_blank", "noopener,noreferrer"); } catch (caught) { setPaymentError(caught instanceof Error ? caught.message : "创建支付失败"); } finally { setPayingProductId(null); } } useEffect(() => { if (!paymentOrder || paymentOrder.status !== "pending") return; const checkPaymentStatus = async () => { const response = await fetch(`/api/payment/epay/status?orderNo=${encodeURIComponent(paymentOrder.orderNo)}`, { cache: "no-store" }); const payload = await response.json().catch(() => null); if (!response.ok) return; const failed = (typeof payload.status === "string" && ["failed", "closed", "cancelled"].includes(payload.status)) || payload.grantStatus === "failed"; const paid = payload.status === "paid" && !failed; setPaymentOrder((current) => current ? { ...current, status: paid ? "paid" : failed ? "failed" : "pending", } : current); if (paid) { if (typeof payload.credits === "number") { notifyBalanceChanged(payload.credits); } } }; let timer = 0; const stopPolling = () => { if (timer) window.clearInterval(timer); timer = 0; }; const startPolling = () => { stopPolling(); timer = window.setInterval(() => void checkPaymentStatus(), 3000); }; const onVisibilityChange = () => { if (document.hidden) { stopPolling(); return; } void checkPaymentStatus(); startPolling(); }; if (!document.hidden) startPolling(); document.addEventListener("visibilitychange", onVisibilityChange); return () => { stopPolling(); document.removeEventListener("visibilitychange", onVisibilityChange); }; }, [paymentOrder]); return { account: toBillingAccount(seedAccount), accountError, activeSubscription, createPayment, currentProductCode, fetchOrders, closeRedeem, highlightedCardRef, highlightedPlan: input.highlightedPlan, initialTab: input.highlightedPlan ? "membership" : input.initialTab, orders, ordersState, packagesError, packagesSettled, payingProductId, paymentEnabled, paymentError, paymentOrder, paymentPackages, redeem, redeemCode, redeemDone, redeemError, redeeming, redeemMessage, requestOrders: () => { if (!ordersRequested.current) void fetchOrders(); }, selectedProductId, setRedeemCode, setRedeemDone, setRedeemError, setRedeemMessage, }; }