From eca21bf627607fd36983d9107968caf21fd007de Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Mon, 23 Mar 2026 16:36:54 +0100 Subject: [PATCH] feat(billing): update frontend for polar migration Update billing types, remove invoice preview, replace Stripe invoice display with Polar orders, update tax ID from array to single object, remove upcoming invoice amount display. --- app/admin/orgs/[id]/page.tsx | 10 +- app/page.tsx | 17 +-- components/PricingSection.tsx | 4 +- components/settings/OrganizationSettings.tsx | 122 +++++-------------- lib/api/admin.ts | 4 +- lib/api/billing.ts | 45 ++----- 6 files changed, 54 insertions(+), 148 deletions(-) diff --git a/app/admin/orgs/[id]/page.tsx b/app/admin/orgs/[id]/page.tsx index 3559ff9..bf93de2 100644 --- a/app/admin/orgs/[id]/page.tsx +++ b/app/admin/orgs/[id]/page.tsx @@ -135,11 +135,11 @@ export default function AdminOrgDetailPage() { {org.current_period_end ? formatDateTime(new Date(org.current_period_end)) : '-'} - Stripe Cust: - {org.stripe_customer_id || '-'} - - Stripe Sub: - {org.stripe_subscription_id || '-'} + Customer ID: + {org.billing_customer_id || '-'} + + Subscription ID: + {org.billing_subscription_id || '-'} diff --git a/app/page.tsx b/app/page.tsx index 24cd274..fda9c5f 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -334,7 +334,7 @@ export default function HomePage() { return `${label} Plan` })()}

