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:
2026-06-17 14:13:42 +08:00
parent cda76a1651
commit 287affa34a
16 changed files with 53 additions and 47 deletions
@@ -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>
);
}