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,13 +217,16 @@ 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 && (
<motion.div ReactDOM.createPortal(
ref={menuRef} (<AnimatePresence>
initial={{ opacity: 0, scale: 0.95, y: -20 }} {isOpen && (
animate={{ opacity: 1, scale: 1, y: 0 }} <motion.div
exit={{ opacity: 0, scale: 0.95, y: -20 }} ref={menuRef}
transition={{ duration: 0.2 }} 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={{ style={{
position: 'fixed', position: 'fixed',
top: '4rem', top: '4rem',
@ -233,14 +266,25 @@ 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>
<Button <div className="flex flex-col gap-2">
variant="outline" <Button
size="sm" variant="outline"
className="text-white border-white hover:bg-white/10 rounded-full px-8" size="sm"
onClick={() => router.push('/pricing')} className="text-white border-white hover:bg-white/10 rounded-full px-8"
> onClick={() => router.push('/pricing')}
Upgrade >
</Button> 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> </div>
{/* Menu Items */} {/* Menu Items */}
@ -274,9 +318,12 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
<div>250819215404 | 2025/8/20 06:00:50</div> <div>250819215404 | 2025/8/20 06:00:50</div>
</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

@ -161,8 +161,8 @@ function HomeModule3() {
</h2> </h2>
<p className="text-white text-[1.125rem] leading-[140%] font-normal text-center"> <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 MovieFlow can make any kind of film in high quality for you
</p> </p>
</div> </div>
{/* 3x3网格布局 */} {/* 3x3网格布局 */}
<div <div
data-alt="vertical-grid-shadow" data-alt="vertical-grid-shadow"
@ -234,14 +234,14 @@ function HomeModule3() {
videoElement.play(); videoElement.play();
}} }}
/> />
</div> </div>
</SwiperSlide> </SwiperSlide>
))} ))}
</Swiper> </Swiper>
</div> </div>
))} ))}
</div> </div>
</div> </div>
); );
} }
/**电影制作工序介绍 */ /**电影制作工序介绍 */
@ -287,7 +287,7 @@ function HomeModule4() {
<h2 className="text-white text-[3.375rem] leading-[100%] font-normal mb-[3rem]"> <h2 className="text-white text-[3.375rem] leading-[100%] font-normal mb-[3rem]">
Edit like you think Edit like you think
</h2> </h2>
</div> </div>
<div className="flex w-full px-[4rem] gap-[2rem]"> <div className="flex w-full px-[4rem] gap-[2rem]">
{/* 左侧四个切换tab */} {/* 左侧四个切换tab */}
@ -445,7 +445,7 @@ function HomeModule5() {
Yearly - <span className="text-[#FFCC6D]">15%</span> Yearly - <span className="text-[#FFCC6D]">15%</span>
</button> </button>
</div> </div>
</div> </div>
{/* 主要价格卡片 */} {/* 主要价格卡片 */}
<div className="grid grid-cols-4 gap-[1.5rem] w-full px-[12rem] mb-[2rem]"> <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 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页面的工具函数
*/ */
@ -111,4 +146,13 @@ export function redirectToCheckout(checkoutUrl: string) {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
window.location.href = checkoutUrl; window.location.href = checkoutUrl;
} }
}
/**
* Customer Portal页面的工具函数
*/
export function redirectToPortal(portalUrl: string) {
if (typeof window !== 'undefined') {
window.location.href = portalUrl;
}
} }