# API reference

## `createYodlSdk(config)`

Returns a `YodlSdk` instance. Call it once for the whole app.

| Option | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `integrationAddress` | `string` | Yes | — | Your integration wallet address. Identifies you to Yodl and attributes payments on chain |
| `supportedChainIds` | `number[]` | Yes | — | Chain IDs the wallet can switch to |
| `yodlUiBaseUrl` | `string` | No | `https://sdk-webview.yodl.me` | Base URL for the hosted Yodl UI |

`integrationAddress` must be a non-empty `0x` address. The SDK sends it alongside package metadata on Yodl-owned requests:

```txt
X-Yodl-Integration-Id: 0x000000000000000000000000000000000000dEaD
X-Yodl-SDK-Name: @yodlpay/react-native
X-Yodl-SDK-Version: <installed package version>
```

:::note
Don't confuse this with the `X-Yodl-Integration` header on [payment notifications](/integrations/notifications). That one travels the other way — Yodl signing a webhook to you. This one is your app identifying itself to Yodl.
:::

Instance members:

* **`sdk.config`** — the resolved config, read-only.

## `isPaymentQr(qrData)`

`string | string[]` → `boolean`. Returns `true` if any entry is a Yodl-supported payment QR.

Pure and standalone: it takes no SDK instance, so you can guard a render before `<YodlProvider>` is mounted.

```tsx
import { isPaymentQr } from '@yodlpay/react-native';

if (!isPaymentQr(scannedQr)) return null;
```

## `<YodlProvider />`

