bolt.new/app/components/auth/SubscriptionDialog.tsx

175 lines
7.0 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 } from 'react';
import { useAuth } from '~/hooks/useAuth';
import { toast } from 'react-toastify';
interface SubscriptionDialogProps {
isOpen: boolean;
onClose: () => void;
}
interface SubscriptionPlan {
_id: number;
name: string;
tokens: number;
price: number;
description: string;
save_percentage?: number;
}
interface UserSubscription {
plan: SubscriptionPlan;
tokensLeft: number;
nextReloadDate: 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 } = useAuth();
useEffect(() => {
if (isOpen && user) {
fetchSubscriptionData();
}
}, [isOpen, user]);
const fetchSubscriptionData = async () => {
setIsLoading(true);
try {
const [plansResponse, userSubResponse] = await Promise.all([
fetch('/api/subscription-plans'),
fetch('/api/user-subscription')
]);
const plans = await plansResponse.json();
const userSub = await userSubResponse.json();
setSubscriptionPlans(plans);
setUserSubscription(userSub);
} catch (error) {
console.error('Error fetching subscription data:', error);
toast.error('获取订阅信息失败,请稍后重试。');
} finally {
setIsLoading(false);
}
};
const handlePurchase = async (planId: number) => {
try {
const response = await fetch('/api/purchase-subscription', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
planId,
billingCycle,
}),
});
const result = await response.json();
if (response.ok) {
toast.success('订阅购买成功!');
fetchSubscriptionData(); // 刷新订阅信息
} else {
toast.error(result.message || '购买失败,请稍后重试。');
}
} catch (error) {
console.error('Error purchasing subscription:', error);
toast.error('购买过程中出现错误,请稍后重试。');
}
};
if (!user || 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>
{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">{userSubscription.tokensLeft.toLocaleString()}</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"></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 ${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 ${
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'
}`}
>
{plan._id === userSubscription?.plan._id ? '管理当前计划' : `升级到${plan.name}`}
</button>
</div>
))}
</div>
</div>
</DialogDescription>
</Dialog>
</DialogRoot>
);
}