"use client"; /** * 订阅页主屏幕 * * 原始 Dart: lib/ui/subscription/... * * 布局(顶到底): * 1. 顶部返回箭头 * 2. 用户信息行 * 3. 渐变横幅 * 4. 三个套餐卡片 * 5. 自动续费说明 * 6. 权益列表卡片 * 7. 主操作按钮 * 8. 协议复选框 */ import { useEffect, useMemo, useRef } from "react"; import { Checkbox, MobileShell } from "@/app/_components/core"; import { AppConstants } from "@/core/app_constants"; import { VIP_BENEFITS } from "@/data/constants/vip-benefits"; import { PendingPaymentOrderStorage } from "@/data/storage"; import { usePaymentDispatch, usePaymentState, } from "@/stores/payment/payment-context"; import { useUserDispatch, useUserState } from "@/stores/user/user-context"; import { SubscriptionBackLink, SubscriptionBanner, SubscriptionBenefitsCard, SubscriptionCheckoutButton, SubscriptionPaymentMethod, SubscriptionPlanCard, SubscriptionUserRow, } from "./components"; import styles from "./components/subscription-screen.module.css"; import { SUBSCRIPTION_BANNER_TITLES, isPlanForType, toPlanView, 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; } export function SubscriptionScreen({ subscriptionType = "vip", shouldResumePendingOrder = false, }: SubscriptionScreenProps) { const user = useUserState(); const userDispatch = useUserDispatch(); const payment = usePaymentState(); const paymentDispatch = usePaymentDispatch(); const refreshedPaidOrderRef = useRef(null); const resumedPendingOrderRef = useRef(null); 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 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)) .map((plan) => toPlanView(plan, subscriptionType)), [payment.plans, subscriptionType], ); const selectedPlan = 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.currentUser?.avatarUrl ?? null; const autoRenewCaption = payment.autoRenew ? "It will automatically renew upon expiration, and can be canceled at any time" : "This plan is a one-time purchase and will not automatically renew"; const plansStatusMessage = subscriptionType === "voice" ? "Voice packages are unavailable." : "Membership plans are unavailable."; const plansAriaLabel = subscriptionType === "voice" ? "Choose a voice package" : "Choose a subscription plan"; useEffect(() => { 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, selectedPlan, subscriptionType, ]); return (
{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 } />
); }