- {(typeof subscription.sites_count === 'number' || (subscription.pageview_limit > 0 && typeof subscription.pageview_usage === 'number') || (subscription.next_invoice_amount_due != null && subscription.next_invoice_currency && !subscription.cancel_at_period_end && (subscription.subscription_status === 'active' || subscription.subscription_status === 'trialing'))) && ( + {(typeof subscription.sites_count === 'number' || (subscription.pageview_limit > 0 && typeof subscription.pageview_usage === 'number') || (!subscription.cancel_at_period_end && (subscription.subscription_status === 'active' || subscription.subscription_status === 'trialing'))) && (

{typeof subscription.sites_count === 'number' && ( Sites: {(() => { @@ -346,20 +346,9 @@ export default function HomePage() { {subscription.pageview_limit > 0 && typeof subscription.pageview_usage === 'number' && ( Pageviews: {subscription.pageview_usage.toLocaleString()}/{subscription.pageview_limit.toLocaleString()} )} - {subscription.next_invoice_amount_due != null && subscription.next_invoice_currency && !subscription.cancel_at_period_end && (subscription.subscription_status === 'active' || subscription.subscription_status === 'trialing') && ( + {!subscription.cancel_at_period_end && (subscription.subscription_status === 'active' || subscription.subscription_status === 'trialing') && subscription.current_period_end && ( - Renews {(() => { - 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 - ? formatDate(d) - : null - const amount = (subscription.next_invoice_amount_due / 100).toLocaleString('en-US', { - style: 'currency', - currency: subscription.next_invoice_currency.toUpperCase(), - }) - return dateStr ? `${dateStr} for ${amount}` : amount - })()} + Renews {formatDate(new Date(subscription.current_period_end))} )}

diff --git a/components/PricingSection.tsx b/components/PricingSection.tsx index 51f5ef8..abe8c1d 100644 --- a/components/PricingSection.tsx +++ b/components/PricingSection.tsx @@ -109,7 +109,7 @@ export default function PricingSection() { const [loadingPlan, setLoadingPlan] = useState(null) const { user } = useAuth() - // * Show toast when redirected from Stripe Checkout with canceled=true + // * Show toast when redirected from Polar Checkout with canceled=true useEffect(() => { if (searchParams.get('canceled') === 'true') { toast.info('Checkout was canceled. You can try again whenever you’re ready.') @@ -196,7 +196,7 @@ export default function PricingSection() { limit, }) - // 3. Redirect to Stripe Checkout + // 3. Redirect to Polar Checkout if (url) { window.location.href = url } else { diff --git a/components/settings/OrganizationSettings.tsx b/components/settings/OrganizationSettings.tsx index 4583879..f9c885e 100644 --- a/components/settings/OrganizationSettings.tsx +++ b/components/settings/OrganizationSettings.tsx @@ -19,7 +19,7 @@ import { OrganizationInvitation, Organization } from '@/lib/api/organization' -import { getSubscription, createPortalSession, getInvoices, cancelSubscription, resumeSubscription, changePlan, previewInvoice, createCheckoutSession, SubscriptionDetails, Invoice, PreviewInvoiceResult } from '@/lib/api/billing' +import { getSubscription, createPortalSession, getOrders, cancelSubscription, resumeSubscription, changePlan, createCheckoutSession, SubscriptionDetails, Order } from '@/lib/api/billing' import { TRAFFIC_TIERS, PLAN_ID_SOLO, PLAN_ID_TEAM, PLAN_ID_BUSINESS, getTierIndexForLimit, getLimitForTierIndex, getSitesLimitForPlan } from '@/lib/plans' import { getAuditLog, AuditLogEntry, GetAuditLogParams } from '@/lib/api/audit' import { getNotificationSettings, updateNotificationSettings } from '@/lib/api/notification-settings' @@ -36,7 +36,6 @@ import { XIcon, Captcha, BookOpenIcon, - DownloadIcon, ExternalLinkIcon, LayoutDashboardIcon, Spinner, @@ -93,10 +92,8 @@ export default function OrganizationSettings() { const [changePlanId, setChangePlanId] = useState(PLAN_ID_SOLO) 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 [orders, setOrders] = useState([]) const [isLoadingInvoices, setIsLoadingInvoices] = useState(false) // Invite State @@ -195,14 +192,14 @@ export default function OrganizationSettings() { } }, [currentOrgId]) - const loadInvoices = useCallback(async () => { + const loadOrders = useCallback(async () => { if (!currentOrgId) return setIsLoadingInvoices(true) try { - const invs = await getInvoices() - setInvoices(invs) + const ords = await getOrders() + setOrders(ords) } catch (error) { - logger.error('Failed to load invoices:', error) + logger.error('Failed to load orders:', error) } finally { setIsLoadingInvoices(false) } @@ -231,9 +228,9 @@ export default function OrganizationSettings() { useEffect(() => { if (activeTab === 'billing' && currentOrgId) { loadSubscription() - loadInvoices() + loadOrders() } - }, [activeTab, currentOrgId, loadSubscription, loadInvoices]) + }, [activeTab, currentOrgId, loadSubscription, loadOrders]) const loadAudit = useCallback(async () => { if (!currentOrgId) return @@ -307,19 +304,8 @@ export default function OrganizationSettings() { useEffect(() => { if (!showChangePlanModal || !hasActiveSubscription) { - setInvoicePreview(null) return } - let cancelled = false - setIsLoadingPreview(true) - setInvoicePreview(null) - const interval = changePlanYearly ? 'year' : 'month' - const limit = getLimitForTierIndex(changePlanTierIndex) - previewInvoice({ plan_id: changePlanId, interval, limit }) - .then((res) => { if (!cancelled) setInvoicePreview(res ?? null) }) - .catch(() => { if (!cancelled) { setInvoicePreview(null) } }) - .finally(() => { if (!cancelled) setIsLoadingPreview(false) }) - return () => { cancelled = true } }, [showChangePlanModal, hasActiveSubscription, changePlanId, changePlanTierIndex, changePlanYearly]) // If no org ID, we are in personal organization context, so don't show org settings @@ -382,7 +368,6 @@ export default function OrganizationSettings() { setChangePlanTierIndex(2) } setChangePlanYearly(subscription?.billing_interval === 'year') - setInvoicePreview(null) setShowChangePlanModal(true) } @@ -954,19 +939,15 @@ export default function OrganizationSettings() { Change plan - {(subscription.business_name || (subscription.tax_ids && subscription.tax_ids.length > 0)) && ( + {(subscription.business_name || subscription.tax_id) && (
{subscription.business_name && (
Billing for: {subscription.business_name}
)} - {subscription.tax_ids && subscription.tax_ids.length > 0 && ( -
- Tax ID{subscription.tax_ids.length > 1 ? 's' : ''}:{' '} - {subscription.tax_ids.map((t) => { - const label = t.type === 'eu_vat' ? 'VAT' : t.type === 'us_ein' ? 'EIN' : t.type.replace(/_/g, ' ').toUpperCase() - return `${label} ${t.value}${t.country ? ` (${t.country})` : ''}` - }).join(', ')} -
+ {subscription.tax_id && ( + + Tax ID: {subscription.tax_id.value} ({subscription.tax_id.type}) + )}
)} @@ -1014,18 +995,11 @@ export default function OrganizationSettings() {
{(() => { - 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 + const ts = subscription.current_period_end + const d = ts ? new Date(ts) : null + return d && !Number.isNaN(d.getTime()) && d.getTime() !== 0 ? formatDate(d) : '—' - 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 })()}
@@ -1062,57 +1036,38 @@ export default function OrganizationSettings() { )} - {/* Invoice History */} + {/* Order History */}
-

Recent invoices

+

Recent orders

{isLoadingInvoices ? ( - ) : invoices.length === 0 ? ( -
No invoices found.
+ ) : orders.length === 0 ? ( +
No orders found.
) : ( <> - {invoices.map((invoice) => ( -
+ {orders.map((order) => ( +
- {(invoice.amount_paid / 100).toLocaleString('en-US', { style: 'currency', currency: invoice.currency.toUpperCase() })} + {(order.total_amount / 100).toLocaleString('en-US', { style: 'currency', currency: order.currency.toUpperCase() })} - {formatDate(new Date(invoice.created * 1000))} + {formatDate(new Date(order.created_at))}
- {invoice.status} + {order.status} - {invoice.invoice_pdf && ( - - - Download PDF - - )} - {invoice.hosted_invoice_url && ( - - - {invoice.status === 'open' ? 'Pay now' : <>View Invoice} - - )}
))} @@ -1595,26 +1550,9 @@ 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 {formatDate(new Date(invoicePreview.period_end * 1000))}{' '} - (prorated) -

- ) : ( -

- Unable to calculate preview. Your next invoice will reflect prorations. -

- )} +

+ Your plan will be updated. Any prorations will be reflected on your next invoice. +

)}
diff --git a/lib/api/admin.ts b/lib/api/admin.ts index f9ab143..8e5cf12 100644 --- a/lib/api/admin.ts +++ b/lib/api/admin.ts @@ -2,8 +2,8 @@ import { authFetch } from './client' export interface AdminOrgSummary { organization_id: string - stripe_customer_id: string - stripe_subscription_id: string + billing_customer_id: string + billing_subscription_id: string plan_id: string billing_interval: string pageview_limit: number diff --git a/lib/api/billing.ts b/lib/api/billing.ts index 0bb3245..49660a6 100644 --- a/lib/api/billing.ts +++ b/lib/api/billing.ts @@ -19,16 +19,10 @@ export interface SubscriptionDetails { sites_count?: number /** Pageviews in current billing period (when pageview_limit > 0). Present when backend supports usage API. */ pageview_usage?: number - /** Business name from Stripe Tax ID collection / business purchase flow (optional). */ + /** Business name from billing (optional). */ business_name?: string - /** Tax IDs collected on the Stripe customer (VAT, EIN, etc.) for invoice verification. */ - tax_ids?: TaxID[] - /** Next invoice amount in cents (for "Renews on X for €Y" display). */ - next_invoice_amount_due?: number - /** Currency for next invoice (e.g. eur). */ - next_invoice_currency?: string - /** Unix timestamp when next invoice period ends. */ - next_invoice_period_end?: number + /** Tax ID collected on the billing customer (VAT, EIN, etc.). */ + tax_id?: TaxID | null } export async function getSubscription(): Promise { @@ -66,22 +60,6 @@ export interface ChangePlanParams { limit: number } -export interface PreviewInvoiceResult { - amount_due: number - currency: string - period_end: number -} - -export async function previewInvoice(params: ChangePlanParams): Promise { - const res = await apiRequest>('/api/billing/preview-invoice', { - method: 'POST', - body: JSON.stringify(params), - }) - if (res && typeof res === 'object' && 'amount_due' in res && typeof (res as PreviewInvoiceResult).amount_due === 'number') { - return res as PreviewInvoiceResult - } - return null -} export async function changePlan(params: ChangePlanParams): Promise<{ ok: boolean }> { return apiRequest<{ ok: boolean }>('/api/billing/change-plan', { @@ -103,17 +81,18 @@ export async function createCheckoutSession(params: CreateCheckoutParams): Promi }) } -export interface Invoice { +export interface Order { id: string - amount_paid: number - amount_due: number + total_amount: number + subtotal_amount: number + tax_amount: number currency: string status: string - created: number - hosted_invoice_url: string - invoice_pdf: string + created_at: string + paid: boolean + invoice_number: string } -export async function getInvoices(): Promise { - return apiRequest('/api/billing/invoices') +export async function getOrders(): Promise { + return apiRequest('/api/billing/invoices') }