SazitoSazito SDKv1.2.2
Guides

Checkout

Drop-in multi-step checkout UI for Next.js apps built on the Sazito SDK.

Overview

@sazito/checkout is a ready-made, fully typed checkout flow with built-in state management. It handles:

  • Cart review (edit quantities, remove items, apply discount codes)
  • Shipping address form with region / city cascading selects
  • Shipping method selection (multiple groups supported)
  • Payment method selection and gateway redirect
  • Post-payment result screen (success, pending, failed, stock violated)
  • Automatic pending-payment polling after gateway return

Steps run in order: cart → shipping → payment → result

The payment step is the last interactive step: its CTA (پایان خرید / "Finish purchase") finalizes the order directly — there is no separate review step.

Installation

pnpm add @sazito/checkout

Import the stylesheet once — typically in your root layout:

import '@sazito/checkout/styles.css';

Setup

Create the Sazito SDK once and wrap your app (or checkout route) with SazitoProvider. Every SazitoCheckoutPage inside reads that same SDK client from context.

// app/providers.tsx
'use client';

import { createSazitoClient } from '@sazito/client-sdk';
import { SazitoProvider } from '@sazito/checkout/next';

const sazito = createSazitoClient({ domain: process.env.NEXT_PUBLIC_SAZITO_DOMAIN! });

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <SazitoProvider client={sazito}>
      {children}
    </SazitoProvider>
  );
}

Basic usage

With SazitoProvider in place, the checkout page only needs credentials and config:

import { SazitoCheckoutPage } from '@sazito/checkout/next';

export default function CheckoutPage() {
  return (
    <SazitoCheckoutPage
      credentials={{ cart: { identifier: '<cart-identifier>' } }}
      config={{
        locale: 'fa',
        continueShoppingUrl: '/',
        theme: { accent: '#4f46e5', radius: 16 },
      }}
    />
  );
}

Props reference

SazitoProvider

Wraps the app once. Accepts either domain (Next.js CORS-free path) or client (existing SDK client).

PropTypeDescription
domainstringStore domain. Checkout routes API calls through Next.js server actions.
clientunknownPre-built SazitoClient. Used directly — no server action routing.

SazitoCheckoutPage

PropTypeDescription
credentialsCheckoutCredentialsCart (and optionally invoice) identifiers to attach to.
configCheckoutConfigLocale, theme, URLs, event hook, behaviour flags.
paymentReturnParamsRecord<string, string>Query params from a returning payment gateway. Triggers result-resolution mode instead of bootstrapping a fresh flow.
classNamestringExtra class on the root element.
renderNextButton(props: RenderButtonProps) => ReactNodeReplace the continue / next button across all steps.
renderBackButton(props: RenderButtonProps) => ReactNodeReplace the back button across all steps.

CheckoutCredentials

interface CheckoutCredentials {
  cart?: { id?: number; identifier: string };
  invoice?: { id: number; identifier: string };
}

Typically only cart.identifier is needed. Supply invoice to re-attach to an already-created invoice (e.g. returning from a gateway).

CheckoutConfig

FieldTypeDefaultDescription
locale'fa' | 'en''fa'UI language. Controls all labels, RTL/LTR layout, and number formatting.
direction'rtl' | 'ltr'derived from localeOverride text direction explicitly.
themeCheckoutThemeVisual overrides — see Theming.
continueShoppingUrlstringURL for the back button on the cart step and the "continue shopping" link on the result screen.
returnUrlstringcurrent URLURL the payment gateway redirects to.
pollIntervalMsnumber15000How often (ms) to poll for a pending payment result after gateway return.
currencyLabelstringlocale defaultOverride the currency unit label (e.g. 'تومان').
onEvent(event: CheckoutEvent) => voidAnalytics hook — called for every checkout lifecycle event. See Analytics.

Theming

All visual tokens are CSS custom properties. Pass a theme object to override them:

config={{
  theme: {
    accent: '#7c3aed',           // primary colour — buttons, active states, selected rows
    accentForeground: '#ffffff', // text rendered on top of accent
    accentSoft: '#ede9fe',       // subtle accent tint — hover, selection background
    background: '#ffffff',       // page background
    foreground: '#0f172a',       // primary text
    muted: '#f1f5f9',            // muted surface background
    mutedForeground: '#64748b',  // secondary text
    border: '#e2e8f0',           // borders and dividers
    card: '#ffffff',             // form, product card, shipping card background
    summaryBackground: '#f8fafc', // order-summary sidebar background
    danger: '#ef4444',           // error and destructive states
    success: '#22c55e',          // success and completed states
    radius: 12,                  // base border-radius in px (scaled per element)
    fontFamily: 'inherit',       // font — defaults to host page font
  },
}}

