# React pay button

```tsx
import { PayButton } from "@mayarin/react";

<PayButton
  checkout={() => fetch("/api/pay", { method: "POST" }).then((r) => r.json())}
>
  Pay with crypto
</PayButton>;
```

`@mayarin/react` is one button and one hook. A click mints a payment intent, then sends the buyer to the hosted checkout, which handles asset choice, the wallet or deposit address, and live status. You do not create products or payment links on the dashboard first\: the amount comes with the click.

## Two modes, and who decides the price

| Mode    | Props                                             | Who sets the amount | Use it for                         |
| ------- | ------------------------------------------------- | ------------------- | ---------------------------------- |
| Server  | `checkout`                                        | Your backend        | Anything you sell                  |
| Browser | `publishableKey`, `baseUrl`, `amount`, `currency` | The browser         | Donations, tips, pay-what-you-want |

**Server mode** calls your backend. Your backend mints the intent with its secret key and returns the URL. The price never leaves your server, so nobody can change it.

```ts
// Your backend — e.g. POST /api/pay
import { createMayarin } from "@mayarin/sdk";

const mayarin = createMayarin({
  baseUrl: "https://api.mayarin.xyz",
  secretKey: process.env.MAYARIN_SECRET_KEY!,
});

const { url } = await mayarin.commerce.carts.checkout({
  currency: "IDR",
  lines: [
    {
      name: "Order #123",
      unitPrice: { amount: "150000.00", asset: "IDR" },
      quantity: 1,
    },
  ],
  merchantReference: "order_123",
  metadata: { checkoutSuccessBaseUrl: "https://shop.example/thanks" },
});

return Response.json({ url });
```

`checkout` may resolve to the URL string or to any object with a `url` field.

**Browser mode** needs no backend. The browser mints the intent itself with a [publishable key](/guides/authentication)\:

```tsx
<PayButton
  publishableKey="pk_…"
  baseUrl="https://api.mayarin.xyz"
  amount="150000.00"
  currency="IDR"
  name="Support the shop"
  merchantReference="donation_42"
  successUrl="https://shop.example/thanks"
>
  Pay with crypto
</PayButton>
```

> **Warning\:** in browser mode the amount is whatever the browser sends, and a buyer with devtools can change it. Before you ship goods paid this way, check the paid amount against your own order in the [webhook](/sdk/typescript/webhooks), matched by `merchantReference`. For a fixed-price sale, use server mode.

What the browser cannot change\: the merchant. A publishable key names its merchant, and the checkout page always shows that merchant's own name, city and country from the dashboard settings. Set your city and country first, or the API refuses the checkout.

## Props

| Prop                | Mode    | Meaning                                                                                |
| ------------------- | ------- | -------------------------------------------------------------------------------------- |
| `checkout`          | Server  | `() => Promise<string \| { url: string }>`                                             |
| `publishableKey`    | Browser | A `pk_…` key. No merchant id needed.                                                   |
| `baseUrl`           | Browser | The payment API origin for your environment.                                           |
| `amount`            | Browser | Decimal string per unit, e.g. `"150000.00"`. Never a number.                           |
| `currency`          | Browser | The currency you price in, e.g. `"IDR"`.                                               |
| `name`              | Browser | The line the buyer sees. Default `"Payment"`.                                          |
| `quantity`          | Browser | Default `1`.                                                                           |
| `merchantReference` | Browser | Your order id. Returned on the payment and in every webhook.                           |
| `successUrl`        | Browser | After paying, the buyer lands on `{successUrl}/{paymentIntentId}`. HTTPS or localhost. |
| `metadata`          | Browser | String key/value pairs stored on the intent.                                           |
| `mode`              | Both    | `"redirect"` (default) or `"modal"`. A modal opens the checkout over your page.        |
| `onSuccess`         | Modal   | `({ intentId, successUrl }) => void`. The payment settled.                             |
| `onFailed`          | Modal   | `({ intentId, status }) => void`. `status` is `"FAILED"` or `"EXPIRED"`.               |
| `onClose`           | Modal   | `() => void`. The modal closed, by the buyer or by you.                                |
| `onRedirect`        | Both    | `(url) => void`. Handle navigation yourself, e.g. open a new tab. Default\: same tab.   |
| `onError`           | Both    | `(error) => void`. The button stays clickable after an error.                          |
| `children`          | Both    | Any React node, or `({ loading }) => node`. Default `"Pay with crypto"`.               |

