# @unifold/connect-react

The complete React SDK for integrating crypto deposits and onramp functionality into your application. Simple, powerful, and fully customizable.

## Installation

```bash
npm install @unifold/connect-react
# or
pnpm add @unifold/connect-react
```

## Quick Start

### 1. Wrap your app with UnifoldProvider

```tsx
import { UnifoldProvider } from '@unifold/connect-react';

function App() {
  return (
    <UnifoldProvider
      publishableKey="pk_test_your_key"
      config={{
        modalTitle: 'Deposit Crypto',
        hideDepositTracker: false,
        display: {
          transferCrypto: {
            exchangeRate: true,
          },
        },
      }}
    >
      <YourApp />
    </UnifoldProvider>
  );
}
```

### 2. Call beginDeposit() anywhere in your app

```tsx
import { useUnifold } from '@unifold/connect-react';

function DepositButton() {
  const { beginDeposit } = useUnifold();

  const handleDeposit = async () => {
    try {
      // beginDeposit returns a Promise
      const result = await beginDeposit({
        // Required fields
        userId: 'user_123', // Your user's unique identifier
        destinationChainId: '137', // Polygon
        destinationTokenAddress: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174',
        destinationTokenSymbol: 'USDC',
        recipientAddress: '0x606C49ca2Fa4982F07016265040F777eD3DA3160',

        // Optional callbacks (fired immediately)
        onSuccess: ({ message }) => {
          console.log('Immediate callback:', message);
        },
        onError: ({ message }) => {
          console.error('Immediate callback error:', message);
        },
      });

      // Handle the result from the promise
      console.log('Deposit completed!', result);
      alert('Success: ' + result.message);
    } catch (error) {
      // Handle the error from the promise
      console.error('Deposit failed:', error);
      alert('Error: ' + error.message);
    }
  };

  return <button onClick={handleDeposit}>Deposit</button>;
}
```

That's it! The modal with full deposit UI will appear automatically.

## API Reference

### UnifoldProvider

Wraps your application and provides the Unifold context.

**Props:**

- `publishableKey` (required): Your Unifold API publishable key
- `config` (optional): Configuration object
  - `modalTitle` (optional): Custom title for the deposit modal
  - `hideDepositTracker` (optional): Hide the deposit tracker option
  - `appearance` (optional): Theme appearance - `'light'` | `'dark'` | `'auto'` (defaults to `'dark'`)
    - `'light'`: Force light mode
    - `'dark'`: Force dark mode (default)
    - `'auto'`: Use system preference and respond to changes
  - `layout` (optional): Deposit menu layout and its options - `{ type, tabOrder }` (`type` defaults to `'tabs'`)
  - `display` (optional): Surface-specific presentation toggles
    - `display.transferCrypto.exchangeRate` (optional): Show exchange rate on Transfer Crypto screens (defaults to `false`)
- `children` (required): Your React components

```tsx
<UnifoldProvider
  publishableKey="pk_test_..."
  config={{
    modalTitle: 'Deposit Crypto',
    hideDepositTracker: false,
    appearance: 'dark', // 'light' | 'dark' | 'auto' (defaults to 'dark')
  }}
>
  {children}
</UnifoldProvider>
```

#### Menu layout

`layout` controls how the deposit menu arranges the funding methods, and carries
the options belonging to that layout:

```tsx
<UnifoldProvider
  publishableKey="pk_test_..."
  config={{
    layout: {
      type: 'tabs',
      tabOrder: ['cash', 'crypto'], // opens on "Use Cash"
    },
  }}
>
  {children}
</UnifoldProvider>
```

`type: 'tabs'` (the default) groups the options under "Use Crypto" and "Use
Cash" tabs, and `tabOrder` controls which tab leads. `'stacked'` puts every
option in a single list instead:

```tsx
config={{ layout: { type: 'stacked' } }}
```

Tabs you list lead, in the order given; any tab you leave out keeps its default
position behind them, so `['cash']` is equivalent to `['cash', 'crypto']`. The
leading tab is the one shown on the main menu until the user picks another —
deep-linking with `initialScreen` still opens that method's own tab. A tab whose
methods are all disabled stays hidden regardless of order, and the tab bar
disappears entirely when only one tab has options.

