fix(billing): filter out free plan for paid users, add direct cancel subscription button, and handle portal errors without crashing Next.js overlay

This commit is contained in:
Antigravity
2026-05-28 19:49:26 +00:00
parent 66bac83a9a
commit f5608372dc
3 changed files with 35 additions and 12 deletions

View File

@@ -117,7 +117,10 @@ export function BillingPlans() {
try {
const res = await fetch('/api/billing/portal', { method: 'POST' });
const data = await res.json();
if (!res.ok) throw new Error(data.error ?? 'Failed to open portal');
if (!res.ok) {
toast.error(data.error || 'Failed to open billing portal.');
return;
}
window.location.href = data.url;
} catch (err) {
console.error('[BillingPlans] portal error:', err);
@@ -237,6 +240,8 @@ export function BillingPlans() {
},
];
const plansToShow = isPaid ? plans.filter((p) => p.id !== 'free') : plans;
const formatDate = (dateStr: string | null | undefined) => {
if (!dateStr) return '—';
try {
@@ -339,15 +344,28 @@ export function BillingPlans() {
</div>
{isPaid && (
<button
type="button"
onClick={handlePortal}
disabled={portalLoading}
className="flex items-center gap-2 px-5 py-2.5 bg-ink text-white dark:bg-white dark:text-black rounded-xl text-xs font-semibold hover:opacity-90 disabled:opacity-60 transition-all shadow-md shadow-black/5"
>
{portalLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : <ExternalLink className="h-4 w-4" />}
{t('billing.manageBilling') || 'Gérer la facturation'}
</button>
<div className="flex flex-wrap gap-3">
<button
type="button"
onClick={handlePortal}
disabled={portalLoading}
className="flex items-center gap-2 px-5 py-2.5 bg-ink text-white dark:bg-white dark:text-black rounded-xl text-xs font-semibold hover:opacity-90 disabled:opacity-60 transition-all shadow-md shadow-black/5"
>
{portalLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : <ExternalLink className="h-4 w-4" />}
{t('billing.manageBilling') || 'Gérer la facturation'}
</button>
{status?.hasStripeSubscription && (
<button
type="button"
onClick={handlePortal}
disabled={portalLoading}
className="flex items-center gap-2 px-5 py-2.5 border border-rose-200 text-rose-600 dark:border-rose-800/40 dark:text-rose-400 hover:bg-rose-50/50 dark:hover:bg-rose-950/15 rounded-xl text-xs font-semibold transition-all"
>
{t('billing.cancelSubscription') || "Résilier l'abonnement"}
</button>
)}
</div>
)}
</div>
@@ -490,8 +508,11 @@ export function BillingPlans() {
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{plans.map((plan) => (
<div className={cn(
"grid grid-cols-1 gap-6",
plansToShow.length === 3 ? "lg:grid-cols-3" : "lg:grid-cols-4"
)}>
{plansToShow.map((plan) => (
<div
key={plan.id}
className={cn(

View File

@@ -2878,6 +2878,7 @@
"noUsage": "No usage data",
"billingHistory": "Billing History",
"viewInvoices": "Manage invoices in portal",
"cancelSubscription": "Cancel subscription",
"nextBillingDate": "Next billing date",
"billingPeriod": "Billing period",
"planSince": "Member since",

View File

@@ -2882,6 +2882,7 @@
"noUsage": "Aucune donnée d'utilisation",
"billingHistory": "Historique de facturation",
"viewInvoices": "Gérer les factures dans le portail",
"cancelSubscription": "Résilier l'abonnement",
"nextBillingDate": "Prochaine date de facturation",
"billingPeriod": "Période de facturation",
"planSince": "Membre depuis",