支付初步对接,等会接口

This commit is contained in:
海龙 2025-08-28 18:27:15 +08:00
parent 1b80180102
commit caa2bfb5e4
8 changed files with 326 additions and 244 deletions

View File

@ -1,24 +1,51 @@
'use client'
import './globals.css';
import type { Metadata } from 'next';
import { createContext, useContext, useEffect, useState } from 'react';
import { Providers } from '@/components/providers';
import { ConfigProvider, theme } from 'antd';
import { useEffect } from 'react';
import { createScreenAdapter } from '@/utils/tools';
import { ScreenAdapter } from './ScreenAdapter';
import CallbackModal from '@/components/common/CallbackModal';
export const metadata: Metadata = {
title: 'AI Movie Flow - Create Amazing Videos with AI',
description: 'Professional AI-powered video creation platform with advanced editing tools',
};
// 创建上下文来传递弹窗控制方法
const CallbackModalContext = createContext<{
setShowCallbackModal: (show: boolean) => void
} | null>(null)
// Hook 来使用弹窗控制方法
export const useCallbackModal = () => {
const context = useContext(CallbackModalContext)
if (!context) {
throw new Error('useCallbackModal must be used within CallbackModalProvider')
}
return context
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
const [showCallbackModal, setShowCallbackModal] = useState(false)
const openCallback = async function (ev: MessageEvent<any>) {
console.log(ev)
if (ev.data.type === 'waiting-payment') {
setShowCallbackModal(true)
}
}
useEffect(() => {
window.addEventListener('message', openCallback)
return () => {
window.removeEventListener('message', openCallback)
}
}, [])
return (
<html lang="en" suppressHydrationWarning>
<meta name="robots" content="noindex"></meta>
<head>
<title>AI Movie Flow - Create Amazing Videos with AI</title>
<meta name="description" content="Professional AI-powered video creation platform with advanced editing tools" />
<meta name="robots" content="noindex" />
</head>
<body className="font-sans antialiased">
<ConfigProvider
theme={{
@ -32,14 +59,17 @@ export default function RootLayout({
},
}}
>
<CallbackModalContext.Provider value={{ setShowCallbackModal }}>
<Providers>
{/* <ScreenAdapter /> */}
<div id="app" className='h-full w-full'>
{children}
{showCallbackModal && <CallbackModal onClose={() => setShowCallbackModal(false)} />}
</div>
</Providers>
</CallbackModalContext.Provider>
</ConfigProvider>
</body>
</html>
);
)
}

View File

