Skip to content
Twilight React Playground
ثيم رائدaren

Cart

interfaceBeginnerlive demo

Describes the shopper's cart as the cart API returns it: items, totals, coupon, free-shipping progress, and the Money price shape.

import { Cart, CartItem, Money, FreeShippingBar, CartItemOffer, CartItemAttachment, CartItemOption, CartOption } from '@salla.sa/twilight-theme-engine/types';

In plain words

Cart is the shape of a shopping cart: the list of items, the totals, the applied coupon, and free_shipping_bar, which says how far the shopper is from free delivery.

Some prices can be written two ways: a plain number such as 115, or a Money object such as { amount: 115, currency: 'SAR' }. The type allows both (number | Money, where | means "or"), so your code has to check which one it got before doing any arithmetic.

Signature

interface Cart {
  id: string;
  count: number;
  items: CartItem[];
  sub_total: number;  total: number | string;  discount: number;  tax_amount: number;
  coupon?: string | null;
  has_shipping: boolean;  is_require_shipping?: boolean;  real_shipping_cost: number;
  free_shipping_bar?: FreeShippingBar | null;
  options: CartOption[];  options_total?: number | null;  total_discount?: number | null;
  gift?: { enabled: boolean; text?: string; type?: string } | null;
  loyalty?: { prize: { points: number; title: string } } | null;
  should_refresh?: boolean;
}

interface CartItem {
  id: string;  product_id: number;  product_name: string;  product_image: string;  url: string;
  type: ProductType | string;
  quantity: number;  max_quantity?: number;
  price: number | Money;  product_price: number | Money;  original_price?: number | Money;
  total: number | Money;  total_special_price?: number | Money;
  is_available: boolean;  is_hidden_quantity: boolean;  is_on_sale?: boolean;  has_discount?: boolean;
  can_add_note: boolean;  can_upload_file: boolean;  notes?: string;  weight_label?: string;
  offer?: CartItemOffer | null;  detailed_offers?: JsonObject[] | null;  donation?: JsonObject | null;
  attachments?: CartItemAttachment[];  options?: CartItemOption[];
  has_pre_order_campaign?: boolean;
}

interface Money { amount: number; currency: string; formatted?: string }

interface FreeShippingBar { minimum_amount: number; has_free_shipping: boolean; percent: number; remaining: number }

interface CartItemOffer { discount: number; is_free: boolean; names?: string }

interface CartItemAttachment { id: number; url: string; product_id: number; item_id: number; name: string }

interface CartItemOption {
  id: number;  name?: string;  value?: string;  quantity?: number;
  options?: Record<string, string | number | boolean>[];
}

interface CartOption { /* the same fields as CartItemOption */ }

Try it live

Your own guest cart on the demo store, read with cart.queries.detail() and checked against Cart. ✗ marks a value the type does not allow.Try this: with an item in the cart, switch the price field: each one is typed number | Money, so read it through a helper.
Real requests to the demo store
Storefront canvas · en · LTR
Runs in the browser…
Controls
What a theme writes
import type { CartItem, Money } from '@salla.sa/twilight-theme-engine/types';
import { useMoney } from '@salla.sa/twilight-theme-engine/hooks/useMoney';

/** Item prices are typed number | Money: never do math on them directly. */
function amountOf(price: number | Money): number {
  return typeof price === 'number' ? price : price.amount;
}

export function ItemPrice({ item }: { item: CartItem }) {
  const { format } = useMoney();
  return <span className="item-price">{format(amountOf(item.price))}</span>;
}

Example

app/components/cart/LineTotal.tsx
import type { CartItem, Money } from '@salla.sa/twilight-theme-engine/types';
import { useMoney } from '@salla.sa/twilight-theme-engine/hooks/useMoney';

/** Cart item prices are typed number | Money: read the number either way. */
function amountOf(price: number | Money): number {
  return typeof price === 'number' ? price : price.amount;
}

export function LineTotal({ item }: { item: CartItem }) {
  const { format } = useMoney();
  const currency = typeof item.total === 'object' ? item.total.currency : undefined;

  return (
    <span className="line-total">
      {item.quantity} × {format(amountOf(item.price), { currency })}
      {' = '}
      {format(amountOf(item.total), { currency })}
    </span>
  );
}

How it behaves

  • The cart comes from cart.get(cartId) or cart.queries.detail(cartId) (@salla.sa/twilight-theme-engine/api/cart), with the numeric id from Salla.cart.api.getCurrentCartId() in the browser. The cart page props, CartSummary, CartItem, SeoCartWidget, the cart context and useGtm all use this type.

  • The API sends loyalty and gift next to the cart; cart.get moves them inside and sets each to null when absent. should_refresh is not sent by the API: the source marks it a React-only flag.

  • The engine CartItem component reads prices the way the example does: the number itself, or amount and currency from a Money.

  • FreeShippingBar.percent runs from 0 to 100 (CartSummary uses it as a CSS width), and remaining is put into the "add :amount more" message unformatted.

  • CartOption (an option on the whole cart) and CartItemOption (an option on one item) have the same fields.

Gotchas

  • price, product_price, original_price, total and total_special_price are number | Money. item.price * item.quantity and item.price.amount do not compile, and in plain JavaScript a Money object in arithmetic gives NaN. Normalize first, as the example does.

  • Cart.total is number | string: cart.total + shipping joins two strings whenever the total is text. Use Number(cart.total).

  • docs/07-data-types.md shows items_count, Money.getMoney() and a required Money.formatted. The code has count, no getMoney, and an optional formatted: cart.items_count does not compile, and in plain JavaScript it is undefined.

Related

Source and docs