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) => {
try {
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();
if (result.successful && result.data) {

View File

@ -43,7 +43,7 @@ export default function PaymentSuccessPage() {
// 使用新的Checkout Session状态查询
const { getCheckoutSessionStatus } = await import('@/lib/stripe');
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) {
setPaymentData(result.data);

View File

@ -12,11 +12,12 @@ import {
LogOut,
PanelsLeftBottom,
} from 'lucide-react';
import { motion } from 'framer-motion';
import { motion, AnimatePresence } from 'framer-motion';
import ReactDOM from 'react-dom';
import { useRouter } from 'next/navigation';
import React, { useRef, useEffect, useLayoutEffect, useState } from 'react';
import { logoutUser } from '@/lib/auth';
import { createPortalSession, redirectToPortal } from '@/lib/stripe';
interface User {
@ -34,6 +35,7 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
const currentUser: User = JSON.parse(localStorage.getItem('currentUser') || '{}');
const [mounted, setMounted] = React.useState(false);
const [isLogin, setIsLogin] = useState(false);
const [isManagingSubscription, setIsManagingSubscription] = useState(false);
useEffect(() => {
const currentUser = localStorage.getItem("currentUser");
if (JSON.parse(currentUser || "{}")?.token) {
@ -48,6 +50,34 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
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(() => {
@ -187,13 +217,16 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
<User className="h-4 w-4" />
</Button>
{mounted && isOpen ? ReactDOM.createPortal(
<motion.div
ref={menuRef}
initial={{ opacity: 0, scale: 0.95, y: -20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: -20 }}
transition={{ duration: 0.2 }}
{mounted && (
ReactDOM.createPortal(
(<AnimatePresence>
{isOpen && (
<motion.div
ref={menuRef}
initial={{ opacity: 0, scale: 0.95, y: -20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: -20 }}
transition={{ duration: 0.2 }}
style={{
position: 'fixed',
top: '4rem',
@ -233,14 +266,25 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
<Sparkles className="h-4 w-4" />
<span className="text-white underline text-sm">100 credits</span>
</div>
<Button
variant="outline"
size="sm"
className="text-white border-white hover:bg-white/10 rounded-full px-8"
onClick={() => router.push('/pricing')}
>
Upgrade
</Button>
<div className="flex flex-col gap-2">
<Button
variant="outline"
size="sm"
className="text-white border-white hover:bg-white/10 rounded-full px-8"
onClick={() => router.push('/pricing')}
>
Upgrade
</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>
{/* Menu Items */}
@ -274,9 +318,12 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
<div>250819215404 | 2025/8/20 06:00:50</div>
</div>
</div>
</motion.div>
, document.body)
: null}
</motion.div>
)}
</AnimatePresence>) as React.ReactElement,
document.body
) as React.ReactNode
)}
</div>
</div>
</div>

View File

@ -161,8 +161,8 @@ function HomeModule3() {
</h2>
<p className="text-white text-[1.125rem] leading-[140%] font-normal text-center">
MovieFlow can make any kind of film in high quality for you
</p>
</div>
</p>
</div>
{/* 3x3网格布局 */}
<div
data-alt="vertical-grid-shadow"
@ -234,14 +234,14 @@ function HomeModule3() {
videoElement.play();
}}
/>
</div>
</div>
</SwiperSlide>
))}
</Swiper>
</div>
))}
</div>
</div>
</div>
</div>
);
}
/**电影制作工序介绍 */
@ -287,7 +287,7 @@ function HomeModule4() {
<h2 className="text-white text-[3.375rem] leading-[100%] font-normal mb-[3rem]">
Edit like you think
</h2>
</div>
</div>
<div className="flex w-full px-[4rem] gap-[2rem]">
{/* 左侧四个切换tab */}
@ -445,7 +445,7 @@ function HomeModule5() {
Yearly - <span className="text-[#FFCC6D]">15%</span>
</button>
</div>
</div>
</div>
{/* 主要价格卡片 */}
<div className="grid grid-cols-4 gap-[1.5rem] w-full px-[12rem] mb-[2rem]">

View File

@ -47,6 +47,19 @@ export interface 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获取所有活跃的订阅计划
@ -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页面的工具函数
*/
@ -111,4 +146,13 @@ export function redirectToCheckout(checkoutUrl: string) {
if (typeof window !== 'undefined') {
window.location.href = checkoutUrl;
}
}
/**
* Customer Portal页面的工具函数
*/
export function redirectToPortal(portalUrl: string) {
if (typeof window !== 'undefined') {
window.location.href = portalUrl;
}
}