forked from 77media/video-flow
238 lines
7.9 KiB
TypeScript
238 lines
7.9 KiB
TypeScript
'use client';
|
||
|
||
import { useEffect, useState } from 'react';
|
||
import { useSearchParams } from 'next/navigation';
|
||
import { CheckCircle, Loader2, XCircle } from 'lucide-react';
|
||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||
import { Button } from '@/components/ui/button';
|
||
|
||
interface PaymentStatus {
|
||
payment_status: 'pending' | 'success' | 'fail';
|
||
biz_order_no: string;
|
||
pay_time?: string;
|
||
subscription?: {
|
||
plan_name: string;
|
||
plan_display_name: string;
|
||
status: string;
|
||
current_period_end?: string;
|
||
};
|
||
}
|
||
|
||
export default function PaymentSuccessPage() {
|
||
const searchParams = useSearchParams();
|
||
const sessionId = searchParams.get('session_id');
|
||
|
||
const [status, setStatus] = useState<'loading' | 'success' | 'failed' | 'timeout'>('loading');
|
||
const [paymentData, setPaymentData] = useState<PaymentStatus | null>(null);
|
||
const [attempts, setAttempts] = useState(0);
|
||
|
||
useEffect(() => {
|
||
if (!sessionId) {
|
||
setStatus('failed');
|
||
return;
|
||
}
|
||
|
||
const pollPaymentStatus = async () => {
|
||
const maxAttempts = 30; // 最多轮询30次
|
||
const interval = 2000; // 每2秒轮询一次
|
||
|
||
for (let i = 0; i < maxAttempts; i++) {
|
||
setAttempts(i + 1);
|
||
|
||
try {
|
||
// 使用新的Checkout Session状态查询
|
||
const { getCheckoutSessionStatus } = await import('@/lib/stripe');
|
||
const User = JSON.parse(localStorage.getItem("currentUser") || "{}");
|
||
const result = await getCheckoutSessionStatus(sessionId, String(User.id));
|
||
|
||
if (result.successful && result.data) {
|
||
setPaymentData(result.data);
|
||
|
||
if (result.data.payment_status === 'success') {
|
||
setStatus('success');
|
||
return;
|
||
} else if (result.data.payment_status === 'fail') {
|
||
setStatus('failed');
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 等待下次轮询
|
||
await new Promise(resolve => setTimeout(resolve, interval));
|
||
} catch (error) {
|
||
console.error('轮询Checkout Session状态失败:', error);
|
||
}
|
||
}
|
||
|
||
// 轮询超时
|
||
setStatus('timeout');
|
||
};
|
||
|
||
pollPaymentStatus();
|
||
}, [sessionId]);
|
||
|
||
const renderContent = () => {
|
||
switch (status) {
|
||
case 'loading':
|
||
return (
|
||
<Card className="max-w-md mx-auto">
|
||
<CardHeader className="text-center">
|
||
<div className="flex justify-center mb-4">
|
||
<Loader2 className="w-16 h-16 text-blue-500 animate-spin" />
|
||
</div>
|
||
<CardTitle>处理中...</CardTitle>
|
||
<CardDescription>
|
||
正在确认您的支付,请稍候
|
||
<br />
|
||
尝试次数: {attempts}/30
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<p className="text-sm text-gray-600 text-center">
|
||
请不要关闭此页面,我们正在处理您的订阅
|
||
</p>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
|
||
case 'success':
|
||
return (
|
||
<Card className="max-w-md mx-auto">
|
||
<CardHeader className="text-center">
|
||
<div className="flex justify-center mb-4">
|
||
<CheckCircle className="w-16 h-16 text-green-500" />
|
||
</div>
|
||
<CardTitle className="text-green-600">支付成功!</CardTitle>
|
||
<CardDescription>
|
||
您的订阅已激活
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
{paymentData?.subscription && (
|
||
<div className="bg-green-50 p-4 rounded-lg">
|
||
<h3 className="font-semibold text-green-800">
|
||
{paymentData.subscription.plan_display_name} 套餐
|
||
</h3>
|
||
<p className="text-sm text-green-600">
|
||
状态: {paymentData.subscription.status}
|
||
</p>
|
||
{paymentData.subscription.current_period_end && (
|
||
<p className="text-sm text-green-600">
|
||
有效期至: {new Date(paymentData.subscription.current_period_end).toLocaleDateString()}
|
||
</p>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
<div className="text-sm text-gray-600">
|
||
<p>订单号: {paymentData?.biz_order_no}</p>
|
||
{paymentData?.pay_time && (
|
||
<p>支付时间: {new Date(paymentData.pay_time).toLocaleString()}</p>
|
||
)}
|
||
</div>
|
||
|
||
<div className="flex gap-2">
|
||
<Button
|
||
onClick={() => window.location.href = '/dashboard'}
|
||
className="flex-1"
|
||
>
|
||
前往控制台
|
||
</Button>
|
||
<Button
|
||
onClick={() => window.location.href = '/'}
|
||
variant="outline"
|
||
className="flex-1"
|
||
>
|
||
返回首页
|
||
</Button>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
|
||
case 'failed':
|
||
return (
|
||
<Card className="max-w-md mx-auto">
|
||
<CardHeader className="text-center">
|
||
<div className="flex justify-center mb-4">
|
||
<XCircle className="w-16 h-16 text-red-500" />
|
||
</div>
|
||
<CardTitle className="text-red-600">支付失败</CardTitle>
|
||
<CardDescription>
|
||
很抱歉,您的支付未能成功完成
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
<p className="text-sm text-gray-600 text-center">
|
||
请检查您的支付信息或稍后重试
|
||
</p>
|
||
|
||
<div className="flex gap-2">
|
||
<Button
|
||
onClick={() => window.location.href = '/pricing'}
|
||
className="flex-1"
|
||
>
|
||
重新选择套餐
|
||
</Button>
|
||
<Button
|
||
onClick={() => window.location.href = '/'}
|
||
variant="outline"
|
||
className="flex-1"
|
||
>
|
||
返回首页
|
||
</Button>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
|
||
case 'timeout':
|
||
return (
|
||
<Card className="max-w-md mx-auto">
|
||
<CardHeader className="text-center">
|
||
<div className="flex justify-center mb-4">
|
||
<Loader2 className="w-16 h-16 text-yellow-500" />
|
||
</div>
|
||
<CardTitle className="text-yellow-600">处理中</CardTitle>
|
||
<CardDescription>
|
||
支付正在处理中,请稍后查看
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
<p className="text-sm text-gray-600 text-center">
|
||
您的支付可能仍在处理中,请稍后检查您的订阅状态
|
||
</p>
|
||
|
||
<div className="flex gap-2">
|
||
<Button
|
||
onClick={() => window.location.reload()}
|
||
className="flex-1"
|
||
>
|
||
刷新页面
|
||
</Button>
|
||
<Button
|
||
onClick={() => window.location.href = '/dashboard'}
|
||
variant="outline"
|
||
className="flex-1"
|
||
>
|
||
查看订阅状态
|
||
</Button>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
|
||
default:
|
||
return null;
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
||
<div className="w-full max-w-lg">
|
||
{renderContent()}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|