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

# React (@ionicfi/react)

> <CheckoutEmbed>: embedded checkout as a React component.

React bindings over the browser SDK: one component that mounts an embedded
checkout session and manages the iframe's lifecycle across re-renders. It
depends on `@ionicfi/js` and bundles no payment logic of its own.

```bash theme={null}
npm install @ionicfi/react
```

## Usage

Create a checkout session on your server with `ui_mode: "embedded"`, hand the
response to the component, and render it where the payment form should appear:

```tsx theme={null}
import { CheckoutEmbed, type EmbeddedCheckoutSession } from "@ionicfi/react";

function PayPage({ session }: { session: EmbeddedCheckoutSession }) {
  return (
    <CheckoutEmbed
      session={session}
      // onComplete is a UI signal only: send the buyer to a confirmation
      // screen. Do NOT fulfill the order here; fulfill from the
      // checkout.session.completed webhook on your server (see below).
      onComplete={() => navigate("/order-confirmation")}
      onError={(e) => setCheckoutError(e)}
    />
  );
}
```

`session` is the create-session API response (`{ id, client_secret,
embed_url }`). The component loads the Ionic SDK from Ionic's CDN on first
mount (an existing `window.Ionic`, for example from a hand-placed script
tag, is reused), so the card-handling runtime is never bundled into your app.

**Fulfill orders from webhooks, not from `onComplete`.** The
`checkout.session.completed` webhook is the reliable signal that payment
succeeded; `onComplete` is a browser event that never fires if the buyer
closes the tab first, and a hostile page could try to emit it. Grant the
goods only when your server receives and verifies the webhook.

**Pass `onError` if you want to own the failure UI.** Without it, the two
failures that leave nothing on the page (the SDK script not loading, and
mount rejecting its options) render a plain built-in message and log the technical
detail to the console. Everything the checkout itself reports (expired session,
terminal payment failure) is already displayed inside the iframe, so the
component does not add a second message for those.

Digital wallets are on by default (`wallets` defaults to `"auto"`): if the
buyer's device and your account support one, it appears above the card form
with no integration work. If a wallet is configured but can't be offered, the
console explains exactly why and what to change.

Don't render the component inside another iframe: the checkout document
controls who may frame it, and payment methods that redirect can't complete
inside a nested frame.

In a Next.js App Router project, import it from a client component. The
published bundle carries the `"use client"` directive, so importing it from a
server component tree works without extra annotation, but the surrounding
page logic (state, callbacks) must itself be client-side.

## Props

| Prop          | Type                      | Description                                                                                                                                                                                                                                                                                                                                                                                                    |
| ------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `session`     | `EmbeddedCheckoutSession` | Required. The create-session response.                                                                                                                                                                                                                                                                                                                                                                         |
| `wallets`     | `"auto" \| WalletId[]`    | Which digital wallets to offer above the card form. Defaults to `"auto"`: every wallet the server reports eligible for the session. Pass an array to restrict the offer, or `[]` for card only. Eligibility (device, domain registration, amount) is decided server-side, so pass a **static** value; don't compute it from an async capability check. Changing which wallets are offered remounts the iframe. |
| `timeoutMs`   | `number`                  | How long to wait for checkout to become ready. Read at mount time; changing it later doesn't remount.                                                                                                                                                                                                                                                                                                          |
| `iframeTitle` | `string`                  | Accessible title for the checkout iframe. Applied in place; changing it doesn't remount.                                                                                                                                                                                                                                                                                                                       |
| `className`   | `string`                  | Applied to the host `div` the iframe mounts into.                                                                                                                                                                                                                                                                                                                                                              |
| `onReady`     | `() => void`              | The form is visible and interactive.                                                                                                                                                                                                                                                                                                                                                                           |
| `onComplete`  | `(e) => void`             | Payment succeeded. Fulfill on your server via webhooks; use this to update the UI.                                                                                                                                                                                                                                                                                                                             |
| `onCancel`    | `(e) => void`             | The buyer backed out. Not terminal; the form stays live.                                                                                                                                                                                                                                                                                                                                                       |
| `onError`     | `(e) => void`             | A failure was reported. See the code list below for which ones are terminal.                                                                                                                                                                                                                                                                                                                                   |