`tabOrder` has no effect under `type: 'stacked'`, where the methods are
interleaved into a single list rather than split across tabs.

> **The default changed to `'tabs'`.** It was previously `'stacked'`, so an
> integration that sets neither `layout` nor `displayMode` will see the tabbed
> menu after upgrading. Pass `layout: { type: 'stacked' }` to keep the single
> list.

##### Migrating from `displayMode`

`displayMode` is deprecated but still honoured, so existing integrations keep
working unchanged. It only sets the layout type:

```tsx
config={{ displayMode: 'stacked' }}          // before
config={{ layout: { type: 'stacked' } }}     // after
```

`layout.type` wins when both are set. A `layout` that omits `type` still falls
back to `displayMode`, so you can adopt the layout's options without changing
how the layout is selected.

### useUnifold()

Hook that provides access to the deposit functionality.

**Returns:**

- `publishableKey`: The current publishable key
- `beginDeposit(config)`: Function to launch the deposit modal (returns a Promise)
- `closeDeposit()`: Function to programmatically close the modal

```tsx
const { beginDeposit, closeDeposit, publishableKey } = useUnifold();
```

### beginDeposit(config)

Launches the deposit modal with the specified configuration. **Returns a Promise** that resolves when the deposit completes or rejects on error/cancellation.

**Parameters (DepositConfig):**

| Parameter                   | Type                                                                                                                        | Required | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `userId`                    | string                                                                                                                      | ✅       | Your user's unique identifier for wallet creation/tracking                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `destinationChainId`        | string                                                                                                                      | ✅       | Target blockchain chain ID                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `destinationTokenAddress`   | string                                                                                                                      | ✅       | Token contract address                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `destinationTokenSymbol`    | string                                                                                                                      | ✅       | Token symbol (e.g., "USDC")                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `recipientAddress`          | string                                                                                                                      | ✅       | Recipient wallet address                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `defaultSourceChainType`    | string                                                                                                                      | -        | Prefer source chain type in Transfer Crypto, Wallet Connect, and Connect Exchange (e.g. `"solana"`, `"ethereum"`). Must be paired with `defaultSourceChainId` + symbol or token address. If omitted or no match, each flow uses its normal ordering.                                                                                                                                                                                                                                                                                                                                                                                                  |
| `defaultSourceChainId`      | string                                                                                                                      | -        | Source chain ID (e.g. `"mainnet"`, `"137"`). Paired with `defaultSourceChainType`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `defaultSourceTokenAddress` | string                                                                                                                      | -        | Source token contract address. Paired with chain type + chain ID.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `defaultSourceSymbol`       | string                                                                                                                      | -        | Source token symbol (e.g. `"USDC"`). Paired with chain type + chain ID.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `prefilledAmountUsd`        | string                                                                                                                      | -        | Optional USD amount prefilled for deposit amount inputs (Connect Wallet, Deposit with Card, Bank Transfer, Cash App). It is applied as initial typed input and does not enable checkout mode.                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `suggestedAmounts`          | `{ usd?: number[]; eur?: number[]; gbp?: number[]; cad?: number[] }`                                                        | -        | Chips per currency, in major units (`25` = $25.00). Each flow reads the currency it shows: `usd` for Deposit with Card (while USD is selected), Apple Pay / Google Pay, Stripe Link, Cash App, Connect Exchange, Pay with Exchange, Bank Transfer (USD rails), and Connect Wallet's fixed chips (`browserWalletAmountQuickSelect: 'usd'`); `cad` for Interac; `eur` / `gbp` for Deposit with Card and Bank Transfer in those currencies. No entry for a currency keeps the built-in chips; `[]` hides them. Otherwise chips show in the order given: values outside the flow's min/max and duplicates are dropped, then the first four are used; if none are left, the built-in chips stay.        |
| `onSuccess`                 | function                                                                                                                    | -        | Success callback (fired immediately)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `onError`                   | function                                                                                                                    | -        | Error callback (fired immediately)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `onClose`                   | function                                                                                                                    | -        | Fired the instant a close is **triggered** (X, Escape, backdrop, or `closeDeposit()`). At this point the dialog is still mounted and its exit animation is just starting.                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `onUnmount`                 | function                                                                                                                    | -        | Fired **once** after the dialog has fully left the DOM — exit animation finished, subtree removed, and the SDK's bundled Radix scroll lock / `body.style.pointerEvents` restored. Use this (not `onClose`) to deterministically remount a host-app modal that sits behind the Unifold dialog. Fires immediately under `prefers-reduced-motion`. See [Sequencing a modal behind the dialog](#sequencing-a-modal-behind-the-dialog).                                                                                                                                                                                                                          |
| `initialScreen`             | `'main' \| 'transfer' \| 'card' \| 'cashapp' \| 'tracker' \| 'pay_with_exchange' \| 'exchange_connect' \| 'wallet_connect'` | -        | `main` (default) = deposit menu. `transfer` / `card` use the **same geo/validation gates** as the menu. `tracker` opens the list **without** those gates. `cashapp`, `pay_with_exchange`, `exchange_connect`, `wallet_connect` open their respective flows and **fall back to `main`** when the matching feature flag (`enableCashApp`, `enablePayWithExchange`, `enableConnectExchange`, `enableConnectWallet`) is disabled. If not `main`, the header **back** is hidden at the flow root (standalone); from `main`, back returns to the menu. Inner steps (card quotes/onramp, tracker detail, exchange pending, cashapp payment) still show back. |

