feat: 更新订阅对话框组件以支持用户登录状态和订阅信息获取

This commit is contained in:
zyh
2024-10-22 09:03:00 +00:00
parent 2c38701de6
commit 4bb2c08a8e
5 changed files with 75 additions and 43 deletions

View File

@@ -4,6 +4,8 @@ import { useAuth } from '~/hooks/useAuth';
import { toast } from 'react-toastify';
import { PaymentModal } from './PaymentModal';
import type { SubscriptionPlan } from '~/types/subscription';
import pkg from 'lodash';
const {toString} = pkg;
interface SubscriptionDialogProps {
isOpen: boolean;
@@ -42,16 +44,19 @@ export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps)
const [subscriptionPlans, setSubscriptionPlans] = useState<SubscriptionPlan[]>([]);
const [userSubscription, setUserSubscription] = useState<UserSubscription | null>(null);
const [isLoading, setIsLoading] = useState(true);
const { user } = useAuth();
const { user, token, isAuthenticated, login } = useAuth();
const [paymentData, setPaymentData] = useState<PaymentResponse | null>(null);
useEffect(() => {
if (isOpen) {
fetchSubscriptionData();
fetchSubscriptionPlans();
if (isAuthenticated && token) {
fetchUserSubscription();
}
}
}, [isOpen]);
}, [isOpen, isAuthenticated, token]);
const fetchSubscriptionData = async () => {
const fetchSubscriptionPlans = async () => {
setIsLoading(true);
try {
const response = await fetch('/api/subscription-plans');
@@ -60,23 +65,50 @@ export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps)
}
const data = await response.json() as SubscriptionPlan[];
setSubscriptionPlans(data);
const userSubResponse = await fetch('/api/user-subscription');
const userSub = await userSubResponse.json() as UserSubscription;
setUserSubscription(userSub);
} catch (error) {
console.error('获取订阅数据时出错:', error);
toast.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) {
// 如果用户未登录,提示用户登录
toast.info('请先登录以继续购买');
// 这里可以触发登录流程,例如打开登录对话框
// openLoginDialog();
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({
@@ -97,24 +129,11 @@ export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps)
};
const handlePaymentSuccess = useCallback(() => {
fetchSubscriptionData(); // 重新获取订阅信息
fetchUserSubscription(); // 重新获取订阅信息
toast.success('订阅成功!');
}, [fetchSubscriptionData]);
}, [fetchUserSubscription]);
// 类型守卫函数
function isSubscriptionPlan(plan: any): plan is SubscriptionPlan {
return (
typeof plan === 'object' &&
typeof plan._id === 'string' &&
typeof plan.name === 'string' &&
typeof plan.tokens === 'number' &&
typeof plan.price === 'number' &&
typeof plan.description === 'string' &&
(plan.save_percentage === null || typeof plan.save_percentage === 'number')
);
}
if (!user || isLoading) return null;
if (isLoading) return null;
return (
<>
@@ -129,11 +148,11 @@ export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps)
</p>
</div>
{userSubscription && (
{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">{userSubscription.tokensLeft.toLocaleString()}</span>
<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()}
@@ -176,7 +195,7 @@ export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps)
<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' : ''}`}>
<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
@@ -191,12 +210,12 @@ export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps)
<button
onClick={() => handlePurchase(plan._id)}
className={`w-full py-2 rounded-md ${
plan._id === userSubscription?.plan._id
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'
}`}
>
{plan._id === userSubscription?.plan._id ? '管理当前计划' : `升级到${plan.name}`}
{isAuthenticated && plan._id === userSubscription?.plan._id ? '管理当前计划' : `升级到${plan.name}`}
</button>
</div>
))}

View File

@@ -12,18 +12,21 @@ export function useAuth() {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [user, setUser] = useState<User | null>(null);
const [token, setToken] = useState<string | null>(null);
const navigate = useNavigate();
useEffect(() => {
const checkAuth = () => {
const token = localStorage.getItem('token');
const storedToken = localStorage.getItem('token');
const storedUser = localStorage.getItem('user');
if (token && storedUser) {
if (storedToken && storedUser) {
setIsAuthenticated(true);
setUser(JSON.parse(storedUser));
setToken(storedToken);
} else {
setIsAuthenticated(false);
setUser(null);
setToken(null);
}
setIsLoading(false);
};
@@ -36,11 +39,12 @@ export function useAuth() {
};
}, []);
const login = (token: string, userData: User) => {
localStorage.setItem('token', token);
const login = (newToken: string, userData: User) => {
localStorage.setItem('token', newToken);
localStorage.setItem('user', JSON.stringify(userData));
setIsAuthenticated(true);
setUser(userData);
setToken(newToken);
};
const logout = () => {
@@ -48,8 +52,9 @@ export function useAuth() {
localStorage.removeItem('user');
setIsAuthenticated(false);
setUser(null);
setToken(null);
navigate('/');
};
return { isAuthenticated, isLoading, user, login, logout };
return { isAuthenticated, isLoading, user, token, login, logout };
}

View File

@@ -1,5 +1,5 @@
import { json } from '@remix-run/cloudflare';
import { db } from '~/lib/db.server';
import { db } from '~/utils/db.server';
export async function loader() {
try {

View File

@@ -1,10 +1,16 @@
import { json } from '@remix-run/cloudflare';
import { db } from '~/lib/db.server';
import { requireUserId } from '~/lib/session.server';
import { db } from '~/utils/db.server';
import { requireAuth } from '~/middleware/auth.server';
export async function loader({ request }) {
const userId = await requireUserId(request);
try {
export async function loader({ request }: { request: Request }) {
let userId;
try {
userId = await requireAuth(request);
} catch (error) {
return error as Response;
}
try {
const userSubscription = await db.select(
'subscription_plans.*',
'user_transactions.tokens as tokensLeft',

View File

@@ -1,6 +1,8 @@
import CryptoJS from "crypto-js";
import { toNumber } from "lodash";
import pkg from 'lodash';
import { env } from "node:process";
const {toNumber} = pkg;
export interface SDNotifyBody {
no: string;