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 { try {
const res = await fetch('/api/billing/portal', { method: 'POST' }); const res = await fetch('/api/billing/portal', { method: 'POST' });
const data = await res.json(); 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; window.location.href = data.url;
} catch (err) { } catch (err) {
console.error('[BillingPlans] portal error:', 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) => { const formatDate = (dateStr: string | null | undefined) => {
if (!dateStr) return '—'; if (!dateStr) return '—';
try { try {
@@ -339,15 +344,28 @@ export function BillingPlans() {
</div> </div>
{isPaid && ( {isPaid && (
<button <div className="flex flex-wrap gap-3">
type="button" <button
onClick={handlePortal} type="button"
disabled={portalLoading} onClick={handlePortal}
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" 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'} {portalLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : <ExternalLink className="h-4 w-4" />}
</button> {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> </div>
@@ -490,8 +508,11 @@ export function BillingPlans() {
</div> </div>
)} )}
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6"> <div className={cn(
{plans.map((plan) => ( "grid grid-cols-1 gap-6",
plansToShow.length === 3 ? "lg:grid-cols-3" : "lg:grid-cols-4"
)}>
{plansToShow.map((plan) => (
<div <div
key={plan.id} key={plan.id}
className={cn( className={cn(

View File

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

View File

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