feat: 修改订阅对话框组件和订阅API,优化了订阅流程

This commit is contained in:
zyh 2024-10-22 08:40:10 +00:00
parent 7c0932e7b5
commit 2c38701de6
9 changed files with 73 additions and 42 deletions

View File

@ -3,21 +3,13 @@ import { useState, useEffect, useCallback } from 'react';
import { useAuth } from '~/hooks/useAuth';
import { toast } from 'react-toastify';
import { PaymentModal } from './PaymentModal';
import type { SubscriptionPlan } from '~/types/subscription';
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;
@ -39,6 +31,12 @@ interface PaymentResponse {
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[]>([]);
@ -48,31 +46,33 @@ export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps)
const [paymentData, setPaymentData] = useState<PaymentResponse | null>(null);
useEffect(() => {
if (isOpen && user) {
if (isOpen) {
fetchSubscriptionData();
}
}, [isOpen, user]);
}, [isOpen]);
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);
const response = await fetch('/api/subscription-plans');
if (!response.ok) {
throw new Error('获取订阅计划失败');
}
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 fetching subscription data:', error);
toast.error('获取订阅信息失败,请稍后重试。');
console.error('获取订阅数据时出错:', error);
toast.error('获取订阅信息失败,请稍后再试');
} finally {
setIsLoading(false);
}
};
const handlePurchase = async (planId: number) => {
const handlePurchase = async (planId: string) => {
try {
const response = await fetch('/api/purchase-subscription', {
method: 'POST',
@ -84,11 +84,11 @@ export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps)
billingCycle,
}),
});
const result = await response.json();
if (response.ok && result.paymentData) {
const result = await response.json() as PurchaseResponse;
if (response.ok && result.success && result.paymentData) {
setPaymentData(result.paymentData);
} else {
toast.error(result.message || '获取支付信息失败,请稍后重试。');
toast.error(result.error || '获取支付信息失败,请稍后重试。');
}
} catch (error) {
console.error('Error initiating purchase:', error);
@ -101,6 +101,19 @@ export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps)
toast.success('订阅成功!');
}, [fetchSubscriptionData]);
// 类型守卫函数
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;
return (

View File

@ -0,0 +1,17 @@
import { json } from '@remix-run/cloudflare';
import { verifyToken } from '~/utils/auth.server';
export async function requireAuth(request: Request) {
const authHeader = request.headers.get('Authorization');
if (!authHeader) {
throw json({ error: '缺少授权头' }, { status: 401 });
}
const token = authHeader.split(' ')[1];
const payload = verifyToken(token);
if (!payload) {
throw json({ error: '无效的令牌' }, { status: 401 });
}
return payload.userId;
}

View File

@ -1,17 +1,23 @@
import { json } from '@remix-run/cloudflare';
import { db } from '~/utils/db.server';
import { requireUserId } from '../utils/session.server'; // 使用相对路径
import SDPay from '~/utils/SDPay.server';
import { requireAuth } from '~/middleware/auth.server';
export async function action({ request }: { request: Request }) {
const userId = await requireUserId(request);
let userId;
try {
userId = await requireAuth(request);
} catch (error) {
return error as Response;
}
const { planId, billingCycle } = await request.json() as { planId: string; billingCycle: string };
try {
// 获取订阅计划详情
const plan = await db('subscription_plans').where('_id', planId).first();
if (!plan) {
return json({ error: 'Invalid subscription plan' }, { status: 400 });
return json({ error: '无效的订阅计划' }, { status: 400 });
}
// 计算实际价格和代币数量
@ -47,7 +53,7 @@ export async function action({ request }: { request: Request }) {
return json({ success: true, paymentData });
} catch (error) {
console.error('Error initiating subscription purchase:', error);
return json({ error: 'Failed to initiate subscription purchase' }, { status: 500 });
console.error('初始化订阅购买时出错:', error);
return json({ error: '初始化订阅购买失败' }, { status: 500 });
}
}

View File

@ -1 +0,0 @@

View File

@ -1 +0,0 @@

View File

@ -1,5 +0,0 @@
import { Login } from '~/components/auth/Login';
export default function LoginPage() {
return <Login />;
}

View File

@ -1,5 +0,0 @@
import { Register } from '~/components/auth/Register';
export default function RegisterPage() {
return <Register />;
}

View File

@ -0,0 +1,8 @@
export interface SubscriptionPlan {
_id: string;
name: string;
tokens: number;
price: number;
description: string;
save_percentage: number | null;
}

View File

@ -1 +0,0 @@