**Returns:** `Promise<DepositResult>`

**DepositResult:**

```typescript
interface DepositResult {
  message: string;
  transaction?: unknown;
  executionId?: string;
}
```

**DepositError:**

```typescript
interface DepositError {
  message: string;
  error?: unknown;
  code?: string; // e.g., 'DEPOSIT_CANCELLED', 'POLLING_ERROR'
}
```

**Example:**

```tsx
// Promise-based (async/await)
try {
  const result = await beginDeposit({
    userId: 'user_123',
    destinationChainId: '137',
    destinationTokenAddress: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174',
    destinationTokenSymbol: 'USDC',
    recipientAddress: '0x606C49ca2Fa4982F07016265040F777eD3DA3160',
  });
  console.log('Success:', result);
} catch (error) {
  console.error('Error:', error);
}

// Standalone Transfer Crypto (geo checks + no back to menu by default)
await beginDeposit({
  externalUserId: 'user_123',
  destinationChainId: '137',
  destinationTokenAddress: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174',
  destinationTokenSymbol: 'USDC',
  recipientAddress: '0x606C49ca2Fa4982F07016265040F777eD3DA3160',
  initialScreen: 'transfer',
});

// Deposit mode with pre-filled amount (no Payment Intent required)
await beginDeposit({
  externalUserId: 'user_123',
  destinationChainId: '137',
  destinationTokenAddress: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174',
  destinationTokenSymbol: 'USDC',
  recipientAddress: '0x606C49ca2Fa4982F07016265040F777eD3DA3160',
  initialScreen: 'wallet_connect',
  prefilledAmountUsd: '25.00',
});

// Custom quick-amount chips (USD flows and Interac)
await beginDeposit({
  externalUserId: 'user_123',
  destinationChainId: '137',
  destinationTokenAddress: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174',
  destinationTokenSymbol: 'USDC',
  recipientAddress: '0x606C49ca2Fa4982F07016265040F777eD3DA3160',
  suggestedAmounts: { usd: [20, 50, 100, 250], cad: [100, 250, 500] },
});

// Hybrid (promise + callbacks)
const depositPromise = beginDeposit({
  userId: 'user_123',
  destinationChainId: '137',
  destinationTokenAddress: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174',
  destinationTokenSymbol: 'USDC',
  recipientAddress: '0x606C49ca2Fa4982F07016265040F777eD3DA3160',
  onSuccess: (data) => {
    // Immediate callback
    showToast('Deposit initiated!');
  },
  onError: (error) => {
    // Immediate callback
    showToast('Error: ' + error.message);
  },
});

// Later handling
depositPromise
  .then((result) => console.log('Completed:', result))
  .catch((error) => console.error('Failed:', error));
```

## Features

- ✅ **Multi-chain Support** - Ethereum, Polygon, Arbitrum, Optimism, Solana, Bitcoin
- ✅ **Fiat Onramp** - Credit/debit card purchases via Meld
- ✅ **Auto-swap** - Automatic token conversion
- ✅ **QR Codes** - Mobile wallet support
- ✅ **Deposit Tracking** - Real-time status updates
- ✅ **TypeScript** - Full type definitions
- ✅ **SSR-safe** - Works with Next.js and other frameworks
- ✅ **Customizable** - Configure per-transaction

