> ## Documentation Index
> Fetch the complete documentation index at: https://rhinestone-fix-widget-proxy-permit-routes.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Migration guide

> Upgrade @rhinestone/deposit-modal — v0.12.0 requires GET /chains on your proxy and drops showHistoryButton; v0.9.0 brought managed accounts, the withdraw callback, and renamed props. Plus v0.1.x / v0.2.x to v0.3.0.

## v0.11.x → v0.12.0

Two changes: one proxy-side, one a single line to delete.

| Change                              | Why it can't wait                                                                                  |
| ----------------------------------- | -------------------------------------------------------------------------------------------------- |
| Forward `GET /chains`               | The only source of the chain set. Without it there are no chains to offer and no deposit can start |
| Delete `uiConfig.showHistoryButton` | The prop is gone from the type, so TypeScript fails to compile until you remove it                 |

### Forward `GET /chains`

The modal has read the chain set from `/chains` since v0.11.0, but it still
carried a compiled-in table it fell back to. That table is gone. A proxy that
does not forward the route no longer degrades to a built-in chain list — it
leaves every picker empty, and the deposit flow reports that supported chains are
unavailable.

`deposit-widget-proxy` has forwarded it since 2026-08-11, so redeploying the
packaged proxy is enough. A hand-written proxy needs the route added to its
allowlist — see [required routes](/deposits/widget/backend#required-routes).
This is not a CORS change: it is a `GET` using headers the modal already sends,
so it cannot break a preflight.

<Note>
  The upside of the removal is that the chain set is now whatever the backend
  serves, in both directions — a chain we add appears without a modal release,
  and one we withdraw stops being offered instead of lingering until you upgrade.
</Note>

### Delete `uiConfig.showHistoryButton`

Deposit history is now always available, so the flag has nothing left to switch.
Delete the line; the button renders regardless, from the screen where the user
picks a deposit method.

```diff theme={null}
  uiConfig={{
-   showHistoryButton: true,
    showBackButton: true,
  }}
```

If your proxy does not forward `GET /deposits`, this is the release where that
becomes visible: the panel is now reachable and shows the failure, where before
the button could be switched off and the gap stayed hidden.

## v0.8.x → v0.9.0

v0.9.0 moves both modals onto service-managed accounts, hands the withdrawal
transfer to your app, and renames or removes props that no longer described what
they did. The account and withdraw changes need code; the
[prop renames](#renamed-and-removed-props) are mechanical.

### Deploy your proxy first

Four of these changes are proxy-side and take effect the moment the new modal
loads in a browser. None of them degrades — the request 404s, or the browser
blocks it at preflight.

| Change                                  | Why it can't wait                                                                                                                                                                      |
| --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Forward `POST /register-managed`        | Replaces `/setup-account` + `/register`. Without it registration 404s: no deposit address and no QR code                                                                               |
| Forward `GET /qr/tokens`                | Replaces `GET /tokens`, with **no fallback**. Without it the QR picker falls back to the modal's built-in token set, which can offer a token your deposit whitelist rejects on arrival |
| Allow `x-deposit-modal-version` in CORS | Every request now carries it. A proxy with an explicit `allowHeaders` that omits it fails the **whole request** at preflight, not just the header                                      |
| Forward `POST /deposits/recover`        | Only if you adopt the new [claim modal](/deposits/widget/claim-modal)                                                                                                                  |

See [required routes](/deposits/widget/backend#required-routes) for the full
table. `/setup-account` and `/register` are no longer called and can be dropped
once no older modal version is in use.

<Warning>
  Bare Hono `cors()` is safe for the version header — with no `allowHeaders` it
  reflects whatever the preflight asks for. An explicit allow-list is what breaks,
  and it breaks on upgrade rather than on first deploy.
</Warning>

**Both modals** — the `signerAddress` and `sessionChainIds` props are gone,
along with the `DEFAULT_SIGNER_ADDRESS`, `EnableSessionDetails`, and
`AccountInitData` exports. Registration now goes through
`POST /register-managed`, which a self-hosted proxy **must** forward before you
ship — see [required routes](/deposits/widget/backend#required-routes).
There is no session key and no signature prompt during setup.

**`<WithdrawModal>`** no longer moves funds. It previously built and submitted a
Safe `execTransaction`, which only worked for apps whose funds sat in a Safe. It
now asks your app to perform one transfer:

```diff theme={null}
  <WithdrawModal
-   safeAddress={userSafe}
+   accountAddress={userAccount}
-   dappWalletClient={walletClient}
-   dappPublicClient={publicClient}
-   dappAddress={ownerEoa}
-   reownAppId={projectId}
-   onSignTransaction={async (request) => ({
-     signature: await signer.signTypedData(request.typedData),
-   })}
+   onSendTransaction={async ({ chainId, token, amount, to }) => ({
+     txHash: await myWallet.sendTransfer({ chainId, token, amount, to }),
+   })}
  />
```

Send to `to` exactly, and return the **on-chain transaction hash** — not a
userOp hash. See [executing the
transfer](/deposits/widget/withdraw-modal#executing-the-transfer) for both rules
and a Safe-backed example, including how to keep gas sponsored.

Also on `<WithdrawModal>`: `onRequestConnect` is removed (the modal needs no
wallet, so there is no connect step — it opens on the withdraw form), the
`SafeTransactionRequest` export is replaced by `WithdrawTransferRequest`, and
the `"submitted"` lifecycle event renames `safeAddress` to `accountAddress`.

`POST /safe/withdraw` still exists — the modal simply stopped calling it.

### Renamed and removed props

Renames, plus the removal of config the server already owns. Nothing here changes
what the modal can do.

| Before                     | After                   |
| -------------------------- | ----------------------- |
| `dappWalletClient`         | `walletClient`          |
| `dappPublicClient`         | `publicClient`          |
| `dappImports`              | `assetMigrations`       |
| `initialDappImport`        | `initialAssetMigration` |
| `fiatOnrampMethods`        | `fiatMethods`           |
| `DappImportsConfig` (type) | `AssetMigrationsConfig` |

```diff theme={null}
  <DepositModal
-   dappWalletClient={walletClient}
-   dappPublicClient={publicClient}
-   dappAddress={userAddress}
+   walletClient={walletClient}
+   publicClient={publicClient}
-   dappImports={{ polymarket: true }}
-   initialDappImport="polymarket"
+   assetMigrations={{ polymarket: true }}
+   initialAssetMigration="polymarket"
  />
```

**`dappAddress` is removed with no replacement.** The modal reads the address off
`walletClient.account`, which nothing previously validated it against — so the modal
could read balances for one address while the user was connected as another.

**`allowedRoutes` and the `RouteConfig` type are removed** from both modals. They
filtered the pickers client-side with nothing enforcing it, so a list that drifted
from your deposit whitelist offered the user a source the processor then rejected.
Set the whitelist via `POST /setup`; to offer a restricted subset, use an API key
whose whitelist matches.

**`enableSolana` is removed** for the same reason — Solana sources follow the deposit
whitelist.

**`uiConfig.checkLiquidity` is removed.** It cost an orchestrator round trip per
continue to compute a warning the review screen never rendered. The cap is still
checked and shown on the QR / transfer screen.

**`rhinestoneApiKey` is removed** from both modals. It was never read — the key
belongs on your [backend proxy](/deposits/widget/backend), which attaches it upstream.
Delete it; nothing consumed it.

**`FiatPaymentMethodOption` is no longer exported.** It described a row descriptor
that `fiatMethods` no longer takes.

**`backendUrl` is now required** on all three modals, and the `DEFAULT_BACKEND_URL`
export is gone. The old default pointed at a Rhinestone-internal service running on
**our** API key, so any integration that omitted the prop was silently routing its
users' deposits through it.

```diff theme={null}
  <DepositModal
    targetChain={8453}
    targetToken={USDC}
    recipient={userAddress}
+   backendUrl={process.env.NEXT_PUBLIC_DEPOSIT_PROXY_URL}
  />
```

If you already set `backendUrl`, nothing changes. If you didn't, you were on our key
and need a [proxy](/deposits/widget/backend) before upgrading. TypeScript flags the
omission; each modal also logs a `console.error` when the value is missing, empty or
whitespace, since `backendUrl={process.env.X ?? ""}` typechecks fine.

**`fiatOnrampMethods` becomes `fiatMethods`**, a boolean map keyed by Swapped
`payment_group` instead of a list of row descriptors:

```diff theme={null}
  <DepositModal
    enableFiatOnramp
-   fiatOnrampMethods={[
-     { method: "creditcard", label: "Debit/Credit card", sublabel: "Instant - $10,000 limit", icon: "card" },
-   ]}
+   fiatMethods={{ creditcard: true }}
  />
```

The old prop made you supply each row's `label`, `sublabel` and `icon`, which meant
pasting our copy and freezing a claim like `"Instant - $10,000 limit"` into your
bundle.

<Warning>
  `fiatOnrampMethods={[]}` used to fall through to offering **every** payment method.
  An empty or all-false `fiatMethods` now offers none. If you computed the list
  dynamically and could produce an empty one, check which you wanted.
</Warning>

### Optional where it was mandatory

`<WithdrawModal>`'s `targetChain` and `targetToken` are now optional. They only ever
seeded the form — the user can pick any supported destination — so omitting them
opens on a same-chain, same-token withdrawal.

`@reown/appkit` and `@reown/appkit-adapter-wagmi` are now **optional** peer
dependencies. An app that passes its own `walletClient` never opens AppKit and no
longer needs it installed. See [install](/deposits/widget/quickstart#install).

### New

* **`enableWallet?: boolean`** (default `true`) on `<DepositModal>` — turn off to
  present a flow with no wallet even when a `walletClient` or `reownAppId` is supplied.
* **[`<ClaimModal>`](/deposits/widget/claim-modal)** and the `./claim` subpath — a user
  pastes a failed or rejected deposit's transaction hash and gets the funds returned.
  The user's own signature authorizes it, so this needs no backend of your own: pass
  `signRecovery` and forward `POST /deposits/recover` on your proxy. For recipients
  that can't sign, `@rhinestone/deposit-modal/server` exports `createRefundHandler` to
  authorize against your own session instead.

### Behavior changes worth checking

* **`connected` no longer fires for flows with no wallet** (QR, fiat, exchange). It
  previously reported the declared address as though a wallet had connected. If you
  used it as a "flow started" signal, switch to `onReady`.
* **A QR-only integration no longer auto-locks to the wallet.** The connect step's
  auto-skip never accounted for `enableQrTransfer` or asset migrations, so it could
  skip past the only funding option you had enabled.
* **Logos load from Rhinestone's asset CDN.** Apps with an explicit `img-src` CSP must
  allow `https://s3.rhinestone.dev` — a blocked image fails silently. See
  [content security policy](/deposits/widget/deposit-modal#content-security-policy).
* **`HYPERCORE_RECIPIENT_NOT_EOA` is no longer emitted.** [HyperCore](/deposits/widget/deposit-modal#hypercore-destinations)
  deposits now accept a smart-account `recipient`, and the pre-screen that blocked one
  is gone. If you branch on that `onError` code, the branch is dead.
* **A chain your deposit whitelist allows nothing on is no longer offered** in the QR
  flow's chain picker, instead of appearing with built-in tokens the deposit would then
  be rejected for. Chains the shortlist says nothing about keep their existing set.
* **Fiat payment methods are personalized by region** unless you pass `fiatMethods`.
  See [regional payment methods](/deposits/widget/deposit-modal#regional-payment-methods).
* **The deposit review shows a single "Fees" row.** The per-category breakdown and its
  tooltips are gone; `uiConfig.feeSponsored` and `uiConfig.feeTooltip` still apply on
  the processing and result screens.

### Removed prop warnings

Both modals log a `console.error` naming the replacement when passed any prop removed
in this release. TypeScript already catches these; the runtime warning is for plain
JavaScript hosts, spread props, and loosely typed call sites, where several of the
removals fail silently rather than visibly.

***

## v0.1.x / v0.2.x → v0.3.0

v0.3.0 collapses each modal's per-event callbacks into a single `onLifecycle`
callback, renames the analytics event types, removes the `/reown` and `/safe`
subpath entry points, and drops `connectButtonLabel`. `<DepositModal>` and
`<WithdrawModal>` share the same callback shape, but their lifecycle payloads
are **not identical** — see [Asymmetries](#asymmetries) below.

## Callback collapse — onLifecycle

Both modals replace their individual callbacks with one `onLifecycle` that
receives a discriminated event. Switch on `event.type`; the payload fields keep
the same names as before.

| Old prop (`<DepositModal>`) | New `event.type`          | Old prop (`<WithdrawModal>`) | New `event.type` |
| --------------------------- | ------------------------- | ---------------------------- | ---------------- |
| `onConnected`               | `"connected"`             | `onConnected`                | `"connected"`    |
| `onDepositSubmitted`        | `"submitted"`             | `onWithdrawSubmitted`        | `"submitted"`    |
| `onDepositComplete`         | `"complete"`              | `onWithdrawComplete`         | `"complete"`     |
| `onDepositFailed`           | `"failed"`                | `onWithdrawFailed`           | `"failed"`       |
| `onTotalBalanceChange`      | `"balance-changed"`       | —                            | —                |
| `onSmartAccountChange`      | `"smart-account-changed"` | —                            | —                |

<CodeGroup dropdown>
  ```diff DepositModal theme={null}
   <DepositModal
     ...
  -  onConnected={({ address, smartAccount }) => trackConnected(address, smartAccount)}
  -  onDepositSubmitted={({ txHash, sourceChain, amount }) => trackSubmitted(txHash, sourceChain, amount)}
  -  onDepositComplete={({ txHash, destinationTxHash }) => trackComplete(txHash, destinationTxHash)}
  -  onDepositFailed={({ txHash, error }) => trackFailed(txHash, error)}
  -  onTotalBalanceChange={(total) => setBalance(total)}
  -  onSmartAccountChange={({ evm, solana }) => setSmartAccount({ evm, solana })}
  -  connectButtonLabel="Connect wallet"
  +  onLifecycle={(event) => {
  +    switch (event.type) {
  +      case "connected":
  +        trackConnected(event.address, event.smartAccount);
  +        break;
  +      case "submitted":
  +        trackSubmitted(event.txHash, event.sourceChain, event.amount);
  +        break;
  +      case "complete":
  +        trackComplete(event.txHash, event.destinationTxHash);
  +        break;
  +      case "failed":
  +        trackFailed(event.txHash, event.error);
  +        break;
  +      case "balance-changed":
  +        setBalance(event.totalUsd);
  +        break;
  +      case "smart-account-changed":
  +        setSmartAccount({ evm: event.evm, solana: event.solana });
  +        break;
  +    }
  +  }}
   />
  ```

  ```diff WithdrawModal theme={null}
   <WithdrawModal
     ...
  -  onConnected={({ address, smartAccount }) => trackConnected(address, smartAccount)}
  -  onWithdrawSubmitted={({ txHash, sourceChain, amount, safeAddress }) => trackSubmitted(txHash, sourceChain, amount, safeAddress)}
  -  onWithdrawComplete={({ txHash, destinationTxHash }) => trackComplete(txHash, destinationTxHash)}
  -  onWithdrawFailed={({ txHash, error }) => trackFailed(txHash, error)}
  -  connectButtonLabel="Connect wallet"
  +  onLifecycle={(event) => {
  +    switch (event.type) {
  +      case "connected":
  +        trackConnected(event.address, event.smartAccount);
  +        break;
  +      case "submitted":
  +        trackSubmitted(event.txHash, event.sourceChain, event.amount, event.accountAddress);
  +        break;
  +      case "complete":
  +        trackComplete(event.txHash, event.destinationTxHash);
  +        break;
  +      case "failed":
  +        trackFailed(event.txHash, event.error);
  +        break;
  +    }
  +  }}
   />
  ```
</CodeGroup>

See [status tracking](/deposits/widget/status-tracking) for the full event
payloads.

## Asymmetries

The two unions look alike but differ — don't assume one helper typechecks
against both.

| Aspect                            | `DepositLifecycleEvent` | `WithdrawLifecycleEvent` |
| --------------------------------- | ----------------------- | ------------------------ |
| `txHash` type                     | `string`                | `Hex`                    |
| `sourceChain` on submit/complete  | `ChainId \| "unknown"`  | `number`                 |
| `sourceToken` on complete         | `string`, optional      | `Address`, required      |
| `targetChain` on complete         | `number \| "solana"`    | `number`                 |
| `targetToken` on complete         | `string`                | `Address`                |
| `accountAddress` on submit        | not present             | `Address`                |
| `"balance-changed"` variant       | yes                     | no                       |
| `"smart-account-changed"` variant | yes                     | no                       |

<Warning>
  `sourceChain: "unknown"` is deposit-only. A webhook-detected deposit can arrive
  without chain or token info, in which case deposit events carry
  `sourceChain: "unknown"` and `sourceToken: undefined`. Handle this branch in
  your deposit `onLifecycle` switch — the wrong branch picks the wrong explorer
  URL. Withdraw flows always know the source chain.
</Warning>

## Analytics type rename

The `onEvent` prop name is unchanged on both modals, but its parameter type was
renamed. The payload shape is unchanged.

```diff theme={null}
- import type { DepositEvent, WithdrawEvent, ModalEvent } from "@rhinestone/deposit-modal";
+ import type {
+   DepositAnalyticsEvent,
+   WithdrawAnalyticsEvent,
+   ModalAnalyticsEvent,
+ } from "@rhinestone/deposit-modal";
```

## Removed

* **`connectButtonLabel`** — gone from both modals. The connect-step copy is
  controlled internally; delete any consumer-side label, there is no
  replacement.
* **`/reown` and `/safe` subpath imports** — they re-exported nothing that
  isn't already on the root entry point.

  ```diff theme={null}
  - import { DepositModal, disconnectWallet } from "@rhinestone/deposit-modal/reown";
  + import { DepositModal, disconnectWallet } from "@rhinestone/deposit-modal";

  - import type { WithdrawModalProps } from "@rhinestone/deposit-modal/safe";
  + import type { WithdrawModalProps } from "@rhinestone/deposit-modal";
  ```

  The `./deposit`, `./withdraw`, `./constants`, and `./styles.css` subpaths
  remain for tree-shaking.

## Additive — no action required

New in v0.3.0; existing code keeps working:

* **`appBalanceUsd?: number`** on `<DepositModal>` — renders a "Balance after
  deposit" row (`appBalanceUsd + amount`) instead of fetching a portfolio
  balance.
* **`dappImports?: DappImportsConfig`** on `<DepositModal>` — pull balances from
  third-party apps. See [migrating assets](/deposits/widget/asset-migrations).
* **`defaultAmount: "max"`** — defaults the input to the user's full
  source-token balance.
* **Solana destinations** — `targetChain: Chain | number | "solana"`,
  `targetToken: Address | string`, `recipient: Address | string`.
* **New root exports** — `DepositLifecycleEvent`, `WithdrawLifecycleEvent`,
  `DappImportsConfig`, `OutputTokenRule`, plus the renamed analytics types.

## Unchanged

`onError`, `onReady`, `onRequestConnect`, the `onEvent` prop name,
`dappWalletClient` / `dappPublicClient` / `reownAppId`, `<WithdrawModal>`'s
`onSignTransaction`, and the `@rhinestone/deposit-modal/styles.css` export all
keep their names and signatures.

<Note>
  Scoped to v0.3.0. Several of these changed again in v0.9.0 — see the v0.8.x → v0.9.0
  section at the top of this page.
</Note>
