diff --git a/src/app/subscription/components/index.ts b/src/app/subscription/components/index.ts index ddc3133b..19cb0abb 100644 --- a/src/app/subscription/components/index.ts +++ b/src/app/subscription/components/index.ts @@ -7,6 +7,7 @@ export * from "./subscription-banner"; export * from "./subscription-benefits-card"; export * from "./subscription-checkout-button"; export * from "./subscription-cta-button"; +export * from "./subscription-payment-method"; export * from "./subscription-plan-card"; export * from "./subscription-user-row"; export * from "./stripe-payment-dialog"; diff --git a/src/app/subscription/components/subscription-cta-button.tsx b/src/app/subscription/components/subscription-cta-button.tsx index f59a4038..f138befd 100644 --- a/src/app/subscription/components/subscription-cta-button.tsx +++ b/src/app/subscription/components/subscription-cta-button.tsx @@ -1,13 +1,5 @@ "use client"; -/** - * 订阅页主操作按钮 - * - * 视觉规格(与设计稿对齐): - * - 全宽,52px 高,--radius-xxxl 圆角 - * - 粉色渐变(#f96ADE → #f657A0) - * - 白字 16px/600 + 粉色阴影 - * - :disabled 灰化 - */ + import type { ButtonHTMLAttributes, ReactNode } from "react"; import styles from "./subscription-cta-button.module.css"; diff --git a/src/app/subscription/components/subscription-payment-method.module.css b/src/app/subscription/components/subscription-payment-method.module.css new file mode 100644 index 00000000..91b73247 --- /dev/null +++ b/src/app/subscription/components/subscription-payment-method.module.css @@ -0,0 +1,80 @@ +.root { + display: flex; + flex-direction: column; + gap: 10px; +} + +.header { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: var(--spacing-sm); + padding: 0 var(--spacing-xs); +} + +.title { + margin: 0; + font-family: var(--font-athelas), var(--font-system); + font-size: var(--font-size-lg); + font-weight: 700; + color: var(--color-auth-text-primary); + line-height: 1.2; +} + +.caption { + color: var(--color-text-secondary); + font-size: var(--font-size-sm); + line-height: 1.2; + white-space: nowrap; +} + +.options { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: var(--spacing-sm); +} + +.option { + display: flex; + min-height: 62px; + align-items: center; + justify-content: center; + padding: 10px 14px; + border: 1px solid rgba(246, 87, 160, 0.18); + border-radius: 18px; + background: #ffffff; + box-shadow: 0 5px 7px 0 rgba(247, 89, 168, 0.08); + color: #3c3b3b; + cursor: pointer; + font: inherit; + text-align: center; + transition: border-color 0.15s ease, box-shadow 0.15s ease, + transform 0.05s ease; +} + +.option:focus-visible { + outline: 2px solid var(--color-accent); + outline-offset: 2px; +} + +.option:not(:disabled):active { + transform: scale(0.98); +} + +.option:disabled { + cursor: not-allowed; + opacity: 0.72; +} + +.selected { + border-color: #f657a0; + border-width: 2px; + box-shadow: 0 6px 12px 0 rgba(247, 89, 168, 0.14); +} + +.optionTitle { + color: #1e1e1e; + font-size: 16px; + font-weight: 700; + line-height: 1.1; +} diff --git a/src/app/subscription/components/subscription-payment-method.tsx b/src/app/subscription/components/subscription-payment-method.tsx new file mode 100644 index 00000000..2a2d1b54 --- /dev/null +++ b/src/app/subscription/components/subscription-payment-method.tsx @@ -0,0 +1,66 @@ +"use client"; + +import type { PayChannel } from "@/data/dto/payment"; + +import styles from "./subscription-payment-method.module.css"; + +const PAYMENT_METHODS: Array<{ + channel: PayChannel; + title: string; +}> = [ + { + channel: "stripe", + title: "Stripe", + }, + { + channel: "ezpay", + title: "Ezpay", + }, +]; + +export interface SubscriptionPaymentMethodProps { + value: PayChannel; + disabled?: boolean; + onChange: (channel: PayChannel) => void; +} + +export function SubscriptionPaymentMethod({ + value, + disabled = false, + onChange, +}: SubscriptionPaymentMethodProps) { + return ( +
+
+

+ Payment Method +

+ Stripe by default +
+
+ {PAYMENT_METHODS.map((method) => { + const selected = method.channel === value; + const classes = [ + styles.option, + selected ? styles.selected : "", + ] + .filter(Boolean) + .join(" "); + + return ( + + ); + })} +
+
+ ); +} diff --git a/src/app/subscription/components/subscription-screen.module.css b/src/app/subscription/components/subscription-screen.module.css index 9569f34c..ef666517 100644 --- a/src/app/subscription/components/subscription-screen.module.css +++ b/src/app/subscription/components/subscription-screen.module.css @@ -59,6 +59,10 @@ min-height: 160px; } +.paymentMethodSlot { + margin-top: var(--spacing-lg); +} + .ctaSlot { margin-top: var(--spacing-xl); } diff --git a/src/app/subscription/subscription-screen.tsx b/src/app/subscription/subscription-screen.tsx index bac4a2e6..5169b290 100644 --- a/src/app/subscription/subscription-screen.tsx +++ b/src/app/subscription/subscription-screen.tsx @@ -32,6 +32,7 @@ import { SubscriptionBenefitsCard, SubscriptionCheckoutButton, SubscriptionCtaButton, + SubscriptionPaymentMethod, SubscriptionPlanCard, SubscriptionUserRow, } from "./components"; @@ -250,6 +251,21 @@ export function SubscriptionScreen({ )} + {!isVip ? ( +
+ + paymentDispatch({ + type: "PaymentPayChannelChanged", + payChannel, + }) + } + /> +
+ ) : null} +
{isVip ? ( diff --git a/src/stores/payment/payment-machine.ts b/src/stores/payment/payment-machine.ts index e5b2de0f..6d09bdc4 100644 --- a/src/stores/payment/payment-machine.ts +++ b/src/stores/payment/payment-machine.ts @@ -108,6 +108,7 @@ export const paymentMachine = setup({ }, PaymentPayChannelChanged: { actions: assign(({ event }) => ({ + ...resetOrderState(), payChannel: event.payChannel, errorMessage: null, })),