InvoiceDetail
Pure presentation of a single invoice. It never fetches; the caller passes the
invoice it owns (via useInvoice).
import { InvoiceDetail } from "@evolve-storefront/feature-invoice/invoice-detail";Renders inside InvoiceProvider.
Design notes
A Server Component, like every part it composes: the parts take the invoice
as a prop rather than reading a context, so nothing here crosses the RSC
boundary.
Download and pay actions are composed in via actions — the
pay page itself is owned by the site.
Preview
Usage
Open, payable invoice spanning two orders: download and pay, and each billed order
links to its order page through renderOrder.
const { data: invoice } = useInvoice(invoiceId);
<InvoiceDetail invoice={invoice} actions={ <> <InvoiceDownloadButton invoice={invoice} /> <InvoicePayButton invoice={invoice} payHref={invoicePayPath(invoice.id)} /> </> } renderOrder={(orderNumber) => ( <Link href={orderPath(orderNumber)}>View order {orderNumber}</Link> )}/>;Props
| Prop | Type | Description |
|---|---|---|
invoice | InvoiceSummary | The resolved invoice. The caller owns the query; the view only renders. |
actions? | ReactNode | Download / pay actions — composed by the caller (site owns the pay page). |
renderOrder? | ((orderNumber: string) => ReactNode) | Render one billed order in the standard layout, e.g. a link to the order page. |
children? | ReactNode | Compose the pieces ( |
Any other props go to the root element, like on a plain HTML element.
Change the layout
Pass children to InvoiceDetail to arrange its parts yourself. Every part is exported from @evolve-storefront/feature-invoice/invoice-detail.
const { data: invoice } = useInvoice(invoiceId);
<InvoiceDetail invoice={invoice} actions={ <> <InvoiceDownloadButton invoice={invoice} /> <InvoicePayButton invoice={invoice} payHref={invoicePayPath(invoice.id)} /> </> } renderOrder={(orderNumber) => ( <Link href={orderPath(orderNumber)}>View order {orderNumber}</Link> )}/>;InvoiceDetailLayoutThe standard arrangement of the detail parts — what InvoiceDetail renders when given no children.
| Prop | Type | Description |
|---|---|---|
invoice | InvoiceSummary | |
actions? | ReactNode | |
renderOrder? | ((orderNumber: string) => ReactNode) |
InvoiceAmountsThe invoice's amount breakdown: total, amount paid, amount due.
| Prop | Type | Description |
|---|---|---|
invoice | InvoiceSummary |
Any other props go to the root element, like on a plain HTML element.
InvoiceDetailHeaderInvoice detail header: number, dates, status, and actions.
| Prop | Type | Description |
|---|---|---|
invoice | InvoiceSummary | |
children? | ReactNode | Download / pay actions, composed by the caller. |
Any other props go to the root element, like on a plain HTML element.
InvoiceDownloadButtonDownload link for the invoice document, where the issuer publishes one (invoiceUrl).
| Prop | Type | Description |
|---|---|---|
invoice | InvoiceSummary | |
isPending? | boolean | The action is running. The button keeps its label, so its width and
accessible name hold, shows a spinner and is |
type? | "reset" | "button" | "submit" | |
variant? | ButtonVariant | |
tone? | ButtonTone | |
isIconOnly? | boolean | The button has no visible label, so it is square. Structural rather than a size: it says the button has nothing to be wide for, and says nothing about how big — the brand's control tokens still decide that. MoreNot inferred from the children. A kebab menu is as often a glyph or an SVG
as an |
data-component? | undefined | A button's identity is not a caller's to rename — see |
Any other props go to the root element, like on a plain HTML element.
InvoiceOrderRefsThe orders an invoice bills.
An order number, not a reference. An invoice is a payment record and carries
no order ids, so a caller that wants a link builds the href from the number
itself. renderOrder is that seam, and it is how this feature reaches the Orders
feature without importing it.
| Prop | Type | Description |
|---|---|---|
invoice | InvoiceSummary | |
renderOrder? | ((orderNumber: string) => ReactNode) | Render one billed order (e.g. a link into the Orders feature). |
Any other props go to the root element, like on a plain HTML element.
InvoicePayButtonPay action for an invoice.
Gated on payable, never on the balance. An invoice with a charge already
underway still shows an amount due, and only the engine knows a payment is in
flight — deriving the control from amountDue > 0 is how the same amount gets
collected twice. Paid, part-paid and credited invoices all render; only a
payable one grows a control.
The label names the balance, not the total. A part-paid invoice is charged
what is left, and a button reading the total beside a smaller charge reads as a
bug. The figure comes from amountDue — the storefront never computes it, and
never sends it: the engine derives the charge from the same amounts.
| Prop | Type | Description |
|---|---|---|
invoice | InvoiceSummary | |
isPending? | boolean | The action is running. The button keeps its label, so its width and
accessible name hold, shows a spinner and is |
type? | "reset" | "button" | "submit" | |
variant? | ButtonVariant | |
tone? | ButtonTone | |
isIconOnly? | boolean | The button has no visible label, so it is square. Structural rather than a size: it says the button has nothing to be wide for, and says nothing about how big — the brand's control tokens still decide that. MoreNot inferred from the children. A kebab menu is as often a glyph or an SVG
as an |
data-component? | undefined | A button's identity is not a caller's to rename — see |
onPay? | (() => void) | Start the pay handoff. The site owns |
payHref? | string | Or link straight to the site's pay page instead of a callback. |
Any other props go to the root element, like on a plain HTML element.
InvoicePayFormThe picker and the pay control, with the chosen method between them.
This is where the client boundary sits. "use client" is contagious, so
putting the state on the surface that composes this would ship the whole
invoice-detail subtree — the amounts, the billed orders, the download link — to a
consumer who renders none of it interactively.
The control names the balance. A part-paid invoice is charged what is left,
and a button reading the total beside a smaller charge reads as a bug. The figure
is amountDue and it is never sent: the engine derives the charge from the same
amounts, so a client naming a figure is a client that can name the wrong one.
The chosen method is local and never leaves. Nothing is written while choosing, so abandoning the page loses nothing and there is no half-finished state to resume.
| Prop | Type | Description |
|---|---|---|
invoice | Pick<InvoiceSummary, "amountDue" | "payable"> | |
methods | readonly InvoicePaymentMethod[] | |
onPay | (method: InvoicePaymentMethodInput) => void | |
isPaying? | boolean | |
error? | ReactNode | Shown above the control when starting the payment failed. |
emptyState? | ReactNode | Rendered when no method is offered for this invoice, buyer and device. |
renderPaymentMethodIcon? | ((method: InvoicePaymentMethod) => ReactNode) | Each method's logo, passed on to |
Any other props go to the root element, like on a plain HTML element.
InvoiceStatusBadgeStatus badge.
Tolerates a status it does not know, because TONE[status] looks total to tsc
while a storefront's union lags the framework schema routinely. Without t.has,
t(status) logs MISSING_MESSAGE and renders its own key path — a shopper reads
Invoice.status.WRITTEN_OFF in the badge. The ?? is belt and braces: StatusBadge
defaults tone itself, and this keeps the fallback from depending on that.
| Prop | Type | Description |
|---|---|---|
status | InvoiceStatus | |
data-component? | undefined | A badge's identity and its published axes are not a caller's to rename. Typed away so a call site renaming one fails to compile rather than silently shadowing the selector a brand was told to use. |
data-tone? | undefined | |
data-status? | undefined |
Any other props go to the root element, like on a plain HTML element.
Loading and empty states
Render these while the data loads or when there is nothing to show, so the page does not jump when it arrives.
| Loading | InvoiceDetailSkeleton | Loading state for InvoiceDetail — the same flex flex-col gap-8 outer section with a header, the amounts block and the billed-orders list. |
Examples
ComposedThe parts as children, for a bespoke page.
// Children replace the standard layout: only what you compose renders.<InvoiceDetail invoice={invoice}> <InvoiceDetailHeader invoice={invoice}> <InvoiceDownloadButton invoice={invoice} /> </InvoiceDetailHeader> <InvoiceAmounts invoice={invoice} /></InvoiceDetail>;
