Redesign billing tab for clarity and usability

- Add prominent trial banner at top with end date and auto-charge notice
- Separate cancel-at-period-end warning into its own banner
- Simplify plan header: name + badge + billing interval left, Change plan right
- Reduce stats grid from 5 cramped columns to 4 clean columns
- Remove redundant "Pageview Limit" stat (duplicated "Pageviews")
- Replace bulky cancel section with inline text links below the card
- Make "Payment method & invoices" a clear link with icon
- Compact invoice rows: remove avatar icons, inline date with amount
- Tighter spacing throughout for cohesive feel

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Usman Baig
2026-02-09 11:11:29 +01:00
parent 4ec68e8aaf
commit 29dd20a4a7

View File

@@ -708,10 +708,10 @@ export default function OrganizationSettings() {
)} )}
{activeTab === 'billing' && ( {activeTab === 'billing' && (
<div className="space-y-12"> <div className="space-y-8">
<div> <div>
<h2 className="text-2xl font-bold text-neutral-900 dark:text-white mb-1">Billing & Subscription</h2> <h2 className="text-2xl font-bold text-neutral-900 dark:text-white mb-1">Billing & Subscription</h2>
<p className="text-sm text-neutral-500 dark:text-neutral-400">Manage your subscription plan and payment methods.</p> <p className="text-sm text-neutral-500 dark:text-neutral-400">Manage your plan, usage, and payment details.</p>
</div> </div>
{isLoadingSubscription ? ( {isLoadingSubscription ? (
@@ -721,60 +721,82 @@ export default function OrganizationSettings() {
) : !subscription ? ( ) : !subscription ? (
<div className="p-8 text-center bg-neutral-50 dark:bg-neutral-900/50 rounded-2xl border border-neutral-200 dark:border-neutral-800"> <div className="p-8 text-center bg-neutral-50 dark:bg-neutral-900/50 rounded-2xl border border-neutral-200 dark:border-neutral-800">
<p className="text-neutral-500">Could not load subscription details.</p> <p className="text-neutral-500">Could not load subscription details.</p>
<Button <Button variant="ghost" onClick={loadSubscription} className="mt-4">Retry</Button>
variant="ghost"
onClick={loadSubscription}
className="mt-4"
>
Retry
</Button>
</div> </div>
) : ( ) : (
<div className="space-y-8"> <div className="space-y-6">
{/* Current Plan */}
<div className="bg-neutral-50 dark:bg-neutral-900/50 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6"> {/* Trial notice */}
<div className="flex items-start justify-between mb-6"> {subscription.subscription_status === 'trialing' && (
<div> <div className="p-4 bg-yellow-50 dark:bg-yellow-900/10 border border-yellow-200 dark:border-yellow-800 rounded-2xl flex flex-col sm:flex-row sm:items-center gap-3">
<h3 className="text-sm font-medium text-neutral-500 uppercase tracking-wider mb-1">Current Plan</h3> <div className="flex-1">
<div className="flex items-center gap-3"> <p className="text-sm font-medium text-yellow-800 dark:text-yellow-200">
<span className="text-2xl font-bold text-neutral-900 dark:text-white capitalize"> Your free trial ends on{' '}
{subscription.plan_id?.startsWith('price_') ? 'Pro' : (subscription.plan_id === 'free' || !subscription.plan_id ? 'Free' : subscription.plan_id)} Plan <span className="font-semibold">
{(() => {
const d = subscription.current_period_end ? new Date(subscription.current_period_end as string) : null
return d && !Number.isNaN(d.getTime()) ? d.toLocaleDateString(undefined, { month: 'long', day: 'numeric', year: 'numeric' }) : ''
})()}
</span> </span>
<span className={`px-2.5 py-0.5 rounded-full text-xs font-medium capitalize ${ </p>
subscription.subscription_status === 'active' <p className="text-xs text-yellow-700 dark:text-yellow-300 mt-0.5">
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300' After the trial you'll be charged automatically unless you cancel before then.
: subscription.subscription_status === 'trialing' </p>
? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-300'
: 'bg-neutral-100 text-neutral-700 dark:bg-neutral-800 dark:text-neutral-300'
}`}>
{subscription.subscription_status === 'trialing' ? 'Trial Active' : (subscription.subscription_status || 'Free')}
</span>
</div>
</div>
<div className="flex flex-col sm:flex-row gap-2 items-start sm:items-center">
<Button
onClick={openChangePlanModal}
disabled={subscription.cancel_at_period_end}
>
Change plan
</Button>
{subscription.has_payment_method && (
<button
type="button"
onClick={handleManageSubscription}
disabled={isRedirectingToPortal}
className="text-sm text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-300 underline disabled:opacity-50"
>
Update payment method or view invoices
</button>
)}
</div> </div>
</div> </div>
)}
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-6 pt-6 border-t border-neutral-200 dark:border-neutral-800"> {/* Cancel-at-period-end notice */}
{subscription.cancel_at_period_end && (
<div className="p-4 bg-amber-50 dark:bg-amber-900/10 border border-amber-200 dark:border-amber-800 rounded-2xl">
<p className="text-sm font-medium text-amber-800 dark:text-amber-200">
Your subscription will end on{' '}
<span className="font-semibold">
{(() => {
const d = subscription.current_period_end ? new Date(subscription.current_period_end as string) : null
return d && !Number.isNaN(d.getTime()) ? d.toLocaleDateString(undefined, { month: 'long', day: 'numeric', year: 'numeric' }) : '—'
})()}
</span>
</p>
<p className="text-xs text-amber-700 dark:text-amber-300 mt-1">
You keep full access until then. Your data is retained for 30 days after. Use "Change plan" to resubscribe.
</p>
</div>
)}
{/* Plan & Usage card */}
<div className="bg-neutral-50 dark:bg-neutral-900/50 border border-neutral-200 dark:border-neutral-800 rounded-2xl overflow-hidden">
{/* Plan header */}
<div className="p-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="flex items-center gap-3">
<span className="text-xl font-bold text-neutral-900 dark:text-white capitalize">
{subscription.plan_id?.startsWith('price_') ? 'Pro' : (subscription.plan_id === 'free' || !subscription.plan_id ? 'Free' : subscription.plan_id)} Plan
</span>
<span className={`px-2.5 py-0.5 rounded-full text-xs font-medium capitalize ${
subscription.subscription_status === 'active'
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300'
: subscription.subscription_status === 'trialing'
? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-300'
: 'bg-neutral-100 text-neutral-700 dark:bg-neutral-800 dark:text-neutral-300'
}`}>
{subscription.subscription_status === 'trialing' ? 'Trial' : (subscription.subscription_status || 'Free')}
</span>
{subscription.billing_interval && (
<span className="text-xs text-neutral-500 capitalize">
Billed {subscription.billing_interval}ly
</span>
)}
</div>
<Button onClick={openChangePlanModal} disabled={subscription.cancel_at_period_end}>
Change plan
</Button>
</div>
{/* Usage stats */}
<div className="border-t border-neutral-200 dark:border-neutral-800 px-6 py-5 grid grid-cols-2 md:grid-cols-4 gap-y-4 gap-x-6">
<div> <div>
<div className="text-sm text-neutral-500 mb-1">Sites</div> <div className="text-xs text-neutral-500 uppercase tracking-wider mb-1">Sites</div>
<div className="font-medium text-neutral-900 dark:text-white"> <div className="text-lg font-semibold text-neutral-900 dark:text-white">
{typeof subscription.sites_count === 'number' {typeof subscription.sites_count === 'number'
? subscription.plan_id === 'solo' ? subscription.plan_id === 'solo'
? `${subscription.sites_count} / 1` ? `${subscription.sites_count} / 1`
@@ -783,109 +805,81 @@ export default function OrganizationSettings() {
</div> </div>
</div> </div>
<div> <div>
<div className="text-sm text-neutral-500 mb-1">Pageviews this period</div> <div className="text-xs text-neutral-500 uppercase tracking-wider mb-1">Pageviews</div>
<div className="font-medium text-neutral-900 dark:text-white"> <div className="text-lg font-semibold text-neutral-900 dark:text-white">
{subscription.pageview_limit > 0 && typeof subscription.pageview_usage === 'number' {subscription.pageview_limit > 0 && typeof subscription.pageview_usage === 'number'
? `${subscription.pageview_usage.toLocaleString()} / ${subscription.pageview_limit.toLocaleString()}` ? `${subscription.pageview_usage.toLocaleString()} / ${subscription.pageview_limit.toLocaleString()}`
: '—'} : '—'}
</div> </div>
</div> </div>
<div> <div>
<div className="text-sm text-neutral-500 mb-1">Billing Interval</div> <div className="text-xs text-neutral-500 uppercase tracking-wider mb-1">
<div className="font-medium text-neutral-900 dark:text-white capitalize"> {subscription.subscription_status === 'trialing' ? 'Trial ends' : (subscription.cancel_at_period_end ? 'Access until' : 'Renews')}
{subscription.billing_interval ? `${subscription.billing_interval}ly` : ''}
</div> </div>
</div> <div className="text-lg font-semibold text-neutral-900 dark:text-white">
<div>
<div className="text-sm text-neutral-500 mb-1">Pageview Limit</div>
<div className="font-medium text-neutral-900 dark:text-white">
{subscription.pageview_limit > 0 ? `${subscription.pageview_limit.toLocaleString()} / month` : 'Unlimited'}
</div>
</div>
<div>
<div className="text-sm text-neutral-500 mb-1">
{subscription.subscription_status === 'trialing' ? 'Trial Ends On' : 'Renews On'}
</div>
<div className="font-medium text-neutral-900 dark:text-white">
{(() => { {(() => {
const raw = subscription.current_period_end const d = subscription.current_period_end ? new Date(subscription.current_period_end as string) : null
const d = raw ? new Date(raw as string) : null return d && !Number.isNaN(d.getTime()) && d.getTime() !== 0 ? d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }) : '—'
const ts = d ? d.getTime() : NaN
return raw && !Number.isNaN(ts) && ts !== 0 ? (d as Date).toLocaleDateString() : ''
})()} })()}
</div> </div>
</div> </div>
<div>
<div className="text-xs text-neutral-500 uppercase tracking-wider mb-1">Limit</div>
<div className="text-lg font-semibold text-neutral-900 dark:text-white">
{subscription.pageview_limit > 0 ? `${subscription.pageview_limit.toLocaleString()} / mo` : 'Unlimited'}
</div>
</div>
</div> </div>
</div> </div>
{/* Cancel-at-period-end notice or Cancel subscription action */} {/* Quick actions */}
{subscription.has_payment_method && (subscription.subscription_status === 'active' || subscription.subscription_status === 'trialing') && ( <div className="flex flex-wrap items-center gap-3">
<> {subscription.has_payment_method && (
{subscription.cancel_at_period_end ? ( <button
<div className="p-6 bg-amber-50 dark:bg-amber-900/10 border border-amber-200 dark:border-amber-800 rounded-2xl"> type="button"
<h3 className="font-medium text-amber-800 dark:text-amber-200 mb-2">Subscription set to cancel</h3> onClick={handleManageSubscription}
<p className="text-sm text-amber-700 dark:text-amber-300 mb-1"> disabled={isRedirectingToPortal}
Your subscription will end on{' '} className="inline-flex items-center gap-1.5 text-sm text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white transition-colors disabled:opacity-50"
{(() => { >
const raw = subscription.current_period_end <ExternalLinkIcon className="w-4 h-4" />
const d = raw ? new Date(raw as string) : null Payment method & invoices
return raw && d && !Number.isNaN(d.getTime()) ? d.toLocaleDateString() : '' </button>
})()} )}
. You can use the app until then. {subscription.has_payment_method && (subscription.subscription_status === 'active' || subscription.subscription_status === 'trialing') && !subscription.cancel_at_period_end && (
</p> <button
<p className="text-xs text-amber-600 dark:text-amber-400"> type="button"
Your data is retained for 30 days after access ends. You can resubscribe anytime using Change plan above. onClick={() => setShowCancelPrompt(true)}
</p> className="inline-flex items-center gap-1.5 text-sm text-red-500 hover:text-red-600 dark:text-red-400 dark:hover:text-red-300 transition-colors"
</div> >
) : ( Cancel subscription
<div className="p-6 bg-neutral-50 dark:bg-neutral-900/50 border border-neutral-200 dark:border-neutral-800 rounded-2xl flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> </button>
<div> )}
<h3 className="font-medium text-neutral-900 dark:text-white mb-1">Cancel subscription</h3> </div>
<p className="text-sm text-neutral-600 dark:text-neutral-400">
After cancellation, you can use the app until the end of your billing period. Your data is retained for 30 days after access ends.
</p>
</div>
<Button
variant="ghost"
className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20 border border-red-200 dark:border-red-800"
onClick={() => setShowCancelPrompt(true)}
>
Cancel subscription
</Button>
</div>
)}
</>
)}
{/* Invoice History */} {/* Invoice History */}
<div> {invoices.length > 0 && (
<h3 className="text-sm font-medium text-neutral-500 uppercase tracking-wider mb-4">Invoice History</h3> <div>
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl overflow-hidden"> <h3 className="text-sm font-medium text-neutral-500 uppercase tracking-wider mb-3">Recent invoices</h3>
{isLoadingInvoices ? ( <div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl overflow-hidden divide-y divide-neutral-200 dark:divide-neutral-800">
<div className="flex items-center justify-center py-8"> {isLoadingInvoices ? (
<div className="w-6 h-6 border-2 border-brand-orange/30 border-t-brand-orange rounded-full animate-spin" /> <div className="flex items-center justify-center py-8">
</div> <div className="w-6 h-6 border-2 border-brand-orange/30 border-t-brand-orange rounded-full animate-spin" />
) : invoices.length === 0 ? ( </div>
<div className="p-8 text-center text-neutral-500">No invoices found.</div> ) : (
) : ( invoices.map((invoice) => (
<div className="divide-y divide-neutral-200 dark:divide-neutral-800"> <div key={invoice.id} className="px-4 py-3 flex items-center justify-between hover:bg-neutral-50 dark:hover:bg-neutral-800/50 transition-colors">
{invoices.map((invoice) => ( <div className="flex items-center gap-3">
<div key={invoice.id} className="p-4 flex items-center justify-between hover:bg-neutral-50 dark:hover:bg-neutral-800/50 transition-colors">
<div className="flex items-center gap-4">
<div className="h-10 w-10 rounded-full bg-neutral-100 dark:bg-neutral-800 flex items-center justify-center text-neutral-500">
<BookOpenIcon className="w-5 h-5" />
</div>
<div> <div>
<div className="font-medium text-neutral-900 dark:text-white"> <span className="font-medium text-sm text-neutral-900 dark:text-white">
{(invoice.amount_paid / 100).toLocaleString('en-US', { style: 'currency', currency: invoice.currency.toUpperCase() })} {(invoice.amount_paid / 100).toLocaleString('en-US', { style: 'currency', currency: invoice.currency.toUpperCase() })}
</div> </span>
<div className="text-xs text-neutral-500"> <span className="text-xs text-neutral-500 ml-2">
{new Date(invoice.created * 1000).toLocaleDateString()} {new Date(invoice.created * 1000).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })}
</div> </span>
</div> </div>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-3">
<span className={`px-2.5 py-0.5 rounded-full text-xs font-medium capitalize ${ <span className={`px-2 py-0.5 rounded-full text-xs font-medium capitalize ${
invoice.status === 'paid' invoice.status === 'paid'
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300' ? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300'
: invoice.status === 'open' : invoice.status === 'open'
@@ -895,34 +889,24 @@ export default function OrganizationSettings() {
{invoice.status} {invoice.status}
</span> </span>
{invoice.invoice_pdf && ( {invoice.invoice_pdf && (
<a <a href={invoice.invoice_pdf} target="_blank" rel="noopener noreferrer"
href={invoice.invoice_pdf} className="p-1.5 text-neutral-400 hover:text-neutral-900 dark:hover:text-white rounded-lg transition-colors" title="Download PDF">
target="_blank" <DownloadIcon className="w-4 h-4" />
rel="noopener noreferrer"
className="p-2 text-neutral-500 hover:text-neutral-900 dark:hover:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors"
title="Download PDF"
>
<DownloadIcon className="w-5 h-5" />
</a> </a>
)} )}
{invoice.hosted_invoice_url && ( {invoice.hosted_invoice_url && (
<a <a href={invoice.hosted_invoice_url} target="_blank" rel="noopener noreferrer"
href={invoice.hosted_invoice_url} className="p-1.5 text-neutral-400 hover:text-neutral-900 dark:hover:text-white rounded-lg transition-colors" title="View invoice">
target="_blank" <ExternalLinkIcon className="w-4 h-4" />
rel="noopener noreferrer"
className="p-2 text-neutral-500 hover:text-neutral-900 dark:hover:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors"
title="View Invoice"
>
<ExternalLinkIcon className="w-5 h-5" />
</a> </a>
)} )}
</div> </div>
</div> </div>
))} ))
</div> )}
)} </div>
</div> </div>
</div> )}
</div> </div>
)} )}
</div> </div>