PageSpeed monitoring, Polar billing, sidebar polish, frontend consistency audit #68

Merged
uz1mani merged 45 commits from staging into main 2026-03-23 19:07:54 +00:00
36 changed files with 2093 additions and 475 deletions
Showing only changes of commit eca21bf627 - Show all commits

View File

@@ -135,11 +135,11 @@ export default function AdminOrgDetailPage() {
{org.current_period_end ? formatDateTime(new Date(org.current_period_end)) : '-'}
</span>
<span className="text-neutral-500">Stripe Cust:</span>
<span className="font-mono text-xs">{org.stripe_customer_id || '-'}</span>
<span className="text-neutral-500">Stripe Sub:</span>
<span className="font-mono text-xs">{org.stripe_subscription_id || '-'}</span>
<span className="text-neutral-500">Customer ID:</span>
<span className="font-mono text-xs">{org.billing_customer_id || '-'}</span>
<span className="text-neutral-500">Subscription ID:</span>
<span className="font-mono text-xs">{org.billing_subscription_id || '-'}</span>
</div>
</div>

View File

@@ -334,7 +334,7 @@ export default function HomePage() {
return `${label} Plan`
})()}
</p>
{(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'))) && (
<p className="text-sm text-neutral-400 mt-1">
{typeof subscription.sites_count === 'number' && (
<span>Sites: {(() => {
@@ -346,20 +346,9 @@ export default function HomePage() {
{subscription.pageview_limit > 0 && typeof subscription.pageview_usage === 'number' && (
<span>Pageviews: {subscription.pageview_usage.toLocaleString()}/{subscription.pageview_limit.toLocaleString()}</span>
)}
{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 && (
<span className="block mt-1">
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))}
</span>
)}
</p>

View File

@@ -109,7 +109,7 @@ export default function PricingSection() {
const [loadingPlan, setLoadingPlan] = useState<string | null>(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 youre 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 {

View File

@@ -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<string>(PLAN_ID_SOLO)
const [changePlanTierIndex, setChangePlanTierIndex] = useState(2)
const [changePlanYearly, setChangePlanYearly] = useState(false)
const [invoicePreview, setInvoicePreview] = useState<PreviewInvoiceResult | null>(null)
const [isLoadingPreview, setIsLoadingPreview] = useState(false)
const [isChangingPlan, setIsChangingPlan] = useState(false)
const [invoices, setInvoices] = useState<Invoice[]>([])
const [orders, setOrders] = useState<Order[]>([])
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
</Button>
</div>
{(subscription.business_name || (subscription.tax_ids && subscription.tax_ids.length > 0)) && (
{(subscription.business_name || subscription.tax_id) && (
<div className="px-6 pb-2 -mt-2 space-y-1 text-sm text-neutral-500 dark:text-neutral-400">
{subscription.business_name && (
<div>Billing for: {subscription.business_name}</div>
)}
{subscription.tax_ids && subscription.tax_ids.length > 0 && (
<div>
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(', ')}
</div>
{subscription.tax_id && (
<span>
Tax ID: {subscription.tax_id.value} ({subscription.tax_id.type})
</span>
)}
</div>
)}
@@ -1014,18 +995,11 @@ export default function OrganizationSettings() {
</div>
<div className="text-lg font-semibold text-neutral-900 dark:text-white">
{(() => {
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
})()}
</div>
</div>
@@ -1062,57 +1036,38 @@ export default function OrganizationSettings() {
)}
</div>
{/* Invoice History */}
{/* Order History */}
<div>
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white mb-3">Recent invoices</h3>
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white mb-3">Recent orders</h3>
<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">
{isLoadingInvoices ? (
<InvoicesListSkeleton />
) : invoices.length === 0 ? (
<div className="p-8 text-center text-neutral-500 dark:text-neutral-400">No invoices found.</div>
) : orders.length === 0 ? (
<div className="p-8 text-center text-neutral-500 dark:text-neutral-400">No orders found.</div>
) : (
<>
{invoices.map((invoice) => (
<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">
{orders.map((order) => (
<div key={order.id} className="px-4 py-3 flex items-center justify-between hover:bg-neutral-50 dark:hover:bg-neutral-800/50 transition-colors">
<div className="flex items-center gap-3">
<div>
<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() })}
{(order.total_amount / 100).toLocaleString('en-US', { style: 'currency', currency: order.currency.toUpperCase() })}
</span>
<span className="text-xs text-neutral-500 ml-2">
{formatDate(new Date(invoice.created * 1000))}
{formatDate(new Date(order.created_at))}
</span>
</div>
</div>
<div className="flex items-center gap-3">
<span className={`px-2 py-0.5 rounded-full text-xs font-medium capitalize ${
invoice.status === 'paid'
order.status === 'paid'
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300'
: invoice.status === 'open'
: order.status === 'open'
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300'
: 'bg-neutral-100 text-neutral-700 dark:bg-neutral-800 dark:text-neutral-300'
}`}>
{invoice.status}
{order.status}
</span>
{invoice.invoice_pdf && (
<a href={invoice.invoice_pdf} target="_blank" rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-2.5 py-1.5 text-xs font-medium text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange" title="Download PDF">
<DownloadIcon className="w-3.5 h-3.5" />
<span className="hidden sm:inline">Download</span> PDF
</a>
)}
{invoice.hosted_invoice_url && (
<a href={invoice.hosted_invoice_url} target="_blank" rel="noopener noreferrer"
className={`inline-flex items-center gap-2 px-2.5 py-1.5 text-xs font-medium rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange ${
invoice.status === 'open'
? 'bg-brand-orange text-white hover:bg-brand-orange-hover'
: 'text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800'
}`}
title={invoice.status === 'open' ? 'Pay now' : 'View invoice'}>
<ExternalLinkIcon className="w-3.5 h-3.5" />
{invoice.status === 'open' ? 'Pay now' : <><span className="hidden sm:inline">View </span>Invoice</>}
</a>
)}
</div>
</div>
))}
@@ -1595,26 +1550,9 @@ export default function OrganizationSettings() {
</div>
{hasActiveSubscription && (
<div className="mt-4 p-4 rounded-lg bg-neutral-100 dark:bg-neutral-800/50 border border-neutral-200 dark:border-neutral-700">
{isLoadingPreview ? (
<div className="flex items-center gap-2 text-sm text-neutral-600 dark:text-neutral-400">
<Spinner className="w-4 h-4" />
Calculating next invoice…
</div>
) : invoicePreview ? (
<p className="text-sm text-neutral-700 dark:text-neutral-300">
Next invoice:{' '}
{(invoicePreview.amount_due / 100).toLocaleString('en-US', {
style: 'currency',
currency: invoicePreview.currency.toUpperCase(),
})}{' '}
on {formatDate(new Date(invoicePreview.period_end * 1000))}{' '}
<span className="text-neutral-500">(prorated)</span>
</p>
) : (
<p className="text-sm text-neutral-600 dark:text-neutral-400">
Unable to calculate preview. Your next invoice will reflect prorations.
</p>
)}
<p className="text-sm text-neutral-600 dark:text-neutral-400">
Your plan will be updated. Any prorations will be reflected on your next invoice.
</p>
</div>
)}
<div className="flex gap-2 mt-6">

View File

@@ -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

View File

@@ -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<SubscriptionDetails> {
@@ -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<PreviewInvoiceResult | null> {
const res = await apiRequest<PreviewInvoiceResult | Record<string, never>>('/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<Invoice[]> {
return apiRequest<Invoice[]>('/api/billing/invoices')
export async function getOrders(): Promise<Order[]> {
return apiRequest<Order[]>('/api/billing/invoices')
}