Skip to content

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

ConsumerOpen in Storybook ↗

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

PropTypeDescription
orderOrder

The resolved order. The caller owns the query; the view only renders.

payAction?ReactNode

What this order offers about money it still owes — normally OrderPayNow, rendered in the receipt under the total. Omit it and no such affordance renders, which is what a read-only order page does.

More

A node rather than a callback, and one node rather than the seven props that fed it (methods, onPay, isPaying, payable, approval, approvers, payError). A function prop makes every caller a Client Component — use client is contagious, so adopting this layout shipped every presentational part with it, and nothing signalled that: the page rendered correctly either way.

Compose OrderPayNow in your own client island — the payment methods are a client query anyway — and pass headingLevel={subLevel(headingLevel)} so it lands where the layout's other sections land.

reorderAction?ReactNode

What lets the buyer order the same again — normally feature-cart's CartAddLinesButton in your own client island, rendered top right beside the order number. Omit it and none renders. A node for the same reason payAction is one.

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.

More

A 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 OrderProvider: the provider is a client context, and reading it here would make this root a client component and drag every part across with it. The provider still carries it for the query hooks, which are client anyway — a connector that has both passes it through.

renderItem?OrderLineItemRenderer<Order["lineItems"][number]>

Replace the line rows in the default layout. Ignored when children is given — compose it onto OrderLineItems yourself there. The item type comes from order, so an extended query's added fields are typed here with no cast.

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 OrderLineReviewAction.

nextSteps?ReactNode

Under paying: what the buyer can do next, normally OrderDetailNextSteps.

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 shipping icon; ignored when children is given.

deliveryPlan?OrderDeliveryPlan | null

The delivery slot or pickup point chosen, from useOrderDeliveryPlan, for the default layout. Shown until the first delivery, whose tracking replaces it.

timeZone?string

The zone a delivery slot is shown in, the store's. The reader's own by default.

children?ReactNode

Compose the pieces (OrderDetailHeader / OrderLineItems / OrderAmountBreakdown / OrderDetailPaymentMethod / OrderDetailAddresses / OrderDeliveryRemarks / OrderDetailDeliveries / OrderDetailDeliveryPlan / OrderDetailRefunds) for a bespoke layout, or omit for the standard one.

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.

PropTypeDescription
orderOrder
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.
PropTypeDescription
orderPick<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.

PropTypeDescription
orderPick<Order, "deliveryRemarks">
headingLevel?HeadingLevel

Any other props go to the root element, like on a plain HTML element.

OrderDetailAddressesBilling + delivery addresses for the order.
PropTypeDescription
orderOrder
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.
PropTypeDescription
orderOrder
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 shipping icon. A delivery records only the carrier's name, so its key is that name in kebab case: "DHL Express" is dhl-express. A node keyed by the name still renders, but is deprecated.

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".
PropTypeDescription
orderPick<Order, "deliveries">
planOrderDeliveryPlan | null | undefined

From useOrderDeliveryPlan; renders nothing while it is null or holds neither.

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.

PropTypeDescription
orderPick<Order, "orderNumber">
action?ReactNode

The page's action, top right: normally feature-cart's CartAddLinesButton to order the same again, in your own client island. A node, so the header stays server-renderable.

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.
PropTypeDescription
orderPick<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.

PropTypeDescription
orderOrder
vatDisplay?VatDisplay

Net vs gross presentation. Defaults to gross.

payAction?ReactNode

What the order offers about money it still owes — normally OrderPayNow, composed by the consumer in their own client island. A node, so this layout and every part in it stay server-renderable.

reorderAction?ReactNode

What lets the buyer order the same again — normally feature-cart's CartAddLinesButton, in the consumer's client island. Rendered top right, beside the order number.

renderItem?OrderLineItemRenderer<Order["lineItems"][number]>

Replace the line rows. Omit for the standard OrderLineItem.

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 OrderDetailNextSteps.

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 shipping icon.

deliveryPlan?OrderDeliveryPlan | null

The slot or pickup point chosen, from useOrderDeliveryPlan. Shown until shipped.

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 subLevel(), so setting it moves the whole outline rather than one tag.

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.
PropTypeDescription
orderPick<Order, "id" | "orderNumber" | "paymentState" | "orderStatus">
invoice?ReactNode

The invoice PDF, such as feature-invoice's InvoiceDownloadButton. Always shown.

returnAction?ReactNode

Starting a return, with its deadline, such as feature-return's entry.

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.

contactPath?string | null

The contact page's path, such as sitePagePath(settings, "contactPage"); the help step links to it with ?order=.

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.

PropTypeDescription
orderOrder

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.

PropTypeDescription
orderOrder

Any other props go to the root element, like on a plain HTML element.

OrderLineItemsThe order's line items.
PropTypeDescription
itemsOrderLineItem[]

The lines to render. Taking the items rather than the whole order is what carries an extended query's item type through — items={myLines} infers Item, so renderItem sees the added fields with no cast. That used to go through the detail context and cost one.

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 OrderLineReviewAction.

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.
PropTypeDescription
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.

lineReviewOrderLineReview | undefined

The line's entry from useOrderLineReviews; nothing renders without it.

askFit?boolean

Ask how the size fits; by default where the line has a size.

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.

PropTypeDescription
methodsreadonly 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.

More

Defaults 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 PaymentMethods' renderIcon.

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.

PropTypeDescription
status?string

The domain status this badge shows, published as data-status.

More

A 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-status="CANCELLED" or data-status="Cancelled" depending on the domain, and attribute selectors are case-sensitive.

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.

PropTypeDescription
statusOrderStatus
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.

LoadingOrderDetailSkeletonLoading 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)} />
)]))}
/>;
Open in Storybook ↗