## Analytics & Journey Events

The SDK emits two complementary streams so you can understand the full user journey — what method they chose, where they dropped off, and whether the transaction completed or failed.

### 1. Developer-facing `onEvent` (deposit & withdraw)

Pass `onEvent` to `beginDeposit` / `beginWithdraw` to receive lifecycle events. Every event shares the same envelope:

```ts
{
  id: string;            // sevt_<ksuid>
  type: string;          // see tables below
  created: number;       // unix seconds
  sessionId: string;     // asess_<ksuid> — one journey, modal open → close
  externalUserId?: string;// the id you opened the flow with (deposit/withdraw)
  method?: DepositMethod;// deposit events only
  data: { object: ... }; // type-specific payload
}
```

`sessionId` is minted when the modal opens and cleared when it closes, so you can correlate every event from one attempt — including the analytics stream below, which sends the same id as `session_id`. Out-of-widget telemetry has no `session_id`.

`externalUserId` is the `externalUserId` you passed to `beginDeposit` / `beginWithdraw`, echoed back so one shared `onEvent` handler can attribute a journey without you threading your own id through the call site. Checkout events don't carry it — you open a checkout with a client secret, and the payment intent you created server-side is the key that maps back to your user.

**Deposit events (`DepositEventType`)**

| `type`                               | When                                                                                                                                                                                                                                      | `data.object`                                                                              |
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `deposit.method_selected`            | User picks a funding method                                                                                                                                                                                                               | `{ method }`                                                                               |
| `deposit.flow_started`               | A funding method that initiates a session/tx begins (card, cash app, bank transfer, apple pay, stripe link, wallet connect, connect-exchange). Not emitted for transfer-crypto / pay-with-exchange, which have no in-SDK initiation step. | `{ method?, provider?, amountUsd?, chain? }`                                               |
| `deposit.token_selected`             | User picks an asset — fires with the full identity whenever the token **or** its chain changes                                                                                                                                            | `{ currency?, tokenAddress?, chain?, chainType?, network?, method? }`                      |
| `deposit.wallet_selected`            | User picks a browser wallet                                                                                                                                                                                                               | `{ wallet, walletName?, installed? }`                                                      |
| `deposit.provider_selected`          | User picks an onramp/exchange provider                                                                                                                                                                                                    | `{ provider, method? }`                                                                    |
| `deposit.verification_started`       | User reached a step asking them to prove something — KYC, a document, a one-time code, an exchange MFA prompt                                                                                                                             | `{ method? }`                                                                              |
| `deposit.verification_completed`     | The check passed                                                                                                                                                                                                                          | `{ method? }`                                                                              |
| `deposit.verification_failed`        | The check didn't pass                                                                                                                                                                                                                     | `{ method? }` — see note below                                                             |
| `deposit.account_connection_started` | User began linking an account held elsewhere (exchange, Stripe Link)                                                                                                                                                                      | `{ provider, method? }`                                                                    |
| `deposit.account_connected`          | The external account is linked and usable                                                                                                                                                                                                 | `{ provider, method? }`                                                                    |
| `deposit.account_connection_failed`  | Linking the external account failed or was abandoned                                                                                                                                                                                      | `{ provider, method? }` — no reason: see note below                                        |
| `deposit.wallet_connection_started`  | User began connecting a browser wallet                                                                                                                                                                                                    | `{ wallet, method?, chainType? }`                                                          |
| `deposit.wallet_connected`           | The wallet is connected and usable                                                                                                                                                                                                        | `{ wallet, method?, chainType? }`                                                          |
| `deposit.wallet_connection_failed`   | The connection failed, or the user declined it in their wallet                                                                                                                                                                            | `{ wallet, method?, chainType?, failureReason? }` — `connect_declined` or `connect_failed` |
| `deposit.flow_failed`                | The deposit failed, including before anything reached a chain — a declined signature, a declined card, a purchase cap                                                                                                                     | `{ method?, message, errorCode? }`                                                         |
| `deposit.limit_reached`              | The user is at a purchase cap they can't raise — offer another method                                                                                                                                                                     | `{ method?, provider? }`                                                                   |
| `onramp_session.created`             | Hosted onramp session opened                                                                                                                                                                                                              | `{}` — a signal; the method and `sessionId` are on the envelope                            |
| `direct_execution.succeeded`         | Deposit confirmed on-chain                                                                                                                                                                                                                | `DirectExecution`                                                                          |
| `direct_execution.failed`            | Deposit failed on-chain — follows `deposit.flow_failed` with the execution detail                                                                                                                                                         | `DirectExecution` (see `failureReason`)                                                    |

