OrderDetail
Pure presentation of a single order. It never fetches; the
caller passes the order it owns (via useOrder).
import { OrderDetail } from "@evolve-storefront/feature-order/order-detail";Renders inside OrderProvider.
Design notes
A Server Component, like every part it composes. The branch resolves before
anything is sent, so the default layout costs a consumer who supplies their own
children nothing at all — which is the whole point of moving the parts off a
context and onto props.
Preview
Usage
Consumer, gross pricing (VAT included), with the chosen payment method in the receipt.
const { data: order } = useOrder(orderNumber);
// Gross is the default, so a consumer storefront passes no `vatDisplay`.<OrderDetail order={order} />;Props
| Prop | Type | Description |
|---|---|---|
order | Order | The resolved order. The caller owns the query; the view only renders. |
payAction? | ReactNode | What this order offers about money it still owes — normally MoreA node rather than a callback, and one node rather than the seven props that
fed it ( Compose |
reorderAction? | ReactNode | What lets the buyer order the same again — normally |
headingLevel? | HeadingLevel | Level for the order number, which is this page's title, and the level every section derives from. Defaults to 1 — an order page's subject is the order. MoreA feature cannot know its own document depth, so this has to be a prop: set it where the order sits inside something that already has a title. |
vatDisplay? | VatDisplay | Net vs gross presentation. A prop, not a read of
|
renderItem? | OrderLineItemRenderer<Order["lineItems"][number]> | Replace the line rows in the default layout. Ignored when |
productLinks? | Readonly<Record<string, string>> | Product page URLs by SKU; a line whose SKU is listed links its name there. |
lineActions? | Readonly<Record<string, ReactNode>> | Under a standard line's details in the default layout, by line id, such as an |
nextSteps? | ReactNode | Under paying: what the buyer can do next, normally |
showOrderedBy? | boolean | Show "Ordered by". Defaults to true; a consumer store passes false. |
carrierLogos? | Readonly<Record<string, string>> | Readonly<Partial<Record<string, ReactNode>>> | Logo image URLs by carrier key for the deliveries in the default layout. A
carrier without one gets the |
deliveryPlan? | OrderDeliveryPlan | null | The delivery slot or pickup point chosen, from |
timeZone? | string | The zone a delivery slot is shown in, the store's. The reader's own by default. |
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 OrderDetail to arrange its parts yourself. Every part is exported from @evolve-storefront/feature-order/order-detail.
<OrderDetail order={order}> <OrderDetailHeader order={order} /> <OrderLineItems items={order.lineItems} /> <PriceSummary> <OrderAmountBreakdown order={order} /> </PriceSummary></OrderDetail>;OrderAmountBreakdownThe itemised amount breakdown: subtotal, discounts, shipping, tax (added lines in net mode), a noted zero VAT row under reverse charge or on an export (vatOutcome), and the grand total, which is always gross.
The rows explain the total and money-contract.test.ts asserts they sum to it — hence
orderDiscountRows, and total.taxes, which includes the tax on shipping.
| Prop | Type | Description |
|---|---|---|
order | Order | |
vatDisplay? | VatDisplay | Net vs gross presentation. Defaults to gross. |
Any other props go to the root element, like on a plain HTML element.
OrderCancelButtonCloses the order through orderCancel.
| Prop | Type | Description |
|---|---|---|
order | Pick<Order, "id" | "orderNumber"> |
OrderDeliveryRemarksThe buyer's note for whoever delivers the goods, as they typed it.
Renders nothing when there is no note, which is most orders — a heading over an empty block reads as something having gone missing.
It exists as a part at all because the field it reads used to be selected and never
shown: Order.shippingNotice answered the literal string "Shipping notice" for every
order, so no buyer's instruction could ever have reached this page, and nothing
rendered it either. Two mistakes hiding each other — a dead selection over a field
that carried nothing, so neither had a visible symptom.
Plain text, deliberately. It is the buyer's own words going to a courier, so there is nothing here to parse or normalise.
| Prop | Type | Description |
|---|---|---|
order | Pick<Order, "deliveryRemarks"> | |
headingLevel? | HeadingLevel |
Any other props go to the root element, like on a plain HTML element.
OrderDetailAddressesBilling + delivery addresses for the order.
| Prop | Type | Description |
|---|---|---|
order | Order | |
headingLevel? | HeadingLevel | Heading level for the two address labels. Defaults to 3; set it where the order detail sits at a different depth, since a feature cannot know its own document depth. |
Any other props go to the root element, like on a plain HTML element.
OrderDetailDeliveriesThe order's deliveries with the lines that went out in each: when it was dispatched, its status, the carrier and tracking code, then a card per line it held.
| Prop | Type | Description |
|---|---|---|
order | Order | |
carrierLogos? | Readonly<Record<string, string>> | Readonly<Partial<Record<string, ReactNode>>> | The carriers' logos. A package cannot ship carriers' brand marks, so a carrier
with no entry gets the neutral |
vatDisplay? | VatDisplay | Net vs gross presentation of the line cards. Defaults to gross. |
renderItem? | OrderLineItemRenderer<Order["lineItems"][number]> | Replace the line cards. Omit for the standard row. |
productLinks? | Readonly<Record<string, string>> | Product page URLs by SKU; a line whose SKU is listed links its name there. |
lineActions? | Readonly<Record<string, ReactNode>> | Under a standard line's details, by line id. |
headingLevel? | HeadingLevel | Level for "Deliveries"; each delivery takes the level below. Defaults to 2. |
Any other props go to the root element, like on a plain HTML element.
OrderDetailDeliveryPlanThe delivery slot or pickup point the buyer chose, as placed: "Expected Thu 15 Oct, 18:00–22:00 · PostNL".
| Prop | Type | Description |
|---|---|---|
order | Pick<Order, "deliveries"> | |
plan | OrderDeliveryPlan | null | undefined | From |
timeZone? | string | The zone the slot is shown in, the store's. The reader's own by default. |
headingLevel? | HeadingLevel |
Any other props go to the root element, like on a plain HTML element.
OrderDetailHeaderThe order number as the page's title, with the page's action beside it.
The number keeps its text classes rather than inheriting the base layer's h1, so
the tag carries the outline without resizing the title on every order page.
| Prop | Type | Description |
|---|---|---|
order | Pick<Order, "orderNumber"> | |
action? | ReactNode | The page's action, top right: normally |
headingLevel? | HeadingLevel | Level for the order number, which is this page's title. Defaults to 1, because that is what it is on the page this part exists for — set it lower where the order is a section of something bigger, since a feature cannot know its own document depth. |
Any other props go to the root element, like on a plain HTML element.
OrderDetailInfoThe order's facts as one table: when it was placed, who placed it, its status and its reference.
| Prop | Type | Description |
|---|---|---|
order | Pick<Order, "paymentState" | "createdAt" | "createdBy" | "orderStatus" | "purchaseOrderNumber"> | |
showOrderedBy? | boolean | Show "Ordered by". Defaults to true; a consumer store, where it is always the shopper, passes false. |
Any other props go to the root element, like on a plain HTML element.
OrderDetailLayoutThe standard arrangement of the detail parts — what OrderDetail renders when given no children.
It takes the order as one prop and distributes it, which keeps every part here server-renderable: a context read would need a hook and pull the subtree across.
| Prop | Type | Description |
|---|---|---|
order | Order | |
vatDisplay? | VatDisplay | Net vs gross presentation. Defaults to gross. |
payAction? | ReactNode | What the order offers about money it still owes — normally |
reorderAction? | ReactNode | What lets the buyer order the same again — normally |
renderItem? | OrderLineItemRenderer<Order["lineItems"][number]> | Replace the line rows. Omit for the standard |
productLinks? | Readonly<Record<string, string>> | Product page URLs by SKU; a line whose SKU is listed links its name there. |
lineActions? | Readonly<Record<string, ReactNode>> | Under a standard line's details, by line id. |
nextSteps? | ReactNode | Under paying: what the buyer can do next, normally |
showOrderedBy? | boolean | Show "Ordered by". Defaults to true; a consumer store passes false. |
carrierLogos? | Readonly<Record<string, string>> | Readonly<Partial<Record<string, ReactNode>>> | Logo image URLs by carrier key. A carrier without one gets the |
deliveryPlan? | OrderDeliveryPlan | null | The slot or pickup point chosen, from |
timeZone? | string | The zone a delivery slot is shown in, the store's. The reader's own by default. |
headingLevel? | HeadingLevel | Level for the order number, which is the page's title. Defaults to 1; every
section below it derives from this with |
OrderDetailNextStepsWhat the buyer can do with the order now, each step only when it applies: the invoice, cancelling while the order is received and orderCancel allows it, a return and how returns work, a review after delivery, and help.
| Prop | Type | Description |
|---|---|---|
order | Pick<Order, "id" | "orderNumber" | "paymentState" | "orderStatus"> | |
invoice? | ReactNode | The invoice PDF, such as |
returnAction? | ReactNode | Starting a return, with its deadline, such as |
review? | ReactNode | A review prompt. Shown once the order is delivered. |
help? | ReactNode | Help for this order, such as a contact link carrying its number. Overrides |
contactPath? | string | null | The contact page's path, such as |
returnsPath? | string | null | The returns page's path; a step links to it where given. |
renderLink? | ((props: { href: string; children: ReactNode; }) => ReactNode) | A built-in step's link with the app's router. A plain anchor by default. |
headingLevel? | HeadingLevel | Level for "Next steps". Defaults to 2. |
Any other props go to the root element, like on a plain HTML element.
OrderDetailPaymentMethodThe payment method the buyer chose, by its localized name.
Its own part rather than a row of OrderAmountBreakdown, whose rows are amounts
that sum to the total.
| Prop | Type | Description |
|---|---|---|
order | Order |
Any other props go to the root element, like on a plain HTML element.
OrderDetailRefundsRefunds (breakdown line 11).
UNVERIFIED: reads order.refunds, a target field the schema has not shipped —
renders nothing until the backend returns it.
| Prop | Type | Description |
|---|---|---|
order | Order |
Any other props go to the root element, like on a plain HTML element.
OrderLineItemsThe order's line items.
| Prop | Type | Description |
|---|---|---|
items | OrderLineItem[] | The lines to render. Taking the items rather than the whole order is what
carries an extended query's item type through — |
vatDisplay? | VatDisplay | Net vs gross presentation. Defaults to gross. |
renderItem? | OrderLineItemRenderer<OrderLineItem> | |
quantities? | ReadonlyMap<string, number> | How much of each line this group holds, by line id, where it is part of the ordered quantity: a delivery's share, or what is still to come. The row then reads "2 of 5" rather than the ordered count. |
productLinks? | Readonly<Record<string, string>> | Product page URLs by SKU; a line whose SKU is listed links its name there. |
lineActions? | Readonly<Record<string, ReactNode>> | Under a standard row's details, by line id, such as an |
Any other props go to the root element, like on a plain HTML element.
OrderLineReviewActionWhat a delivered order line offers about reviewing it: a button that opens a review form, or, once reviewed, where the review is in moderation.
| Prop | Type | Description |
|---|---|---|
order | { id: string; orderNumber: string; } | The order the line is on. |
line | { id: string; name: string; size?: string | undefined; } | The line, by its id and the name the dialog shows, and the size bought, such as its variant's size, which the form shows above the fit question. |
lineReview | OrderLineReview | undefined | The line's entry from |
askFit? | boolean | Ask how the size fits; by default where the line has a |
canAddPhotos? | boolean | Let the buyer add up to five photos; needs a gateway with review photo uploads. |
OrderPayFormThe 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 order-detail subtree
— the money formatter, the addresses, the line items — to a consumer who renders none
of it interactively.
The chosen method is local and never leaves. Nothing is written to the order while choosing, so abandoning the page loses nothing and there is no half-finished state to resume.
| Prop | Type | Description |
|---|---|---|
methods | readonly OrderPaymentMethod[] | |
onPay | (method: OrderPaymentMethodInput) => void | |
isPaying? | boolean | |
error? | ReactNode | Shown above the control when starting the payment failed. |
emptyState? | ReactNode | Rendered when no method is offered for this order, buyer and device. MoreDefaults to the order's own wording, which tells the buyer to get in touch. The shared picker cannot know that, and an order with nothing offered is a dead end without it. |
renderPaymentMethodIcon? | ((method: OrderPaymentMethod) => ReactNode) | Each method's logo, passed on to |
Any other props go to the root element, like on a plain HTML element.
OrderPaymentBadgeWhether the order has been paid — a second badge beside the status one, not part of it.
An order can be in transit and still owe money. The two were one enum once and the combination could not be expressed, so folding this into the status badge would rebuild exactly the problem that splitting them removed.
Renders nothing when the state is absent, and that is not "paid". The gateway
leaves it unset where nothing is recorded, so the honest rendering of "we do not
know" is silence — a "Paid" badge would be a claim, and PAID is emitted only when
the gateway actually says so.
Tolerates a state it does not know, showing the raw value neutrally, for the same reason the status badge does: a storefront's union can lag the schema, and an unrecognised value with no tone would otherwise be an invisible failure inside a list rather than a visible oddity.
| Prop | Type | Description |
|---|---|---|
status? | string | The domain status this badge shows, published as MoreA feature already holds this value and used to drop it: the label is localized, so the rendered text is not a selector, and the tone is shared by every status that maps to it. Without this a brand can reach "the negative ones" but never "a returned order". Passed through verbatim — status casing mirrors the framework's GraphQL enums,
so it is |
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 | |
state? | OrderPaymentState | null | Renders nothing when the gateway recorded no payment state. |
Any other props go to the root element, like on a plain HTML element.
OrderPayNowPay an order that still owes money, or say why it cannot be paid.
Owing is decided by naming the owing states, never by inverting PAID — the payment
state is nullable. A hold replaces the picker rather than sitting beside it, a
cancellation keys on the order's own status, and a failed attempt keeps the picker.
Which branch renders when, and why payable is read: README, "Paying an order".
No props of its own.
OrderStatusBadgeStatus badge.
Tolerates a status it does not know, because the alternative is what this
component just shipped: the union said PascalCase, the server sends
SCREAMING_SNAKE, and TONE[status] was undefined — a badge with no tone, no
warning and no type error, since the type made the lookup look total. A
storefront's union can also simply lag the schema. An unrecognised value now
gets a neutral badge showing the raw string, which is a far better failure
inside an order list than an invisible one.
| Prop | Type | Description |
|---|---|---|
status | OrderStatus | |
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 | OrderDetailSkeleton | Loading state for OrderDetail. |
Examples
Line ReviewsA delivered line offers a review through lineActions, and a reviewed one says where its review is in moderation.
const { data: lineReviews } = useOrderLineReviews(order.orderNumber, { enabled: reviewsEnabled });<OrderDetail order={order} lineActions={Object.fromEntries(order.lineItems.map((line) => [line.id, ( <OrderLineReviewAction order={order} line={line} lineReview={lineReviews?.find((entry) => entry.lineItem.id === line.id)} /> )]))}/>;
