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') }