forked from 77media/video-flow
204 lines
6.6 KiB
TypeScript
204 lines
6.6 KiB
TypeScript
|
||
|
||
'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({ onClose }: { 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, 2000)
|
||
}
|
||
} 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?.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 = 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)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
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>
|
||
)
|
||
}
|