> For the complete documentation index, see [llms.txt](https://docs.orda.network/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.orda.network/developers/widget.md).

# Widget

The orda Widget is a comprehensive, embeddable React component that enables\
multi-chain cryptocurrency payments in your application — swap, on-ramp, and\
off-ramp flows with built-in wallet connection, all wrapped around the orda\
TypeScript SDK.

{% hint style="info" %}
**Replaces the old `/developers/widget-beta` page.** The widget moved out of `@ordanetwork/sdk/react` (deleted in the SDK v2 release) into its own package **`@ordanetwork/widget@1.0.0`**. If you were on the beta, see Migrating from `@ordanetwork/sdk/react` at the bottom.Try the [widget →](https://widget.orda.network/)
{% endhint %}

### What it does

* **Multi-chain support** — EVM (Ethereum, Polygon, Arbitrum, Base, Optimism, plus plasma / stable / megaeth) and Solana
* **Three transaction flows** — Swap, Off-ramp (crypto → BRL via PIX), On-ramp (BRL → crypto via PIX)
* **Wallet integration** — WalletConnect / Reown AppKit modal mounted for you
* **Lifecycle callbacks** — 10 typed events for quote / transaction / wallet / error, plus a firehose for analytics
* **Recipient management** — save and manage payment recipients
* **Token prices & balances** — real-time USD prices and multi-chain balance fetching
* **Responsive UI** — desktop and mobile
* **Customizable theming** — CSS variables

### Prerequisites

1. WalletConnect / Reown project id — [cloud.reown.com](https://cloud.reown.com/)
2. orda API credentials — `clientId` and `clientSecret`
3. React 18 or React 19
4. Node.js 18+

### Installation

```bash
npm install @ordanetwork/widget @ordanetwork/sdk
# peers (you almost certainly have most of these already)
npm install react react-dom @tanstack/react-query wagmi viem
```

#### Version compatibility

| Package                 | Version                |
| ----------------------- | ---------------------- |
| `@ordanetwork/widget`   | `^1.0.0`               |
| `@ordanetwork/sdk`      | `^1.3.0 \|\| ^2.0.0`   |
| `react`                 | `^18.0.0 \|\| ^19.0.0` |
| `react-dom`             | `^18.0.0 \|\| ^19.0.0` |
| `@tanstack/react-query` | `^5.0.0`               |
| `wagmi`                 | `^2.0.0`               |
| `viem`                  | `^2.0.0`               |

`@solana/web3.js`, `@reown/appkit*`, and the radix-ui primitives are direct dependencies of `@ordanetwork/widget` — you don't need to install them yourself.

### Quick start

#### Step 1 — Set environment variables (`.env.local`)

```bash
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=your_walletconnect_project_id
ORDA_CLIENT_ID=prj_your_client_id
ORDA_CLIENT_SECRET=sec_your_client_secret
```

#### Step 2 — Create the JWT auth route (`app/api/auth/jwt/route.ts`)

The widget calls `config.getToken()` to obtain a JWT; you handle credential exchange server-side so the secret never reaches the browser.

```ts
import { NextResponse } from 'next/server';
import { UniversalHttpClient, JWT } from '@ordanetwork/sdk';

const CLIENT_ID = process.env.ORDA_CLIENT_ID!;
const CLIENT_SECRET = process.env.ORDA_CLIENT_SECRET!;
const API_URL = process.env.NEXT_PUBLIC_ORDA_API_BASE_URL || 'https://api.orda.network/v1';

export async function POST() {
  try {
    if (!CLIENT_ID || !CLIENT_SECRET) {
      return NextResponse.json(
        { error: 'Server configuration error: missing API credentials' },
        { status: 500 },
      );
    }

    const http = new UniversalHttpClient(API_URL, 30_000);
    const jwt = new JWT(http);

    const { token, expiresAt } = await jwt.generate({
      clientId: CLIENT_ID,
      clientSecret: CLIENT_SECRET,
      permissions: [
        'quotes:read',
        'offramp:read',
        'onramp:read',
        'transactions:read',
        'recipients:read',
        'recipients:write',
        'balances:read',
        'prices:read',
      ],
    });

    return NextResponse.json({
      jwt: token,
      expiresAt: Math.floor(new Date(expiresAt).getTime() / 1000),
    });
  } catch (err: any) {
    return NextResponse.json(
      { error: err?.message ?? 'Failed to generate token' },
      { status: err?.statusCode ?? 500 },
    );
  }
}
```

#### Step 3 — Create the provider (`app/providers.tsx`)

```tsx
'use client';

import { OrdaProvider, createAppKitConfig } from '@ordanetwork/widget';

// Module scope (SSR-safe).
const appKitConfig = createAppKitConfig({
  projectId: process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID!,
  metadata: {
    name: 'Your App',
    description: 'Your app description',
    url: 'https://yourapp.com',
    icons: ['https://yourapp.com/icon.png'],
  },
});

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <OrdaProvider
      config={{
        getToken: async () => {
          const res = await fetch('/api/auth/jwt', { method: 'POST' });
          if (!res.ok) {
            const { error } = await res.json();
            throw new Error(error || 'Failed to get JWT');
          }
          return res.json(); // { jwt: string, expiresAt: number }
        },
        appKitConfig,
        debug: process.env.NODE_ENV === 'development',
      }}
    >
      {children}
    </OrdaProvider>
  );
}
```

#### Step 4 — Mount the provider + styles in your root layout (`app/layout.tsx`)

```tsx
import './globals.css';
import '@ordanetwork/widget/styles.css';
import { Providers } from './providers';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}
```

#### Step 5 — Drop in the Widget (`app/page.tsx`)

```tsx
'use client';

import { Widget } from '@ordanetwork/widget';

export default function Home() {
  return (
    <main className="min-h-screen flex items-center justify-center p-4 bg-zinc-100">
      <div className="w-full max-w-md">
        <h1 className="text-2xl font-bold text-center mb-6">Crypto Payments</h1>
        <Widget
          onTransactionSettled={(e) => console.log('settled', e.payload)}
          onError={(e) => console.error(e.payload.message)}
        />
      </div>
    </main>
  );
}
```

#### Step 6 — Run

```bash
npm run dev
```

### Lifecycle callbacks

All optional props on `<Widget />`. Every event fires alongside `onAnalyticsEvent` — dedupe by `event.id` if you wire both (see Analytics firehose below).

| Callback                 | Fires when                                                                                  |
| ------------------------ | ------------------------------------------------------------------------------------------- |
| `onQuoteRequested`       | The widget asked the SDK for a quote (before network round-trip).                           |
| `onQuoteAccepted`        | A quote returned successfully (or was served from the 30 s cache — `payload.cached: true`). |
| `onQuoteFailed`          | Quote request failed (network / validation / SDK error).                                    |
| `onTransactionInitiated` | User confirmed; the wallet was asked to sign / submit.                                      |
| `onTransactionSettled`   | Transaction confirmed on-chain **or** settled by the backend.                               |
| `onTransactionFailed`    | Transaction reverted, was rejected, or failed before settling.                              |
| `onWalletConnected`      | An EVM or Solana wallet just connected.                                                     |
| `onWalletDisconnected`   | A wallet just disconnected.                                                                 |
| `onError`                | Any user-visible error (fires alongside the specific `*_failed` callback when applicable).  |
| `onAnalyticsEvent`       | **Firehose** — every event in order.                                                        |

Every event payload shares the same envelope:

```ts
{
  id: string;          // stable nanoid per event
  timestamp: number;   // Date.now() at emit
  flow?: 'swap' | 'onramp' | 'offramp';
  type: WidgetEventType;
  payload: { ... };    // discriminated by type
}
```

#### Analytics firehose

`onAnalyticsEvent` fires for **every** event, alongside the specific callback. Dedupe by `event.id` when you wire both:

```tsx
const seen = new Set<string>();

<Widget
  onTransactionSettled={(e) => {
    if (seen.has(e.id)) return;
    seen.add(e.id);
    yourTxHistoryStore.add(e.payload);
  }}
  onAnalyticsEvent={(e) => {
    if (seen.has(e.id)) return;
    seen.add(e.id);
    analytics.track(e.type, e.payload); // safe to forward verbatim — see below
  }}
/>
```

#### Payload redaction (PII)

{% hint style="success" %}
Event payloads are deliberately redacted so they are safe to forward verbatim to a third-party analytics sink (Segment / GA / PostHog / Mixpanel).
{% endhint %}

* **`quote_accepted`** carries only a serializable summary — `transactionId`, `provider`, `fromSymbol`/`toSymbol`, `fromAmount`/`toAmount`, `fromChainId`/`toChainId`, `estimatedDuration`, and `cached`. It does **not** include PIX key / QR code, deposit address, reference id, raw `transactionRequest` / `approvalTxParams` (signed calldata), or wallet addresses.
* **`quote_failed`, `transaction_failed`, and `error`** expose a human-readable `message` plus an optional `causeMessage` string. The underlying error object is **never** forwarded — viem / RPC errors embed tx params and request payloads, so only a message string crosses the callback boundary.

If you need the full, unredacted quote (e.g. to render PIX deposit instructions yourself), read it from the hook return values (`useOnRamp().quote`, etc.) rather than from events.

### Configuration flags

All optional on the `config` object passed to `<OrdaProvider>`:

| Field                 | Default      | Effect                                                                                                                                                                                 |
| --------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `getToken`            | — (required) | Returns `{ jwt, expiresAt }`. Called on mount and \~5 min before expiry.                                                                                                               |
| `appKitConfig`        | — (required) | From `createAppKitConfig({ projectId, metadata })`.                                                                                                                                    |
| `debug`               | `false`      | Verbose console logs for SDK init / refresh.                                                                                                                                           |
| `notifications`       | `true`       | When `true`, mounts a Sonner `<Toaster />`. When `false`, in-progress states render as inline `<Alert />` banners and success / error states surface only through lifecycle callbacks. |
| `disableOnRamp`       | `false`      | Hide on-ramp (fiat → crypto) flows.                                                                                                                                                    |
| `disableOffRamp`      | `false`      | Hide off-ramp (crypto → fiat) flows.                                                                                                                                                   |
| `enableOfframpSolana` | `false`      | Allow Solana as a source chain for off-ramp.                                                                                                                                           |

Apps controlling these via env (e.g. `NEXT_PUBLIC_ON_RAMP_DISABLED`) should map the env value to the config field when constructing the provider — the widget does not read `process.env.*` directly.

### SSR / RSC

`@ordanetwork/widget` is a client component. The bundle starts with `'use client';` so importing it from a React Server Component is safe — you get the `'use client'` boundary automatically. You do **not** need to wrap it.

`createAppKitConfig()` must be called at module scope (outside components) for SSR safety — see Step 3 above.

### Wallet mode

`@ordanetwork/widget@1.0.0` ships one wallet mode: **`appkit`** (the default). The widget mounts Wagmi + the Reown AppKit modal internally; you supply a `projectId` via `createAppKitConfig`.

{% hint style="info" %}
**BYOW (`wallet: 'external'`) is coming in 1.1.0** — for hosts that already own a `<WagmiProvider>` and want the widget to consume it. The plumbing exists internally; a few downstream call sites are still being migrated. Tracked as **COR-963**.
{% endhint %}

### TypeScript

All types ship with the package; no `@types/...` install needed.

```ts
import type {
  WidgetEvent,
  WidgetCallbacks,
  QuoteAcceptedPayload,
  TransactionSettledPayload,
  TransactionFailedPayload,
  OrdaProviderConfig,
} from '@ordanetwork/widget';
```

The full event union is discriminated by `type`, so narrowing in a callback works without casts.

### Security note

{% hint style="danger" %}
Never expose `ORDA_CLIENT_SECRET` to the browser. Always exchange credentials for a JWT on a server-side route (Step 2) and let the widget call that route via `config.getToken`.
{% endhint %}

### Migrating from `@ordanetwork/sdk/react`

If you integrated against the beta (when the widget lived inside the core SDK), here's the migration:

| Before (`@ordanetwork/sdk/react`)                                                   | After (`@ordanetwork/widget`)                                                    |
| ----------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| `npm install @ordanetwork/sdk`                                                      | `npm install @ordanetwork/widget @ordanetwork/sdk`                               |
| `import { OrdaProvider, Widget, createAppKitConfig } from '@ordanetwork/sdk/react'` | `import { OrdaProvider, Widget, createAppKitConfig } from '@ordanetwork/widget'` |
| `import '@ordanetwork/sdk/react/styles.css'`                                        | `import '@ordanetwork/widget/styles.css'`                                        |

No runtime behavior changes — the same components, the same `OrdaProvider` config shape, the same `Widget` props. The split is purely about packaging: the React UI now ships independently of the core SDK so it can version on its own cadence.

The `@ordanetwork/sdk` itself remains a peer dependency (the widget uses it internally for quote / transaction / recipient API calls).
