diff --git a/app/components/auth/SubscriptionDialog.tsx b/app/components/auth/SubscriptionDialog.tsx index 510cf0e..3dbe53b 100644 --- a/app/components/auth/SubscriptionDialog.tsx +++ b/app/components/auth/SubscriptionDialog.tsx @@ -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([]); const [userSubscription, setUserSubscription] = useState(null); const [isLoading, setIsLoading] = useState(true); - const { user } = useAuth(); + const { user, token, isAuthenticated, login } = useAuth(); const [paymentData, setPaymentData] = useState(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)

- {userSubscription && ( + {isAuthenticated && userSubscription && (
- {userSubscription.tokensLeft.toLocaleString()} + {toString(userSubscription.tokensLeft)} 代币剩余。 {userSubscription.plan.tokens.toLocaleString()}代币将在{new Date(userSubscription.nextReloadDate).toLocaleDateString()}后添加。 @@ -176,7 +195,7 @@ export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps)
{subscriptionPlans.map((plan) => ( -
+

{plan.name}

{(plan.tokens / 1000000).toFixed(0)}M 代币 @@ -191,12 +210,12 @@ export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps)
))} diff --git a/app/hooks/useAuth.ts b/app/hooks/useAuth.ts index 6544ff6..63ba6a5 100644 --- a/app/hooks/useAuth.ts +++ b/app/hooks/useAuth.ts @@ -12,18 +12,21 @@ export function useAuth() { const [isAuthenticated, setIsAuthenticated] = useState(false); const [isLoading, setIsLoading] = useState(true); const [user, setUser] = useState(null); + const [token, setToken] = useState(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 }; } diff --git a/app/routes/api.subscription-plans.ts b/app/routes/api.subscription-plans.ts index 96730e8..59b53da 100644 --- a/app/routes/api.subscription-plans.ts +++ b/app/routes/api.subscription-plans.ts @@ -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 { diff --git a/app/routes/api.user-subscription.ts b/app/routes/api.user-subscription.ts index a4ccde8..8910f5d 100644 --- a/app/routes/api.user-subscription.ts +++ b/app/routes/api.user-subscription.ts @@ -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', diff --git a/app/utils/SDPay.server.ts b/app/utils/SDPay.server.ts index 342ed03..16317e0 100644 --- a/app/utils/SDPay.server.ts +++ b/app/utils/SDPay.server.ts @@ -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;