当前订阅
+Active Plan
+{currentPlan.name}
++ 月度出图额度:{formatLimit(currentPlan.monthlyImageLimit)} +
+
+
+ 查看当前套餐、权益额度,并可进入 Stripe 门户管理续费、取消和发票信息。 +
+Active Plan
+{currentPlan.name}
++ 月度出图额度:{formatLimit(currentPlan.monthlyImageLimit)} +
+
+
+
+ 订阅权益按 Webhook 自动写回账户额度(`monthlyImageLimit`)。 +
+- 支付已完成,正在同步你的订阅权益{selectedPlan ? `(${selectedPlan})` : ""}。如未生效,请稍后刷新。 + 支付已完成,正在同步你的订阅权益{selectedPlan ? `(${selectedPlan})` : ""}。如未生效,请稍后刷新, + 或前往 + + 订阅中心 + + 查看状态。
) : (支付已取消,你可以随时重新发起订阅。
diff --git a/components/billing/manage-subscription-button.tsx b/components/billing/manage-subscription-button.tsx new file mode 100644 index 0000000..96acbe4 --- /dev/null +++ b/components/billing/manage-subscription-button.tsx @@ -0,0 +1,63 @@ +"use client"; + +import { Loader2 } from "lucide-react"; +import { useState } from "react"; +import { cn } from "@/lib/utils"; + +export function ManageSubscriptionButton({ + className, +}: { + className?: string; +}) { + const [isPending, setIsPending] = useState(false); + + async function handleOpenPortal() { + if (isPending) { + return; + } + + setIsPending(true); + + try { + const response = await fetch("/api/billing/portal", { + method: "POST", + }); + + const data = (await response.json()) as { error?: string; url?: string }; + + if (!response.ok || !data.url) { + window.alert(data.error || "打开订阅管理失败,请稍后重试。"); + return; + } + + window.location.assign(data.url); + } catch (error) { + console.error("[ManageSubscriptionButton] portal error", error); + window.alert("打开订阅管理失败,请稍后重试。"); + } finally { + setIsPending(false); + } + } + + return ( + + ); +} diff --git a/components/sidebar-user-nav.tsx b/components/sidebar-user-nav.tsx index 8f5ab93..026e676 100644 --- a/components/sidebar-user-nav.tsx +++ b/components/sidebar-user-nav.tsx @@ -79,6 +79,17 @@ export function SidebarUserNav({ user }: { user: User }) { > 切换到 {resolvedTheme === "light" ? "深色" : "浅色"} 模式 + {!isGuest ? ( +