Skip to content

Commit d04f28e

Browse files
committed
feat: Stripe Payment added
1 parent dab86d2 commit d04f28e

28 files changed

+349
-70
lines changed
0 Bytes
Binary file not shown.
0 Bytes
Binary file not shown.
-32.2 KB
Binary file not shown.

kommande-access-gui/src/app/api/stripe/checkout.js

Lines changed: 0 additions & 25 deletions
This file was deleted.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { NextResponse } from "next/server";
2+
3+
const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY);
4+
5+
export async function GET(request) {
6+
try {
7+
const { searchParams } = new URL(request.url);
8+
const sessionId = searchParams.get("session_id");
9+
10+
if (!sessionId) {
11+
return NextResponse.json(
12+
{ error: "Missing session_id parameter" },
13+
{ status: 400 }
14+
);
15+
}
16+
17+
// 从Stripe获取会话详情
18+
const session = await stripe.checkout.sessions.retrieve(sessionId, {
19+
expand: ["line_items", "payment_intent"],
20+
});
21+
22+
if (!session) {
23+
return NextResponse.json(
24+
{ error: "Session not found" },
25+
{ status: 404 }
26+
);
27+
}
28+
29+
// 格式化响应数据
30+
const orderData = {
31+
id: session.id,
32+
customer_email: session.customer_details?.email,
33+
amount_total: session.amount_total / 100, // 转换为美元
34+
currency: session.currency,
35+
payment_status: session.payment_status,
36+
created_at: new Date(session.created * 1000).toISOString(),
37+
items: session.line_items?.data.map(item => ({
38+
name: item.description,
39+
quantity: item.quantity,
40+
price: item.amount_total / 100 / item.quantity, // 单价
41+
total: item.amount_total / 100,
42+
})) || [],
43+
metadata: session.metadata,
44+
};
45+
46+
return NextResponse.json(orderData);
47+
} catch (error) {
48+
console.error("Error retrieving order:", error);
49+
return NextResponse.json(
50+
{ error: error.message || "Failed to retrieve order details" },
51+
{ status: error.statusCode || 500 }
52+
);
53+
}
54+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { NextResponse } from "next/server";
2+
3+
const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY);
4+
5+
export async function POST(request) {
6+
try {
7+
const body = await request.json();
8+
const { lineItems } = body;
9+
10+
if (!lineItems || !Array.isArray(lineItems) || lineItems.length === 0) {
11+
return NextResponse.json(
12+
{ error: "Invalid request: lineItems is required and must be a non-empty array" },
13+
{ status: 400 }
14+
);
15+
}
16+
17+
const session = await stripe.checkout.sessions.create({
18+
payment_method_types: ["card"],
19+
line_items: lineItems,
20+
mode: "payment",
21+
success_url: `${request.headers.get("origin")}/checkout/success?session_id={CHECKOUT_SESSION_ID}`,
22+
cancel_url: `${request.headers.get("origin")}/checkout/canceled`,
23+
metadata: {
24+
order_id: `order_${Date.now()}` // 可以添加更多元数据
25+
}
26+
});
27+
28+
return NextResponse.json({ url: session.url });
29+
} catch (error) {
30+
console.error("Stripe checkout error:", error);
31+
return NextResponse.json(
32+
{ error: error.message || "An error occurred during checkout" },
33+
{ status: error.statusCode || 500 }
34+
);
35+
}
36+
}
37+
38+
// 处理其他HTTP方法
39+
export async function GET() {
40+
return NextResponse.json({ error: "Method not allowed" }, { status: 405 });
41+
}
42+
43+
export async function PUT() {
44+
return NextResponse.json({ error: "Method not allowed" }, { status: 405 });
45+
}
46+
47+
export async function DELETE() {
48+
return NextResponse.json({ error: "Method not allowed" }, { status: 405 });
49+
}

kommande-access-gui/src/app/api/stripe/route.jsx

