video-flow-b/components/common/CallbackModal.tsx
2025-09-30 14:33:59 +08:00

260 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import React, { useEffect, useState } from 'react'
import { Result, Button, Spin, Card, Modal } from 'antd'
import { CheckCircleOutlined, CloseCircleOutlined, LoadingOutlined } from '@ant-design/icons'
import { useRouter, useSearchParams } from 'next/navigation'
import { getCheckoutSessionStatus, PaymentStatusResponse } from '@/lib/stripe'
/**
* 支付状态枚举
*/
enum PaymentStatus {
LOADING = 'loading',
SUCCESS = 'success',
FAILED = 'failed'
}
/**
* 支付回调模态框组件
* 处理Stripe Checkout支付完成后的状态展示和用户操作
*/
export default function CallbackModal({
paymentType = 'subscription',
onClose
}: {
paymentType?: 'subscription' | 'token';
onClose: () => void;
}) {
const router = useRouter()
const searchParams = useSearchParams()
const [paymentStatus, setPaymentStatus] = useState<PaymentStatus>(PaymentStatus.LOADING)
const [paymentInfo, setPaymentInfo] = useState<any>(null)
const [isModalOpen, setIsModalOpen] = useState(true)
const callBackUrl = localStorage.getItem('callBackUrl') || '/'
/**
* 获取Stripe Checkout Session支付状态
*/
const fetchPaymentStatus = async () => {
try {
// 从URL参数获取session_id和user_id
const sessionId = searchParams.get('session_id')
const userId = searchParams.get('user_id')
if (!sessionId || !userId) {
throw new Error('Missing required parameters: session_id or user_id')
}
// 调用真实的Stripe API获取支付状态
const response = await getCheckoutSessionStatus(sessionId, userId)
if (response.successful && response.data) {
const { payment_status, biz_order_no, pay_time, subscription } = response.data
if (payment_status === 'success') {
setPaymentStatus(PaymentStatus.SUCCESS)
setPaymentInfo({
orderId: biz_order_no,
sessionId,
paymentTime: pay_time,
subscription: subscription ? {
planName: subscription.plan_name,
planDisplayName: subscription.plan_display_name,
status: subscription.status,
currentPeriodEnd: subscription.current_period_end
} : null
})
} else if (payment_status === 'fail') {
setPaymentStatus(PaymentStatus.FAILED)
setPaymentInfo({
orderId: biz_order_no,
sessionId,
errorMessage: 'Payment processing failed, please try again'
})
} else {
// pending状态继续等待
setTimeout(fetchPaymentStatus, 5000)
}
} else {
throw new Error(response.message || 'Failed to get payment status')
}
} catch (error) {
console.error('Failed to get payment status:', error)
setPaymentStatus(PaymentStatus.FAILED)
setPaymentInfo({
errorMessage: error instanceof Error ? error.message : 'Network error, unable to get payment status'
})
}
}
// 渲染模态框内容
const renderModalContent = () => {
// 加载状态
if (paymentStatus === PaymentStatus.LOADING) {
return (
<div className="text-center py-8">
<Spin
indicator={<LoadingOutlined style={{ fontSize: 48, color: 'white' }} spin />}
size="large"
/>
<div className="mt-6 text-lg text-white">Getting payment status...</div>
<div className="mt-2 text-sm text-white/70">Please wait, we are processing your payment information</div>
</div>
)
}
// 支付成功状态
if (paymentStatus === PaymentStatus.SUCCESS) {
return (
<div className="text-center py-4">
<div className="mb-6">
<CheckCircleOutlined className="text-white text-6xl mb-4" />
<h1 className="text-white text-2xl font-bold mb-2">Payment Successful!</h1>
</div>
<div className="text-center text-white/70 mb-8 space-y-2">
{paymentInfo?.subscription && (
<>
<p>Subscription Plan: {paymentInfo.subscription.planDisplayName}</p>
<p>Subscription Status: {paymentInfo.subscription.status}</p>
{paymentInfo.subscription.currentPeriodEnd && (
<p>Expiry Date: {new Date(paymentInfo.subscription.currentPeriodEnd).toLocaleString()}</p>
)}
</>
)}
{paymentInfo?.tokenPurchase && (
<>
<p>Token Purchase Successful!</p>
<p>Your credits will be updated shortly.</p>
</>
)}
{paymentInfo?.paymentTime && (
<p>Payment Time: {new Date(paymentInfo.paymentTime).toLocaleString()}</p>
)}
<p>Thank you for your purchase!</p>
</div>
</div>
)
}
// 支付失败状态
return (
<div className="text-center py-4">
<div className="mb-6">
<CloseCircleOutlined className="text-white text-6xl mb-4" />
<h1 className="text-white text-2xl font-bold mb-2">Payment Failed</h1>
<p className="text-white/70 text-base">{paymentInfo?.errorMessage || 'An error occurred during payment processing'}</p>
</div>
<div className="text-center text-white/70 mb-8 space-y-2">
<p>If the problem persists, please contact customer service</p>
</div>
</div>
)
}
const watResult = async function (ev: MessageEvent<{
type: 'payment-callback',
canceled: boolean,
sessionId: string,
userId: string
}>) {
if (ev.data.type === 'payment-callback') {
if (ev.data.canceled) {
setPaymentStatus(PaymentStatus.FAILED)
return
}
const userId = typeof window !== 'undefined' ? JSON.parse(localStorage.getItem('currentUser') || '{}').id : 0
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
})
// 通知定价页面支付成功
window.postMessage({ type: 'payment-result', success: true }, '*')
} else if (res.data.payment_status === 'fail') {
setPaymentStatus(PaymentStatus.FAILED)
// 通知定价页面支付失败
window.postMessage({ type: 'payment-result', success: false }, '*')
}
} else {
setPaymentStatus(PaymentStatus.FAILED)
// 通知定价页面支付失败
window.postMessage({ type: 'payment-result', success: false }, '*')
}
} 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)
}
}
}
useEffect(() => {
window.addEventListener('message', watResult)
return () => {
window.removeEventListener('message', watResult)
}
}, [])
return (
<Modal
open={isModalOpen}
closable={true}
closeIcon={<CloseCircleOutlined style={{ color: 'white', fontSize: '16px' }} />}
maskClosable={false}
keyboard={false}
footer={null}
width={500}
centered
title={null}
className="payment-callback-modal"
onCancel={onClose}
styles={{
content: {
backgroundColor: 'black',
border: '1px solid rgba(255, 255, 255, 0.2)',
},
mask: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
},
header: {
borderBottom: 'none',
}
}}
>
{renderModalContent()}
</Modal>
)
}