Every other prop (`className`, `style`, `id`, `disabled`, `aria-*`) is passed to the `<button>` unchanged. The button has no styling of its own, and it never submits a surrounding form.

## Open the checkout in a modal

By default a click sends the buyer to the hosted checkout. With `mode="modal"` the checkout opens over your page instead, as a centred card on a wide screen and a sheet from the bottom on a phone, so the buyer never leaves your site\:

```tsx
<PayButton
  mode="modal"
  checkout={createCheckout}
  onSuccess={({ intentId }) => router.push(`/thanks/${intentId}`)}
  onFailed={({ status }) => showRetry(status)}
  onClose={() => refreshOrder()}
>
  Pay with crypto
</PayButton>
```

The modal stays open on the receipt until the buyer taps Done, then `onClose` runs. Without `onSuccess`, a payment minted with a `successUrl` goes there when it settles, as the redirect form would. The checkout never navigates its own frame.

> **Warning\:** `onSuccess` is a signal for your UI, not proof of payment. Confirm the payment in your [webhook](/sdk/typescript/webhooks) before you ship anything, as you would after a redirect.

### Tips and donations in a modal

Browser mode works in a modal too, so a tip or donation needs no backend\: the buyer types an amount on your page, and the checkout opens over it.

```tsx
const [amount, setAmount] = useState("50000.00");

<input inputMode="decimal" value={amount} onChange={(e) => setAmount(e.target.value)} />

<PayButton
  mode="modal"
  publishableKey="pk_…"
  baseUrl="https://api.mayarin.xyz"
  amount={amount}
  currency="IDR"
  name="Tip for the team"
  merchantReference={`tip_${crypto.randomUUID()}`}
  onSuccess={() => showThanks()}
>
  Send a tip
</PayButton>;
```

`amount` is read when the button is clicked, so it is always what the input holds then. The amount comes from the browser, which is fine for a tip and not for a sale; see [who decides the price](#two-modes-and-who-decides-the-price).

If you would rather the buyer type the amount inside the checkout, create a payment link with `kind: "open"` and open its URL instead\: [open amount in the modal](/sdk/typescript/browser#let-the-buyer-type-the-amount).

Not using React? [`openCheckout`](/sdk/typescript/browser#open-the-checkout-in-a-modal) does the same in plain JavaScript, and [React Native](/sdk/typescript/react-native) has a bottom sheet of its own.

## Custom content and busy state

```tsx
<PayButton checkout={createCheckout} className="btn-primary">
  {({ loading }) =>
    loading ? (
      <Spinner />
    ) : (
      <>
        <CryptoIcon /> Continue
      </>
    )
  }
</PayButton>
```

While the intent is being minted the button is `disabled` with `aria-busy="true"`. A second click does nothing.

## Headless\: `useMayarinCheckout`

Use the hook when the trigger is not a plain button, such as a design-system component or a form submit\:

```tsx
import { useMayarinCheckout } from "@mayarin/react";

const { redirect, loading, error } = useMayarinCheckout({
  checkout: createCheckout,
});

<Button onClick={redirect} loading={loading}>
  Pay
</Button>;
{
  error && <p role="alert">{error.message}</p>;
}
```

It takes the same options as `PayButton`. They are read when `redirect` runs, so a cart total that changes after the first render is always the current one.

The [demo storefront](https\://github.com/playriglabs/mayarin-demo) uses the hook in server mode on its shipping step.
