diff --git a/dashboard/src/app/[slug]/Client.tsx b/dashboard/src/app/[slug]/Client.tsx
index 04f3e80..d16e205 100644
--- a/dashboard/src/app/[slug]/Client.tsx
+++ b/dashboard/src/app/[slug]/Client.tsx
@@ -29,7 +29,6 @@ import ProductDownload from '@components/marketing/ProductDownload'
import ProductEditions from '@components/marketing/ProductEditions'
import ProductFaq from '@components/marketing/ProductFaq'
import ProductFeatures from '@components/marketing/ProductFeatures'
-import ProductBillingActions from '@components/marketing/ProductBillingActions'
import ProductHero from '@components/marketing/ProductHero'
import ProductScenarios from '@components/marketing/ProductScenarios'
import type { ProductConfig } from '@modules/products/registry'
@@ -185,7 +184,6 @@ export default function Client({ config }: ClientProps) {
-
diff --git a/dashboard/src/components/billing/utils.ts b/dashboard/src/components/billing/utils.ts
new file mode 100644
index 0000000..d5d5371
--- /dev/null
+++ b/dashboard/src/components/billing/utils.ts
@@ -0,0 +1,23 @@
+'use client'
+
+export function resolveBillingClientId(planClientId?: string) {
+ if (planClientId && planClientId.trim().length > 0) {
+ return planClientId.trim()
+ }
+
+ if (typeof process !== 'undefined' && typeof process.env.NEXT_PUBLIC_PAYPAL_CLIENT_ID === 'string') {
+ const candidate = process.env.NEXT_PUBLIC_PAYPAL_CLIENT_ID.trim()
+ if (candidate.length > 0) {
+ return candidate
+ }
+ }
+
+ if (typeof window !== 'undefined') {
+ const globalCandidate = (window as typeof window & { __PAYPAL_CLIENT_ID__?: string }).__PAYPAL_CLIENT_ID__
+ if (typeof globalCandidate === 'string' && globalCandidate.trim().length > 0) {
+ return globalCandidate.trim()
+ }
+ }
+
+ return ''
+}
diff --git a/dashboard/src/components/marketing/ProductBillingActions.tsx b/dashboard/src/components/marketing/ProductBillingActions.tsx
index 23a86b1..4cc4a7f 100644
--- a/dashboard/src/components/marketing/ProductBillingActions.tsx
+++ b/dashboard/src/components/marketing/ProductBillingActions.tsx
@@ -4,28 +4,10 @@ import { useCallback, useMemo, useState } from 'react'
import Link from 'next/link'
import { PayPalPayGoButton, PayPalSubscriptionButton } from '@components/billing/PayPalButtons'
+import { resolveBillingClientId } from '@components/billing/utils'
import CryptoBillingWidget from '@components/billing/CryptoBillingWidget'
import type { BillingPaymentMethod, ProductConfig } from '@modules/products/registry'
-function resolveClientId(planClientId?: string) {
- if (planClientId && planClientId.trim().length > 0) {
- return planClientId.trim()
- }
- if (typeof process !== 'undefined' && typeof process.env.NEXT_PUBLIC_PAYPAL_CLIENT_ID === 'string') {
- const candidate = process.env.NEXT_PUBLIC_PAYPAL_CLIENT_ID.trim()
- if (candidate.length > 0) {
- return candidate
- }
- }
- if (typeof window !== 'undefined') {
- const globalCandidate = (window as typeof window & { __PAYPAL_CLIENT_ID__?: string }).__PAYPAL_CLIENT_ID__
- if (typeof globalCandidate === 'string' && globalCandidate.trim().length > 0) {
- return globalCandidate.trim()
- }
- }
- return ''
-}
-
type ProductBillingActionsProps = {
config: ProductConfig
lang: 'zh' | 'en'
@@ -36,7 +18,7 @@ export default function ProductBillingActions({ config, lang }: ProductBillingAc
const billing = config.billing
const clientId = useMemo(() => {
- return resolveClientId(billing?.saas?.clientId || billing?.paygo?.clientId)
+ return resolveBillingClientId(billing?.saas?.clientId || billing?.paygo?.clientId)
}, [billing?.paygo?.clientId, billing?.saas?.clientId])
const handleSync = useCallback(
diff --git a/dashboard/src/modules/extensions/builtin/user-center/account/BillingOptionsPanel.tsx b/dashboard/src/modules/extensions/builtin/user-center/account/BillingOptionsPanel.tsx
new file mode 100644
index 0000000..9a30bb7
--- /dev/null
+++ b/dashboard/src/modules/extensions/builtin/user-center/account/BillingOptionsPanel.tsx
@@ -0,0 +1,224 @@
+'use client'
+
+import { useCallback, 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'
+import type { BillingPaymentMethod, BillingPlan, ProductConfig } from '@modules/products/registry'
+import { PRODUCT_LIST } from '@modules/products/registry'
+
+type SyncPayload = {
+ externalId: string
+ kind: string
+ planId?: string
+ status: string
+ provider?: string
+ paymentMethod?: string
+ paymentQr?: string
+ meta?: Record
+}
+
+type BillingOptionProps = {
+ 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'}
+
+
+
+
+
+ {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 },
+ })
+ }
+ />
+ )}
+
+
+ {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}
+
+
+ )
+}
+
+export default function BillingOptionsPanel() {
+ const [statusMessage, setStatusMessage] = useState(null)
+
+ const products = useMemo(
+ () => PRODUCT_LIST.filter((item) => item.billing && (item.billing.paygo || item.billing.saas)),
+ [],
+ )
+
+ const handleSync = useCallback(async (payload: SyncPayload) => {
+ try {
+ setStatusMessage(null)
+ const response = await fetch('/api/auth/subscriptions', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ provider: payload.provider || 'paypal',
+ paymentMethod: payload.paymentMethod || payload.provider || 'paypal',
+ paymentQr: payload.paymentQr,
+ ...payload,
+ }),
+ })
+
+ if (!response.ok) {
+ const data = await response.json().catch(() => ({}))
+ setStatusMessage((data?.message as string) || '同步支付信息失败,请稍后重试。')
+ return
+ }
+
+ setStatusMessage('支付记录已同步到账户。')
+ } catch (error) {
+ console.warn('Failed to sync subscription', error)
+ setStatusMessage('同步支付记录时出错。')
+ }
+ }, [])
+
+ if (!products.length) {
+ return null
+ }
+
+ return (
+
+
+
+
支付与订阅
+
完成订阅并同步到账户
+
+ 支持 PayPal、以太坊与 USDT 的扫码或直连支付,订单会实时同步到下方的订阅列表。
+
+
+ {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}
+
+
+ )
+ })}
+
+
+ )
+}
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 de6681c..15f97d6 100644
--- a/dashboard/src/modules/extensions/builtin/user-center/routes/subscription.tsx
+++ b/dashboard/src/modules/extensions/builtin/user-center/routes/subscription.tsx
@@ -1,4 +1,5 @@
import Card from '../components/Card'
+import BillingOptionsPanel from '../account/BillingOptionsPanel'
import SubscriptionPanel from '../account/SubscriptionPanel'
export default function UserCenterSubscriptionRoute() {
@@ -10,6 +11,7 @@ export default function UserCenterSubscriptionRoute() {
支持 PayPal、以太坊与 USDT 的订阅与按量计费,扫码或直连支付后都可同步到账户中心。
+
)