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 的订阅与按量计费,扫码或直连支付后都可同步到账户中心。

+ )