From 152ebe374c6e2a299b464a165cc1795ddea1c84f Mon Sep 17 00:00:00 2001 From: shenlan Date: Fri, 21 Nov 2025 21:54:25 +0800 Subject: [PATCH] Refine subscription payment layout (#686) --- .../account/BillingOptionsPanel.tsx | 428 ++++++++++++------ .../user-center/routes/subscription.tsx | 7 +- 2 files changed, 292 insertions(+), 143 deletions(-) diff --git a/dashboard/src/modules/extensions/builtin/user-center/account/BillingOptionsPanel.tsx b/dashboard/src/modules/extensions/builtin/user-center/account/BillingOptionsPanel.tsx index 9a30bb7..9c20ca5 100644 --- a/dashboard/src/modules/extensions/builtin/user-center/account/BillingOptionsPanel.tsx +++ b/dashboard/src/modules/extensions/builtin/user-center/account/BillingOptionsPanel.tsx @@ -1,8 +1,7 @@ 'use client' -import { useCallback, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' -import CryptoBillingWidget from '@components/billing/CryptoBillingWidget' import { PayPalPayGoButton, PayPalSubscriptionButton } from '@components/billing/PayPalButtons' import { resolveBillingClientId } from '@components/billing/utils' import Card from '../components/Card' @@ -20,123 +19,64 @@ type SyncPayload = { meta?: Record } -type BillingOptionProps = { +type ProductOption = { + product: ProductConfig plan: BillingPlan kind: 'paygo' | 'subscription' clientId: string - product: ProductConfig - onSync: (payload: SyncPayload) => Promise } -function BillingOption({ plan, kind, clientId, product, onSync }: BillingOptionProps) { - return ( -
-
-
-

- {kind === 'paygo' ? 'Pay-as-you-go' : 'SaaS'} -

-

{plan.name}

- {plan.description ? ( -

{plan.description}

- ) : null} -

- {plan.currency} {plan.price.toFixed(2)} - {kind === 'subscription' && plan.interval ? ` / ${plan.interval}` : null} -

-
-
- {clientId ? 'PayPal / ETH / USDT 结算' : '尚未配置 PayPal Client ID'} -
-
+const kindLabel: Record<'paygo' | 'subscription', string> = { + paygo: 'PAY-AS-YOU-GO', + subscription: 'SAAS', +} -
-
- {kind === 'paygo' ? ( - - onSync({ - externalId: orderId, - kind, - planId: plan.planId, - status: 'active', - provider: 'paypal', - paymentMethod: 'paypal', - meta: { ...plan.meta, product: product.slug, paypal: data }, - }) - } - /> - ) : ( - - onSync({ - externalId: subscriptionId, - kind, - planId: plan.planId, - status: 'active', - provider: 'paypal', - paymentMethod: 'paypal', - meta: { ...plan.meta, product: product.slug, paypal: data }, - }) - } - /> - )} -
+const actionLabel: Record<'paygo' | 'subscription', string> = { + paygo: '立即购买', + subscription: '立即订阅', +} - {plan.paymentMethods?.length ? ( -
-

- {kind === 'paygo' ? '扫码付款' : '扫码订阅'}(PayPal / 以太坊 / USDT) -

-
- {plan.paymentMethods.map((method: BillingPaymentMethod) => ( - - onSync({ - externalId: details.externalId, - kind, - planId: plan.planId, - status: details.status || 'pending', - provider: method.type, - paymentMethod: method.type, - paymentQr: details.paymentQr, - meta: { ...plan.meta, ...details.meta, product: product.slug }, - }) - } - /> - ))} -
-
- ) : null} -
-
- ) +const methodHints: Record = { + paypal: '建议使用 PayPal App 扫码,支付成功后系统自动确认订单。', + ethereum: '使用以太坊(ERC20)转账,支付后订单会自动识别并激活。', + usdt: '使用 USDT(TRC20)转账,支付完成后自动续订或开通。', } export default function BillingOptionsPanel() { const [statusMessage, setStatusMessage] = useState(null) + const [selected, setSelected] = useState(null) const products = useMemo( () => PRODUCT_LIST.filter((item) => item.billing && (item.billing.paygo || item.billing.saas)), [], ) + const productOptions = useMemo(() => { + const options: ProductOption[] = [] + + products.forEach((product) => { + const paygo = product.billing?.paygo + const saas = product.billing?.saas + const clientId = resolveBillingClientId(saas?.clientId || paygo?.clientId) + + if (paygo) { + options.push({ product, plan: paygo, kind: 'paygo', clientId }) + } + + if (saas) { + options.push({ product, plan: saas, kind: 'subscription', clientId }) + } + }) + + return options + }, [products]) + + useEffect(() => { + if (!selected && productOptions.length) { + setSelected(productOptions[0]) + } + }, [productOptions, selected]) + const handleSync = useCallback(async (payload: SyncPayload) => { try { setStatusMessage(null) @@ -170,54 +110,260 @@ export default function BillingOptionsPanel() { return null } + const activeMethods = useMemo(() => { + if (!selected?.plan.paymentMethods) return [] + const order: BillingPaymentMethod['type'][] = ['paypal', 'ethereum', 'usdt'] + return order + .map((type) => selected.plan.paymentMethods?.find((method) => method.type === type)) + .filter(Boolean) as BillingPaymentMethod[] + }, [selected?.plan.paymentMethods]) + + const handleCryptoRecord = useCallback( + (method: BillingPaymentMethod) => { + if (!selected) return + const externalId = `${method.type}-${selected.plan.planId || selected.kind}-${Date.now()}` + + handleSync({ + externalId, + kind: selected.kind, + planId: selected.plan.planId, + status: 'pending', + provider: method.type, + paymentMethod: method.type, + paymentQr: method.qrCode, + meta: { + ...selected.plan.meta, + product: selected.product.slug, + paymentMethod: method.type, + address: method.address, + network: method.network, + instructions: method.instructions, + }, + }) + }, + [handleSync, selected], + ) + + const renderCryptoCard = (method: BillingPaymentMethod) => { + const address = method.address?.trim() + const network = method.network?.trim() + const qrCode = method.qrCode?.trim() + + return ( +
+
+

{method.label || method.type}

+

{method.type === 'ethereum' ? 'ETH 扫码支付' : 'USDT(TRC20)扫码'}

+

{methodHints[method.type]}

+
+ + {qrCode ? ( +
+ {`${method.label +
+ ) : null} + + {address ? ( +
+
+
+ 钱包地址 + + {address} + + {network ? 网络 / Network:{network} : null} +
+ +
+
+ ) : null} + +
+ +
+
+ ) + } + + const renderPayPalCard = (method: BillingPaymentMethod) => { + if (!selected) return null + + return ( +
+
+

PayPal

+

PayPal 扫码

+

{methodHints.paypal}

+
+ + {method.qrCode ? ( +
+ PayPal QR +
+ ) : null} + +
+
+ {selected.kind === 'paygo' ? ( + + handleSync({ + externalId: orderId, + kind: selected.kind, + planId: selected.plan.planId, + status: 'active', + provider: 'paypal', + paymentMethod: 'paypal', + meta: { ...selected.plan.meta, product: selected.product.slug, paypal: data }, + }) + } + /> + ) : ( + + handleSync({ + externalId: subscriptionId, + kind: selected.kind, + planId: selected.plan.planId, + status: 'active', + provider: 'paypal', + paymentMethod: 'paypal', + meta: { ...selected.plan.meta, product: selected.product.slug, paypal: data }, + }) + } + /> + )} +
+ +
+
+ ) + } + return ( -
+

支付与订阅

-

完成订阅并同步到账户

+

扫码支付,自动识别到账

- 支持 PayPal、以太坊与 USDT 的扫码或直连支付,订单会实时同步到下方的订阅列表。 + 支持 PayPal / 以太坊 ETH / USDT(TRC20)扫码支付。完成后系统会自动识别订单并开通或续订服务。

- {statusMessage ?

{statusMessage}

: null} -
-
- {products.map((product) => { - const paygo = product.billing?.paygo - const saas = product.billing?.saas - const clientId = resolveBillingClientId(saas?.clientId || paygo?.clientId) - - return ( -
-
-
-

{product.name}

-

{product.title}

-

{product.tagline_zh}

-
-

- {clientId ? '已启用 PayPal 结算' : '尚未配置 PayPal Client ID'} -

-
-
- {paygo ? ( - - ) : null} - {saas ? ( - - ) : null} -
+
+
+
+

选择产品

+

先确认购买或订阅的方案

+

PAYG 与 SaaS 分离展示,避免二维码混在一起。

- ) - })} + {selected?.clientId ? ( +

已启用 PayPal / 加密货币结算

+ ) : ( +

尚未配置 PayPal Client ID

+ )} +
+
+ {productOptions.map((option) => { + const isActive = selected?.plan.planId === option.plan.planId && selected.kind === option.kind + return ( +
+
+
+

+ {kindLabel[option.kind]} +

+

{option.plan.name}

+ {option.plan.description ? ( +

{option.plan.description}

+ ) : null} +

+ {option.plan.currency} {option.plan.price.toFixed(2)} + {option.kind === 'subscription' && option.plan.interval ? ` / ${option.plan.interval}` : null} +

+
+
+
+ +
+
+ ) + })} +
+
+ + {selected ? ( +
+
+

选择支付方式

+

支付后系统自动识别并开通

+

+ 扫码支付 → 系统识别付款 → 订单自动激活或续订。PayPal 无需地址,ETH/USDT 展示钱包地址与复制按钮。 +

+
+ +
+ {activeMethods.map((method) => + method.type === 'paypal' ? renderPayPalCard(method) : renderCryptoCard(method), + )} +
+
+ ) : null} + + {statusMessage ?

{statusMessage}

: null}
) diff --git a/dashboard/src/modules/extensions/builtin/user-center/routes/subscription.tsx b/dashboard/src/modules/extensions/builtin/user-center/routes/subscription.tsx index 15f97d6..01bee33 100644 --- a/dashboard/src/modules/extensions/builtin/user-center/routes/subscription.tsx +++ b/dashboard/src/modules/extensions/builtin/user-center/routes/subscription.tsx @@ -6,9 +6,12 @@ export default function UserCenterSubscriptionRoute() { return (
-

订阅与支付

+

支付与订阅

- 支持 PayPal、以太坊与 USDT 的订阅与按量计费,扫码或直连支付后都可同步到账户中心。 + 支持 PayPal / 以太坊 ETH / USDT(TRC20)扫码支付。完成后系统会自动识别订单并开通或续订。 +

+

+ 流程:先选择产品模式(PAYG / SaaS),再选择支付方式,扫码支付 → 自动识别到账。