feat: integrate Stripe payment with subscription plans
Add Stripe payment integration across the project: - Add @stripe/stripe-js dependency - Configure Stripe environment variables (secret key, publishable key, webhook secret) for dev, local, and production environments - Add Product/Price IDs for monthly, quarterly, and annual subscription tiers - Extend subscription plan data with voiceMinutesPerDay quotas (30/45/60 minutes per tier) - Update .gitignore to exclude implementation_plan.md
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
"use client";
|
||||
/**
|
||||
* 订阅成功回跳页
|
||||
*
|
||||
* 流程:
|
||||
* 1. 用户在 Stripe 托管页完成结账
|
||||
* 2. Stripe 重定向到 `/subscription/success?session_id=xxx`
|
||||
* 3. 这个页面显示 "购买成功" + 调 `getCurrentUser` 刷新 user
|
||||
* 4. 点 "返回订阅页" 跳回 `/subscription`
|
||||
*
|
||||
* 注意:webhook 会异步触发 + 更新 user.isVip + voiceMinutes
|
||||
* 此页面拿到 session_id 后 轮询几次 getCurrentUser 等待 webhook 完成(避免 webhook 还没到 user 就还没更新的情况)
|
||||
*/
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { MobileShell } from "@/app/_components/core/mobile-shell";
|
||||
import { authRepository } from "@/data/repositories/auth_repository";
|
||||
import { ROUTES } from "@/router/routes";
|
||||
|
||||
export default function SubscriptionSuccessPage() {
|
||||
const router = useRouter();
|
||||
const [status, setStatus] = useState<"polling" | "ok" | "error">("polling");
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const hasRefreshed = useRef(false);
|
||||
|
||||
// 轮询 getCurrentUser 等待 webhook 完成
|
||||
useEffect(() => {
|
||||
if (hasRefreshed.current) return;
|
||||
hasRefreshed.current = true;
|
||||
|
||||
let cancelled = false;
|
||||
const tryRefresh = async (attemptsLeft: number) => {
|
||||
if (cancelled) return;
|
||||
const r = await authRepository.getCurrentUser();
|
||||
if (cancelled) return;
|
||||
if (r.success) {
|
||||
if (r.data.isVip) {
|
||||
setStatus("ok");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
setErrorMessage(r.error?.message ?? "Unknown error");
|
||||
}
|
||||
if (attemptsLeft > 0) {
|
||||
setTimeout(() => void tryRefresh(attemptsLeft - 1), 2000);
|
||||
} else {
|
||||
setStatus("error");
|
||||
}
|
||||
};
|
||||
void tryRefresh(5);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<MobileShell>
|
||||
<div
|
||||
style={{
|
||||
padding: "2rem 1.5rem",
|
||||
textAlign: "center",
|
||||
minHeight: "60vh",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: "1rem",
|
||||
}}
|
||||
>
|
||||
{status === "polling" ? (
|
||||
<>
|
||||
<h1 style={{ fontSize: "1.5rem", fontWeight: 600 }}>
|
||||
Processing your subscription…
|
||||
</h1>
|
||||
<p style={{ color: "#666" }}>
|
||||
Your payment was received. We are confirming the details with
|
||||
our server.
|
||||
</p>
|
||||
</>
|
||||
) : status === "ok" ? (
|
||||
<>
|
||||
<h1 style={{ fontSize: "1.5rem", fontWeight: 600, color: "#1f8b3a" }}>
|
||||
🎉 Subscription activated!
|
||||
</h1>
|
||||
<p style={{ color: "#666" }}>
|
||||
Welcome to VIP. Enjoy unlimited chat, free voice minutes, and
|
||||
more.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push(ROUTES.chat)}
|
||||
style={{
|
||||
padding: "0.75rem 1.5rem",
|
||||
borderRadius: "12px",
|
||||
background: "linear-gradient(135deg, #f96ADE, #f657A0)",
|
||||
color: "white",
|
||||
border: "none",
|
||||
fontSize: "1rem",
|
||||
fontWeight: 600,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Start chatting
|
||||
</button>
|
||||
<Link
|
||||
href={ROUTES.subscription}
|
||||
style={{ color: "#888", fontSize: "0.875rem" }}
|
||||
>
|
||||
Back to subscription
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<h1 style={{ fontSize: "1.5rem", fontWeight: 600, color: "#c0392b" }}>
|
||||
Subscription status unclear
|
||||
</h1>
|
||||
<p style={{ color: "#666" }}>
|
||||
We received your payment but could not confirm your VIP
|
||||
status. {errorMessage ?? "Please refresh in a few seconds."}
|
||||
</p>
|
||||
<Link
|
||||
href={ROUTES.subscription}
|
||||
style={{
|
||||
padding: "0.75rem 1.5rem",
|
||||
borderRadius: "12px",
|
||||
background: "#f0f0f0",
|
||||
color: "#333",
|
||||
textDecoration: "none",
|
||||
fontSize: "1rem",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Back to subscription
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</MobileShell>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user