update for pay manage

This commit is contained in:
Zixin Zhou 2025-08-28 17:04:00 +08:00
parent 0032ff1aa9
commit 865defe2ec
5 changed files with 119 additions and 28 deletions

View File

@ -59,7 +59,7 @@ export default function DashboardPage() {
const fetchPaymentDetails = async (sessionId: string) => { const fetchPaymentDetails = async (sessionId: string) => {
try { try {
const User = JSON.parse(localStorage.getItem("currentUser") || "{}"); const User = JSON.parse(localStorage.getItem("currentUser") || "{}");
const response = await fetch(`/api/payment/checkout-status/${sessionId}?user_id=${User.id}`); const response = await fetch(`/api/payment/checkout-status/${sessionId}?user_id=${String(User.id)}`);
const result = await response.json(); const result = await response.json();
if (result.successful && result.data) { if (result.successful && result.data) {

View File

@ -43,7 +43,7 @@ export default function PaymentSuccessPage() {
// 使用新的Checkout Session状态查询 // 使用新的Checkout Session状态查询
const { getCheckoutSessionStatus } = await import('@/lib/stripe'); const { getCheckoutSessionStatus } = await import('@/lib/stripe');
const User = JSON.parse(localStorage.getItem("currentUser") || "{}"); const User = JSON.parse(localStorage.getItem("currentUser") || "{}");
const result = await getCheckoutSessionStatus(sessionId, User.id); const result = await getCheckoutSessionStatus(sessionId, String(User.id));
if (result.successful && result.data) { if (result.successful && result.data) {
setPaymentData(result.data); setPaymentData(result.data);

View File

@ -12,11 +12,12 @@ import {
LogOut, LogOut,
PanelsLeftBottom, PanelsLeftBottom,
} from 'lucide-react'; } from 'lucide-react';
import { motion } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import React, { useRef, useEffect, useLayoutEffect, useState } from 'react'; import React, { useRef, useEffect, useLayoutEffect, useState } from 'react';
import { logoutUser } from '@/lib/auth'; import { logoutUser } from '@/lib/auth';
import { createPortalSession, redirectToPortal } from '@/lib/stripe';
interface User { interface User {
@ -34,6 +35,7 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
const currentUser: User = JSON.parse(localStorage.getItem('currentUser') || '{}'); const currentUser: User = JSON.parse(localStorage.getItem('currentUser') || '{}');
const [mounted, setMounted] = React.useState(false); const [mounted, setMounted] = React.useState(false);
const [isLogin, setIsLogin] = useState(false); const [isLogin, setIsLogin] = useState(false);
const [isManagingSubscription, setIsManagingSubscription] = useState(false);
useEffect(() => { useEffect(() => {
const currentUser = localStorage.getItem("currentUser"); const currentUser = localStorage.getItem("currentUser");
if (JSON.parse(currentUser || "{}")?.token) { if (JSON.parse(currentUser || "{}")?.token) {
@ -48,6 +50,34 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
return () => console.log('Cleanup mounted effect'); return () => console.log('Cleanup mounted effect');
}, []); }, []);
// 处理订阅管理
const handleManageSubscription = async () => {
if (!currentUser?.id) {
console.error('用户未登录');
return;
}
setIsManagingSubscription(true);
try {
const response = await createPortalSession({
user_id: String(currentUser.id),
return_url: window.location.origin + '/dashboard'
});
if (response.successful && response.data?.portal_url) {
redirectToPortal(response.data.portal_url);
} else {
console.error('创建订阅管理会话失败:', response.message);
alert('无法打开订阅管理页面,请稍后重试');
}
} catch (error) {
console.error('打开订阅管理页面失败:', error);
alert('无法打开订阅管理页面,请稍后重试');
} finally {
setIsManagingSubscription(false);
}
};
// 处理点击事件 // 处理点击事件
useEffect(() => { useEffect(() => {
@ -187,7 +217,10 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
<User className="h-4 w-4" /> <User className="h-4 w-4" />
</Button> </Button>
{mounted && isOpen ? ReactDOM.createPortal( {mounted && (
ReactDOM.createPortal(
(<AnimatePresence>
{isOpen && (
<motion.div <motion.div
ref={menuRef} ref={menuRef}
initial={{ opacity: 0, scale: 0.95, y: -20 }} initial={{ opacity: 0, scale: 0.95, y: -20 }}
@ -233,6 +266,7 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
<Sparkles className="h-4 w-4" /> <Sparkles className="h-4 w-4" />
<span className="text-white underline text-sm">100 credits</span> <span className="text-white underline text-sm">100 credits</span>
</div> </div>
<div className="flex flex-col gap-2">
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
@ -241,6 +275,16 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
> >
Upgrade Upgrade
</Button> </Button>
<Button
variant="ghost"
size="sm"
className="text-white hover:bg-white/10 rounded-full px-4 text-xs"
onClick={handleManageSubscription}
disabled={isManagingSubscription}
>
{isManagingSubscription ? 'Loading...' : 'Manage Subscription'}
</Button>
</div>
</div> </div>
{/* Menu Items */} {/* Menu Items */}
@ -275,8 +319,11 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
</div> </div>
</div> </div>
</motion.div> </motion.div>
, document.body) )}
: null} </AnimatePresence>) as React.ReactElement,
document.body
) as React.ReactNode
)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -47,6 +47,19 @@ export interface CreateCheckoutSessionData {
export type CreateCheckoutSessionResponse = ApiResponse<CreateCheckoutSessionData>; export type CreateCheckoutSessionResponse = ApiResponse<CreateCheckoutSessionData>;
export interface CreatePortalSessionRequest {
user_id: string;
return_url?: string;
}
export interface CreatePortalSessionData {
portal_url: string;
session_id: string;
customer_id: string;
}
export type CreatePortalSessionResponse = ApiResponse<CreatePortalSessionData>;
/** /**
* *
* API获取所有活跃的订阅计划 * API获取所有活跃的订阅计划
@ -104,6 +117,28 @@ export async function getCheckoutSessionStatus(
} }
} }
/**
* Customer Portal Session
*
* Customer Portal中
* 1.
* 2.
* 3.
* 4.
* 5.
* 6.
*/
export async function createPortalSession(
request: CreatePortalSessionRequest
): Promise<CreatePortalSessionResponse> {
try {
return await post<CreatePortalSessionResponse>('/api/payment/portal-session', request);
} catch (error) {
console.error('创建Customer Portal Session失败:', error);
throw error;
}
}
/** /**
* Checkout页面的工具函数 * Checkout页面的工具函数
*/ */
@ -112,3 +147,12 @@ export function redirectToCheckout(checkoutUrl: string) {
window.location.href = checkoutUrl; window.location.href = checkoutUrl;
} }
} }
/**
* Customer Portal页面的工具函数
*/
export function redirectToPortal(portalUrl: string) {
if (typeof window !== 'undefined') {
window.location.href = portalUrl;
}
}