Payment gateway return

When a gateway redirects back, render SazitoCheckoutPage with paymentReturnParams. The checkout auto-resolves the outcome and shows the result screen.

// app/checkout/return/page.tsx
import { SazitoCheckoutPage } from '@sazito/checkout/next';

export default function ReturnPage({
  searchParams,
}: {
  searchParams: Record<string, string>;
}) {
  return (
    <SazitoCheckoutPage
      credentials={{ cart: { identifier: '<cart-identifier>' } }}
      config={{ locale: 'fa', returnUrl: '/checkout/return' }}
      paymentReturnParams={searchParams}
    />
  );
}

If the gateway returns status: 'pending', the checkout polls every pollIntervalMs until it resolves.

Analytics

Pass onEvent to receive structured events at every step. Use it to send to any analytics provider:

config={{
  onEvent(event) {
    analytics.track(event.name, {
      step: event.step,
      value: event.value,         // order total in Rials where applicable
      ...event.metadata,
      timestamp: event.timestamp,
    });
  },
}}

Event names

EventFired when
checkout_viewedCheckout mounts
step_viewedUser lands on a step
address_submittedShipping address saved
shipping_rate_selectedUser picks a shipping method
discount_appliedDiscount code accepted
discount_removedDiscount code removed
payment_method_selectedUser picks a payment gateway
payment_initiatedOrder placed, gateway redirect started
payment_succeededGateway return resolved as success
payment_failedGateway return resolved as failed
payment_pendingGateway return resolved as pending (polling starts)

Validation gates

The next button disables automatically — no configuration needed:

StepDisabled when
CartCart is empty
ShippingShipping rates loaded but no rate selected (skipped for digital-only orders)
PaymentNo payment methods available, or none selected

Use renderNextButton / renderBackButton to swap built-in footer buttons for components from your own design system. Both receive pre-computed checkout state.

import type { RenderButtonProps } from '@sazito/checkout/next';

<SazitoCheckoutPage
  credentials={{ cart: { identifier: '<cart-identifier>' } }}
  config={{ locale: 'fa' }}
  renderNextButton={({ loading, disabled, onClick, children }) => (
    <MyButton loading={loading} disabled={disabled} onClick={onClick}>
      {children}
    </MyButton>
  )}
  renderBackButton={({ onClick, children }) => (
    <MyGhostButton onClick={onClick}>{children}</MyGhostButton>
  )}
/>

RenderButtonProps

PropTypeDescription
loadingbooleanAction is in-flight — show a spinner
disabledbooleanValidation not met — button should be inert
onClick() => voidTriggers the checkout action for this step
childrenReactNodeLocalised label for the current step

The children label changes per step: ادامه, ذخیره اطلاعات ارسال, ادامه به پرداخت, نهایی کردن سفارش (Persian) or the equivalent English strings.

Replacing primitives with asChild

All built-in primitives accept an asChild prop. When set, the component merges its behaviour props onto your single child element instead of rendering its own DOM node — the same pattern used by Radix UI and shadcn/ui.

Class names are merged (primitive class + child class). Event handlers are composed (both called).

Button

import { Button } from '@sazito/checkout/next';

<Button asChild loading={isLoading} disabled={isDisabled} onClick={handleClick}>
  <MyButton variant="filled">Continue</MyButton>
</Button>

Props forwarded: className, disabled, aria-busy, onClick, and all standard button attributes.

Field

Keeps the label, optional marker, and error message. Swaps the <input> element.

import { Field } from '@sazito/checkout/next';

<Field label="Email" id="email" error={errors.email} asChild>
  <MyInput placeholder="you@example.com" />
</Field>

Spinner

import { Spinner } from '@sazito/checkout/next';

<Spinner asChild>
  <MyLoadingIndicator />
</Spinner>

ErrorBanner

Merges role="alert" and class onto your component.

import { ErrorBanner } from '@sazito/checkout/next';

<ErrorBanner message={errorMessage} asChild>
  <MyAlert />
</ErrorBanner>

SectionTitle

Swaps the default <h3> for any heading element or typography component.

import { SectionTitle } from '@sazito/checkout/next';

<SectionTitle asChild>
  <h2 className="text-xl font-bold">Payment method</h2>
</SectionTitle>

ProductPlaceholder

Replaces the default box icon shown when a product has no image.

