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.
| Prop | Type | Description |
|---|---|---|
children | ReactNode | |
mode? | CartMode |
|
hasSession? | boolean | Gates every cart query until the shopper's session is established. Pass
More
|
isAuthenticated?deprecated | boolean | |
businessUnitId? | string | Scopes the cart keys to a company. Required for |
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 |
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. MoreA prefetch of It is not about the gate. A hydrated entry under a matching key reports
Ignored the moment |
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.
|
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
|
ensureSession? | (() => void | Promise<void>) | Establish a session before any cart write — see
MoreSet 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…| Option | Type | Description |
|---|---|---|
gqlClient | ClientFetcher | |
cartId | string | |
device | ClientDevice | |
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…| Option | Type | Description |
|---|---|---|
gqlClient | ClientFetcher | |
cartId | string | |
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 […| Option | Type | Description |
|---|---|---|
gqlClient | ClientFetcher | |
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…| Option | Type | Description |
|---|---|---|
gqlClient | ClientFetcher | |
businessUnitId | string | |
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…| Option | Type | Description |
|---|---|---|
gqlClient | ClientFetcher | |
cartId | string | |
device | ClientDevice | |
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">| Option | Type | Description |
|---|---|---|
hasSession? | boolean | |
initialSession? | CartInitialSession | |
businessUnitId? | string | |
customerId? | string |
Returns Pick<CartContextValue, "hasSession" | "businessUnitId" | "customerId">

