Skip to content

feature-cart setup

Styles

Import them once, in the stylesheet where you load Tailwind:

@import "@evolve-storefront/feature-cart/styles.css";

Translations

The package ships its texts in @evolve-storefront/feature-cart/messages. evolve messages merges them into your message files and never overwrites a text you changed.

CartProvider

Supplies the cart's auth/session/mode context.

import { CartProvider } from "@evolve-storefront/feature-cart/provider";
Design notes

There is deliberately no current-cart id here. The current cart is whatever the server's cart query returns, and the list marks its match by id — see useCurrentCartId. A client-held current id is the thing that makes two associates of one company disagree about which cart is theirs, and it is what forced the reload-after-switch workarounds in the implementations this package replaces.

PropTypeDescription
childrenReactNode
mode?CartMode

single (B2C) or multi (business). Defaults to single.

hasSession?boolean

Gates every cart query until the shopper's session is established. Pass true for a guest — a guest has a session and has a cart.

More

undefined means "cannot answer yet", and is what makes initialSession apply: an app whose auth resolves in a mount effect passes auth.loading ? undefined : hasToken. With no initialSession there is nothing to fall back to and undefined gates exactly as before.

isAuthenticated?deprecatedboolean
businessUnitId?string

Scopes the cart keys to a company. Required for multi — not merely meaningful: the carts list hangs off BusinessUnit.carts, so without it the list query does not run.

customerId?string

The signed-in customer's id. Multi-cart only, and only to tell the buyer's own carts from a colleague's — see CartContextValue.customerId.

initialSession?CartInitialSession

The session the server read on this request, used for the cart keys until the client answers for itself. Pass it on a route that prefetches.

More

A prefetch of cartQueryOptions is only ever read if businessUnitId matches on the first client render, because it is half of every cart key. An app decoding its session in a mount effect has no business unit on that render, so the server prefetches under the company's scope while the first render looks under personal, finds nothing, and paints the consumer's no-cart branch — telling a buyer holding a full cart that it is empty, before settling through a skeleton to the truth. Three paints, and the first one false.

It is not about the gate. A hydrated entry under a matching key reports isSuccess with its data even while enabled is false, so the key is the whole of it; the seed implying a session only saves an un-prefetched query one render.

Ignored the moment hasSession is a boolean, and ignored as a unit — see resolveCartSession.

vatDisplay?VatDisplay
activateOnCreate?boolean

Multi-cart: make a newly created cart active.

undoTimeout?number

How long a removed line keeps its "removed. Undo" row, in ms. Defaults to no timer, so the row stays until the next removal: a timed undo fails WCAG 2.2.1. 0 removes immediately and offers no undo.

onLineItemsAdded?((result: LineItemsAddedResult) => void)

The app-wide add-to-cart confirmation: a toast, a mini-cart, a cross-sell dialog. Set once here rather than per button — see CartContextValue.onLineItemsAdded.

ensureSession?(() => void | Promise<void>)

Establish a session before any cart write — see CartContextValue.ensureSession. Required for a guest storefront: without it the first add-to-cart of a visitor with no session is left to whatever the server does about that.

More

Set once here rather than per call site, because add-to-cart appears on product cards, PLP rows, the PDP, quick view, reorder and quote-to-cart.

onTokenChange?(() => void)

Called after every cart write, which rotates the session token.

Prefetching on the server

Fetch the data in a Server Component so the page renders with it instead of a skeleton.

availablePaymentMethodsQueryOptionsThe payment methods on offer for the current cart.
import { availablePaymentMethodsQueryOptions } from "@evolve-storefront/feature-cart/queries";

device is part of the key because it changes the answer, not just the presentation — a device wallet is offered on a phone and not on a desktop.

