mirror of
https://github.com/stackblitz/bolt.new
synced 2025-06-26 18:17:50 +00:00
feat: 修改订阅对话框组件和订阅API,优化了订阅流程
This commit is contained in:
parent
7c0932e7b5
commit
2c38701de6
@ -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 (
|
||||
|
||||
17
app/middleware/auth.server.ts
Normal file
17
app/middleware/auth.server.ts
Normal 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;
|
||||
}
|
||||
@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@ -1 +0,0 @@
|
||||
|
||||
@ -1 +0,0 @@
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
import { Login } from '~/components/auth/Login';
|
||||
|
||||
export default function LoginPage() {
|
||||
return <Login />;
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
import { Register } from '~/components/auth/Register';
|
||||
|
||||
export default function RegisterPage() {
|
||||
return <Register />;
|
||||
}
|
||||
8
app/types/subscription.ts
Normal file
8
app/types/subscription.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface SubscriptionPlan {
|
||||
_id: string;
|
||||
name: string;
|
||||
tokens: number;
|
||||
price: number;
|
||||
description: string;
|
||||
save_percentage: number | null;
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
|
||||
Loading…
Reference in New Issue
Block a user