> ## Documentation Index
> Fetch the complete documentation index at: https://docs.passage.coinlist.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Building the Checkout flow

> Render CheckoutContainer with an offer, a chain, your wallets and one config object. It picks the right provider's flow for you.

`CheckoutContainer` is the **plug-and-play checkout**: hand it an offer and it renders whichever provider that offer belongs to. Superstate swap, Ondo buy, CoinList token sale - you write one integration, not three.

This is the highest rung of the SDK (**L3, components**) and the one to start from. Reach for a provider-specific container or the hooks below it only when you need markup the SDK does not give you.

<Note>
  Prefer `CheckoutContainer` over `OndoBuyCheckoutContainer` or `SuperstateSwapCheckoutContainer` even when you know the offer type. The provider containers exist, but routing on `offer.type` yourself means a new provider is your problem; letting `CheckoutContainer` do it means a new provider is a version bump.
</Note>

## Prerequisites

* A completed [OAuth](/sdk/oauth-authentication) session with a working `CoinListProvider`
* An `OfferDetail` from [Display offer details](/sdk/sale-details)
* A [`CheckoutWalletSelection`](/sdk/wallets#checkoutwalletselection-what-checkout-takes) built from your wallet stack

## Render it

```tsx theme={null}
"use client";

import {
  CheckoutContainer,
  defaultCheckoutConfig,
  type CheckoutWalletSelection,
} from "@coinlist-co/react";
import { AssetSymbol, type OfferDetail } from "@coinlist-co/react/shared";

// Per-integration, not per-offer: build it once and reuse it for every offer.
const checkoutConfig = defaultCheckoutConfig({
  "ondo::swap": {
    symbol: (offer) => AssetSymbol(offer.asset.code),
  },
  "coinlist::token_sale": {
    render: (offer) => <MyTokenSalePage offer={offer} />,
  },
});

export function Checkout({
  offer,
  wallets,
}: {
  offer: OfferDetail;
  wallets: CheckoutWalletSelection;
}) {
  return (
    <CheckoutContainer
      key={`${offer.id}:ethereum_mainnet`}
      offer={offer}
      chain="ethereum_mainnet"
      wallets={wallets}
      config={checkoutConfig}
    />
  );
}
```

That is the whole integration. The container is **self-scoped**: it renders fully styled with no style provider in the tree.

### Props

| Prop        | Type                      | Description                                                                                                                        |
| ----------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `offer`     | `OfferDetail`             | The offer being bought. Its `type` decides which provider renders. Fixed for the lifetime of the mount.                            |
| `chain`     | `EthereumChain`           | Where the order executes: balance reads, the approval, and the broadcast all go here. Fixed for the lifetime of the mount.         |
| `wallets`   | `CheckoutWalletSelection` | The wallets the buyer can spend from, plus connect and disconnect lambdas. See [Wallets](/sdk/wallets).                            |
| `config`    | `CheckoutConfig`          | What each offer type needs beyond the offer itself. See below.                                                                     |
| `enabled`   | `boolean`                 | Default `true`. `false` turns off every request and timer in every provider, for a checkout that is mounted but not on screen yet. |
| `className` | `string`                  | Applied to the rendered provider view.                                                                                             |

## One mount, one offer, one chain

**To render a different offer or chain, remount rather than reassign.** Give the container a `key` that changes with them, as the example above does.

`offer` and `chain` are read as fixed for the lifetime of the mount. Each provider's viewmodel owns a step machine - which wallet was authorized, how much is being spent - and that progress is bound to the offer it was made against: a Superstate wallet authorization is allowlisted per offer, and an ERC-20 approval is granted per chain. Changing either prop in place would carry a completed step from one offer onto another. Worse, a swap could land mid-execution, between an approval and the broadcast it was granted for.

React discards the old flow's state along with its component instance, which is the only reset that cannot strand an in-flight transaction.

## CheckoutConfig

`CheckoutConfig` is keyed by `OfferType`, and **every key is required**.

That is not an oversight. A host that renders `CheckoutContainer` is saying it will handle whatever offer it is given, so when a new offer type ships, every host breaks **at compile time** rather than rendering a blank screen their users find first. A new offer type arriving in a catalogue is something you have to decide about.

Nothing in the config is per-offer. It is per-*integration*, so you build one object and pass the same one for every offer you render.

| Key                    | Field                                | What you supply                                                                                                                                                       |
| ---------------------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ondo::swap`           | `symbol: (offer) => AssetSymbol`     | **Required.** Ondo's API symbol for the offer's asset, e.g. `AAPLon`.                                                                                                 |
|                        | `onOrderConfirmed?: (order) => void` | Optional. Fired once an Ondo swap has mined. The confirmation dialog shows either way.                                                                                |
| `superstate::swap`     | `onOrderConfirmed?: (order) => void` | Optional. Superstate needs nothing required today: the swap contract is looked up from the chain, and the funding assets and issuer are the provider's own constants. |
| `coinlist::token_sale` | `render: (offer) => ReactNode`       | **Required.** The SDK ships no token-sale UI yet, so this branch is yours. An L3 token-sale component is planned.                                                     |

`defaultCheckoutConfig({ ... })` fills in the entries that have a sensible default, so you only write the ones that do not. Today that is `superstate::swap`; it exists mostly so a provider that gains an *optional* setting can be defaulted there rather than appearing in every host's config object.

### Why `symbol` takes the offer

`symbol` is a function of the offer rather than a bare value because the symbol is per-*asset* while the config is per-integration: a host listing `AAPLon` and `TSLAon` side by side has one config and two answers.

The SDK cannot answer it for you. Ondo's symbol tracks the underlying ticker and changes on a rebrand, and it already disagrees with `offer.asset.code` on Sepolia, where a mock asset stands in. If your catalogue does agree, return `AssetSymbol(offer.asset.code)`; if it does not, map the exceptions.

<Warning>
  **`symbol` must be total.** React hooks cannot be called conditionally, so Ondo's viewmodel runs for *every* offer, including Superstate and token-sale ones, where its result is handed to a disabled viewmodel and never read. Throwing on an offer you do not recognise would take down a checkout Ondo has no part in. Return anything.
</Warning>

## What happens underneath

Every provider's viewmodel is called on every render, because React hooks cannot be called conditionally. Only the one matching `offer.type` is `enabled`, so the other providers fetch nothing and run no timers. That is why every checkout viewmodel has an `enabled` flag.

The `switch` on `offer.type` is exhaustive, and its `default` branch assigns to `const exhaustive: never`, so a new offer type is a compile error inside the SDK as well as in your config.

<AccordionGroup>
  <Accordion title="Build your own UI with hooks (L2)">
    Each product exposes a **god viewmodel** wrapping its entire flow, plus the smaller hooks it is built from. Call one behind your own markup when you want the SDK's logic, state and error handling but not its UI.

    ```tsx theme={null}
    import { useOndoBuyCheckoutViewModel } from "@coinlist-co/react";

    const { state, onEvent } = useOndoBuyCheckoutViewModel({
      offer,
      ondoSymbol: AssetSymbol(offer.asset.code),
      chain: "ethereum_mainnet",
      wallets,
    });
    ```

    `useSuperstateSwapCheckoutViewModel` takes the same options minus `ondoSymbol`. Both return `{ state, onEvent }`, where `state` is a discriminated union you `switch` on exhaustively.

    If you call these directly you are routing on `offer.type` yourself, and every hook must be called unconditionally - pass `enabled: offer.type === "ondo::swap"` to the ones that should be idle. That bookkeeping is exactly what `CheckoutContainer` does for you.

    Per-provider detail: [Superstate Swap](/sdk/swap-flow), [Ondo Swap Buy](/sdk/ondo-swap-buy), [CoinList Token Sale](/sdk/invest-flow). The conventions the hooks share - `enabled`, `data`, and the shape of `state` - are in [SDK structure](/sdk/structure#hooks).
  </Accordion>

  <Accordion title="Drive it yourself with the client (L1)">
    Below the hooks are plain TypeScript namespaces on `CoinListClient`, with no React and no state. Use them for a non-React app, a custom state layer, or a step the SDK's flows do not cover.

    ```ts theme={null}
    const { coinlist } = useCoinList();

    // Ondo: approve, then commit a firm quote, then broadcast it.
    const prepared = await coinlist.ondo.prepareSwap({ /* ... */ });
    const filled = await coinlist.ondo.executeSwap({ /* ... */ });

    // Superstate: prove the wallet, then swap.
    await coinlist.superstate.authorizeWallet({ /* ... */ });
    await coinlist.superstate.execute({ /* ... */ });

    // CoinList token sale: approve and record a participation.
    await coinlist.tokenSale.execute({ /* ... */ });
    ```

    These return **step-tagged results rather than throwing**, so you can map each failure to your own copy. The full parameters, progress phases and error steps live on each provider's page.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Superstate Swap" icon="arrows-rotate" href="/sdk/swap-flow">
    Wallet allowlisting, live quotes, and what Superstate needs from you.
  </Card>

  <Card title="Ondo Swap Buy" icon="chart-line" href="/sdk/ondo-swap-buy">
    The symbol resolver, the committed quote, and the two-call order.
  </Card>

  <Card title="CoinList Token Sale" icon="receipt" href="/sdk/invest-flow">
    The render slot, the approval, and tracking participations.
  </Card>

  <Card title="Errors and edge cases" icon="triangle-exclamation" href="/sdk/errors">
    Every error shape the flows return, and how to handle it.
  </Card>
</CardGroup>
