From f8b2ec2d4b5aa3208b8bb6bafd75a4ef8f96370a Mon Sep 17 00:00:00 2001 From: Zixin Zhou Date: Tue, 9 Sep 2025 22:24:39 +0800 Subject: [PATCH] adds alipay --- app/layout.tsx | 4 +- app/pricing/page.tsx | 9 +++- components/common/CallbackModal.tsx | 64 +++++++++++++++++++++++++---- components/layout/top-bar.tsx | 9 +++- lib/stripe.ts | 21 ++++++++++ 5 files changed, 95 insertions(+), 12 deletions(-) diff --git a/app/layout.tsx b/app/layout.tsx index 1dd6e9c..3b449f8 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -25,8 +25,10 @@ export default function RootLayout({ children: React.ReactNode }) { const [showCallbackModal, setShowCallbackModal] = useState(false) + const [paymentType, setPaymentType] = useState<'subscription' | 'token'>('subscription') const openCallback = async function (ev: MessageEvent) { if (ev.data.type === 'waiting-payment') { + setPaymentType(ev.data.paymentType || 'subscription') setShowCallbackModal(true) } } @@ -84,7 +86,7 @@ export default function RootLayout({ {/* */}
{children} - {showCallbackModal && setShowCallbackModal(false)} />} + {showCallbackModal && setShowCallbackModal(false)} />}
diff --git a/app/pricing/page.tsx b/app/pricing/page.tsx index 5c27bac..545b1a4 100644 --- a/app/pricing/page.tsx +++ b/app/pricing/page.tsx @@ -28,8 +28,8 @@ export default function PricingPage() { /**价格方案 */ function HomeModule5() { const [billingType, setBillingType] = useState<"month" | "year">("month"); - const [plans, setPlans] = useState([]); + const [loadingPlan, setLoadingPlan] = useState(null); // 跟踪哪个计划正在加载 // 从后端获取订阅计划数据 useEffect(() => { @@ -71,6 +71,7 @@ function HomeModule5() { }, [plans, billingType]); const handleSubscribe = async (planName: string) => { + setLoadingPlan(planName); // 设置加载状态 try { const { createCheckoutSession, redirectToCheckout } = await import( @@ -94,10 +95,14 @@ function HomeModule5() { if (!result.successful || !result.data) { throw new Error("create checkout session failed"); } - window.opener?.postMessage({ type: "waiting-payment" }, "*"); + window.opener?.postMessage({ + type: "waiting-payment", + paymentType: "subscription" + }, "*"); // 2. 直接跳转到Stripe托管页面(就这么简单!) window.location.href = result.data.checkout_url; } catch (error) { + setLoadingPlan(null); // 出错时清除加载状态 throw new Error("create checkout session failed, please try again later"); } }; diff --git a/components/common/CallbackModal.tsx b/components/common/CallbackModal.tsx index 49770a3..7dd1e2e 100644 --- a/components/common/CallbackModal.tsx +++ b/components/common/CallbackModal.tsx @@ -21,7 +21,13 @@ enum PaymentStatus { * 支付回调模态框组件 * 处理Stripe Checkout支付完成后的状态展示和用户操作 */ -export default function CallbackModal({ onClose }: { onClose: () => void }) { +export default function CallbackModal({ + paymentType = 'subscription', + onClose +}: { + paymentType?: 'subscription' | 'token'; + onClose: () => void; +}) { const router = useRouter() const searchParams = useSearchParams() const [paymentStatus, setPaymentStatus] = useState(PaymentStatus.LOADING) @@ -119,6 +125,12 @@ export default function CallbackModal({ onClose }: { onClose: () => void }) { )} )} + {paymentInfo?.tokenPurchase && ( + <> +

Token Purchase Successful!

+

Your credits will be updated shortly.

+ + )} {paymentInfo?.paymentTime && (

Payment Time: {new Date(paymentInfo.paymentTime).toLocaleString()}

)} @@ -154,14 +166,52 @@ export default function CallbackModal({ onClose }: { onClose: () => void }) { setPaymentStatus(PaymentStatus.FAILED) return } + const userId = JSON.parse(localStorage.getItem('currentUser') || '{}').id - const res = await getCheckoutSessionStatus(ev.data.sessionId, userId) - if (res.successful && res.data) { - if(res.data.payment_status === 'success'){ - setPaymentStatus(PaymentStatus.SUCCESS) - }else{ - setPaymentStatus(PaymentStatus.FAILED) + + try { + if (paymentType === 'subscription') { + // 订阅支付状态查询 + const res = await getCheckoutSessionStatus(ev.data.sessionId, userId) + if (res.successful && res.data) { + if (res.data.payment_status === 'success') { + setPaymentStatus(PaymentStatus.SUCCESS) + setPaymentInfo({ + orderId: res.data.biz_order_no, + sessionId: ev.data.sessionId, + paymentTime: res.data.pay_time, + subscription: res.data.subscription + }) + } else if (res.data.payment_status === 'fail') { + setPaymentStatus(PaymentStatus.FAILED) + } + } else { + setPaymentStatus(PaymentStatus.FAILED) + } + } else if (paymentType === 'token') { + // Token购买状态查询 + const { getTokenPurchaseStatus } = await import('@/lib/stripe') + const res = await getTokenPurchaseStatus(ev.data.sessionId) + if (res.successful && res.data) { + if (res.data.payment_status === 'success') { + setPaymentStatus(PaymentStatus.SUCCESS) + setPaymentInfo({ + orderId: ev.data.sessionId, + sessionId: ev.data.sessionId, + tokenPurchase: { success: true } + }) + // Token购买成功后,延迟刷新页面以更新积分显示 + setTimeout(() => window.location.reload(), 2000) + } else if (res.data.payment_status === 'fail') { + setPaymentStatus(PaymentStatus.FAILED) + } + } else { + setPaymentStatus(PaymentStatus.FAILED) + } } + } catch (error) { + console.error('支付状态查询失败:', error) + setPaymentStatus(PaymentStatus.FAILED) } } } diff --git a/components/layout/top-bar.tsx b/components/layout/top-bar.tsx index 49a438e..e4e6101 100644 --- a/components/layout/top-bar.tsx +++ b/components/layout/top-bar.tsx @@ -100,8 +100,13 @@ export function TopBar({ collapsed, isDesktop=true }: { collapsed: boolean, isDe }); if (response.successful && response.data?.checkout_url) { - // 跳转到Stripe支付页面 - window.location.href = response.data.checkout_url; + // 通知当前窗口等待支付,标识为Token购买 + window.postMessage({ + type: "waiting-payment", + paymentType: "token" + }, "*"); + // 在新标签页中打开Stripe支付页面 + window.open(response.data.checkout_url, '_blank'); } else { console.error("创建Token购买失败:", response.message); } diff --git a/lib/stripe.ts b/lib/stripe.ts index a06159d..a2fdae9 100644 --- a/lib/stripe.ts +++ b/lib/stripe.ts @@ -77,6 +77,12 @@ export interface BuyTokensData { export type BuyTokensResponse = ApiResponse; +export interface TokenPurchaseStatusData { + payment_status: "success" | "fail" | "pending"; +} + +export type TokenPurchaseStatusResponse = ApiResponse; + /** * 获取订阅计划列表 * 从后端API获取所有活跃的订阅计划,后端已经过滤了活跃计划 @@ -219,6 +225,21 @@ export async function buyTokens( } } +/** + * 查询Token购买状态 + * + * 用于轮询检查Token购买的支付状态 + */ +export async function getTokenPurchaseStatus( + sessionId: string +): Promise { + try { + return await get(`/api/payment/token-purchase-status/${sessionId}`); + } catch (error) { + throw error; + } +} + /** * 简单的跳转到Customer Portal页面的工具函数 */