**Withdraw events (`WithdrawEventType`)**

| `type`                       | When                                                                                                        | `data.object`                                                                                                      |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `withdraw.token_selected`    | User picks the destination asset — fires with the full identity whenever the token **or** its chain changes | `{ currency?, tokenAddress?, chain?, chainType?, network?, method? }`                                              |
| `withdraw.flow_started`      | Withdraw form confirmed                                                                                     | `{ token?, chain?, amount?, amountBaseUnit?, amountUsd? }` (telemetry additionally carries `currency` + `network`) |
| `direct_execution.succeeded` | Withdraw confirmed on-chain                                                                                 | `DirectExecution`                                                                                                  |
| `direct_execution.failed`    | Withdraw failed                                                                                             | `DirectExecution`                                                                                                  |

**Checkout events (`CheckoutEventType`)**

| `type`                               | When                                                                                                                                                  | `data.object`                                                         |
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `checkout.method_selected`           | User picks how to pay                                                                                                                                 | `{ method }`                                                          |
| `checkout.token_selected`            | User picks an asset to pay with — fires with the full identity whenever the token **or** its chain changes                                            | `{ currency?, tokenAddress?, chain?, chainType?, network?, method? }` |
| `checkout.wallet_selected`           | User picks a browser wallet                                                                                                                           | `{ wallet, walletName?, installed? }`                                 |
| `checkout.wallet_connection_started` | User began connecting a browser wallet                                                                                                                | `{ wallet, method?, chainType? }`                                     |
| `checkout.wallet_connected`          | The wallet is connected and usable                                                                                                                    | `{ wallet, method?, chainType? }`                                     |
| `checkout.wallet_connection_failed`  | The connection failed, or the user declined it in their wallet                                                                                        | `{ wallet, method?, chainType?, failureReason? }`                     |
| `checkout.flow_started`              | The payment was submitted                                                                                                                             | `{ method?, amountUsd?, chain? }`                                     |
| `checkout.flow_failed`               | The payment attempt failed, including before anything reached a chain — a signature declined in the wallet, no deposit address for the wallet's chain | `{ method?, message, errorCode? }`                                    |
| `payment_intent.succeeded`           | Payment confirmed                                                                                                                                     | `CheckoutPaymentIntent`                                               |

> Note: `sessionId` is a required field on the envelope. If you construct events yourself (e.g. in tests/mocks), include it.

> Note: checkout envelopes carry `paymentIntentId` rather than the `externalUserId` that deposit and withdraw echo — you open a checkout with a client secret, and the intent you created server-side is the key back to your own records. Pass `externalUserId` to `beginCheckout` if you'd rather attribute by user; it's echoed onto every envelope and sent nowhere else.

> Note: verification tells you only that a check started, passed or didn't. Which gate it was, which tier, the provider's verdict, the field that didn't match — all of that describes your user rather than the transaction, so it stays in our funnel where support can reach it. The same goes for why an exchange account wouldn't link. Note also that `deposit.verification_failed` can fire on an attempt the user recovers from, such as a mistyped code, and that a provider verifying in tiers produces one started/outcome cycle per tier; when a verification actually ends the flow you get `deposit.limit_reached` or `deposit.flow_failed`, and those are the ones worth acting on. Wallet connections are the exception to all of this: `deposit.wallet_connection_failed` does carry a reason, because declining a prompt in your own wallet is your own visible action.

### 2. Telemetry (funnel analytics)