Supplies the SDK instance and wallet state to the components. Render it once, high in your tree.

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `sdk` | `YodlSdk` | Yes | Instance from `createYodlSdk()` |
| `provider` | `EIP1193Provider` | Yes | EIP-1193 provider from any wallet SDK (`@yodlpay/sdk-core`'s type: `request` only, events not required; `withYodl7702` accepts and returns the same type, so no cast is needed). Prefer the wallet's native EIP-5792 support; only wrap with `withYodl7702(...)` if it lacks it — see [EIP-7702 provider](/sdk/eip-7702-provider) |
| `address` | `string` | Yes | Connected wallet address |
| `chainId` | `number` | Yes | Chain ID the wallet is currently on |
| `theme` | `YodlTheme` | No | Default hosted-UI palette; all fields optional |
| `themeMode` | `'system' \| 'light' \| 'dark'` | No | Default host-controlled mode |

`address` and `chainId` are the wallet's **live** account and chain — pass them straight from your wallet SDK's hooks (`useWallets()`, `useAccount()`, …), not as fixed values. They tell the hosted UI who is connected and on which chain, and the SDK pushes `accountsChanged` / `chainChanged` into the UI whenever they change. The `provider` is used only as the RPC and signing transport.

## Shared visual props

`<YodlSignup>`, `<YodlPayment>` and `<YodlDashboard>` all accept these, in addition to their own props below.

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `theme` | `YodlTheme` | No | Palette override; inherits the provider `theme` when omitted |
| `themeMode` | `'system' \| 'light' \| 'dark'` | No | Mode override; inherits the provider `themeMode` when omitted |
| `backgroundColor` | `string` | No | Colour behind the WebView before first paint; overrides the theme background |
| `renderLoading` | `() => ReactElement \| null` | No | Replaces the loading overlay's contents; `null` suppresses the overlay entirely |
| `onReady` | `() => void` | No | Called once per mount, the first time the hosted UI is revealed |
| `style` | `ViewStyle` | No | Container style |

See [theming](/sdk/react-native/theming) for how overrides resolve, and [loading and readiness](#loading-and-readiness) for the last two.

### Loading and readiness

Until the hosted UI reports it has painted, the SDK covers the WebView with an overlay filled with `backgroundColor` and centred on an `ActivityIndicator`. It exists so the user never sees react-native-webview's hardcoded-white loading view, or the document's own white before its CSS lands.

`renderLoading` and `onReady` are two ways to take that over, and they answer different questions. **Pick one.** Using both is how you end up with the two loaders this exists to prevent.

#### Recipe A — Yodl's overlay, your loader

The SDK still owns the cover; you supply what goes inside it. Nothing in your own tree needs to know when the hosted UI is up, so `onReady` has no job here.

```tsx
<YodlPayment
  qrData={qrData}
  // Painted behind the WebView and in the overlay, so there is one ground colour.
  backgroundColor="#18181b"
  renderLoading={() => <MyBrandLoader />}
/>
```

`renderLoading` replaces what goes inside the overlay:

| Return value | Result |
| --- | --- |
| An element | Rendered centred on `backgroundColor`. The wrapper stays, so there is still no white flash |
| `null` | **No overlay at all** — no view is mounted, so nothing sits over the WebView intercepting touches. `backgroundColor` is still painted behind it |
| Prop omitted | The default `ActivityIndicator`, in the theme's `textSecondary` |

It is called on every render while the UI is still loading, so keep it cheap and start no work inside it.

#### Recipe B — your own screen covers it

Your app is already showing a full-screen loading state over the component. Suppress Yodl's overlay so there is only one, and let `onReady` tell you when to take yours down.

```tsx
<>
  {!ready && <MyLoadingScreen />}
  <YodlPayment
    qrData={qrData}
    backgroundColor="#18181b"
    renderLoading={() => null}
    onReady={() => setReady(true)}
  />
</>
```

`onReady` is the only way to do this: `renderLoading` is called *during render*, so you cannot `setState` from it.

`onReady` fires **once per mount**, the first time the UI is revealed. It does not fire again. The hosted document re-posts readiness when it reloads, but the overlay has already lifted and does not come back, so there is nothing to re-announce — it lifts once and stays lifted.

:::note
**The reveal has an 8-second ceiling.** If the hosted UI never reports readiness, the SDK reveals anyway and fires `onReady`, rather than leaving an overlay up forever.

It is a last resort for a page that is genuinely dead, not a routine path. A reveal on that timer arrives *without* the readiness handshake, so the hosted UI has received neither your theme nor the SDK's capability announcement. A current hosted build runs its own, shorter ceiling and always reports readiness first, so you only reach 8 seconds when the page never loaded at all. The ceiling reveals — it does not report an error; use `onError` for that.
:::

## `<YodlSignup />`

Renders the hosted signup flow as a full-screen component inside `<YodlProvider>`. Also accepts the [shared visual props](#shared-visual-props).

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `onStatusChange` | `(status: 'checking' \| 'signed_out' \| 'signing_up' \| 'signed_up') => void` | No | Called when the hosted flow reports signup status |
| `onSignedUp` | `() => void` | No | Called when signup completes |
| `onDismiss` | `() => void` | No | Called when the hosted success screen is dismissed |
| `onError` | `(error: Error) => void` | No | Called on SIWE, registration, verification or RPC errors |
| `fetchEmailAttestation` | `(request: YodlEmailAttestationRequest) => Promise<YodlEmailAttestation \| null>` | No | Supplies an [email attestation](/integrations/email-attestation) so the user skips Yodl's OTP |

### `fetchEmailAttestation`

Called once per registration attempt, at the moment the hosted flow is about to create the account — after the wallet signature, before the email is submitted. Return `null` to let the user verify by code as usual.

The request carries the wallet and nothing else. Your backend decides which address to attest — from its own session, never from anything the hosted page sent — and the `email` you return is the one the Yodl account is created with, so return it every time. A token with no `email` is treated as a decline and the user gets the OTP.

```ts
interface YodlEmailAttestationRequest {
  // The wallet the hosted flow proved with SIWE. Mint `sub` for exactly this.
  address: `0x${string}`;
  // Aborted on timeout, unmount, or session change. Forward it into your fetch.
  signal: AbortSignal;
}

interface YodlEmailAttestation {
  token: string; // the compact JWS your backend signed
  email?: string; // the token's `email` claim — the address you verified, required in practice
}
```

| Behaviour | Result |
| --- | --- |
| Resolves a `token` and an `email` | Sent to Yodl; the OTP is skipped if it verifies |
| Resolves a `token` with no `email` | Treated as a decline, normal OTP flow |
| Resolves `null` | Normal OTP flow. **Not** reported through `onError` |
| Throws | Normal OTP flow |
| Takes longer than 30 seconds | Aborted via `signal`, normal OTP flow |
| Prop omitted | Normal OTP flow, with no added latency |

:::warning
Mint a fresh token on every call. The token is single use and lives at most 5 minutes, so a cached one fails — and once its `jti` is spent, replaying it fails permanently rather than falling back cleanly on the next attempt.
:::

Yodl calls this at most 3 times per hosted session, and only when a flow actually registers an account. `<YodlPayment />` and `<YodlDashboard />` take the prop as well: a wallet with no Yodl account is asked to register inside the payment flow, and that registration skips the OTP on the same terms. Omit the prop on a surface and a registration there falls back to the code flow.

The 30-second budget is the same one the SDK gives its own network calls. It is deliberately generous: a mint that is cut short costs the user the wait *and* the OTP, and because attestation failures are silent it would not show up as an error anywhere. Signing itself takes milliseconds, so the budget is really covering the round trip to your backend on a bad mobile connection.

## `<YodlPayment />`

Renders the payment flow for a scanned QR code. Renders nothing if `qrData` is empty or unsupported. Also accepts the [shared visual props](#shared-visual-props).

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `qrData` | `string` | Yes | Raw scanned QR data string |
| `navigateOnSubmit` | `'always' \| 'never'` | No | Whether the hosted UI moves to its own status screen once the transaction is submitted. Defaults to `'always'` |
| `onTransactionSent` | `(txHash: string) => void` | No | Called after a transaction is sent |
| `onPaymentDetails` | `(details: YodlPaymentDetails) => void` | No | Called with payment lifecycle details from the UI |
| `onError` | `(error: Error) => void` | No | Called on RPC or payment errors |
| `fetchEmailAttestation` | `(request: YodlEmailAttestationRequest) => Promise<YodlEmailAttestation \| null>` | No | Supplies an [email attestation](#fetchemailattestation) if the flow asks a wallet without a Yodl account to register |

### `navigateOnSubmit`

By default the hosted UI moves itself to its payment-status screen the moment the transaction is **submitted** — not when it settles. If your app also transitions on `onTransactionSent`, the user sees two root transitions: the hosted one, then yours.

`'never'` hands that decision to you. The hosted UI parks on a terminal "sent" frame and stays there — nothing to press, and it never navigates again. It does keep polling behind that frame, so `onPaymentDetails` reports the payment through to `'success'` or `'failure'` exactly as it does under `'always'`.

```tsx
<YodlPayment
  qrData={qrData}
  navigateOnSubmit="never"
  onTransactionSent={(txHash) => router.replace(`/receipt/${txHash}`)}
/>
```

:::warning
Take this only if you actually navigate. A host that sets `'never'` and ignores both `onTransactionSent` and `onPaymentDetails` leaves the user parked on that frame with no way forward — which is why the default is `'always'` and omitting the prop is exactly today's behaviour.
:::

The prop feeds the hosted UI's URL, and the WebView is keyed by that URL, so changing the value on a mounted component remounts and reloads it. Pass a constant, not something that flips mid-flow.

An older hosted build silently drops the parameter and navigates as it always has, so the prop degrades rather than breaking.

## `<YodlDashboard />`

Renders the full Yodl account dashboard. Also accepts the [shared visual props](#shared-visual-props).

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `onTransactionSent` | `(txHash: string) => void` | No | Called after a transaction is sent |
| `onPaymentDetails` | `(details: YodlPaymentDetails) => void` | No | Called with payment lifecycle details from the UI |
| `onError` | `(error: Error) => void` | No | Called on RPC errors |
| `fetchEmailAttestation` | `(request: YodlEmailAttestationRequest) => Promise<YodlEmailAttestation \| null>` | No | Supplies an [email attestation](#fetchemailattestation) if the flow asks a wallet without a Yodl account to register |

## Payment lifecycle events

`onPaymentDetails` is optional and additive — it does not change `onTransactionSent` or `onError` behaviour.

The hosted UI emits generalized payment lifecycle details once it has payment status, and the SDK forwards them. Normalized fields cover transaction hash, chain, lifecycle status, recipient, invoice, token amounts, refund state and rewards where available. The original payload may be included under `raw`.

Status moves through `'processing'` → `'success' | 'failure'`. Under [`navigateOnSubmit="never"`](#navigateonsubmit) a `'submitted'` message opens the sequence, so hosts on that path see `'submitted'` → `'processing'` → `'success' | 'failure'`.

`'processing'` repeats while the payment settles — it is a poll, not a single transition. Treat `onPaymentDetails` as idempotent, and be careful about navigating from inside it.

### `source`

`details.source` says where the payload's figures came from, which `status` does not tell you:

| Value | Meaning |
| --- | --- |
| `'optimistic'` | Yodl's backend has not indexed the transaction yet. Every figure is the paying device's own pre-submit data. |
| `'indexed'` | Yodl's backend payment record is behind the payload. |

The two disagree on `invoice`. An optimistic payload reports the **settlement** amount and currency there (e.g. `0.78` `USD`); an indexed one reports the **local fiat** amount and currency (e.g. `20000` `VND`) and moves settlement into `settlementAmount`. Render amounts from an optimistic payload and you will show the wrong currency.

If you display figures rather than just tracking progress, wait for `'indexed'`:

```tsx
onPaymentDetails={(details) => {
  setStatus(details.status);
  if (details.source === 'indexed') setAmount(details.payment?.invoice);
}}
```

The two are independent — a payload can be `'indexed'` while `status` is still `'processing'`.

:::note
`source` is absent on hosted builds older than the one that introduced it, so treat a missing value as `'optimistic'`. Some fields — the recipient in particular — can still carry device data on an early `'indexed'` payload while the payment is settling.
:::

:::note
The SDK never fetches transaction details itself — it only bridges messages from the UI. If you need authoritative payment records on your own backend, use [payment notifications](/integrations/notifications) or the [Payments API](/integrations/payments-api).
:::
