2025-08-29 03:18:20 +08:00

238 lines
8.0 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>Processing...</CardTitle>
<CardDescription>
Confirming your payment, please wait
<br />
Attempts: {attempts}/30
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-gray-600 text-center">
Please do not close this page, we are processing your subscription
</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">Payment successful!</CardTitle>
<CardDescription>
Your subscription has been activated
</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} Plan
</h3>
<p className="text-sm text-green-600">
Status: {paymentData.subscription.status}
</p>
{paymentData.subscription.current_period_end && (
<p className="text-sm text-green-600">
Valid until: {new Date(paymentData.subscription.current_period_end).toLocaleDateString()}
</p>
)}
</div>
)}
<div className="text-sm text-gray-600">
<p>Order number: {paymentData?.biz_order_no}</p>
{paymentData?.pay_time && (
<p>Payment time: {new Date(paymentData.pay_time).toLocaleString()}</p>
)}
</div>
<div className="flex gap-2">
<Button
onClick={() => window.location.href = '/dashboard'}
className="flex-1"
>
Go to dashboard
</Button>
<Button
onClick={() => window.location.href = '/'}
variant="outline"
className="flex-1"
>
Go to home
</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">Payment failed</CardTitle>
<CardDescription>
Sorry, your payment was not successful
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-gray-600 text-center">
Please check your payment information or try again later
</p>
<div className="flex gap-2">
<Button
onClick={() => window.location.href = '/pricing'}
className="flex-1"
>
Re-select plan
</Button>
<Button
onClick={() => window.location.href = '/'}
variant="outline"
className="flex-1"
>
Go to home
</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">Processing</CardTitle>
<CardDescription>
Payment is being processed, please check later
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-gray-600 text-center">
Your payment may still be processing, please check your subscription status later
</p>
<div className="flex gap-2">
<Button
onClick={() => window.location.reload()}
className="flex-1"
>
Refresh page
</Button>
<Button
onClick={() => window.location.href = '/dashboard'}
variant="outline"
className="flex-1"
>
Check subscription status
</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>
);
}