Lines changed: 0 additions & 24 deletions
This file was deleted.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
'use client';
2+
3+
import { XCircle, ArrowLeft, ShoppingBag } from 'lucide-react';
4+
5+
export default function CanceledPage() {
6+
return (
7+
<div className="min-h-screen bg-gray-100 dark:bg-gray-900 py-12 px-4 sm:px-6 lg:px-8 flex items-center justify-center">
8+
<div className="max-w-2xl w-full bg-white dark:bg-gray-800 rounded-lg shadow-lg overflow-hidden">
9+
<div className="p-8">
10+
<div className="flex flex-col items-center justify-center space-y-4 mb-8">
11+
<XCircle className="w-24 h-24 text-red-500" />
12+
<h1 className="text-3xl font-bold text-center dark:text-white">
13+
Payment Canceled
14+
</h1>
15+
<p className="text-gray-600 dark:text-gray-300 text-center">
16+
Your payment was canceled and no charges were made.
17+
</p>
18+
</div>
19+
20+
<div className="mt-8 flex flex-col sm:flex-row gap-4 justify-center">
21+
<a href="/users/Cart" className="btn btn-primary flex items-center gap-2">
22+
<ShoppingBag className="w-5 h-5" />
23+
Return to Cart
24+
</a>
25+
<a href="/" className="btn btn-outline flex items-center gap-2">
26+
<ArrowLeft className="w-5 h-5" />
27+
Continue Shopping
28+
</a>
29+
</div>
30+
31+
<div className="mt-8 text-center">
32+
<p className="text-gray-500 dark:text-gray-400">
33+
If you encountered any issues during checkout, please contact our customer support.
34+
</p>
35+
</div>
36+
</div>
37+
</div>
38+
</div>
39+
);
40+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
'use client';
2+
3+
import { useEffect, useState, useRef } from 'react';
4+
import { useSearchParams, useRouter } from 'next/navigation';
5+
import { CheckCircle, ShoppingBag, Home, User } from 'lucide-react';
6+
import Link from 'next/link';
7+
import { useCart } from '@/components/CartContext';
8+
9+
export default function SuccessPage() {
10+
const searchParams = useSearchParams();
11+
const router = useRouter();
12+
const { clearCart } = useCart();
13+
const [orderDetails, setOrderDetails] = useState(null);
14+
const [loading, setLoading] = useState(true);
15+
const fetchedRef = useRef(false); // 用于跟踪是否已经获取过订单信息
16+
17+
// 从URL获取会话ID
18+
const sessionId = searchParams.get('session_id');
19+
20+
useEffect(() => {
21+
// 如果没有session_id,重定向到首页
22+
if (!sessionId) {
23+
router.push('/');
24+
return;
25+
}
26+
27+
// 清空购物车(只执行一次)
28+
clearCart();
29+
30+
// 如果已经获取过订单信息,则不再重复获取
31+
if (fetchedRef.current) return;
32+
33+
// 标记为已经获取过
34+
fetchedRef.current = true;
35+
36+
// 获取订单详情
37+
const fetchOrderDetails = async () => {
38+
try {
39+
const response = await fetch(`/api/stripe/order?session_id=${sessionId}`);
40+
if (response.ok) {
41+
const data = await response.json();
42+
setOrderDetails(data);
43+
}
44+
} catch (error) {
45+
console.error('Error fetching order details:', error);
46+
} finally {
47+
setLoading(false);
48+
}
49+
};
50+
51+
fetchOrderDetails();
52+
}, [sessionId, router]); // 移除clearCart依赖项
53+
54+
// 处理手动重新加载页面的函数
55+
const handleRefresh = () => {
56+
if (!fetchedRef.current) return;
57+
58+
setLoading(true);
59+
fetchedRef.current = false; // 重置标记以允许再次获取
60+
61+
const fetchOrderDetails = async () => {
62+
try {
63+
const response = await fetch(`/api/stripe/order?session_id=${sessionId}`);
64+
if (response.ok) {
65+
const data = await response.json();
66+
setOrderDetails(data);
67+
}
68+
} catch (error) {
69+
console.error('Error re-fetching order details:', error);
70+
} finally {
71+
setLoading(false);
72+
}
73+
};
74+
75+
fetchOrderDetails();
76+
};
77+
78+
if (loading) {
79+
return (
80+
<div className="flex items-center justify-center min-h-screen bg-gray-100 dark:bg-gray-900">
81+
<div className="loading loading-spinner loading-lg"></div>
82+
</div>
83+
);
84+
}
85+
86+
return (
87+
<div className="min-h-screen bg-gray-100 dark:bg-gray-900 py-12 px-4 sm:px-6 lg:px-8">
88+
<div className="max-w-3xl mx-auto bg-white dark:bg-gray-800 rounded-lg shadow-lg overflow-hidden">
89+
<div className="p-8">
90+
<div className="flex flex-col items-center justify-center space-y-4 mb-8">
91+
<CheckCircle className="w-24 h-24 text-green-500" />
92+
<h1 className="text-3xl font-bold text-center dark:text-white">
93+
Thank you for your order!
94+
</h1>
95+
<p className="text-gray-600 dark:text-gray-300 text-center">
96+
Your payment was processed successfully. A confirmation email has been sent to your email address.
97+
</p>
98+
</div>
99+
100+
<div className="border-t border-gray-200 dark:border-gray-700 pt-8 mt-8">
101+
<h2 className="text-xl font-semibold mb-4 dark:text-white">Order Information</h2>
102+
<p className="text-gray-600 dark:text-gray-300 mb-2">
103+
Order ID: <span className="font-medium">{sessionId?.substring(0, 12)}...</span>
104+
</p>
105+
<p className="text-gray-600 dark:text-gray-300 mb-2">
106+
Date: <span className="font-medium">{new Date().toLocaleDateString()}</span>
107+
</p>
108+
</div>
109+
110+
<div className="mt-8 flex flex-col sm:flex-row gap-4 justify-center">
111+
<a href="/users/Cart" className="btn btn-primary flex items-center gap-2">
112+
<ShoppingBag className="w-5 h-5" />
113+
Return to Cart
114+
</a>
115+
<a href="/" className="btn btn-outline flex items-center gap-2">
116+
<Home className="w-5 h-5" />
117+
Continue Shopping
118+
</a>
119+
<a href="/users/Dashboard" className="btn btn-outline flex items-center gap-2">
120+
<User className="w-5 h-5" />
121+
My Account
122+
</a>
123+
</div>
124+
</div>
125+
</div>
126+
</div>
127+
);
128+
}

0 commit comments

Comments
 (0)