refactor(app): move route screens out of components
Exclude test and spec files from generated barrels so page imports do not load test modules at runtime.
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
"use client";
|
||||
/**
|
||||
* 订阅页主屏幕
|
||||
*
|
||||
* 原始 Dart: lib/ui/subscription/...
|
||||
*
|
||||
* 布局(顶到底):
|
||||
* 1. 顶部返回箭头
|
||||
* 2. 用户信息行
|
||||
* 3. 渐变横幅
|
||||
* 4. 三个套餐卡片
|
||||
* 5. 自动续费说明
|
||||
* 6. 权益列表卡片
|
||||
* 7. 主操作按钮
|
||||
* 8. 协议复选框
|
||||
*/
|
||||
import { useState } from "react";
|
||||
|
||||
import { Checkbox, MobileShell } from "@/app/_components/core";
|
||||
import { AppConstants } from "@/core/app_constants";
|
||||
import {
|
||||
SUBSCRIPTION_PLANS,
|
||||
type SubscriptionPlanId,
|
||||
} from "@/data/constants/subscription-plans";
|
||||
import { VIP_BENEFITS } from "@/data/constants/vip-benefits";
|
||||
import { useUserState } from "@/stores/user/user-context";
|
||||
|
||||
import {
|
||||
SubscriptionBackLink,
|
||||
SubscriptionBanner,
|
||||
SubscriptionBenefitsCard,
|
||||
SubscriptionCheckoutButton,
|
||||
SubscriptionManageButton,
|
||||
SubscriptionPlanCard,
|
||||
SubscriptionUserRow,
|
||||
} from "./components";
|
||||
import styles from "./components/subscription-screen.module.css";
|
||||
|
||||
const FALLBACK_USERNAME = "User name";
|
||||
|
||||
export function SubscriptionScreen() {
|
||||
const user = useUserState();
|
||||
|
||||
const initialPlanId =
|
||||
SUBSCRIPTION_PLANS.find((p) => p.highlight)?.id ?? null;
|
||||
|
||||
// 强制类型为 `SubscriptionPlanId` —— 套餐表只有 3 个 id,不会越界
|
||||
const [selectedPlanId, setSelectedPlanId] = useState<SubscriptionPlanId | null>(
|
||||
initialPlanId as SubscriptionPlanId | null,
|
||||
);
|
||||
const [agreed, setAgreed] = useState(false);
|
||||
|
||||
const canActivate = selectedPlanId !== null && agreed;
|
||||
const isVip = user.currentUser?.isVip ?? false;
|
||||
const stripeCustomerId = user.currentUser?.stripeCustomerId ?? null;
|
||||
|
||||
const name = user.currentUser?.username ?? FALLBACK_USERNAME;
|
||||
const avatarUrl = user.currentUser?.avatarUrl ?? null;
|
||||
|
||||
return (
|
||||
<MobileShell>
|
||||
<div className={styles.shell}>
|
||||
<header className={styles.header}>
|
||||
<SubscriptionBackLink className={styles.backSlot} />
|
||||
</header>
|
||||
|
||||
<section className={styles.userSlot}>
|
||||
<SubscriptionUserRow name={name} avatarUrl={avatarUrl} />
|
||||
</section>
|
||||
|
||||
<section className={styles.bannerSlot}>
|
||||
<SubscriptionBanner />
|
||||
</section>
|
||||
|
||||
<section
|
||||
className={styles.plansRow}
|
||||
aria-label="Choose a subscription plan"
|
||||
>
|
||||
{SUBSCRIPTION_PLANS.map((plan) => (
|
||||
<SubscriptionPlanCard
|
||||
key={plan.id}
|
||||
plan={plan}
|
||||
selected={selectedPlanId === plan.id}
|
||||
onClick={() => setSelectedPlanId(plan.id)}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<p className={styles.autoRenewCaption}>
|
||||
It will automatically renew upon expiration, and can be canceled at
|
||||
any time
|
||||
</p>
|
||||
|
||||
<section className={styles.benefitsSlot}>
|
||||
<SubscriptionBenefitsCard
|
||||
title="VIP Membership Benefits"
|
||||
items={VIP_BENEFITS}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<div className={styles.ctaSlot}>
|
||||
{/*
|
||||
* 已为 VIP 的用户 → 显示 Manage VIP 按钮(跳转到 Stripe Customer Portal)
|
||||
* VIP 状态从 `user.isVip` 取(webhook 会设置)
|
||||
*/}
|
||||
{isVip ? (
|
||||
<SubscriptionManageButton customerId={stripeCustomerId} />
|
||||
) : (
|
||||
<SubscriptionCheckoutButton
|
||||
planId={selectedPlanId ?? "monthly"}
|
||||
disabled={!canActivate}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.agreementSlot}>
|
||||
<Checkbox
|
||||
checked={agreed}
|
||||
onChange={setAgreed}
|
||||
label={
|
||||
<>
|
||||
I agree to the{" "}
|
||||
<a
|
||||
href={AppConstants.privacyPolicyUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.agreementLink}
|
||||
>
|
||||
VIP Membership Benefits Agreement
|
||||
</a>{" "}
|
||||
and{" "}
|
||||
<a
|
||||
href={AppConstants.termsOfServiceUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.agreementLink}
|
||||
>
|
||||
Automatic renewal Agreement
|
||||
</a>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</MobileShell>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user