import { ProductPlaceholder } from '@sazito/checkout/next';

<ProductPlaceholder asChild>
  <img src="/fallback-product.png" alt="" />
</ProductPlaceholder>

Programmatic control with useCheckout

Use useCheckout inside any component rendered within CheckoutProvider to read state or trigger actions directly.

'use client';

import { useCheckout } from '@sazito/checkout/next';

export function DiscountWidget() {
  const { state, actions } = useCheckout();

  return (
    <div>
      <input
        value={state.discountCode}
        onChange={(e) => actions.setDiscountCode(e.target.value)}
      />
      <button
        onClick={() => actions.applyDiscount()}
        disabled={state.flags.applyingDiscount}
      >
        Apply
      </button>
      {/* Invalid/expired codes set an inline error, not the global banner. */}
      {state.discountError ? <p role="alert">{state.discountError}</p> : null}
      {state.appliedDiscountCode ? (
        <button onClick={() => actions.removeDiscount()}>
          Remove {state.appliedDiscountCode}
        </button>
      ) : null}
    </div>
  );
}

CheckoutState fields

FieldTypeDescription
stepCheckoutStepActive step: cart | shipping | payment | result
statusCheckoutStatusCoarse engine status: idle | bootstrapping | working | redirecting | polling | error
cartCart | nullCurrent cart
invoiceInvoice | nullCurrent invoice
addressFormAddressFormValuesLive shipping address form values
shippingGroupsShippingGroup[]Grouped shipping rates for each item bundle
paymentMethodsPaymentMethod[]Available payment gateways
selectedPaymentMethodIdnumber | nullSelected gateway
discountCodestringInput value of the discount field
appliedDiscountCodestring | nullSuccessfully applied code
discountErrorstring | nullInline error for the discount field (e.g. invalid code); cleared on edit/apply/remove
resultCheckoutResult | nullPost-payment result (status, order, message)
errorCheckoutError | nullLast error with message, code, step
flagsCheckoutFlagsGranular async flags (see below)

CheckoutFlags

FlagFires during
bootstrappingInitial cart + invoice load
updatingCartQuantity change or item remove
savingAddressShipping form submit
loadingShippingFetching applicable rates after address save
selectingRateSelecting a shipping rate
applyingDiscountDiscount code submission
loadingPaymentsFetching payment methods
placingOrderOrder placement + gateway redirect

CheckoutActions

MethodDescription
start()Bootstrap cart and invoice. Called automatically.
next()Advance to the next step (validates current step).
back()Return to the previous step.
goToStep(step)Jump to any step directly.
updateItemQuantity(cartProductId, variantId, quantity)Update a cart line quantity.
removeItem(cartProductId, variantId)Remove a cart line.
setAddressField(key, value)Update a single address form field.
submitAddress()Save address and load applicable shipping rates.
selectShippingRate(groupKey, rateId)Select a rate within a shipping group.
setDiscountCode(code)Update the discount code input.
applyDiscount()Submit the discount code.
removeDiscount()Remove the applied discount.
selectPaymentMethod(id)Select a payment method.
placeOrder()Place the order and redirect to the gateway.
resolvePaymentReturn(params)Resolve a return from the payment gateway.
reset()Reset checkout state to initial.

Custom layout with CheckoutProvider

SazitoCheckoutPage bundles provider + UI. To build a fully custom layout, use the primitives directly:

import {
  CheckoutProvider,
  SazitoCheckout,
  OrderSummary,
  Stepper,
} from '@sazito/checkout/next';

// SazitoProvider must still wrap the tree above this component.
export default function CheckoutPage() {
  return (
    <CheckoutProvider
      credentials={{ cart: { identifier: '<cart-identifier>' } }}
      config={{ locale: 'fa' }}
    >
      <div className="my-layout">
        <Stepper />
        <main>
          <SazitoCheckout />
        </main>
        <aside>
          <OrderSummary />
        </aside>
      </div>
    </CheckoutProvider>
  );
}

Error codes

CheckoutError.code gives a machine-readable reason:

CodeMeaning
no_cartCart identifier not found
no_invoiceInvoice could not be created or found
min_basketCart total below store minimum
rate_limitedToo many requests
cart_invalidCart is empty or contains invalid items
invoice_lockedInvoice is locked (already placed or expired)
stock_violatedOne or more items went out of stock during checkout
shipping_requiredCart requires shipping but no address or rate provided
address_requiredShipping address missing
payment_failedGateway reported payment failure
networkNetwork or server error
validationInvalid input sent to the API
unknownUnclassified error

On this page