Mersi

Cart & Checkout

Zustand cart store, optimistic cart mutations, Crossmint checkout flow, and order tracking.

Cart

cartStorelib/cart-store.ts

Zustand store (no persistence — cart is re-hydrated from the backend on every authenticated page load).

type CartStore = {
  items: CartItem[]       // { product: Product; backendId?: string }[]
  isOpen: boolean
  add: (product: Product, backendId?: string) => void
  remove: (productId: string) => void
  hydrate: (items: CartItem[]) => void   // merges backend items with pending local items
  setBackendId: (productId: string, backendId: string) => void
  open: () => void
  close: () => void
  reset: () => void
}

backendId is the UUID assigned by the backend when POST /api/cart returns. It is required for DELETE /api/cart/:id and POST /api/checkout (via cartItemId).

Cart API — lib/api/cart.ts

All mutations are optimistic: the store is updated before the backend call resolves.

MethodAPI callNotes
cartApi.hydrate()GET /api/cartMerges backend items into store; pending local items (no backendId) are preserved
cartApi.addItem(product)POST /api/cartAdds product to store immediately; sets backendId from response
cartApi.removeItem(productId)DELETE /api/cart/:backendIdRemoves from store immediately; restores on backend failure

CartHydrator

components/ui/CartHydrator calls cartApi.hydrate() once on mount inside the (main) layout. This syncs the backend cart (including items added in previous sessions) into the local store.

CartSidebar / CartPanel

CartSidebar (components/ui/CartSidebar) is rendered in the main layout and is controlled by cartStore.isOpen. It displays cartStore.items, the subtotal, and a checkout button per item. Removing an item calls cartApi.removeItem.

Checkout Flow

Initiate

User clicks the checkout button for a cart item inside CartSidebar. The item's backendId is passed to checkoutItem(cartItemId) (lib/api/checkout.ts).

Backend creates order

POST /api/checkout (60 s timeout) contacts Crossmint, creates an order, and returns a CheckoutResponse containing orderId, crossmintOrderId, phase, serializedTransaction, and walletAddress.

Email OTP approval

The user receives an email from Crossmint and enters the OTP in a prompt shown by the frontend. Crossmint confirms the transaction server-side.

Webhook callback

Crossmint fires a webhook to the backend (POST /api/webhooks/crossmint), which updates the order's status and phase in the database.

Order confirmed

The frontend polls GET /api/orders/:orderId (or receives an update via ordersStore) until the status is terminal. The order appears in OrdersSidebar.

checkoutItem has a hard 60-second timeout. If the backend does not respond in time, the UI shows "Checkout request timed out after 60 seconds". Specific error codes (CheckoutNoWalletError, CheckoutMissingAddressError, InsufficientFundsError) are surfaced as user-readable messages.

CheckoutResponse type

type CheckoutResponse = {
  orderId: string
  crossmintOrderId: string
  phase: string
  serializedTransaction: string
  walletAddress: string
}

Orders

ordersStorelib/orders-store.ts

Lightweight Zustand store that controls the OrdersSidebar open/close state. Order data itself is fetched with TanStack Query (see State Management).

type OrdersStore = { isOpen: boolean; open: () => void; close: () => void; reset: () => void }

Orders API — lib/api/checkout.ts

FunctionCallDescription
getOrder(orderId)GET /api/orders/:idSingle order with status
listOrders(params)GET /api/ordersPaginated list; supports type, phase, status filters

OrdersSidebar / OrdersList

Rendered in the main layout, controlled by ordersStore.isOpen. Displays orders returned by listOrders, grouped or filtered by status. Each order shows product image, name, price, and the current status string (e.g. payment_confirmed, in_progress, delivered).

Flow Diagram

How is this guide?

On this page