# React checkout widget

**`@payitgo/react`** shows a payment on your own page, in a modal: the
customer chooses the coin, sees the exact amount, network, QR code, address
(and XRP destination tag), pays from their wallet, and watches the payment
confirm. It needs no key. Your server creates the payment and hands the
browser its `checkout_token`; the widget calls the
[public checkout API](/docs/checkout#the-public-checkout-api) and follows the
payment [live](/docs/checkout#follow-it-live).

| | |
|---|---|
| Install | `npm install @payitgo/react` |
| Source | [github.com/payitgo/react-sdk](https://github.com/payitgo/react-sdk) |
| Needs | React 18 or 19; it brings `qrcode.react` and `socket.io-client` |
| Module | ESM, with TypeScript types. The components are marked `'use client'` |

## Show a payment

In the browser, a button that opens the checkout:

```tsx
import { PayitgoButton, payitgoCheckout } from "@payitgo/react";

const adapter = payitgoCheckout({
  baseUrl: "https://api.payitgo.co",
  // Ask your server for the payment and return its checkout_token.
  createPayment: async () =>
    (await fetch("/orders/42/payitgo", { method: "POST" }).then((r) => r.json())).checkout_token,
});

export function PayButton() {
  return (
    <PayitgoButton
      adapter={adapter}
      payment={{ title: "Order #42", amount: { value: 49.9, currency: "USD" } }}
      onSuccess={(result) => showThanks(result.paymentId)}
    >
      Pay with crypto
    </PayitgoButton>
  );
}
```

On your server, the route the widget calls creates the payment with your
secret key ([Node.js library](/docs/sdk-node) here; any library, or
[`POST /v1/payments`](/docs/payments#create-a-payment), does the same) and
answers with its `checkout_token`:

```ts
import { Payitgo } from "@payitgo/node";

const payitgo = new Payitgo(process.env.PAYITGO_KEY!, { baseUrl: "https://api.payitgo.co" });

app.post("/orders/:id/payitgo", async (req, res) => {
  const order = await orders.get(req.params.id);
  // No token: the customer chooses the coin in the widget.
  const payment = await payitgo.payments.create({ order_id: order.id, amount_usd: order.total });
  res.json({ checkout_token: payment.checkout_token });
});
```

Never put a secret key in browser code. The checkout token only shows the
payer's view of one payment.

Each time the widget opens it calls `createPayment()` again. While the order
has an open payment, creating it again returns that same payment
([safe retries](/docs/payments#safe-retries)), so a customer who closes and
reopens the widget sees the same address and amount.

`onSuccess` runs in the customer's browser and is **not proof of payment**.
Fulfil orders from the [`payment.confirmed` webhook](/docs/webhooks).

### Without a server: a payment link

A [payment link](/docs/checkout#payment-links-in-email-or-chat) needs no key to
pay, so a page without a server of its own can start its payment from the
browser with `POST /links/{id}/checkout`:

```ts
const adapter = payitgoCheckout({
  baseUrl: "https://api.payitgo.co",
  createPayment: async () => {
    const r = await fetch(`https://api.payitgo.co/links/${linkId}/checkout`, { method: "POST" });
    const body = await r.json();
    if (!r.ok) throw new Error(body.error); // "this payment link can no longer be paid"…
    return body.checkout_token;
  },
});
```

### Next.js

Render the widget from a client component: the adapter and `onSuccess` are
functions, which a server component cannot pass down. Nothing renders on the
server; the modal opens in the browser.

## The adapter

```ts
payitgoCheckout(options: PayitgoCheckoutOptions): PayitgoAdapter
```

Connects the widget to payitgo.

| Option | Type | Required | Default | Description |
|---|---|---|---|---|
| `createPayment` | `(payment: PayitgoPayment) => Promise<string>` | yes | | Called when the widget opens (and on "Start again"): returns the payment's `checkout_token`, as it is |
| `baseUrl` | `string` | no | The address the package was released with | The gateway's address, `https://api.payitgo.co`. Trailing slashes are removed |
| `pollIntervalMs` | `number` | no | `4000` | How often to read the payment while the live connection is down |
| `fetch` | `typeof fetch` | no | The global `fetch` | For tests, or older runtimes |

What it calls, all from the browser, with no credentials:

| Call | When |
|---|---|
| `GET {baseUrl}/checkout/{checkout_token}` | When the widget opens, and every `pollIntervalMs` while the socket is down |
| `POST {baseUrl}/checkout/{checkout_token}/token` | The customer chose the coin (a `409`, chosen already, reads the payment instead) |
| `POST {baseUrl}/checkout/{checkout_token}/simulate` | A test payment's simulate buttons |
| socket.io at `{baseUrl}/socket.io/`, default namespace | `subscribe` with the checkout token on every connection; each `payment:update` carries the whole payment |

All of them answer any origin, so no proxy is needed. A `POST` answered
`429` is sent again once, a second later. If you put a proxy in front of the
gateway anyway (`baseUrl: "https://shop.example/payitgo"`), it must forward
WebSocket upgrades on `/payitgo/socket.io/`: a plain HTTP rewrite cannot carry
the WebSocket (the widget then polls instead).

### PayitgoError

What the adapter throws: the API's message (capitalised) and HTTP status.

| Property | Type | Description |
|---|---|---|
| `message` | `string` | `"Payment not found"`, `"This payment can no longer be paid"`… |
| `status` | `number` | The HTTP status, or `0` when the gateway could not be reached |

The widget shows them itself, with a way to try again; you only meet them in
a custom adapter or around your own calls.

## Components

### PayitgoButton

A button that opens the checkout. It takes every
[`PayitgoCheckout`](#payitgocheckout) prop except `open`, plus:

| Prop | Type | Description |
|---|---|---|
| `children` | `ReactNode` | The label. Default: "Pay with crypto" |
| `className`, `style` | | Your own styles for the button |
| `disabled` | `boolean` | |
| `onClose` | `() => void` | The customer closed the checkout |

### PayitgoCheckout

The modal itself, controlled: render it anywhere, it portals to
`document.body` while `open`. Every opening starts with `createPayment()`.

| Prop | Type | Required | Description |
|---|---|---|---|
| `open` | `boolean` | yes | |
| `onClose` | `() => void` | yes | Escape, the close button, a click outside, or "Done" |
| `payment` | `PayitgoPayment` | yes | Display text: `title`, `amount: { value, currency: "USD" }`, and optionally `description`, `period`, `badge`, `lineItem`, `due`. Once loaded, the amount shown is the payment's own |
| `adapter` | `PayitgoAdapter` | no | `payitgoCheckout({…})`. Default: `demoAdapter`, an offline simulation |
| `onSuccess` | `(result: PayitgoResult) => void` | no | Once, when the payment is confirmed: `{ paymentId, status: "confirmed", token, amount, txId }`, `amount` a decimal string in `token` |
| `theme` | `{ mode?: "dark" \| "light", accent?: string }` | no | Dark by default; `accent` is any CSS colour |
| `returnLinks` | `boolean` | no | Show the payment's `success_url` and `cancel_url` as "Back to the shop" links. Default `false`: on your own page, closing the modal is the way back |

## What the customer sees

| `status` | The widget |
|---|---|
| `pending`, no coin yet | The payment's `token_options`, each at its price (cheapest first when you pass the fees on), and the time left to choose |
| `pending` | The exact amount and network, a QR code of the address, the address, the XRP destination tag (marked required), each with a copy button, and the countdown. At zero: "Time is up, checking…" until the gateway closes the payment |
| `partially_paid` | What arrived and what is still missing, to the same address |
| `detecting` | "Payment received, confirming…" with its confirmations; no address, so nobody pays twice |
| `confirmed` | "Paid", what arrived, the transaction |
| `expired` | "This payment expired". It keeps following the payment: money that arrives within 24 hours still completes it. A new payment is offered at once only if no coin was chosen; after an address was shown, only once the customer confirms they sent nothing |
| `underpaid` | What arrived in time, and that the rest can still be sent within 24 hours |
| `failed` | "The payment could not be confirmed", with the order id |

Every transfer in `deposits[]` is listed with its confirmations, marked
"Not counted" (another coin) or "Late". A payment
[priced in your currency](/docs/payments#prices-in-other-currencies) shows
that price next to the dollars. An unknown checkout token shows "Payment not
found", with no retry and no new payment.

## Test payments

A test payment shows a "Test payment" banner and buttons that play what the
customer does, through the real processing: deposits, status changes and
your webhooks ([Test mode](/docs/testing)). Live payments never show them.

| Button | Outcome |
|---|---|
| Pay in full | `confirmed` |
| Overpay | `confirmed` with `overpaid: true` |
| Pay half | `partially_paid`; the payment stays open |
| Underpay | `partially_paid`, then `underpaid` |
| Let it expire | `expired` (the only one before a coin is chosen) |
| Drop the transaction | `detecting`, then `failed` |
| Pay in another coin | A deposit in another coin (not on Bitcoin, Litecoin, Dogecoin or XRP) |
| Pay late | `partially_paid`, `underpaid`, then `confirmed` with `paid_late: true` |

## Custom adapters

`PayitgoAdapter` is an interface: `start(payment)` returns a checkout token,
`get(checkoutToken)` and `chooseToken(checkoutToken, token)` return the
payment as [`GET /checkout/{token}`](/docs/checkout#read-the-payment) does.
Two optional methods turn on more: `subscribe(checkoutToken, { onPayment,
onConnection, onNotFound })` follows the payment live (it returns the
function that stops), and `simulate(checkoutToken, outcome)` shows the
simulate buttons on test payments. Without `subscribe`, the widget polls
`get()` every `pollIntervalMs`.

`demoAdapter` simulates everything offline, with addresses no wallet accepts:
for previews and design work.

## Content Security Policy

The widget adds one `<style>` element to the page the first time it opens,
and styles some elements inline: a `style-src` must allow `'unsafe-inline'`.
`connect-src` must allow the gateway, over `https:` and `wss:`.