function availablePaymentMethodsQueryOptions(
args: { gqlClient: ClientFetcher; businessUnitId?: string | undefined; cartId: string; device: ClientDevice; enabled?: boolean | undefined; },
): OmitKeyof<UseQueryOptions<PaymentMethod[], Error, PaymentMethod[], readonly ["cart-methods", string, string, "payment", string]>, "queryFn"> & { queryFn?: Query…
OptionTypeDescription
gqlClientClientFetcher
cartIdstring
deviceClientDevice
businessUnitId?string
enabled?boolean
availableShippingMethodsQueryOptionsThe delivery methods on offer for the current cart.
import { availableShippingMethodsQueryOptions } from "@evolve-storefront/feature-cart/queries";

Keyed apart from the cart entity but derived from it — see cartMethodKeys. useCartMutation invalidates this after every cart write, so a call site does not have to remember that setting an address changes the answer.

cartId is required rather than resolved here: this query needs the cart to exist, and a caller that has not loaded one yet should not be issuing it.

function availableShippingMethodsQueryOptions(
args: { gqlClient: ClientFetcher; businessUnitId?: string | undefined; cartId: string; enabled?: boolean | undefined; },
): OmitKeyof<UseQueryOptions<ShippingMethod[], Error, ShippingMethod[], readonly ["cart-methods", string, string, "shipping"]>, "queryFn"> & { queryFn?: QueryFunct…
OptionTypeDescription
gqlClientClientFetcher
cartIdstring
businessUnitId?string
enabled?boolean
cartQueryOptionsThe caller's current cart for the business unit of the current session.
import { cartQueryOptions } from "@evolve-storefront/feature-cart/queries";

cart returning null is the empty state, not an error — the server resolves and repairs a dangling current-cart pointer internally, so CART_NOT_FOUND never comes back from this query. It only appears on operations against an explicit cart id.

A prefetch of this is only ever read if businessUnitId matches on the first client render. It is half of the key, and an app that decodes its session in a mount effect has no business unit on that render — so the route prefetches under the company's scope, the first render looks under personal, and the miss is indistinguishable from never having prefetched at all. Pass CartProvider's initialSession on a prefetching route; the gate is not the problem, a hydrated entry under a matching key answers from the cache even while enabled is false.

function cartQueryOptions(
args: { gqlClient: ClientFetcher; businessUnitId?: string | undefined; enabled?: boolean | undefined; },
): OmitKeyof<UseQueryOptions<Cart | null, Error, Cart | null, readonly ["cart", string, "current"]>, "queryFn"> & { queryFn?: QueryFunction<Cart | null, readonly […
OptionTypeDescription
gqlClientClientFetcher
businessUnitId?string
enabled?boolean
cartsQueryOptionsOne page of the business unit's carts, filtered server-side by the caller's associate role.
import { cartsQueryOptions } from "@evolve-storefront/feature-cart/queries";

The list is viewer-relative, because the role filter is: two associates of one business unit are answered with different carts. That is why the key is scoped by business unit and never shared across users.

It is business-unit-scoped, not a root field — businessUnitId is required, so a personal session has no carts list at all. And it is paginated: a caller that renders one page must say so rather than assume the answer is complete.

function cartsQueryOptions(
args: { gqlClient: ClientFetcher; businessUnitId: string; page?: number | undefined; pageSize?: number | undefined; enabled?: boolean | undefined; },
): OmitKeyof<UseQueryOptions<CartsPage | null, Error, CartsPage | null, readonly ["carts", string, number, number | "default"]>, "queryFn"> & { queryFn?: QueryFunc…
OptionTypeDescription
gqlClientClientFetcher
businessUnitIdstring
page?number
pageSize?number
enabled?boolean
walletMethodsQueryOptionsThe wallet methods on offer for the current cart, with the configuration their buttons need.
import { walletMethodsQueryOptions } from "@evolve-storefront/feature-cart/queries";
function walletMethodsQueryOptions(
args: { gqlClient: ClientFetcher; businessUnitId?: string | undefined; cartId: string; device: ClientDevice; enabled?: boolean | undefined; },
): OmitKeyof<UseQueryOptions<WalletMethod[], Error, WalletMethod[], readonly ["cart-methods", string, string, "wallets", string]>, "queryFn"> & { queryFn?: QueryFu…
OptionTypeDescription
gqlClientClientFetcher
cartIdstring
deviceClientDevice
businessUnitId?string
enabled?boolean
resolveCartSessionWhich session the context carries: the client's own answer, or the server's seed while the client has none.
import { resolveCartSession } from "@evolve-storefront/feature-cart/provider";

Applied as a unit, never field by field. businessUnitId ?? initialSession.businessUnitId is the tempting version and it never lets go — a shopper who signs out resolves to no business unit, while the seed still holds the company the server read on this request, because signing out does not re-render the server. Their guest cart would then be cached under that company's scope, and be waiting there under the company key when they sign back in.

function resolveCartSession(
props: { hasSession?: boolean | undefined; initialSession?: CartInitialSession | undefined; businessUnitId?: string | undefined; customerId?: string | undefined; },
): Pick<CartContextValue, "hasSession" | "businessUnitId" | "customerId">
OptionTypeDescription
hasSession?boolean
initialSession?CartInitialSession
businessUnitId?string
customerId?string

Returns Pick<CartContextValue, "hasSession" | "businessUnitId" | "customerId">