Independently, the SDK forwards funnel events (`widget_opened`, `screen_viewed`, `payment_method_selected`, `token_selected`, `wallet_selected`, `provider_selected`, `payment_method_type_selected`, `verification_started`, `verification_submitted`, `verification_completed`, `verification_failed` (funnel only — only the failure reaches `onEvent`), `account_connection_started`, `account_connected`, `account_connection_failed`, `wallet_connection_started`, `wallet_connected`, `wallet_connection_failed`, `flow_started`, `flow_completed`, `flow_failed`, `back_clicked`, `widget_closed`) to the Unifold backend. Each carries `session_id` (`asess_…`) and the internal `user_id` — so drop-off funnels can be grouped per journey and per user. Abandonment is derived from the funnel (`widget_opened` − `flow_completed`).

The internal id comes from the API — deposit addresses, the execution query, a payment intent — so it lands on the journey's events from the moment the first of those responses names the user, whichever screen the user is on. Nothing is needed on your side beyond the `externalUserId` you already pass. The events that fire before that response, `widget_opened` and possibly the first `screen_viewed`, carry `session_id` and no `user_id`; join them to the rest of the journey on `session_id`, which every event in it shares. A journey never carries a *different* user's id: each one starts from a clean slate rather than inheriting what the last flow resolved.

The terminal flow events carry the id of the record they produced: `flow_completed` and `flow_failed` on a deposit or withdrawal carry `execution_id` (`exec_…`, the direct execution the poll resolved), and a checkout's `flow_completed` carries `payment_intent_id` (`pi_…`). They are the only high-cardinality properties in the funnel — join keys back to the execution or the intent, not breakdown dimensions. `execution_id` is absent when a flow completes without one (a provider reporting success before any execution row exists).

