# React (/developers/sdks/react)

`@fianto/react` wraps [`@fianto/js`](/developers/sdks/js) for React: `FiantoButton` is the branded
pay button, and `useCheckout` tracks one checkout in React state for a button you design yourself.
There is no provider and no key to configure: your server creates the session.

## Before you start [#before-you-start]

* React 19 or later (the peer dependency).
* A checkout route on your server that answers `{ id, url }`, such as `Checkout()` from
  [`@fianto/nextjs`](/developers/sdks/nextjs).
* The page with the button is served from the same origin as the session's `success_url`.

## Steps [#steps]

**Step 1.**

### Install the package [#install-the-package]

#### pnpm

```bash
pnpm add @fianto/react
```

#### npm

```bash
npm install @fianto/react
```

#### yarn

```bash
yarn add @fianto/react
```

#### bun

```bash
bun add @fianto/react
```

It installs `@fianto/js` with it, and re-exports `fetchCheckoutSession` and the error classes, so
you import everything from `@fianto/react`.

**Step 2.**

### Render FiantoButton in a client component [#render-fiantobutton-in-a-client-component]

Every file the package ships starts with `'use client'`, so you can import it from a Server
Component module. The component that renders the button and handles its callbacks must be a client
component itself.

```tsx
// app/checkout-button.tsx
'use client';

import { FiantoButton, fetchCheckoutSession } from '@fianto/react';
import { useState } from 'react';

const MESSAGES: Record<string, string> = {
  succeeded: 'Thanks! We are confirming your payment.',
  canceled: 'Checkout canceled.',
  expired: 'That checkout link expired. Check your order status before trying again.',
  closed: 'We could not tell what happened. Check your order status before paying again.',
};

export function CheckoutButton({ orderId }: { orderId: string }) {
  const [message, setMessage] = useState<string | null>(null);
  return (
    <div>
      <FiantoButton
        session={() => fetchCheckoutSession('/api/checkout', { body: { orderId } })}
        theme="brand"
        label="pay"
        onResult={(result) => setMessage(MESSAGES[result.status] ?? null)}
        onError={(error) => console.error(error)} // the route's message is for you
      />
      {message ? <p role="status">{message}</p> : null}
    </div>
  );
}
```

The button renders plain React markup, so the server and the client render the same DOM. When your
route refuses, it shows the payer a short line chosen by the error's `code` and passes the full error
to `onError`. A click while a checkout is open brings its popup to the front.

| Prop                  | Type                                                                 | Default                                                     |
| --------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------- |
| `session`             | `{ id, url }` or `() => Promise<{ id, url }>` (required)             | —                                                           |
| `theme`               | `'brand' \| 'dark' \| 'light' \| 'outline' \| 'auto'`                | `'brand'`                                                   |
| `label`               | `'plain' \| 'pay' \| 'buy' \| 'checkout' \| 'subscribe' \| 'donate'` | `'pay'`                                                     |
| `shape`               | `'rect' \| 'rounded' \| 'pill'`                                      | `'rounded'`                                                 |
| `size`                | `'static' \| 'fill'`                                                 | `'static'`                                                  |
| `locale`              | `'en' \| 'vi'`                                                       | `en` on the server, then the browser's language after mount |
| `fallback`            | `'redirect' \| 'none'`                                               | `'redirect'`                                                |
| `loading`, `disabled` | `boolean`                                                            | `false`                                                     |
| `onResult`            | `(result: CheckoutResult) => void`                                   | —                                                           |
| `onError`             | `(error: Error) => void`                                             | —                                                           |
| `className`, `style`  | as on any element                                                    | —                                                           |

`ref` reaches the underlying `<button>`. Pass `locale` when the server must render the same language
the visitor sees; without it, a non-English visitor sees English until the component mounts.

**Step 3.**

### Or build your own button with `useCheckout` [#or-build-your-own-button-with-usecheckout]

```tsx
// app/pay-link.tsx
'use client';

import { fetchCheckoutSession, useCheckout } from '@fianto/react';

export function PayLink({ orderId }: { orderId: string }) {
  const { open, focus, isOpen, result, error } = useCheckout({
    session: () => fetchCheckoutSession('/api/checkout', { body: { orderId } }),
  });

  return (
    <div>
      {/* Call open() directly in the click handler, never after an await. */}
      <button onClick={() => (isOpen ? focus() : void open())} aria-busy={isOpen}>
        {isOpen ? 'Checkout open…' : 'Pay'}
      </button>
      {result ? <p role="status">Result: {result.status}</p> : null}
      {error ? <p role="alert">Checkout could not be started. Please try again.</p> : null}
    </div>
  );
}
```

`useCheckout({ session, fallback? })` returns:

| Field    | Type                                                                                                                   |
| -------- | ---------------------------------------------------------------------------------------------------------------------- |
| `open`   | Returns a promise of the result, or of `undefined`. Never throws: on failure it sets `error` and resolves `undefined`. |
| `focus`  | `() => boolean`. Brings the open popup to the front; `false` when there is none.                                       |
| `status` | `'idle' \| 'open' \| 'done' \| 'error'`                                                                                |
| `result` | The last `CheckoutResult`, or `null`                                                                                   |
| `error`  | The last error, or `null`. A `CheckoutSessionError` with a `code` when your route refused.                             |
| `isOpen` | `status === 'open'`                                                                                                    |

Only the latest `open()` drives the state. Calling it again takes over the popup, and the earlier
checkout resolves `closed` with reason `superseded`.

**Step 4.**

### Read the result safely [#read-the-result-safely]

`result.status` is `succeeded`, `canceled`, `expired` or `closed`, the same as
[`openCheckout`](/developers/sdks/js), with a `reason` on `closed`.

> **Fulfil from Fianto, not from the button:**
>
> `succeeded` means the payer's transaction was confirmed on the checkout page, not that the order is
> paid. Mark it paid only from a verified `order.paid` webhook or a server-side retrieve.

> **closed means unknown:**
>
> `closed` tells you nothing about the payment: one may still be confirming. With reason
> `unreachable`, checkout may still be open. Ask the payer to check their order status first.

## Check it worked [#check-it-worked]

> Click the button. A popup opens Fianto's checkout page for the session your route created. Close the popup after the page has loaded: `onResult` receives `closed`
> with reason `closed_by_payer`.

## Troubleshooting [#troubleshooting]

| You see | Why | Fix |
|---|---|---|
| The popup is blocked | `open()` ran after an `await`, a `setTimeout` or inside a `useEffect`. | Call `open()` directly in the click handler. |
| `closed` with reason `unreachable` right away | The page sends `Cross-Origin-Opener-Policy: same-origin`. | Send `same-origin-allow-popups` on that page. |
| Next.js errors when a Server Component renders `FiantoButton` or calls `useCheckout` | `session`, `onResult` and `onError` are functions, and hooks run only in client components. | Move that code into a file that starts with `'use client'`. |
| `error.code` is `internal_error` | Fianto refused for a reason your route does not pass to the browser. | Read what the route's `onError` logged on your server. |
| The button shows English first, then another language | No `locale` prop: the server renders `en` and the browser language applies after mount. | Pass `locale` when the server knows the language. |

## See also [#see-also]

- [Browser (@fianto/js)](/developers/sdks/js): Statuses, popup blockers, COOP and the web component.

- [Next.js](/developers/sdks/nextjs): The checkout route behind the button.

- [Quickstart](/get-started/quickstart): Route, button and webhook together.