Billing Page
Current plan status, payment method management, invoice history, and upgrade/cancel flow.
Components used: Layout · Sidebar · Header · Card · Badge · Table · Button · Modal · Section
Code
- HTML · @primus/ui-core
- React
- Angular
<div style="background:var(--background);width:100%;padding:1.5rem;box-sizing:border-box;display:flex;flex-direction:column;gap:1.25rem">
<!-- Page title -->
<div style="display:flex;align-items:center;justify-content:space-between">
<div>
<h1 style="margin:0 0 0.25rem;font-size:1.25rem;font-weight:700">Billing</h1>
<p style="margin:0;font-size:0.8125rem;color:var(--muted-foreground)">Manage your subscription and payment details</p>
</div>
</div>
<!-- Current plan card -->
<div class="card" style="padding:1.5rem">
<div style="display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:1.25rem">
<div>
<p style="margin:0 0 0.25rem;font-size:0.75rem;text-transform:uppercase;letter-spacing:0.06em;color:var(--muted-foreground);font-weight:600">Current Plan</p>
<div style="display:flex;align-items:center;gap:0.75rem">
<span style="font-size:1.5rem;font-weight:700">Enterprise</span>
<span class="badge success">Active</span>
</div>
<p style="margin:0.25rem 0 0;font-size:0.8125rem;color:var(--muted-foreground)">$9,600 / year · Renews Apr 1, 2027</p>
</div>
<div class="hstack">
<button class="outline small">Change plan</button>
<button class="ghost small" style="color:var(--danger)">Cancel</button>
</div>
</div>
<div style="height:1px;background:var(--border);margin-bottom:1.25rem"></div>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:1rem;font-size:0.8125rem">
<div style="padding:0.875rem;background:var(--muted);border-radius:6px">
<p style="margin:0 0 0.375rem;color:var(--muted-foreground)">Users</p>
<p style="margin:0;font-weight:600">Unlimited</p>
</div>
<div style="padding:0.875rem;background:var(--muted);border-radius:6px">
<p style="margin:0 0 0.375rem;color:var(--muted-foreground)">Storage</p>
<p style="margin:0;font-weight:600">1 TB</p>
</div>
<div style="padding:0.875rem;background:var(--muted);border-radius:6px">
<p style="margin:0 0 0.375rem;color:var(--muted-foreground)">API calls</p>
<p style="margin:0;font-weight:600">Unlimited</p>
</div>
<div style="padding:0.875rem;background:var(--muted);border-radius:6px">
<p style="margin:0 0 0.375rem;color:var(--muted-foreground)">SLA</p>
<p style="margin:0;font-weight:600">99.99%</p>
</div>
</div>
</div>
<!-- Payment method card -->
<div class="card" style="padding:1.5rem">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1.25rem">
<h3 style="margin:0;font-size:0.9375rem;font-weight:600">Payment method</h3>
<button class="outline small">Update</button>
</div>
<div style="display:flex;align-items:center;gap:1rem;padding:0.875rem;border:1px solid var(--border);border-radius:8px;background:var(--muted)">
<div style="width:52px;height:32px;background:white;border:1px solid var(--border);border-radius:5px;display:flex;align-items:center;justify-content:center;font-size:0.65rem;font-weight:800;color:#1a1f71;letter-spacing:0.02em;flex-shrink:0">VISA</div>
<div style="flex:1">
<p style="margin:0;font-size:0.875rem;font-weight:500">•••• •••• •••• 4242</p>
<p style="margin:0;font-size:0.775rem;color:var(--muted-foreground)">Expires 12/28 · Jane Doe</p>
</div>
<span class="badge secondary" style="font-size:0.7rem">Default</span>
</div>
</div>
<!-- Invoice history card -->
<div class="card" style="padding:0;overflow:hidden">
<div style="padding:1rem 1.5rem;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between">
<h3 style="margin:0;font-size:0.9375rem;font-weight:600">Invoice history</h3>
<button class="ghost small">Export all</button>
</div>
<table style="width:100%;border-collapse:collapse;font-size:0.8125rem">
<thead>
<tr style="border-bottom:1px solid var(--border)">
<th style="padding:0.75rem 1.5rem;text-align:left;font-weight:500;color:var(--muted-foreground)">Date</th>
<th style="padding:0.75rem 0.75rem;text-align:left;font-weight:500;color:var(--muted-foreground)">Description</th>
<th style="padding:0.75rem 0.75rem;text-align:right;font-weight:500;color:var(--muted-foreground)">Amount</th>
<th style="padding:0.75rem 0.75rem;text-align:left;font-weight:500;color:var(--muted-foreground)">Status</th>
<th style="padding:0.75rem 1.5rem;text-align:right;font-weight:500;color:var(--muted-foreground)"></th>
</tr>
</thead>
<tbody>
<tr style="border-bottom:1px solid var(--border)">
<td style="padding:0.875rem 1.5rem;color:var(--muted-foreground)">Apr 1, 2026</td>
<td style="padding:0.875rem 0.75rem;font-weight:500">Enterprise — Annual</td>
<td style="padding:0.875rem 0.75rem;text-align:right;font-weight:600">$9,600</td>
<td style="padding:0.875rem 0.75rem"><span class="badge success" style="font-size:0.7rem">Paid</span></td>
<td style="padding:0.875rem 1.5rem;text-align:right"><a href="#" style="font-size:0.8rem;color:var(--primary);text-decoration:none;font-weight:500">PDF</a></td>
</tr>
<tr style="border-bottom:1px solid var(--border)">
<td style="padding:0.875rem 1.5rem;color:var(--muted-foreground)">Apr 1, 2025</td>
<td style="padding:0.875rem 0.75rem;font-weight:500">Enterprise — Annual</td>
<td style="padding:0.875rem 0.75rem;text-align:right;font-weight:600">$9,600</td>
<td style="padding:0.875rem 0.75rem"><span class="badge success" style="font-size:0.7rem">Paid</span></td>
<td style="padding:0.875rem 1.5rem;text-align:right"><a href="#" style="font-size:0.8rem;color:var(--primary);text-decoration:none;font-weight:500">PDF</a></td>
</tr>
<tr>
<td style="padding:0.875rem 1.5rem;color:var(--muted-foreground)">Apr 1, 2024</td>
<td style="padding:0.875rem 0.75rem;font-weight:500">Pro — Annual</td>
<td style="padding:0.875rem 0.75rem;text-align:right;font-weight:600">$4,788</td>
<td style="padding:0.875rem 0.75rem"><span class="badge success" style="font-size:0.7rem">Paid</span></td>
<td style="padding:0.875rem 1.5rem;text-align:right"><a href="#" style="font-size:0.8rem;color:var(--primary);text-decoration:none;font-weight:500">PDF</a></td>
</tr>
</tbody>
</table>
</div>
</div>
import {
PrimusLayout, PrimusSidebar, PrimusHeader,
Card, Badge, Table, PrimusButton, PrimusModal, PrimusSection,
} from 'primus-react-ui';
import { useState } from 'react';
const invoiceColumns = [
{ key: 'date', header: 'Date' },
{ key: 'description', header: 'Description' },
{ key: 'amount', header: 'Amount' },
{ key: 'status', header: 'Status',
render: row => <Badge variant="success">{row.status}</Badge> },
{ key: 'pdf', header: '',
render: row => <a href={row.pdfUrl}>PDF</a> },
];
export function BillingPage({ plan, paymentMethod, invoices }) {
const [cancelOpen, setCancelOpen] = useState(false);
return (
<PrimusLayout
sidebar={<PrimusSidebar items={NAV_ITEMS} activeId="billing" />}
header={
<PrimusHeader title="Billing"
subtitle="Manage your subscription and payment details" />
}
>
{/* Current plan */}
<Card padding="lg" style={{ marginBottom: '1.25rem' }}>
<PrimusSection title="Current Plan"
actions={<Badge variant="success">Active</Badge>}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<h2 style={{ margin: '0 0 0.25rem' }}>{plan.name}</h2>
<p style={{ margin: 0, color: 'var(--muted-foreground)' }}>
{plan.price} / {plan.cycle} · Renews {plan.renewsAt}
</p>
</div>
<div className="hstack">
<PrimusButton variant="outline" size="sm"
onClick={() => navigate('/billing/change')}>
Change plan
</PrimusButton>
<PrimusButton variant="ghost" size="sm"
style={{ color: 'var(--danger)' }}
onClick={() => setCancelOpen(true)}>
Cancel
</PrimusButton>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: '1rem', marginTop: '1.25rem' }}>
{plan.features.map(f => (
<div key={f.label} style={{ padding: '0.875rem', background: 'var(--muted)', borderRadius: 6 }}>
<p style={{ margin: '0 0 0.375rem', color: 'var(--muted-foreground)', fontSize: '0.8125rem' }}>{f.label}</p>
<p style={{ margin: 0, fontWeight: 600, fontSize: '0.8125rem' }}>{f.value}</p>
</div>
))}
</div>
</PrimusSection>
</Card>
{/* Payment method */}
<Card padding="lg" style={{ marginBottom: '1.25rem' }}>
<PrimusSection title="Payment method"
actions={<PrimusButton variant="outline" size="sm">Update</PrimusButton>}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', padding: '0.875rem', background: 'var(--muted)', borderRadius: 8 }}>
<div className="card-brand-logo">VISA</div>
<div style={{ flex: 1 }}>
<p style={{ margin: '0 0 0.125rem', fontWeight: 500 }}>•••• •••• •••• {paymentMethod.last4}</p>
<p style={{ margin: 0, fontSize: '0.775rem', color: 'var(--muted-foreground)' }}>
Expires {paymentMethod.expiry} · {paymentMethod.name}
</p>
</div>
<Badge variant="secondary">Default</Badge>
</div>
</PrimusSection>
</Card>
{/* Invoice history */}
<Card padding="none">
<PrimusSection title="Invoice history"
actions={<PrimusButton variant="ghost" size="sm">Export all</PrimusButton>}
/>
<Table columns={invoiceColumns} data={invoices} rowKey="id" />
</Card>
{/* Cancel confirmation modal */}
<PrimusModal open={cancelOpen} title="Cancel subscription" size="sm"
onClose={() => setCancelOpen(false)}
>
<p>Your plan will remain active until the end of the billing period.</p>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
<PrimusButton variant="outline" onClick={() => setCancelOpen(false)}>Keep plan</PrimusButton>
<PrimusButton variant="danger" onClick={handleCancel}>Cancel subscription</PrimusButton>
</div>
</PrimusModal>
</PrimusLayout>
);
}
<!-- billing.component.html -->
<primus-layout>
<primus-sidebar [items]="navItems" activeId="billing"></primus-sidebar>
<primus-header title="Billing"
subtitle="Manage your subscription and payment details">
</primus-header>
<div class="vstack" style="gap:1.25rem">
<!-- Current plan -->
<primus-card padding="lg">
<primus-section title="Current Plan">
<div style="display:flex;justify-content:space-between;align-items:center">
<div>
<h2>{{ plan.name }}</h2>
<p>{{ plan.price }} / {{ plan.cycle }} · Renews {{ plan.renewsAt }}</p>
</div>
<div class="hstack">
<primus-button variant="outline" size="sm"
(clicked)="changePlan()">Change plan</primus-button>
<primus-button variant="ghost" size="sm"
style="color:var(--danger)"
(clicked)="cancelOpen = true">Cancel</primus-button>
</div>
</div>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:1rem;margin-top:1.25rem">
<div *ngFor="let f of plan.features"
style="padding:0.875rem;background:var(--muted);border-radius:6px">
<p style="color:var(--muted-foreground)">{{ f.label }}</p>
<p style="font-weight:600">{{ f.value }}</p>
</div>
</div>
</primus-section>
</primus-card>
<!-- Payment method -->
<primus-card padding="lg">
<primus-section title="Payment method">
<div style="display:flex;align-items:center;gap:1rem;padding:0.875rem;background:var(--muted);border-radius:8px">
<div class="card-brand-logo">VISA</div>
<div style="flex:1">
<p style="font-weight:500">•••• •••• •••• {{ paymentMethod.last4 }}</p>
<p style="color:var(--muted-foreground);font-size:0.775rem">
Expires {{ paymentMethod.expiry }} · {{ paymentMethod.name }}
</p>
</div>
<primus-badge variant="secondary">Default</primus-badge>
</div>
<div style="margin-top:0.75rem;text-align:right">
<primus-button variant="outline" size="sm"
(clicked)="updatePayment()">Update</primus-button>
</div>
</primus-section>
</primus-card>
<!-- Invoice history -->
<primus-card padding="none">
<primus-data-table
[columns]="invoiceColumns"
[data]="invoices"
rowKey="id"
[paginated]="true"
[pageSize]="10">
</primus-data-table>
</primus-card>
</div>
<!-- Cancel modal -->
<primus-modal [open]="cancelOpen" title="Cancel subscription" size="sm"
(onClose)="cancelOpen = false">
<p>Your plan will remain active until the end of the billing period.</p>
<div modal-footer>
<primus-button variant="outline" (clicked)="cancelOpen = false">Keep plan</primus-button>
<primus-button variant="danger" (clicked)="onCancel()">Cancel subscription</primus-button>
</div>
</primus-modal>
</primus-layout>
Props
Plan object
| Field | Type | Description |
|---|---|---|
name | string | Plan name (Starter, Pro, Enterprise) |
price | string | Formatted price string |
cycle | string | Billing cycle |
renewsAt | string | Renewal date |
features | {label,value}[] | Feature entitlement list |
Payment method object
| Field | Type | Description |
|---|---|---|
last4 | string | Last 4 card digits |
expiry | string | Card expiry MM/YY |
name | string | Cardholder name |