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

CartSummary

componentBeginnerserverbrowserlive demo

The cart page sidebar: free-shipping bar, loyalty and gift widgets, totals, a coupon box and the Complete Order button.

import { CartSummary } from '@salla.sa/twilight-theme-engine/components/cart';

In plain words

The box beside the cart lines: how far the shopper is from free shipping, the items total, shipping, discount and VAT rows, the grand total, a coupon field, and the button that starts checkout.

You pass the cart you read. It is lazy, so it goes inside <Suspense>.

Signature

const CartSummary: React.LazyExoticComponent<(props: {
  cart: Cart;
  applyCouponEnabled?: boolean;   // true
  taxAmount?: number;             // 0 or missing: no VAT row
  loyalty?: Cart['loyalty'];
  gift?: Cart['gift'];
  loyaltyPoints?: number;         // the customer's points
}) => JSX.Element>

Try it live

The order summary for your own guest cart. Coupons are sent to the demo store; Complete Order is disabled here.Try this: set taxAmount to 15: a VAT row appears and "Items total" becomes "Items total (excl. tax)". Then untick applyCouponEnabled.
Real requests to the demo store
Storefront canvas · ar · RTL
Runs in the browser…
Controls
applyCouponEnabled
0 uses the cart’s own tax_amount.
What a theme writes
import { Suspense } from 'react';
import { CartSummary } from '@salla.sa/twilight-theme-engine/components/cart';
import { useStore } from '@salla.sa/twilight-theme-engine/hooks/useStore';
import type { Cart } from '@salla.sa/twilight-theme-engine/types';

export function Summary({ cart, points }: { cart: Cart; points?: number }) {
  const { settings } = useStore();
  return (
    <Suspense fallback={null}>
      <CartSummary
        key={cart.id}
        cart={cart}
        applyCouponEnabled={settings.cart?.apply_coupon_enabled}
        taxAmount={cart.tax_amount}
        loyalty={cart.loyalty}
        gift={cart.gift}
        loyaltyPoints={points}
      />
    </Suspense>
  );
}

Example

app/components/cart/Summary.tsx
import { Suspense } from 'react';
import { CartSummary } from '@salla.sa/twilight-theme-engine/components/cart';
import { useStore } from '@salla.sa/twilight-theme-engine/hooks/useStore';
import type { Cart } from '@salla.sa/twilight-theme-engine/types';

export function Summary({ cart }: { cart: Cart }) {
  const { settings } = useStore();
  return (
    <Suspense fallback={null}>
      <CartSummary
        key={cart.id}
        cart={cart}
        applyCouponEnabled={settings.cart?.apply_coupon_enabled}
        taxAmount={cart.tax_amount}
        loyalty={cart.loyalty}
        gift={cart.gift}
      />
    </Suspense>
  );
}

How it behaves

  • Rows and their conditions: free-shipping bar (cart.free_shipping_bar), loyalty (loyalty and loyaltyPoints), gifting (gift.enabled), items total, options total (cart.options not empty), shipping (has_shipping), coupon box (applyCouponEnabled), discount (total_discount), VAT (taxAmount), total. Amounts go through useMoney().format.

  • The coupon box uses useCoupon(): it applies on submit, and the same button removes an applied coupon. Errors show under the field.

  • Hook slots, without context: cart:summary.start, cart:submit.start (the engine puts a price-quote button there when the store has that feature), cart:submit.end, cart:summary.end.

  • Complete Order calls Salla.cart.submit(), which starts checkout. SallaTieredOffer renders under the box.

  • The engine's cart page passes applyCouponEnabled={store.settings?.cart?.apply_coupon_enabled}, taxAmount={cart.tax_amount}, the cart's loyalty and gift, loyaltyPoints from loyalty.queries.points(), and key={cart.id}.

Gotchas

  • Complete Order swallows every error from Salla.cart.submit() (an empty catch), so a failed checkout start does nothing visible. Listen to the SDK's events if you need to react.

  • When the cart has loyalty, a customer with loyaltyPoints of 0 gets a stray "0": the loyalty card is guarded with loyalty && loyaltyPoints && (…), and React renders the number. Pass undefined instead of 0.

  • The coupon box keeps its own state: it shows useCoupon().value ?? cart.coupon in an uncontrolled input (defaultValue). Removing a coupon the cart was loaded with falls back to cart.coupon, so the box still shows it as applied; a coupon that arrives with a re-read cart leaves the field disabled but empty. Re-read the cart after a change and give the summary a key that changes with each read (the demo uses dataUpdatedAt). The engine's cart page uses key={cart.id}, which never changes.

  • taxAmount also switches the first row's label: with any truthy value it reads "Items total (excl. tax)", otherwise "Items total".

Related

Source and docs