@ -1,197 +1,21 @@
'use client'
"use client";
import React, { useEffect, useState } from 'react'
import { Result, Button, Spin, Card } from 'antd'
import { CheckCircleOutlined, CloseCircleOutlined, LoadingOutlined } from '@ant-design/icons'
import { useRouter, useSearchParams } from 'next/navigation'
import { getCheckoutSessionStatus, PaymentStatusResponse } from '@/lib/stripe'
import { useSearchParams } from "next/navigation";
import { useEffect } from "react";
/**
*
*/
enum PaymentStatus {
LOADING = 'loading',
SUCCESS = 'success',
FAILED = 'failed'
}
/**
*
* Stripe Checkout支付完成后的状态展示和用户操作
*/
export default function PayCallbackPage() {
const router = useRouter()
const searchParams = useSearchParams()
const [paymentStatus, setPaymentStatus] = useState<PaymentStatus>(PaymentStatus.LOADING)
const [paymentInfo, setPaymentInfo] = useState<any>(null)
const callBackUrl = localStorage.getItem('callBackUrl') || '/'
/**
* Stripe Checkout Session支付状态
*/
const fetchPaymentStatus = async () => {
try {
// 从URL参数获取session_id和user_id
const sessionId = searchParams.get('session_id')
const userId = searchParams.get('user_id')
if (!sessionId || !userId) {
throw new Error('缺少必要的参数: session_id 或 user_id')
}
// 调用真实的Stripe API获取支付状态
const response = await getCheckoutSessionStatus(sessionId, userId)
if (response.successful && response.data) {
const { payment_status, biz_order_no, pay_time, subscription } = response.data
if (payment_status === 'success') {
setPaymentStatus(PaymentStatus.SUCCESS)
setPaymentInfo({
orderId: biz_order_no,
sessionId,
paymentTime: pay_time,
subscription: subscription ? {
planName: subscription.plan_name,
planDisplayName: subscription.plan_display_name,
status: subscription.status,
currentPeriodEnd: subscription.current_period_end
} : null
})
} else if (payment_status === 'fail') {
setPaymentStatus(PaymentStatus.FAILED)
setPaymentInfo({
orderId: biz_order_no,
sessionId,
errorMessage: '支付处理失败,请重试'
})
} else {
// pending状态继续等待
setTimeout(fetchPaymentStatus, 2000)
}
} else {
throw new Error(response.message || '获取支付状态失败')
}
} catch (error) {
console.error('获取支付状态失败:', error)
setPaymentStatus(PaymentStatus.FAILED)
setPaymentInfo({
errorMessage: error instanceof Error ? error.message : '网络错误,无法获取支付状态'
})
}
}
/**
*
*/
const handleGoBack = () => {
router.push(callBackUrl)
}
/**
*
*/
const handleRetryPayment = () => {
router.push('/pricing')
}
export default function payCallback() {
const searchParams = useSearchParams();
const sessionId = searchParams.get("session_id");
const userId = searchParams.get("user_id");
const canceled = searchParams.get("canceled")||false;
useEffect(() => {
fetchPaymentStatus()
}, [])
window.opener.postMessage(
{ type: "payment-callback", canceled, sessionId, userId },
"*"
);
window.close();
}, []);
// 加载状态
if (paymentStatus === PaymentStatus.LOADING) {
return (
<div className="min-h-screen flex items-center justify-center bg-black">
<div className="w-96 text-center bg-black border border-white/20 rounded-lg p-8">
<Spin
indicator={<LoadingOutlined style={{ fontSize: 48, color: 'white' }} spin />}
size="large"
/>
<div className="mt-6 text-lg text-white">...</div>
<div className="mt-2 text-sm text-white/70"></div>
</div>
</div>
)
}
// 支付成功状态
if (paymentStatus === PaymentStatus.SUCCESS) {
return (
<div className="min-h-screen flex items-center justify-center bg-black">
<div className="w-[32rem] text-center bg-black border border-white/20 rounded-lg p-8">
<div className="mb-6">
<CheckCircleOutlined className="text-white text-6xl mb-4" />
<h1 className="text-white text-2xl font-bold mb-2"></h1>
<p className="text-white/70 text-base">{`订单号: ${paymentInfo?.orderId || 'N/A'}`}</p>
</div>
<div className="text-center text-white/70 mb-8 space-y-2">
{paymentInfo?.subscription && (
<>
<p>: {paymentInfo.subscription.planDisplayName}</p>
<p>: {paymentInfo.subscription.status}</p>
{paymentInfo.subscription.currentPeriodEnd && (
<p>: {new Date(paymentInfo.subscription.currentPeriodEnd).toLocaleString()}</p>
)}
</>
)}
{paymentInfo?.paymentTime && (
<p>: {new Date(paymentInfo.paymentTime).toLocaleString()}</p>
)}
<p></p>
</div>
<Button
type="primary"
size="large"
onClick={handleGoBack}
data-alt="back-to-previous-page-button"
className="w-full h-12 bg-white text-black hover:bg-white/90 border border-white/20"
>
</Button>
</div>
</div>
)
}
// 支付失败状态
return (
<div className="min-h-screen flex items-center justify-center bg-black">
<div className="w-[32rem] text-center bg-black border border-white/20 rounded-lg p-8">
<div className="mb-6">
<CloseCircleOutlined className="text-white text-6xl mb-4" />
<h1 className="text-white text-2xl font-bold mb-2"></h1>
<p className="text-white/70 text-base">{paymentInfo?.errorMessage || '支付处理过程中发生错误'}</p>
</div>
<div className="text-center text-white/70 mb-8 space-y-2">
<p>: {paymentInfo?.orderId || 'N/A'}</p>
<p></p>
</div>
<div className="space-y-3">
<Button
type="primary"
size="large"
onClick={handleRetryPayment}
data-alt="retry-payment-button"
className="w-full h-12 bg-white text-black hover:bg-white/90 border border-white/20"
>
</Button>
<Button
size="large"
onClick={handleGoBack}
data-alt="back-to-previous-page-button"
className="w-full h-12 bg-black text-white hover:bg-white hover:text-black border border-white/20"
>
</Button>
</div>
</div>
</div>
)
return <></>;
}

View File

