0548a08cbb
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
143 lines
4.4 KiB
TypeScript
143 lines
4.4 KiB
TypeScript
"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>
|
|
);
|
|
}
|