bolt.new/app/components/auth/SubscriptionDialog.tsx
2024-10-22 09:23:57 +00:00

273 lines
10 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.

import { Dialog, DialogTitle, DialogDescription, DialogRoot } from '~/components/ui/Dialog';
import { useState, useEffect, useCallback } from 'react';
import { useAuth } from '~/hooks/useAuth';
import { toast } from 'react-toastify';
import { PaymentModal } from './PaymentModal';
import { LoginRegisterDialog } from './LoginRegisterDialog';
import type { SubscriptionPlan } from '~/types/subscription';
import pkg from 'lodash';
const {toString} = pkg;
import { TokenReloadModal } from './TokenReloadModal'; // 新增导入
interface SubscriptionDialogProps {
isOpen: boolean;
onClose: () => void;
}
interface UserSubscription {
plan: SubscriptionPlan;
tokensLeft: number;
nextReloadDate: string;
}
interface PaymentResponse {
status: string;
msg: string;
no: string;
pay_type: string;
order_amount: string;
pay_amount: string;
qr_money: string;
qr: string;
qr_img: string;
did: string;
expires_in: string;
return_url: string;
}
interface PurchaseResponse {
success: boolean;
paymentData?: PaymentResponse;
error?: string;
}
export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps) {
const [billingCycle, setBillingCycle] = useState<'monthly' | 'yearly'>('monthly');
const [subscriptionPlans, setSubscriptionPlans] = useState<SubscriptionPlan[]>([]);
const [userSubscription, setUserSubscription] = useState<UserSubscription | null>(null);
const [isLoading, setIsLoading] = useState(true);
const { user, token, isAuthenticated, login } = useAuth();
const [paymentData, setPaymentData] = useState<PaymentResponse | null>(null);
const [isLoginRegisterOpen, setIsLoginRegisterOpen] = useState(false);
const [isTokenReloadModalOpen, setIsTokenReloadModalOpen] = useState(false);
useEffect(() => {
if (isOpen) {
fetchSubscriptionPlans();
if (isAuthenticated && token) {
fetchUserSubscription();
}
}
}, [isOpen, isAuthenticated, token]);
const fetchSubscriptionPlans = async () => {
setIsLoading(true);
try {
const response = await fetch('/api/subscription-plans');
if (!response.ok) {
throw new Error('获取订阅计划失败');
}
const data = await response.json() as SubscriptionPlan[];
setSubscriptionPlans(data);
} catch (error) {
console.error('获取订阅计划时出错:', error);
toast.error('获取订阅计划失败,请稍后再试');
} finally {
setIsLoading(false);
}
};
const fetchUserSubscription = async () => {
if (!token) return;
try {
const headers = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
};
const userSubResponse = await fetch('/api/user-subscription', { headers });
if (!userSubResponse.ok) {
throw new Error('获取用户订阅信息失败');
}
const userSub = await userSubResponse.json() as UserSubscription;
setUserSubscription(userSub);
} catch (error) {
console.error('获取用户订阅信息时出错:', error);
toast.error('获取用户订阅信息失败,请稍后再试');
}
};
const handlePurchase = async (planId: string) => {
if (!isAuthenticated) {
setIsLoginRegisterOpen(true);
return;
}
if (!token) {
toast.error('登录状态异常,请重新登录');
return;
}
try {
const response = await fetch('/api/purchase-subscription', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
planId,
billingCycle,
}),
});
const result = await response.json() as PurchaseResponse;
if (response.ok && result.success && result.paymentData) {
setPaymentData(result.paymentData);
} else {
toast.error(result.error || '获取支付信息失败,请稍后重试。');
}
} catch (error) {
console.error('Error initiating purchase:', error);
toast.error('购买过程中出现错误,请稍后重试。');
}
};
const handlePaymentSuccess = useCallback(() => {
fetchUserSubscription(); // 重新获取订阅信息
toast.success('订阅成功!');
}, [fetchUserSubscription]);
const handleLoginSuccess = useCallback(() => {
setIsLoginRegisterOpen(false);
fetchUserSubscription();
toast.success('登录成功!');
}, [fetchUserSubscription]);
const handleTokenReloadClick = () => {
setIsTokenReloadModalOpen(true);
};
const handleTokenReloadSuccess = useCallback(() => {
fetchUserSubscription(); // 重新获取用户订阅信息
toast.success('代币充值成功!');
}, [fetchUserSubscription]);
if (isLoading) return null;
return (
<>
<DialogRoot open={isOpen}>
<Dialog onBackdrop={onClose} onClose={onClose} className="w-full max-w-4xl">
<DialogTitle></DialogTitle>
<DialogDescription>
<div className="space-y-6">
<div className="text-center">
<p className="text-bolt-elements-textSecondary">
</p>
</div>
{isAuthenticated && userSubscription && (
<div className="bg-bolt-elements-background-depth-2 p-4 rounded-lg">
<div className="flex justify-between items-center">
<div>
<span className="text-bolt-elements-textPrimary font-bold">{toString(userSubscription.tokensLeft)}</span>
<span className="text-bolt-elements-textSecondary"> </span>
<span className="text-bolt-elements-textSecondary">
{userSubscription.plan.tokens.toLocaleString()}{new Date(userSubscription.nextReloadDate).toLocaleDateString()}
</span>
</div>
<div className="text-right">
<span className="text-bolt-elements-textSecondary"></span>
<br />
<span className="text-bolt-elements-textSecondary">
<a
href="#"
className="text-bolt-elements-item-contentAccent hover:underline"
onClick={(e) => {
e.preventDefault();
handleTokenReloadClick();
}}
>
</a>
</span>
</div>
</div>
</div>
)}
<div className="flex justify-center space-x-4">
<button
onClick={() => setBillingCycle('monthly')}
className={`px-4 py-2 rounded-md ${
billingCycle === 'monthly'
? 'bg-bolt-elements-button-primary-background text-bolt-elements-button-primary-text'
: 'bg-bolt-elements-button-secondary-background text-bolt-elements-button-secondary-text'
}`}
>
</button>
<button
onClick={() => setBillingCycle('yearly')}
className={`px-4 py-2 rounded-md ${
billingCycle === 'yearly'
? 'bg-bolt-elements-button-primary-background text-bolt-elements-button-primary-text'
: 'bg-bolt-elements-button-secondary-background text-bolt-elements-button-secondary-text'
}`}
>
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{subscriptionPlans.map((plan) => (
<div key={plan._id} className={`bg-bolt-elements-background-depth-2 p-4 rounded-lg ${isAuthenticated && plan._id === userSubscription?.plan._id ? 'border-2 border-bolt-elements-item-contentAccent' : ''}`}>
<h3 className="text-bolt-elements-textPrimary font-bold text-lg">{plan.name}</h3>
<div className="text-bolt-elements-textSecondary mb-2">
{(plan.tokens / 1000000).toFixed(0)}M
{plan.save_percentage && (
<span className="ml-2 text-green-500"> {plan.save_percentage}%</span>
)}
</div>
<p className="text-bolt-elements-textTertiary text-sm mb-4">{plan.description}</p>
<div className="text-bolt-elements-textPrimary font-bold text-2xl mb-2">
¥{plan.price * (billingCycle === 'yearly' ? 10 : 1)}/{billingCycle === 'yearly' ? '年' : '月'}
</div>
<button
onClick={() => handlePurchase(plan._id)}
className={`w-full py-2 rounded-md ${
isAuthenticated && plan._id === userSubscription?.plan._id
? 'bg-bolt-elements-button-secondary-background text-bolt-elements-button-secondary-text'
: 'bg-bolt-elements-button-primary-background text-bolt-elements-button-primary-text'
}`}
>
{isAuthenticated && plan._id === userSubscription?.plan._id ? '管理当前计划' : `升级到${plan.name}`}
</button>
</div>
))}
</div>
</div>
</DialogDescription>
</Dialog>
</DialogRoot>
{paymentData && (
<PaymentModal
isOpen={!!paymentData}
onClose={() => setPaymentData(null)}
paymentData={paymentData}
onPaymentSuccess={handlePaymentSuccess}
/>
)}
<LoginRegisterDialog
isOpen={isLoginRegisterOpen}
onClose={() => setIsLoginRegisterOpen(false)}
onLoginSuccess={handleLoginSuccess}
/>
<TokenReloadModal
isOpen={isTokenReloadModalOpen}
onClose={() => setIsTokenReloadModalOpen(false)}
onReloadSuccess={handleTokenReloadSuccess}
/>
</>
);
}