"use client"; /** * 订阅页主屏幕 * * 布局(顶到底): * 1. 顶部返回箭头 * 2. 用户信息行 * 3. 渐变横幅 * 4. 三个套餐卡片 * 5. 自动续费说明 * 6. 权益列表卡片 * 7. 主操作按钮 * 8. 协议复选框 */ import { useEffect, useMemo, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { Checkbox, MobileShell } from "@/app/_components/core"; import { AppConstants } from "@/core/app_constants"; import { PendingPaymentOrderStorage } from "@/data/storage"; import { usePaymentDispatch, usePaymentState, } from "@/stores/payment/payment-context"; import { useUserDispatch, useUserState } from "@/stores/user/user-context"; import { ROUTES } from "@/router/routes"; import { SubscriptionBackLink, SubscriptionBanner, SubscriptionCheckoutButton, SubscriptionCoinsOfferSection, SubscriptionPaymentMethod, SubscriptionPaymentSuccessDialog, SubscriptionPlanCard, SubscriptionUserRow, SubscriptionVipOfferSection, } from "./components"; import styles from "./components/subscription-screen.module.css"; import { SUBSCRIPTION_BANNER_TITLES, isCreditPlan, isPlanForType, isVipPlan, toCoinsOfferPlanView, toPlanView, toVipOfferPlanView, type SubscriptionType, } from "./subscription-screen.helpers"; const FALLBACK_USERNAME = "User name"; export type { SubscriptionType } from "./subscription-screen.helpers"; export interface SubscriptionScreenProps { subscriptionType?: SubscriptionType; shouldResumePendingOrder?: boolean; returnTo?: "chat" | null; } export function SubscriptionScreen({ subscriptionType = "vip", shouldResumePendingOrder = false, returnTo = null, }: SubscriptionScreenProps) { const router = useRouter(); const user = useUserState(); const userDispatch = useUserDispatch(); const payment = usePaymentState(); const paymentDispatch = usePaymentDispatch(); const refreshedPaidOrderRef = useRef(null); const resumedPendingOrderRef = useRef(null); const successDialogShownOrderRef = useRef(null); const syncedVipOrderRef = useRef(null); const [showPaymentSuccessDialog, setShowPaymentSuccessDialog] = useState(false); const [pendingChatReturnAfterVipSync, setPendingChatReturnAfterVipSync] = useState(false); const isVipSubscription = subscriptionType === "vip"; useEffect(() => { if (payment.status === "idle") { paymentDispatch({ type: "PaymentInit" }); } }, [payment.status, paymentDispatch]); useEffect(() => { if (!payment.isPaid || !payment.currentOrderId) return; if (refreshedPaidOrderRef.current === payment.currentOrderId) return; refreshedPaidOrderRef.current = payment.currentOrderId; userDispatch({ type: "UserFetch" }); }, [payment.currentOrderId, payment.isPaid, userDispatch]); useEffect(() => { const currentUser = user.currentUser; const canMarkVipSynced = subscriptionType === "vip" && payment.isPaid && payment.currentOrderId && payment.vipStatus?.isVip === true && currentUser && currentUser.isVip !== true; if (!canMarkVipSynced) return; if (syncedVipOrderRef.current === payment.currentOrderId) return; syncedVipOrderRef.current = payment.currentOrderId; userDispatch({ type: "UserUpdate", user: { ...currentUser, isVip: true, }, }); }, [ payment.currentOrderId, payment.isPaid, payment.vipStatus?.isVip, subscriptionType, user.currentUser, userDispatch, ]); useEffect(() => { if (!payment.isPaid || !payment.currentOrderId) return; if (successDialogShownOrderRef.current === payment.currentOrderId) return; successDialogShownOrderRef.current = payment.currentOrderId; setShowPaymentSuccessDialog(true); }, [payment.currentOrderId, payment.isPaid]); useEffect(() => { const canInspectPendingOrder = payment.status === "ready" || (!shouldResumePendingOrder && (payment.isPollingOrder || payment.isPaid || payment.status === "failed")); if (!canInspectPendingOrder) return; let cancelled = false; const handlePendingOrder = async () => { const result = await PendingPaymentOrderStorage.getPendingOrderForType( subscriptionType, ); if (cancelled || !result.success || result.data === null) return; if (!shouldResumePendingOrder) { await PendingPaymentOrderStorage.clearPendingOrder(); if ( payment.currentOrderId === result.data.orderId && (payment.isPollingOrder || payment.isPaid || payment.status === "failed") ) { paymentDispatch({ type: "PaymentReset" }); } return; } if (payment.currentOrderId === result.data.orderId) return; if (resumedPendingOrderRef.current === result.data.orderId) return; resumedPendingOrderRef.current = result.data.orderId; paymentDispatch({ type: "PaymentReturned", orderId: result.data.orderId, }); }; void handlePendingOrder(); return () => { cancelled = true; }; }, [ payment.currentOrderId, payment.isPaid, payment.isPollingOrder, payment.status, paymentDispatch, shouldResumePendingOrder, subscriptionType, ]); useEffect(() => { if (!payment.currentOrderId) return; if (!payment.isPaid && payment.status !== "failed") return; void PendingPaymentOrderStorage.clearPendingOrder(); }, [payment.currentOrderId, payment.isPaid, payment.status]); const plans = useMemo( () => payment.plans .filter((plan) => isPlanForType(plan, subscriptionType)) .slice(0, 3) .map((plan) => toPlanView(plan, subscriptionType)), [payment.plans, subscriptionType], ); const vipOfferPlans = useMemo( () => payment.plans .filter(isVipPlan) .slice(0, 3) .map(toVipOfferPlanView), [payment.plans], ); const directCoinsPlans = useMemo( () => payment.plans.filter(isCreditPlan).map(toCoinsOfferPlanView), [payment.plans], ); const selectedPlan = (isVipSubscription ? [...vipOfferPlans, ...directCoinsPlans].find( (plan) => plan.id === payment.selectedPlanId, ) : plans.find((plan) => plan.id === payment.selectedPlanId)) ?? null; const isPaymentBusy = payment.isCreatingOrder || payment.isPollingOrder || payment.isCheckingVipStatus; const canActivate = selectedPlan !== null && payment.agreed && !isPaymentBusy; const name = user.currentUser?.username ?? FALLBACK_USERNAME; const avatarUrl = user.avatarUrl ?? user.currentUser?.avatarUrl ?? null; const plansStatusMessage = subscriptionType === "voice" ? "Voice packages are unavailable." : "Membership plans are unavailable."; const plansAriaLabel = subscriptionType === "voice" ? "Choose a voice package" : "Choose a subscription plan"; const finishPaymentSuccessClose = () => { setPendingChatReturnAfterVipSync(false); setShowPaymentSuccessDialog(false); paymentDispatch({ type: "PaymentReset" }); if (returnTo === "chat") { router.replace(ROUTES.chat); } }; const handlePaymentSuccessClose = () => { const shouldWaitForVipSync = returnTo === "chat" && subscriptionType === "vip" && user.currentUser?.isVip !== true; if (shouldWaitForVipSync) { setPendingChatReturnAfterVipSync(true); if (payment.vipStatus?.isVip === true && user.currentUser) { userDispatch({ type: "UserUpdate", user: { ...user.currentUser, isVip: true, }, }); } else { userDispatch({ type: "UserFetch" }); } return; } finishPaymentSuccessClose(); }; useEffect(() => { if (!pendingChatReturnAfterVipSync) return; if (subscriptionType === "vip" && user.currentUser?.isVip !== true) return; const timer = window.setTimeout(() => { finishPaymentSuccessClose(); }, 0); return () => window.clearTimeout(timer); // `finishPaymentSuccessClose` intentionally stays local to this component; // this effect is gated by pendingChatReturnAfterVipSync to avoid repeated closes. // eslint-disable-next-line react-hooks/exhaustive-deps }, [pendingChatReturnAfterVipSync, subscriptionType, user.currentUser?.isVip]); useEffect(() => { if (isVipSubscription) { const firstVipPlanId = vipOfferPlans[0]?.id ?? ""; const hasSelectedVipPlan = vipOfferPlans.some( (plan) => plan.id === payment.selectedPlanId, ); const hasSelectedCoinsPlan = directCoinsPlans.some( (plan) => plan.id === payment.selectedPlanId, ); const canSelectPlan = firstVipPlanId.length > 0 && (payment.status === "ready" || payment.status === "paid" || payment.status === "failed"); if (!canSelectPlan || hasSelectedVipPlan || hasSelectedCoinsPlan) return; paymentDispatch({ type: "PaymentPlanSelected", planId: firstVipPlanId, }); return; } if (payment.isLoadingPlans || payment.plans.length === 0) return; if (selectedPlan !== null) return; const nextPlan = payment.plans.find((plan) => isPlanForType(plan, subscriptionType), ); if (!nextPlan) return; paymentDispatch({ type: "PaymentPlanSelected", planId: nextPlan.planId, }); }, [ payment.isLoadingPlans, payment.plans, paymentDispatch, payment.selectedPlanId, payment.status, directCoinsPlans, isVipSubscription, selectedPlan, subscriptionType, vipOfferPlans, ]); return (
{isVipSubscription ? ( <> paymentDispatch({ type: "PaymentPlanSelected", planId, }) } /> paymentDispatch({ type: "PaymentPlanSelected", planId, }) } /> ) : ( <>
{plans.length > 0 ? ( plans.map((plan, index) => ( paymentDispatch({ type: "PaymentPlanSelected", planId: plan.id, }) } /> )) ) : (

{payment.isLoadingPlans ? "Loading membership plans..." : payment.errorMessage ?? plansStatusMessage}

)}
)} {/*

{autoRenewCaption}

{subscriptionType === "voice" ? (
*/}
paymentDispatch({ type: "PaymentPayChannelChanged", payChannel, }) } />
paymentDispatch({ type: "PaymentAgreementChanged", agreed, }) } label={ I agree to the{" "} VIP Membership Benefits Agreement {" "} and{" "} Automatic renewal Agreement } />
); }