@ -1,11 +1,17 @@
'use client';
"use client";
import { useState, useEffect, useMemo } from 'react';
import { useRouter } from 'next/navigation';
import { Check, ArrowLeft } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { fetchSubscriptionPlans, SubscriptionPlan } from '@/lib/stripe';
import { useState, useEffect, useMemo } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { Check, ArrowLeft } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { fetchSubscriptionPlans, SubscriptionPlan } from "@/lib/stripe";
export default function PricingPage() {
@ -18,9 +24,7 @@ export default function PricingPage() {
}
/**价格方案 */
function HomeModule5() {
const [billingType, setBillingType] = useState<'month' | 'year'>(
"month"
);
const [billingType, setBillingType] = useState<"month" | "year">("month");
const [plans, setPlans] = useState<SubscriptionPlan[]>([]);
@ -31,24 +35,29 @@ function HomeModule5() {
const plansData = await fetchSubscriptionPlans();
setPlans(plansData);
} catch (err) {
console.error('加载订阅计划失败:', err);
console.error("加载订阅计划失败:", err);
}
};
loadPlans();
}, []);
const pricingPlans = useMemo<{
const pricingPlans = useMemo<
{
title: string;
price: number;
credits: string;
buttonText: string;
features: string[];
}[]>(() => {
}[]
>(() => {
return plans.map((plan) => {
return {
title: plan.display_name || plan.name,
price: billingType === "month" ? plan.price_month/100 : plan.price_year/100,
price:
billingType === "month"
? plan.price_month / 100
: plan.price_year / 100,
credits: plan.description,
buttonText: plan.is_free ? "Try For Free" : "Subscribe Now",
features: plan.features || [],
@ -57,13 +66,15 @@ function HomeModule5() {
}, [plans, billingType]);
const handleSubscribe = async (planName: string) => {
if (planName === 'hobby') {
if (planName === "hobby") {
return;
}
try {
// 使用新的Checkout Session方案更简单
const { createCheckoutSession, redirectToCheckout } = await import('@/lib/stripe');
const { createCheckoutSession, redirectToCheckout } = await import(
"@/lib/stripe"
);
// 从localStorage获取当前用户信息
const User = JSON.parse(localStorage.getItem("currentUser") || "{}");
@ -76,16 +87,15 @@ function HomeModule5() {
const result = await createCheckoutSession({
user_id: String(User.id),
plan_name: planName,
billing_cycle: billingType
billing_cycle: billingType,
});
if (!result.successful || !result.data) {
throw new Error("create checkout session failed");
}
window.opener.postMessage({ type: "waiting-payment" }, "*");
// 2. 直接跳转到Stripe托管页面就这么简单
redirectToCheckout(result.data.checkout_url);
window.location.href = result.data.checkout_url;
} catch (error) {
throw new Error("create checkout session failed, please try again later");
}
@ -147,7 +157,10 @@ function HomeModule5() {
<p className="text-white text-[0.875rem] mb-[1rem]">
{plan.credits}
</p>
<button onClick={() => handleSubscribe(plan.title)} className="w-full bg-white text-black py-[0.75rem] rounded-full mb-[1rem] hover:bg-black hover:text-white transition-colors border border-white/20">
<button
onClick={() => handleSubscribe(plan.title)}
className="w-full bg-white text-black py-[0.75rem] rounded-full mb-[1rem] hover:bg-black hover:text-white transition-colors border border-white/20"
>
{plan.buttonText}
</button>
<p className="w-full text-center text-white/60 text-[0.75rem] mb-[2rem]">

View File

@ -0,0 +1,205 @@
'use client'
import React, { useEffect, useState } from 'react'
import { Result, Button, Spin, Card, Modal } from 'antd'
import { CheckCircleOutlined, CloseCircleOutlined, LoadingOutlined } from '@ant-design/icons'
import { useRouter, useSearchParams } from 'next/navigation'
import { getCheckoutSessionStatus, PaymentStatusResponse } from '@/lib/stripe'
/**
*
*/
enum PaymentStatus {
LOADING = 'loading',
SUCCESS = 'success',
FAILED = 'failed'
}
/**
*
* Stripe Checkout支付完成后的状态展示和用户操作
*/
export default function CallbackModal({ onClose }: { onClose: () => void }) {
const router = useRouter()
const searchParams = useSearchParams()
const [paymentStatus, setPaymentStatus] = useState<PaymentStatus>(PaymentStatus.LOADING)
const [paymentInfo, setPaymentInfo] = useState<any>(null)
const [isModalOpen, setIsModalOpen] = useState(true)
const callBackUrl = localStorage.getItem('callBackUrl') || '/'
/**
* Stripe Checkout Session支付状态
*/
const fetchPaymentStatus = async () => {
try {
// 从URL参数获取session_id和user_id
const sessionId = searchParams.get('session_id')
const userId = searchParams.get('user_id')
if (!sessionId || !userId) {
throw new Error('Missing required parameters: session_id or user_id')
}
// 调用真实的Stripe API获取支付状态
const response = await getCheckoutSessionStatus(sessionId, userId)
if (response.successful && response.data) {
const { payment_status, biz_order_no, pay_time, subscription } = response.data
if (payment_status === 'success') {
setPaymentStatus(PaymentStatus.SUCCESS)
setPaymentInfo({
orderId: biz_order_no,
sessionId,
paymentTime: pay_time,
subscription: subscription ? {
planName: subscription.plan_name,
planDisplayName: subscription.plan_display_name,
status: subscription.status,
currentPeriodEnd: subscription.current_period_end
} : null
})
} else if (payment_status === 'fail') {
setPaymentStatus(PaymentStatus.FAILED)
setPaymentInfo({
orderId: biz_order_no,
sessionId,
errorMessage: 'Payment processing failed, please try again'
})
} else {
// pending状态继续等待
setTimeout(fetchPaymentStatus, 2000)
}
} else {
throw new Error(response.message || 'Failed to get payment status')
}
} catch (error) {
console.error('Failed to get payment status:', error)
setPaymentStatus(PaymentStatus.FAILED)
setPaymentInfo({
errorMessage: error instanceof Error ? error.message : 'Network error, unable to get payment status'
})
}
}
// 渲染模态框内容
const renderModalContent = () => {
// 加载状态
if (paymentStatus === PaymentStatus.LOADING) {
return (
<div className="text-center py-8">
<Spin
indicator={<LoadingOutlined style={{ fontSize: 48, color: 'white' }} spin />}
size="large"
/>
<div className="mt-6 text-lg text-white">Getting payment status...</div>
<div className="mt-2 text-sm text-white/70">Please wait, we are processing your payment information</div>
</div>
)
}
// 支付成功状态
if (paymentStatus === PaymentStatus.SUCCESS) {
return (
<div className="text-center py-4">
<div className="mb-6">
<CheckCircleOutlined className="text-white text-6xl mb-4" />
<h1 className="text-white text-2xl font-bold mb-2">Payment Successful!</h1>
<p className="text-white/70 text-base">{`Order ID: ${paymentInfo?.orderId || 'N/A'}`}</p>
</div>
<div className="text-center text-white/70 mb-8 space-y-2">
{paymentInfo?.subscription && (
<>
<p>Subscription Plan: {paymentInfo.subscription.planDisplayName}</p>
<p>Subscription Status: {paymentInfo.subscription.status}</p>
{paymentInfo.subscription.currentPeriodEnd && (
<p>Expiry Date: {new Date(paymentInfo.subscription.currentPeriodEnd).toLocaleString()}</p>
)}
</>
)}
{paymentInfo?.paymentTime && (
<p>Payment Time: {new Date(paymentInfo.paymentTime).toLocaleString()}</p>
)}
<p>Thank you for your purchase!</p>
</div>
</div>
)
}
// 支付失败状态
return (
<div className="text-center py-4">
<div className="mb-6">
<CloseCircleOutlined className="text-white text-6xl mb-4" />
<h1 className="text-white text-2xl font-bold mb-2">Payment Failed</h1>
<p className="text-white/70 text-base">{paymentInfo?.errorMessage || 'An error occurred during payment processing'}</p>
</div>
<div className="text-center text-white/70 mb-8 space-y-2">
<p>Order ID: {paymentInfo?.orderId || 'N/A'}</p>
<p>If the problem persists, please contact customer service</p>
</div>
</div>
)
}
const watResult = async function (ev: MessageEvent<{
type: 'payment-callback',
canceled: boolean,
sessionId: string,
userId: string
}>) {
if (ev.data.type === 'payment-callback') {
if (ev.data.canceled) {
setPaymentStatus(PaymentStatus.FAILED)
return
}
const userId = JSON.parse(localStorage.getItem('currentUser') || '{}').id
const res = await getCheckoutSessionStatus(ev.data.sessionId, userId)
if (res.successful && res.data) {
if(res.data.payment_status === 'success'){
setPaymentStatus(PaymentStatus.SUCCESS)
}else{
setPaymentStatus(PaymentStatus.FAILED)
}
}
}
}
useEffect(() => {
window.addEventListener('message', watResult)
return () => {
window.removeEventListener('message', watResult)
}
}, [])
return (
<Modal
open={isModalOpen}
closable={true}
closeIcon={<CloseCircleOutlined style={{ color: 'white', fontSize: '16px' }} />}
maskClosable={false}
keyboard={false}
footer={null}
width={500}
centered
title={null}
className="payment-callback-modal"
onCancel={onClose}
styles={{
content: {
backgroundColor: 'black',
border: '1px solid rgba(255, 255, 255, 0.2)',
},
mask: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
},
header: {
borderBottom: 'none',
}
}}
>
{renderModalContent()}
</Modal>
)
}

View File

@ -41,7 +41,7 @@ export function TopBar({
);
const [mounted, setMounted] = React.useState(false);
const [isLogin, setIsLogin] = useState(false);
const pathname = usePathname()
const pathname = usePathname();
useEffect(() => {
const currentUser = localStorage.getItem("currentUser");
if (JSON.parse(currentUser || "{}")?.token) {
@ -161,7 +161,7 @@ export function TopBar({
size="sm"
onClick={() => {
localStorage.setItem("callBackUrl", pathname);
router.push("/pricing");
window.open("/pricing", "_blank");
}}
className="text-gray-300 hover:text-white"
>
@ -255,7 +255,9 @@ export function TopBar({
variant="outline"
size="sm"
className="text-white border-white hover:bg-white/10 rounded-full px-3 py-1 text-[10px]"
onClick={() => router.push("/pricing")}
onClick={() => {
window.open("/pricing", "_blank");
}}
>
Upgrade
</Button>
@ -263,7 +265,9 @@ export function TopBar({
variant="outline"
size="sm"
className="text-white border-white hover:bg-white/10 rounded-full px-3 py-1 text-[10px]"
onClick={() => router.push("/pricing")}
onClick={() => {
window.open("/pricing", "_blank");
}}
>
Manage
</Button>

View File

@ -27,6 +27,7 @@ import { getResourcesList, Resource } from "@/api/resources";
import { Carousel } from "antd";
import { TextCanvas } from "../common/TextCanvas";
import { fetchSubscriptionPlans, SubscriptionPlan } from "@/lib/stripe";
import { useCallbackModal } from "@/app/layout";
export function HomePage2() {
const [hPading, setHPading] = useState(0);
@ -407,6 +408,7 @@ function HomeModule5() {
const [billingType, setBillingType] = useState<"month" | "year">("month");
const [plans, setPlans] = useState<SubscriptionPlan[]>([]);
const { setShowCallbackModal } = useCallbackModal();
const pathname = usePathname();
// 从后端获取订阅计划数据
useEffect(() => {
@ -474,8 +476,8 @@ function HomeModule5() {
throw new Error("create checkout session failed");
}
// 2. 直接跳转到Stripe托管页面就这么简单
redirectToCheckout(result.data.checkout_url);
setShowCallbackModal(true)
window.open(result.data.checkout_url, '_blank');
} catch (error) {
throw new Error("create checkout session failed, please try again later");
}

View File

@ -109,6 +109,6 @@ export async function getCheckoutSessionStatus(
*/
export function redirectToCheckout(checkoutUrl: string) {
if (typeof window !== 'undefined') {
window.location.href = checkoutUrl;
window.open(checkoutUrl, '_blank');
}
}

View File

@ -34,16 +34,20 @@ const nextConfig = {
},
async rewrites() {
// 使用环境变量,如果没有则使用默认值
const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || 'https://77.smartvideo.py.qikongjian.com'
console.log('Environment BASE_URL:', process.env)
console.log('Using BASE_URL:', BASE_URL)
return [
{
source: '/api/proxy/:path*',
destination: BASE_URL+'/:path*',
destination: `${BASE_URL}/:path*`,
},
{
source: '/api/resources/:path*',
destination: BASE_URL+'/:path*',
destination: `${BASE_URL}/:path*`,
},
];
},