## Error codes

`onError` receives `{ code, message, terminal }`.

**Branch on `terminal`, not on `code`.** `terminal: false` means the iframe is
still live and may yet succeed, so replacing it with an error screen would take
a working checkout away from the buyer. New codes are added over time, so a
`switch` on `code` needs a `default` branch; `terminal` never needs updating.

```tsx theme={null}
<CheckoutEmbed
  session={session}
  onError={(e) => {
    if (!e.terminal) return showBanner(e.message); // still recoverable
    showFailurePage(e);
  }}
/>
```

| Code                  | Source                                              | `terminal`                                                                         |
| --------------------- | --------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `sdk_load_failed`     | This package: the CDN script failed to load         | `true`; remount to retry (see below)                                               |
| `mount_failed`        | This package: `mount()` rejected its options        | `true`                                                                             |
| `checkout_timeout`    | SDK: the iframe didn't become ready in `timeoutMs`  | **`false`**; the iframe stays live and may still become ready on a slow connection |
| `init_timeout`        | Checkout: the handshake inside the iframe timed out | `true`                                                                             |
| `payment_failed`      | Checkout: terminal payment failure                  | `true`                                                                             |
| `expired`             | Checkout: the session expired                       | `true`; create a new session                                                       |
| `not_found`           | Checkout: the session doesn't exist                 | `true`                                                                             |
| `tokenization_failed` | Checkout: card tokenization failed terminally       | `true`                                                                             |

Retryable declines (an issuer saying no) never reach `onError`; the checkout
handles them in the iframe and the buyer can try another card.

New codes may be added over time; always handle unknown codes in a default
branch rather than treating the list as closed.

## Retrying after a load failure

After `sdk_load_failed`, remount the component to retry: the loader clears
the failed load before retrying, so a fresh mount injects a fresh script:

```tsx theme={null}
function CheckoutWithRetry() {
  const [attempt, setAttempt] = useState(0);
  const [retryVisible, setRetryVisible] = useState(false);

  return (
    <>
      <CheckoutEmbed
        key={attempt}
        session={session}
        onError={(e) => {
          if (e.code === "sdk_load_failed") setRetryVisible(true);
        }}
      />
      {retryVisible && (
        <button onClick={() => setAttempt((n) => n + 1)}>Try again</button>
      )}
    </>
  );
}
```

## Re-render behavior

The iframe holds the buyer's in-progress card entry, so the component tears it
down and remounts only when the values that define the mount change: the
session's fields (`id`, `client_secret`, `embed_url`) or the set of wallets
offered. New
callback identities (inline arrow functions), new `session` object identities
with unchanged values, `timeoutMs`, and `iframeTitle` never remount. Callbacks
always fire with their latest render's identity, and never after the component
unmounts.

## Content Security Policy

If your page sends a `Content-Security-Policy`, it needs `script-src
https://js.ionicfi.com` (the SDK is loaded at runtime, never bundled) and
`frame-src` set to the origin of the session's `embed_url`. A blocked script
renders no payment form. Wallets need additional origins; see
[Embedded checkout: Content Security Policy](https://docs.ionicfi.com/guides/embedded-checkout#content-security-policy).

## Module format

This package is ESM-only, like `@ionicfi/js`. Jest projects still running
CommonJS transforms need the package excluded from `transformIgnorePatterns`:

```js theme={null}
// jest.config.js
transformIgnorePatterns: ["/node_modules/(?!@ionicfi/)"],
```

## Security notes

The component passes `session.client_secret` to the SDK and never logs or
stores it. It does live in React props, so tooling that captures props (React
DevTools, error reporters configured to serialize component trees) can see
it. The secret authorizes completing this one checkout session only.
