From cc89a2797278afe7b9c14422636ead8868fa8886 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Fri, 20 Feb 2026 16:18:00 +0100 Subject: [PATCH] feat: add invoice preview functionality in OrganizationSettings to enhance user experience with upcoming billing details --- components/settings/OrganizationSettings.tsx | 54 ++++++++++++++++++-- lib/api/billing.ts | 23 +++++++++ 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/components/settings/OrganizationSettings.tsx b/components/settings/OrganizationSettings.tsx index 6ea4cfc..cf1d82f 100644 --- a/components/settings/OrganizationSettings.tsx +++ b/components/settings/OrganizationSettings.tsx @@ -16,7 +16,7 @@ import { OrganizationInvitation, Organization } from '@/lib/api/organization' -import { getSubscription, createPortalSession, getInvoices, cancelSubscription, resumeSubscription, changePlan, createCheckoutSession, SubscriptionDetails, Invoice } from '@/lib/api/billing' +import { getSubscription, createPortalSession, getInvoices, cancelSubscription, resumeSubscription, changePlan, previewInvoice, createCheckoutSession, SubscriptionDetails, Invoice, PreviewInvoiceResult } from '@/lib/api/billing' import { TRAFFIC_TIERS, PLAN_ID_SOLO, getTierIndexForLimit, getLimitForTierIndex, getSitesLimitForPlan } from '@/lib/plans' import { getAuditLog, AuditLogEntry, GetAuditLogParams } from '@/lib/api/audit' import { getNotificationSettings, updateNotificationSettings } from '@/lib/api/notification-settings' @@ -87,6 +87,8 @@ export default function OrganizationSettings() { const [showChangePlanModal, setShowChangePlanModal] = useState(false) const [changePlanTierIndex, setChangePlanTierIndex] = useState(2) const [changePlanYearly, setChangePlanYearly] = useState(false) + const [invoicePreview, setInvoicePreview] = useState(null) + const [isLoadingPreview, setIsLoadingPreview] = useState(false) const [isChangingPlan, setIsChangingPlan] = useState(false) const [invoices, setInvoices] = useState([]) const [isLoadingInvoices, setIsLoadingInvoices] = useState(false) @@ -349,11 +351,27 @@ export default function OrganizationSettings() { setChangePlanTierIndex(2) } setChangePlanYearly(subscription?.billing_interval === 'year') + setInvoicePreview(null) setShowChangePlanModal(true) } const hasActiveSubscription = subscription?.subscription_status === 'active' || subscription?.subscription_status === 'trialing' + useEffect(() => { + if (!showChangePlanModal || !hasActiveSubscription) { + setInvoicePreview(null) + return + } + let cancelled = false + setIsLoadingPreview(true) + const interval = changePlanYearly ? 'year' : 'month' + const limit = getLimitForTierIndex(changePlanTierIndex) + previewInvoice({ plan_id: PLAN_ID_SOLO, interval, limit }) + .then((res) => { if (!cancelled) setInvoicePreview(res ?? null) }) + .finally(() => { if (!cancelled) setIsLoadingPreview(false) }) + return () => { cancelled = true } + }, [showChangePlanModal, hasActiveSubscription, changePlanTierIndex, changePlanYearly]) + const handleChangePlanSubmit = async () => { const interval = changePlanYearly ? 'year' : 'month' const limit = getLimitForTierIndex(changePlanTierIndex) @@ -925,8 +943,18 @@ export default function OrganizationSettings() {
{(() => { - const d = subscription.current_period_end ? new Date(subscription.current_period_end as string) : null - return d && !Number.isNaN(d.getTime()) && d.getTime() !== 0 ? d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }) : '—' + const ts = subscription.next_invoice_period_end ?? subscription.current_period_end + const d = ts ? new Date(typeof ts === 'number' ? ts * 1000 : ts) : null + const dateStr = d && !Number.isNaN(d.getTime()) && d.getTime() !== 0 + ? d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }) + : '—' + const amount = subscription.next_invoice_amount_due != null && subscription.next_invoice_currency + ? (subscription.next_invoice_amount_due / 100).toLocaleString('en-US', { + style: 'currency', + currency: subscription.next_invoice_currency.toUpperCase(), + }) + : null + return amount && dateStr !== '—' ? `${dateStr} for ${amount}` : dateStr })()}
@@ -1426,6 +1454,26 @@ export default function OrganizationSettings() { + {hasActiveSubscription && ( +
+ {isLoadingPreview ? ( +
+ + Calculating next invoice… +
+ ) : invoicePreview ? ( +

+ Next invoice:{' '} + {(invoicePreview.amount_due / 100).toLocaleString('en-US', { + style: 'currency', + currency: invoicePreview.currency.toUpperCase(), + })}{' '} + on {new Date(invoicePreview.period_end * 1000).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })}{' '} + (prorated) +

+ ) : null} +
+ )}