`flow_started` can't carry `execution_id`, because at that point there is no execution: one is created when funds land on the deposit wallet, which is after the user has paid, sent, or been handed off to a provider. It carries `deposit_wallet_id` (`wallet_…`) instead — the wallet the flow is funding, and the same id the execution carries when it appears. That's what joins a journey that started and never completed to whatever did (or didn't) follow. A deposit wallet is reused across a user's deposits to the same destination, so pair it with the journey's time window rather than treating it as unique per execution.

Events that name an asset carry `currency` and `network` next to the raw `token` and `chain`. Those are the slugs the API serves on every token and chain — lowercase, with non-alphanumerics collapsed to `_` (`USDC.e` → `usdc_e`, `USDC (Perp)` → `usdc_perp`, `Base Sepolia` → `base_sepolia`) — and they are the same identifiers the payment-intents API takes as `destination_currency` / `destination_network`. Group funnels on those; read `token`/`chain` when you want the symbol as displayed or the exact chain id. A currency is not unique per network (Polygon carries both `usdc` and `usdc_e`, HyperCore both `usdc` and `usdc_perp`), so the pair is the key, never the currency alone.

## Advanced Usage

### Pattern 1: Promise-based (Recommended for Modern Apps)

Use async/await for clean, linear code flow:

```tsx
const { beginDeposit } = useUnifold();

const handleDeposit = async () => {
  try {
    const result = await beginDeposit({
      destinationChainId: '137',
      destinationTokenAddress: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174',
      destinationTokenSymbol: 'USDC',
      recipientAddress: '0x606C49ca2Fa4982F07016265040F777eD3DA3160',
    });

    // Update your UI state
    setDepositStatus('completed');
    showSuccessMessage(result.message);
  } catch (error) {
    // Handle errors
    setDepositStatus('failed');
    showErrorMessage(error.message);
  }
};
```

### Pattern 2: Callback-based (Fire-and-Forget)

Use callbacks for immediate side effects without awaiting:

```tsx
const { beginDeposit } = useUnifold();

const handleDeposit = () => {
  beginDeposit({
    destinationChainId: '137',
    destinationTokenAddress: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174',
    destinationTokenSymbol: 'USDC',
    recipientAddress: '0x606C49ca2Fa4982F07016265040F777eD3DA3160',
    onSuccess: (data) => {
      console.log('Deposit initiated:', data);
      showToast('Deposit in progress!');
    },
    onError: (error) => {
      console.error('Deposit failed:', error);
      showToast('Error: ' + error.message);
    },
  });

  // Code continues immediately without waiting
  console.log('Deposit modal opened');
};
```

### Pattern 3: Hybrid (Promise + Callbacks)

Combine both for maximum flexibility:

```tsx
const { beginDeposit } = useUnifold();

const handleDeposit = async () => {
  try {
    // Get the promise
    const depositPromise = beginDeposit({
      destinationChainId: '137',
      destinationTokenAddress: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174',
      destinationTokenSymbol: 'USDC',
      recipientAddress: '0x606C49ca2Fa4982F07016265040F777eD3DA3160',

      // Immediate callbacks for real-time feedback
      onSuccess: (data) => {
        showToast('Deposit detected!');
        trackAnalyticsEvent('deposit_initiated', data);
      },
      onError: (error) => {
        showToast('Error: ' + error.message);
      },
    });

    // Continue other work immediately
    setModalOpen(true);

    // Await the final result
    const result = await depositPromise;

    // Update final state
    setDepositComplete(true);
    navigateToSuccessPage(result);
  } catch (error) {
    setDepositError(error);
  }
};
```

### Programmatic Control

Close the modal programmatically:

```tsx
const { beginDeposit, closeDeposit } = useUnifold();

// Open modal
beginDeposit({
  destinationChainId: '137',
  destinationTokenAddress: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174',
  destinationTokenSymbol: 'USDC',
  recipientAddress: '0x606C49ca2Fa4982F07016265040F777eD3DA3160',
});

// Close modal after 10 seconds
setTimeout(() => {
  closeDeposit();
}, 10000);
```

**Note:** Closing the modal via `closeDeposit()` or clicking outside will reject the promise with code `'DEPOSIT_CANCELLED'`.

### Sequencing a modal behind the dialog

If your app renders its own modal behind the Unifold deposit dialog, closing them
out of order can leave `document.body.style.pointerEvents === 'none'` (two
independent Radix copies each restore body styles on unmount). The robust fix is
to unmount your modal for the duration of the Unifold flow and remount it only
once Unifold's dialog is fully gone from the DOM.

`onClose` is **too early** for this — it fires when the close is _triggered_,
while Unifold's dialog (and its body scroll lock / `pointerEvents`) is still
mounted. Use `onUnmount`, which fires after the dialog has left the DOM and the
SDK's body-level side effects are restored:

```tsx
const { beginDeposit } = useUnifold();

// 1. Unmount your own modal before opening Unifold's dialog
setAddFundModalMounted(false);

beginDeposit({
  destinationChainId: '137',
  destinationTokenAddress: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174',
  destinationTokenSymbol: 'USDC',
  recipientAddress: '0x606C49ca2Fa4982F07016265040F777eD3DA3160',
  // onClose fires immediately on close-trigger — do NOT remount here
  onClose: () => console.log('close triggered (exit animation starting)'),
  // 2. onUnmount fires once the dialog is gone and the body is unlocked — safe to remount
  onUnmount: () => setAddFundModalMounted(true),
});
```

This drops any dependence on guessing exit-animation timing and is safe under
`prefers-reduced-motion` (where `onUnmount` fires immediately).

## TypeScript

Full TypeScript support with type definitions included:

```tsx
import {
  UnifoldProvider,
  useUnifold,
  DepositConfig,
  UnifoldConnectProviderConfig,
} from '@unifold/connect-react';

// Provider config
const providerConfig: UnifoldConnectProviderConfig = {
  publishableKey: 'pk_test_...',
  config: {
    modalTitle: 'Deposit',
    hideDepositTracker: false,
    display: {
      transferCrypto: {
        exchangeRate: true,
      },
    },
  },
};

// Deposit config
const depositConfig: DepositConfig = {
  destinationChainId: '137',
  destinationTokenAddress: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174',
  destinationTokenSymbol: 'USDC',
  recipientAddress: '0x606C49ca2Fa4982F07016265040F777eD3DA3160',
  onSuccess: (data) => console.log(data),
  // TypeScript will validate all fields
};

const { beginDeposit } = useUnifold();
beginDeposit(depositConfig);
```

## Architecture

`@unifold/connect-react` is built on a clean, modular architecture:

```
@unifold/connect-react (this package)
  ├─ @unifold/react-provider (base context)
  └─ @unifold/ui-react (UI components)
```

## License

MIT

## Support

- 📧 Email: support@unifold.io
- 🐛 Issues: [GitHub Issues](https://github.com/unifold/unifold-platform/issues)
- 📖 Documentation: [unifold.io/docs](https://unifold.io/docs)
