From 096978e7035d6ae277a16bc39af45ee1bfad7197 Mon Sep 17 00:00:00 2001 From: chenhang Date: Mon, 29 Jun 2026 14:45:01 +0800 Subject: [PATCH] feat(subscription): split vip and topup flows --- .../chat-quota-exhausted-banner.tsx | 2 +- src/app/sidebar/components/index.ts | 1 - .../components/sidebar-wallet-card.module.css | 49 +++++-- .../components/sidebar-wallet-card.tsx | 19 ++- .../components/voice-package-card.module.css | 136 ------------------ .../sidebar/components/voice-package-card.tsx | 70 --------- src/app/sidebar/sidebar-screen.tsx | 40 +----- .../subscription-checkout-button.tsx | 14 +- .../subscription-payment-success-dialog.tsx | 6 +- src/app/subscription/return/page.tsx | 2 +- .../subscription/subscription-page-client.tsx | 2 +- .../subscription-screen.helpers.ts | 68 +-------- src/app/subscription/subscription-screen.tsx | 118 ++++----------- src/app/subscription/success/page.tsx | 3 +- .../payment/pending_payment_order_storage.ts | 2 +- src/router/routes.ts | 2 +- src/tokens/colors.css | 4 - 17 files changed, 106 insertions(+), 432 deletions(-) delete mode 100644 src/app/sidebar/components/voice-package-card.module.css delete mode 100644 src/app/sidebar/components/voice-package-card.tsx diff --git a/src/app/chat/components/chat-quota-exhausted-banner.tsx b/src/app/chat/components/chat-quota-exhausted-banner.tsx index 36d16c06..93358e59 100644 --- a/src/app/chat/components/chat-quota-exhausted-banner.tsx +++ b/src/app/chat/components/chat-quota-exhausted-banner.tsx @@ -12,7 +12,7 @@ * * 业务关系: * - 与 `src/app/sidebar/components/vip-benefits-card.tsx` 的 "Activate VIP Membership" 行为一致 - * - 与 `src/app/sidebar/components/voice-package-card.tsx` 的 "Buy Package" 行为一致 + * - 与订阅页 VIP 入口行为一致 * * 不做的事: * - 不直接管理 state machine(chat 机器不感知 UI 层) diff --git a/src/app/sidebar/components/index.ts b/src/app/sidebar/components/index.ts index 04faf02b..2851c683 100644 --- a/src/app/sidebar/components/index.ts +++ b/src/app/sidebar/components/index.ts @@ -6,4 +6,3 @@ export * from "./back-bar"; export * from "./sidebar-wallet-card"; export * from "./user-header"; export * from "./vip-benefits-card"; -export * from "./voice-package-card"; diff --git a/src/app/sidebar/components/sidebar-wallet-card.module.css b/src/app/sidebar/components/sidebar-wallet-card.module.css index 5950cc62..4c4d7208 100644 --- a/src/app/sidebar/components/sidebar-wallet-card.module.css +++ b/src/app/sidebar/components/sidebar-wallet-card.module.css @@ -98,26 +98,52 @@ transform: translateY(1px); } +.actionColumn { + display: flex; + flex: 0 0 auto; + flex-direction: column; + gap: 9px; + align-items: stretch; +} + +.activateButton, .topUpButton { display: inline-flex; min-width: 92px; - min-height: 48px; + min-height: 44px; align-items: center; justify-content: center; padding: 0 18px; - border: 0; + border: 1px solid transparent; border-radius: 18px; + cursor: pointer; + font: inherit; + font-size: 15px; + font-weight: 850; + letter-spacing: 0.1px; + transition: box-shadow 0.18s ease, opacity 0.18s ease, transform 0.18s ease, + background 0.18s ease; +} + +.activateButton { + background: #ffffff; + color: #f657a0; + border-color: rgba(246, 87, 160, 0.28); + box-shadow: 0 10px 22px rgba(246, 87, 160, 0.12); +} + +.topUpButton { background: linear-gradient(135deg, #f657a0 0%, #ff7ac3 100%), #f657a0; color: #ffffff; - cursor: pointer; - font: inherit; - font-size: 16px; - font-weight: 850; - letter-spacing: 0.1px; box-shadow: 0 12px 26px rgba(246, 87, 160, 0.28); - transition: box-shadow 0.18s ease, opacity 0.18s ease, transform 0.18s ease; +} + +.activateButton:hover { + background: #fff5fa; + box-shadow: 0 14px 28px rgba(246, 87, 160, 0.18); + transform: translateY(-1px); } .topUpButton:hover { @@ -125,12 +151,14 @@ transform: translateY(-1px); } +.activateButton:active, .topUpButton:active { opacity: 0.92; transform: translateY(1px); } .rulesButton:focus-visible, +.activateButton:focus-visible, .topUpButton:focus-visible { outline: 2px solid #f657a0; outline-offset: 3px; @@ -142,11 +170,12 @@ padding: 16px; } + .activateButton, .topUpButton { min-width: 78px; - min-height: 44px; + min-height: 40px; padding: 0 14px; - font-size: 15px; + font-size: 14px; } .balanceValue { diff --git a/src/app/sidebar/components/sidebar-wallet-card.tsx b/src/app/sidebar/components/sidebar-wallet-card.tsx index c2ce1566..c0987eb9 100644 --- a/src/app/sidebar/components/sidebar-wallet-card.tsx +++ b/src/app/sidebar/components/sidebar-wallet-card.tsx @@ -6,6 +6,7 @@ import styles from "./sidebar-wallet-card.module.css"; export interface SidebarWalletCardProps { creditBalance: number; + onActivateVip?: () => void; onTopUp: () => void; onRulesClick: () => void; } @@ -15,6 +16,7 @@ const formatCoins = (value: number): string => export function SidebarWalletCard({ creditBalance, + onActivateVip, onTopUp, onRulesClick, }: SidebarWalletCardProps) { @@ -42,9 +44,20 @@ export function SidebarWalletCard({ - +
+ {onActivateVip ? ( + + ) : null} + +
); } diff --git a/src/app/sidebar/components/voice-package-card.module.css b/src/app/sidebar/components/voice-package-card.module.css deleted file mode 100644 index 9d37891c..00000000 --- a/src/app/sidebar/components/voice-package-card.module.css +++ /dev/null @@ -1,136 +0,0 @@ -.card { - background-image: - linear-gradient( - 180deg, - rgba(250, 208, 198, 0.57) 0%, - rgba(250, 176, 165, 0.57) 0%, - rgba(250, 143, 132, 0.57) 0%, - rgba(251, 176, 156, 0.57) 8%, - rgba(251, 208, 180, 0.57) 16%, - rgba(255, 255, 255, 0.57) 38%, - rgba(255, 255, 255, 0.57) 100% - ), - linear-gradient(#ffffff, #ffffff); - background-blend-mode: normal, normal; - border: solid 1px #fbbcb6; - border-radius: 21px; - width: 100%; - padding: var(--sidebar-card-padding-y, 18px) - var(--sidebar-card-padding-right, 12px) - var(--sidebar-card-padding-y, 18px) - var(--sidebar-card-padding-x, 18px); - box-sizing: border-box; - overflow: hidden; -} - -.content { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: var(--spacing-md, 12px); -} - -.copy { - display: flex; - flex: 1 1 auto; - min-width: 0; - flex-direction: column; - align-items: flex-start; - gap: var(--spacing-sm, 8px); -} - -.header { - padding: 0; -} - -.title { - font-size: var(--font-responsive-xl, 20px); - font-weight: 700; - color: var(--color-accent, #f84d96); -} - -.micIcon { - flex: 0 0 auto; - width: var(--sidebar-voice-image-size, 100px); - height: var(--sidebar-voice-image-size, 100px); - display: flex; - align-items: center; - justify-content: center; -} - -.micImage { - display: block; - width: var(--sidebar-voice-image-size, 100px); - height: var(--sidebar-voice-image-size, 100px); - object-fit: contain; -} - -.usage { - display: inline-flex; - align-items: baseline; - flex-wrap: wrap; - margin: 0; - padding: 0; - font-size: var(--font-size-md, 14px); - color: var(--color-text-secondary, #9e9e9e); - line-height: 1.4; -} - -.usedNumber { - display: inline-flex; - color: #fa438b; - font-size: var(--font-responsive-number-lg, 30px); - font-weight: 700; - line-height: 1; -} - -.totalNumber { - display: inline-flex; - color: #000000; - font-size: var(--font-responsive-number-lg, 30px); - font-weight: 700; - line-height: 1; -} - -.usageUnit { - color: var(--color-text-secondary, #9e9e9e); - display: inline-flex; - font: 400 var(--font-responsive-lg, 18px) / 1 var(--font-sans); -} - -.usageText { - color: #adabab; - font-size: var(--font-responsive-lg, 18px); - font-weight: 400; -} - -.footer { - padding: 0; -} - -.buyBtn { - display: inline-flex; - align-items: center; - justify-content: center; - padding: var(--spacing-xs) var(--spacing-lg, 16px); - border-radius: 22px; - background-image: - linear-gradient(91deg, #f67382 0%, #f66690 44%, #f657a0 100%), - linear-gradient(#f69757, #f69757); - background-blend-mode: normal, normal; - color: var(--color-text-primary, #ffffff); - font-size: var(--font-responsive-md, 16px); - font-weight: 600; - border: 0; - cursor: pointer; - box-sizing: border-box; -} - -.buyBtn:hover { - opacity: 0.9; -} - -.buyBtn:focus-visible { - outline: 2px solid var(--color-accent, #f84d96); - outline-offset: 2px; -} diff --git a/src/app/sidebar/components/voice-package-card.tsx b/src/app/sidebar/components/voice-package-card.tsx deleted file mode 100644 index 640c5835..00000000 --- a/src/app/sidebar/components/voice-package-card.tsx +++ /dev/null @@ -1,70 +0,0 @@ -"use client"; - -import Image from "next/image"; - -import styles from "./voice-package-card.module.css"; - -export interface VoicePackageCardProps { - /** 已使用分钟数 */ - usedMinutes: number; - /** 总分钟数 */ - totalMinutes: number; - /** "Buy Package" 按钮点击 */ - onBuyPackage: () => void; -} - -/** - * Voice Message Package 卡片 - * - * 视觉规格(与设计稿对齐): - * - 白色卡片 + 粉色→橙色渐变描边 - * - 标题 "Voice Message Package"(粉色加粗) - * - "0min/0min used/remaining" 文案(前缀粉色加粗) - * - 底部 "Buy Package" 粉色胶囊按钮 - */ -export function VoicePackageCard({ - usedMinutes, - totalMinutes, - onBuyPackage, -}: VoicePackageCardProps) { - return ( -
-
-
-
-

Voice Message Package

-
- -

- {usedMinutes} - min/ - {totalMinutes} - min - {"\u00a0\u00a0"}used/remaining -

- -
- -
-
- - -
-
- ); -} diff --git a/src/app/sidebar/sidebar-screen.tsx b/src/app/sidebar/sidebar-screen.tsx index a9bd35cf..2097db7e 100644 --- a/src/app/sidebar/sidebar-screen.tsx +++ b/src/app/sidebar/sidebar-screen.tsx @@ -20,23 +20,8 @@ import styles from "./components/sidebar-screen.module.css"; const FALLBACK_USERNAME = "User name"; const VIP_SUBSCRIPTION_URL = `${ROUTES.subscription}?type=vip`; +const TOP_UP_SUBSCRIPTION_URL = `${ROUTES.subscription}?type=topup`; -const STATUS_LABELS: Record = { - guest: "Sign in to keep your coins and unlock more features.", - member: "Manage your wallet, subscription and account settings.", - vip: "Your VIP benefits are active. Enjoy the full experience.", -}; - -/** - * 侧边栏主屏幕 - * - * 根据 `loginStatus + currentUser.isVip` 派生三种状态: - * - guest:未登录 - * - member:已登录未购买 VIP - * - vip:已登录且购买 VIP - * - * - */ export function SidebarScreen() { const router = useRouter(); const user = useUserState(); @@ -74,12 +59,6 @@ export function SidebarScreen() { -
-

Cozsweet

-

Account

-

{STATUS_LABELS[sidebarState]}

-
-
router.push(VIP_SUBSCRIPTION_URL)} + onActivateVip={ + sidebarState === "vip" + ? undefined + : () => router.push(VIP_SUBSCRIPTION_URL) + } + onTopUp={() => router.push(TOP_UP_SUBSCRIPTION_URL)} onRulesClick={() => router.push(ROUTES.coinsRules)} />
- {/* -
- router.push(VOICE_SUBSCRIPTION_URL)} - /> -
- */} - {sidebarState !== "guest" ? (

Other

diff --git a/src/app/subscription/components/subscription-checkout-button.tsx b/src/app/subscription/components/subscription-checkout-button.tsx index 90444f36..b71d3c27 100644 --- a/src/app/subscription/components/subscription-checkout-button.tsx +++ b/src/app/subscription/components/subscription-checkout-button.tsx @@ -23,7 +23,7 @@ const log = new Logger("SubscriptionCheckoutButton"); export interface SubscriptionCheckoutButtonProps { /** 是否可用(未选套餐 / 未勾选协议 → false) */ disabled?: boolean; - subscriptionType: "vip" | "voice"; + subscriptionType: "vip" | "topup"; returnTo?: "chat" | null; } @@ -97,13 +97,19 @@ export function SubscriptionCheckoutButton({ const isLoading = payment.isCreatingOrder || payment.isPollingOrder; + const readyLabel = + subscriptionType === "topup" ? "Pay and Top Up" : "Pay and Activiate"; + const agreementLabel = + subscriptionType === "topup" + ? "Confirm the agreement and Top Up" + : "Confirm the agreement and Activate"; const label = payment.isPollingOrder ? "Processing payment..." : payment.isCreatingOrder ? "Creating order..." : payment.agreed - ? "Pay and Activiate" - : "Confirm the agreement and Activate"; + ? readyLabel + : agreementLabel; const handleClick = () => { if (disabled || isLoading) return; @@ -244,7 +250,7 @@ function isEzpayPayment(payParams: Record): boolean { interface RedirectToEzpayInput { orderId: string | null; paymentUrl: string; - subscriptionType: "vip" | "voice"; + subscriptionType: "vip" | "topup"; returnTo?: "chat" | null; onFailed: (errorMessage: string) => void; } diff --git a/src/app/subscription/components/subscription-payment-success-dialog.tsx b/src/app/subscription/components/subscription-payment-success-dialog.tsx index 8e827f20..67f45240 100644 --- a/src/app/subscription/components/subscription-payment-success-dialog.tsx +++ b/src/app/subscription/components/subscription-payment-success-dialog.tsx @@ -4,7 +4,7 @@ import styles from "./subscription-payment-success-dialog.module.css"; export interface SubscriptionPaymentSuccessDialogProps { open: boolean; - subscriptionType: "vip" | "voice"; + subscriptionType: "vip" | "topup"; onClose: () => void; } @@ -16,8 +16,8 @@ export function SubscriptionPaymentSuccessDialog({ if (!open) return null; const content = - subscriptionType === "voice" - ? "Your voice package purchase was completed successfully." + subscriptionType === "topup" + ? "Your coins have been added to your wallet successfully." : "Your VIP membership has been activated successfully."; return ( diff --git a/src/app/subscription/return/page.tsx b/src/app/subscription/return/page.tsx index 405b02cb..50d2d2f4 100644 --- a/src/app/subscription/return/page.tsx +++ b/src/app/subscription/return/page.tsx @@ -10,7 +10,7 @@ import { ROUTES } from "@/router/routes"; type ReturnStatus = "loading" | "missing"; function buildSubscriptionUrl( - subscriptionType: "vip" | "voice", + subscriptionType: "vip" | "topup", returnTo?: "chat", ): string { const params = new URLSearchParams({ diff --git a/src/app/subscription/subscription-page-client.tsx b/src/app/subscription/subscription-page-client.tsx index 76322519..f6b1faf9 100644 --- a/src/app/subscription/subscription-page-client.tsx +++ b/src/app/subscription/subscription-page-client.tsx @@ -8,7 +8,7 @@ import { } from "./subscription-screen"; function toSubscriptionType(value: string | null): SubscriptionType { - return value === "voice" ? "voice" : "vip"; + return value === "topup" ? "topup" : "vip"; } function toReturnTo(value: string | null): "chat" | null { diff --git a/src/app/subscription/subscription-screen.helpers.ts b/src/app/subscription/subscription-screen.helpers.ts index c122144a..3e8d4225 100644 --- a/src/app/subscription/subscription-screen.helpers.ts +++ b/src/app/subscription/subscription-screen.helpers.ts @@ -1,15 +1,9 @@ import type { PaymentPlan } from "@/data/dto/payment"; import type { CoinsOfferPlanView } from "./components/subscription-coins-offer-section"; -import type { SubscriptionPlanView } from "./components/subscription-plan-card"; import type { VipOfferPlanView } from "./components/subscription-vip-offer-section"; -export type SubscriptionType = "vip" | "voice"; - -export const SUBSCRIPTION_BANNER_TITLES: Record = { - vip: "Subscribe to become the VIP Member", - voice: "Buy Voice Message Package", -}; +export type SubscriptionType = "vip" | "topup"; export function isVipPlan(plan: PaymentPlan): boolean { return plan.vipDays !== null; @@ -19,32 +13,6 @@ export function isCreditPlan(plan: PaymentPlan): boolean { return plan.dolAmount !== null; } -function isVoicePackagePlan(plan: PaymentPlan): boolean { - const orderType = plan.orderType.toLowerCase(); - const planId = plan.planId.toLowerCase(); - return ( - isCreditPlan(plan) || - orderType.includes("voice") || - planId.includes("voice") - ); -} - -export function isPlanForType( - plan: PaymentPlan, - subscriptionType: SubscriptionType, -): boolean { - return subscriptionType === "voice" - ? isVoicePackagePlan(plan) - : isVipPlan(plan); -} - -function currencySymbol(currency: string): string { - const normalized = currency.toUpperCase(); - if (normalized === "CNY") return "¥"; - if (normalized === "USD") return "$"; - return `${currency} `; -} - function formatAmount(amountCents: number | null): string | null { if (amountCents === null) return null; if (!Number.isFinite(amountCents)) return null; @@ -78,40 +46,6 @@ function vipOfferTitle(plan: PaymentPlan): string { return plan.planName; } -function planCaption( - plan: PaymentPlan, - subscriptionType: SubscriptionType, -): string { - if (subscriptionType === "voice") { - return plan.dolAmount === null - ? "Voice package" - : `${plan.dolAmount} voice messages`; - } - if (plan.dailyPriceCents !== null) { - const dailyPrice = formatAmount(plan.dailyPriceCents); - return dailyPrice === null - ? "Lifetime" - : `${currencySymbol(plan.currency)}${dailyPrice}/day`; - } - if (plan.vipDays === null) return "Lifetime"; - const fallbackDailyPrice = plan.amountCents / Math.max(plan.vipDays, 1); - return `${currencySymbol(plan.currency)}${formatAmount(fallbackDailyPrice)}/day`; -} - -export function toPlanView( - plan: PaymentPlan, - subscriptionType: SubscriptionType, -): SubscriptionPlanView { - return { - id: plan.planId, - name: plan.planName, - price: formatAmount(plan.amountCents) ?? "", - originalPrice: formatAmount(plan.originalAmountCents), - perDay: planCaption(plan, subscriptionType), - currencySymbol: currencySymbol(plan.currency), - }; -} - export function toVipOfferPlanView(plan: PaymentPlan): VipOfferPlanView { return { id: plan.planId, diff --git a/src/app/subscription/subscription-screen.tsx b/src/app/subscription/subscription-screen.tsx index 3ba92e55..ca9ef45f 100644 --- a/src/app/subscription/subscription-screen.tsx +++ b/src/app/subscription/subscription-screen.tsx @@ -9,33 +9,26 @@ import { usePaymentDispatch, usePaymentState, } from "@/stores/payment/payment-context"; -import { useUserDispatch, useUserState } from "@/stores/user/user-context"; +import { useUserDispatch } 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 { @@ -50,7 +43,6 @@ export function SubscriptionScreen({ returnTo = null, }: SubscriptionScreenProps) { const router = useRouter(); - const user = useUserState(); const userDispatch = useUserDispatch(); const payment = usePaymentState(); const paymentDispatch = usePaymentDispatch(); @@ -59,7 +51,7 @@ export function SubscriptionScreen({ const successDialogShownOrderRef = useRef(null); const [showPaymentSuccessDialog, setShowPaymentSuccessDialog] = useState(false); - const isVipSubscription = subscriptionType === "vip"; + const canSubscribeVip = subscriptionType === "vip"; useEffect(() => { if (payment.status === "idle") { @@ -139,14 +131,6 @@ export function SubscriptionScreen({ 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 @@ -161,28 +145,18 @@ export function SubscriptionScreen({ ); const selectedPlan = - (isVipSubscription + (canSubscribeVip ? [...vipOfferPlans, ...directCoinsPlans].find( (plan) => plan.id === payment.selectedPlanId, ) - : plans.find((plan) => plan.id === payment.selectedPlanId)) ?? null; + : directCoinsPlans.find((plan) => plan.id === payment.selectedPlanId)) ?? + null; const isPaymentBusy = payment.isCreatingOrder || payment.isPollingOrder; 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 = () => { setShowPaymentSuccessDialog(false); paymentDispatch({ type: "PaymentReset" }); @@ -196,7 +170,7 @@ export function SubscriptionScreen({ }; useEffect(() => { - if (isVipSubscription) { + if (canSubscribeVip) { const firstVipPlanId = vipOfferPlans[0]?.id ?? ""; const hasSelectedVipPlan = vipOfferPlans.some( (plan) => plan.id === payment.selectedPlanId, @@ -219,28 +193,21 @@ export function SubscriptionScreen({ return; } - if (payment.isLoadingPlans || payment.plans.length === 0) return; + if (payment.isLoadingPlans || directCoinsPlans.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, + planId: directCoinsPlans[0]?.id ?? "", }); }, [ + canSubscribeVip, + directCoinsPlans, payment.isLoadingPlans, - payment.plans, paymentDispatch, payment.selectedPlanId, payment.status, - directCoinsPlans, - isVipSubscription, selectedPlan, - subscriptionType, vipOfferPlans, ]); @@ -251,8 +218,8 @@ export function SubscriptionScreen({ - {isVipSubscription ? ( -
+
+ {canSubscribeVip ? ( - - 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} -

- )} -
- - )} + ) : null} + + paymentDispatch({ + type: "PaymentPlanSelected", + planId, + }) + } + /> +

- Welcome to VIP. Enjoy unlimited chat, free voice minutes, and - more. + Welcome to VIP. Enjoy unlimited chat and more premium benefits.