> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rhinestone.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Migration guide

> Upgrade @rhinestone/deposit-modal through the v0.17.0 arrival estimates, the v0.16.0 claim modal removal, and earlier modal contract changes.

## v0.16.x → v0.17.0

v0.17.0 shows arrival estimates while a deposit is in flight. Nothing in the prop, proxy, or callback surface changes; the only contract worth checking is when `PROCESS_TIMEOUT` fires.

| Change                                                    | Action                                   |
| --------------------------------------------------------- | ---------------------------------------- |
| `PROCESS_TIMEOUT` fires at the served escalated threshold | None, unless you time your own UI off it |
| Pending screen states an expectation, then delay framing  | None                                     |
| History rows show the same treatments while in flight     | None                                     |
| Review and QR screens state the end-to-end duration       | None                                     |

### `PROCESS_TIMEOUT` moves earlier

The advisory `onError` callback still fires at most once per deposit, with the same code and message, and still means the deposit is slow rather than failed. It now fires at the escalated threshold served for that deposit — five to ten minutes in practice, rather than a fixed ten minutes. With no served estimate it keeps the previous fixed threshold. If your app starts a support timer or a warning banner from this code, re-check the copy against the earlier trigger.

### Unaffected

Hosts see no new props, proxy routes, or configuration — the estimate rides the existing `POST /quotes/preview` and `GET /deposits` routes. The withdraw modal's pending screen is served no estimate and is unchanged, including its fixed `PROCESS_TIMEOUT` threshold. A deposit with no served estimate behaves exactly as it did in v0.16.x. See [arrival estimates](/deposits/overview/widget/callbacks-and-error-handling#arrival-estimates).

## v0.15.x → v0.16.0

v0.16.0 removes the standalone claim modal. It is breaking, and the version number does not say so: under 0.x semver there is no major to spend, so a breaking change is a minor bump.

| Change                                           | Action                                                         |
| ------------------------------------------------ | -------------------------------------------------------------- |
| `ClaimModal` removed                             | Move to recovery from history in the deposit or withdraw modal |
| `./claim` subpath removed                        | Delete the import — the subpath no longer resolves             |
| `ClaimModalProps`, `ClaimLifecycleEvent` removed | Drop the annotations; there is no claim lifecycle callback     |
| Embed `mode: "claim"` removed                    | Run the page in `deposit` or `withdraw` mode                   |
| `defaultTxHash`, `defaultDestination` removed    | Delete them from `EmbedConfig`                                 |
| `CLAIM_LOOKUP_FAILED`, `CLAIM_FAILED` removed    | Drop those arms from your `onError` handling                   |

### Use recovery from history instead

Pass `signRecovery` to `DepositModal` or `WithdrawModal`, and a failed row offers a Recover action. A deposit history never lists is reachable through the panel's transaction-hash lookup. See [history and recovery](/deposits/overview/widget/history-and-recovery) — nothing about that flow changes in this release.

One capability leaves the package: the standalone lookup matched a transaction hash against any account, while history's fallback is scoped to the account the modal is mounted for. Cross-account support cases go through the [dashboard workflow](/deposits/overview/resources/troubleshooting).

### Unaffected

The whole `ClaimAnalytics*` family stays, still importable from `.`, `./deposit` and `./withdraw` — recovery from history reports in it. So do `SignRecovery`, `SignRecoveryPayload`, `RecoveryErrorCode`, and `createRefundHandler` from `@rhinestone/deposit-modal/server`, which never depended on the modal.

### The hosted page is not version-pinned

A host adopts the removed import on its own schedule, but the [native embed](/deposits/overview/widget/mobile-apps) is served by us. Claim mode disappears for every native host when the page deploys. Asking for it returns the page's existing unsupported-mode error frame naming the mode.

## v0.14.x → v0.15.0

v0.15.0 adds account-scoped history recovery to the deposit and withdraw modals. Existing integrations remain compatible: if you omit `signRecovery`, history is read-only and the fallback transaction-hash lookup is absent.

| Change                       | Action                                                                                 |
| ---------------------------- | -------------------------------------------------------------------------------------- |
| Optional recovery signer     | Add `signRecovery` to either host modal only when you can sign for its history account |
| Destination-chain signing    | Smart-account signers must use `SignRecoveryPayload.chainId` as the verifier chain     |
| Widened host `onEvent` types | Include `ClaimAnalyticsEvent` in explicit deposit and withdraw callback annotations    |
| New history event variants   | Update exhaustive switches over `DepositAnalyticsEvent` and `WithdrawAnalyticsEvent`   |
| New claim correlation values | Handle history entry sources, correlation fields, and the `history_back` close source  |
| New public types             | Import history and claim correlation types if you model their values explicitly        |

### Enable recovery when the account can sign

Pass the existing `SignRecovery` callback to enable row recovery and account-scoped transaction-hash fallback. Deposit history always uses the configured `recipient`. Withdraw history follows the valid withdrawal destination, so provide the callback only if your app can sign for destinations users choose.

```tsx theme={null}
<DepositModal
  // ...required props
  signRecovery={signRecovery}
/>

<WithdrawModal
  // ...required props
  signRecovery={signRecovery}
/>
```

Omitting the prop needs no migration work. History remains visible and read-only; it does not show fallback lookup or a recovery status promise. The existing `GET /deposits` and `POST /deposits/recover` proxy routes serve the new flow. See [history and recovery](/deposits/overview/widget/history-and-recovery).

### Use the destination verifier chain

`SignRecoveryPayload.chainId` existed before v0.15.0, but the standalone runtime previously supplied the source/refund chain. It now supplies the deposit destination chain, where the recipient's EOA, ERC-1271, or ERC-6492 verifier lives.

EOA signing is unaffected. A smart-account integration that selects account deployment data, owners, or an RPC by chain must select them with the supplied destination `chainId`. Do not derive the signing chain from the refund transaction.

### Widen callbacks and exhaustive switches

History-originated recovery sends claim analytics through the host modal callback:

```diff theme={null}
- onEvent={(event: DepositAnalyticsEvent | AnalyticsIngestFailureEvent) => {
+ onEvent={(event: DepositAnalyticsEvent | ClaimAnalyticsEvent | AnalyticsIngestFailureEvent) => {
    analytics.track(event.type, event);
  }}
```

Make the equivalent change for `WithdrawAnalyticsEvent`. Inferred callback parameters update automatically. No claim lifecycle variants are added to deposit or withdraw `onLifecycle`.

Exhaustive analytics switches must also handle the five `*_modal_history_*` variants documented in [history analytics](/deposits/overview/widget/analytics#history-analytics). Claim switches must accept `entry_source` values `history_row` and `history_fallback`, the optional cross-session correlation fields, and `history_back` in `ClaimAnalyticsCloseSource`.

v0.15.0 publicly exports `SignRecovery`, `SignRecoveryPayload`, `RecoveryErrorCode`, `HistoryStatus`, `HistoryAnalyticsBadge`, `HistoryClaimEntrySource`, `ClaimAnalyticsEntrySource`, and `ClaimAnalyticsCorrelation` from the relevant root and modal entry points. The deposit and withdraw entry points also expose the existing claim analytics types required by their widened callbacks. There is no separate `HistoryAnalyticsEvent`; history variants are members of `DepositAnalyticsEvent` and `WithdrawAnalyticsEvent`.

## v0.13.x → v0.14.0

v0.14.0 replaces the analytics contracts for all three modals and adds a session envelope. No flow, prop, or UI behavior changes, but annotated callback types and switches over old event names need updates.

| Change                        | Action                                                                                 |
| ----------------------------- | -------------------------------------------------------------------------------------- |
| Widened `onEvent` type        | Include `AnalyticsIngestFailureEvent` in explicit callback annotations                 |
| Replaced event contracts      | Migrate switches over old deposit, withdraw, and claim event names                     |
| Changed reason contract       | Route by declared family, remove `retryable` from friction, and rename changed reasons |
| Added session envelope        | Read configuration and targets from modal-specific `session_properties`                |
| Widened `ModalAnalyticsEvent` | Handle the new diagnostic member in exhaustive switches                                |
| Added ingest-token route      | Forward `POST /analytics/ingest-token` for attribution                                 |
| Added browser ingest origin   | Allow it in an explicit `connect-src` policy                                           |

### Update callback and union handling

`onEvent` also delivers [ingest failures](/deposits/overview/widget/analytics#ingest-failures). Widen explicitly annotated callbacks:

```diff theme={null}
- onEvent={(event: DepositAnalyticsEvent) => {
+ onEvent={(event: DepositAnalyticsEvent | AnalyticsIngestFailureEvent) => {
    analytics.track(event.type, event);
  }}
```

Do the same for `WithdrawAnalyticsEvent` and `ClaimAnalyticsEvent`. An inferred parameter needs no annotation change. This widening handles the diagnostic type only; it does not migrate switches over removed event names.

`ModalAnalyticsEvent` now has four members: the three modal funnel unions plus `AnalyticsIngestFailureEvent`. Add an `analytics_ingest_failure` branch to exhaustive modal-wide switches.

### Read funnel position from `step`

The v0.14.0 contracts remove `last_step`. Read `step` on every position-bearing event. Abandonment always has a non-null `step`. Close has the latest step, or `step: null` only when the session closes before entering the funnel. UI outcomes use the latest logical step and do not prove backend fulfillment.

### Update reason routing

Friction means progress is blocked without a failed attempted operation. It carries `step` and `reason`, but no `retryable`. Failure means an attempted operation failed or the flow reached a terminal condition. It carries `step`, `reason`, and `retryable`. A retry is a subsequent explicit attempt after the reported reason.

Bounded friction, failure, and retry reasons begin with an exported `AnalyticsReasonFamily`: `account_setup`, `amount`, `exchange`, `lookup`, `migration`, `modal`, `processor`, `provider`, `quote`, `recipient`, `recovery`, `refund`, `regional_methods`, `registration`, `route`, `signature`, `submission`, `swapped`, `transfer`, or `wallet`.

Match the declared list longest-prefix first. Do not split at the first underscore. Abandonment, close-source, and ingest-diagnostic vocabularies are separate. Already-routable reason values are unchanged.

| Modal    | Before                           | v0.14.0                                 |
| -------- | -------------------------------- | --------------------------------------- |
| Deposit  | `portfolio_load_failed`          | `wallet_portfolio_load_failed`          |
| Deposit  | `no_supported_assets`            | `wallet_no_supported_assets`            |
| Deposit  | `no_funded_assets`               | `wallet_no_funded_assets`               |
| Deposit  | `invalid_amount`                 | `amount_invalid`                        |
| Deposit  | `insufficient_balance`           | `amount_insufficient_balance`           |
| Deposit  | `minimum_amount`                 | `amount_below_minimum`                  |
| Deposit  | `maximum_amount`                 | `amount_above_maximum`                  |
| Deposit  | `source_price_unavailable`       | `quote_source_price_unavailable`        |
| Deposit  | `target_price_unavailable`       | `quote_target_price_unavailable`        |
| Deposit  | `chain_switch_rejected`          | `wallet_chain_switch_rejected`          |
| Deposit  | `chain_switch_failed`            | `wallet_chain_switch_failed`            |
| Deposit  | `permit_preparation_unavailable` | `wallet_permit_preparation_unavailable` |
| Deposit  | `signature_rejected`             | `wallet_signature_rejected`             |
| Deposit  | `clipboard_failed`               | `transfer_clipboard_failed`             |
| Deposit  | `status_poll_failed`             | `processor_status_poll_failed`          |
| Withdraw | `send_handler_missing`           | `submission_handler_missing`            |
| Withdraw | `balance_unavailable`            | `wallet_balance_unavailable`            |
| Withdraw | `target_tokens_unavailable`      | `route_target_tokens_unavailable`       |
| Withdraw | `invalid_recipient`              | `recipient_invalid`                     |
| Withdraw | `invalid_amount`                 | `amount_invalid`                        |
| Withdraw | `insufficient_balance`           | `amount_insufficient_balance`           |
| Withdraw | `target_changed`                 | `registration_target_changed`           |
| Withdraw | `status_poll_failed`             | `processor_status_poll_failed`          |
| Claim    | `invalid_transaction_hash`       | `lookup_transaction_hash_invalid`       |
| Claim    | `no_deposits_found`              | `lookup_no_deposits_found`              |
| Claim    | `no_eligible_deposits`           | `lookup_no_eligible_deposits`           |
| Claim    | `invalid_destination`            | `refund_destination_invalid`            |
| Claim    | `deposit_data_incomplete`        | `recovery_deposit_data_incomplete`      |
| Claim    | `deposit_not_recoverable`        | `recovery_deposit_not_recoverable`      |
| Claim    | `verification_unavailable`       | `signature_verification_unavailable`    |
| Claim    | `service_unreachable`            | `refund_service_unreachable`            |
| Claim    | `close_refused_in_flight`        | `modal_close_refused_in_flight`         |

### Replace deposit events

All eight v0.13.x events are removed. v0.14.0 uses a method-aware taxonomy with `step` and top-level `funding_method`. See the [current deposit analytics contract](/deposits/overview/widget/analytics#deposit-analytics).

| v0.13.x event                                                     | v0.14.0 replacement                                                                                                                                                                 | Removed fields                                                                                       |
| ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `deposit_modal_fiat_methods_rendered`                             | No direct replacement. Read provenance from `session_properties.fiat_methods`; degraded resolution is friction at `fiat_regional_payment_method` with a `regional_methods_*` reason | `country`, `payment_methods`                                                                         |
| `deposit_modal_fiat_method_click`                                 | `deposit_modal_method_selected` with `funding_method: "fiat_onramp"` and `{ payment_method, source }` identifiers                                                                   | `country`                                                                                            |
| `deposit_modal_connected_wallet_select_source_open`               | `deposit_modal_step_open` at `wallet_source_asset`                                                                                                                                  | `total_balance_in_external_wallet`, `pred_balance`                                                   |
| `deposit_modal_connected_wallet_select_source_cta_click`          | `deposit_modal_step_complete` at `wallet_source_asset`                                                                                                                              | `token_name`, `token_balance`, `total_balance_in_external_wallet`, `pred_balance`                    |
| `deposit_modal_transfer_crypto_open`                              | `deposit_modal_step_open` at `transfer_address_shown`, then a transfer handoff after the picker default settles and source identity resolves                                        | `default_chain`, `default_token`, `pred_balance`                                                     |
| `deposit_modal_transfer_crypto_cta_click` with `cta_name: "copy"` | `deposit_modal_step_complete` at `transfer_copy`; failed copy is failure there with `transfer_clipboard_failed`                                                                     | `default_chain`, `default_token`, `pred_balance`                                                     |
| `deposit_modal_connected_wallet_enter_value_open`                 | `deposit_modal_step_open` at `wallet_amount`                                                                                                                                        | `send_token`, `receive_token`, `pred_balance`                                                        |
| `deposit_modal_connected_wallet_enter_value_cta_click`            | `deposit_modal_step_complete` at `wallet_amount`, for `continue` only                                                                                                               | `cta_name`, `send_token`, `receive_token`, `pred_balance`; percentage and Max shortcuts emit nothing |

Balances, amounts, token labels, country, and shortcut CTA details leave the event stream with no replacement.

Method selection and handoff now use method-aware identifier bags:

| Event and method                    | `identifiers`                                                                                             |
| ----------------------------------- | --------------------------------------------------------------------------------------------------------- |
| Method selected: `wallet`           | Required `network` (`evm` \| `solana`) and `integration` (`modal_connected` \| `host_supplied` \| `none`) |
| Method selected: `transfer`         | Absent                                                                                                    |
| Method selected: `fiat_onramp`      | Required `payment_method` and `source` (`personalized` \| `fallback` \| `configured`)                     |
| Method selected: `exchange_connect` | Optional, exactly `exchange` when present                                                                 |
| Method selected: `asset_migration`  | Optional, exactly `provider` when present                                                                 |
| Handoff: `transfer`                 | Required `source_chain` and `source_token`                                                                |
| Handoff: every other method         | Absent                                                                                                    |

Transfer handoff identities describe the source the user pays from, not the session target. `source_chain` is CAIP-2 (`eip155:<id>`, the Solana mainnet namespace, or `hypercore:spot`). `source_token` is the on-chain identity: lowercase for EVM and HyperCore hex values, case-sensitive for Solana mints, with native SOL represented by the system-program mint rather than `native`. The handoff uses a `deposit_address` correlator. Non-transfer handoffs use `transaction_hash`, `deposit_id`, or `swapped_external_customer_id`; those four correlator types can also appear on `deposit_modal_correlator_observed`.

Do not treat the source displayed while the transfer picker is loading as the selected source. The handoff waits for the picker to settle and never publishes that temporary default. A session whose source never resolves emits step events but no transfer handoff; a later selection of a distinct source can emit another handoff.

When rebuilding same-route deposit funnels, preserve the processing-step order. A transfer emits `deposit_modal_step_open` at `transfer_tracking` before `deposit_modal_ui_outcome` reports `outcome: "completed"` at that step. A wallet deposit does the same at `wallet_processing`. These completed outcomes are widget observations, not proof of backend or on-chain fulfillment. See the [current deposit analytics contract](/deposits/overview/widget/analytics#deposit-analytics).

### Replace withdraw events

Both v0.13.x amount-screen events are removed. See the [current withdraw events](/deposits/overview/widget/analytics#withdraw-analytics).

| v0.13.x event                            | v0.14.0 replacement                      | Removed fields                                                           |
| ---------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------ |
| `withdraw_modal_select_amount_open`      | `withdraw_modal_step_open` at `form`     | `pred_balance`, `default_token`, `default_chain`                         |
| `withdraw_modal_select_amount_cta_click` | `withdraw_modal_step_complete` at `form` | `pred_balance`, `selected_token`, `selected_chain`, `amount`, `cta_name` |

Balances, amount, and token and chain labels leave the stream. `withdraw_modal_handoff` adds `transaction_hash`, `managed_account`, and `same_route` as the join identities and route classification.

`same_route` is unknown before submit, pinned on handoff, and repeated on later events. Same-route withdrawals create no backend bridge row, so their UI outcome is only a client observation.

### Replace claim events

All five v0.13.x events are replaced. See the [current claim events](/deposits/overview/widget/analytics#claim-analytics).

| v0.13.x event              | v0.14.0 replacement                                                                               | Removed fields                          |
| -------------------------- | ------------------------------------------------------------------------------------------------- | --------------------------------------- |
| `claim_modal_open`         | Same name, now the first event of a session with the envelope instead of an `isOpen` notification | —                                       |
| `claim_modal_lookup`       | `claim_modal_lookup_result`, splitting the count into `matches` and `eligible`                    | The old single `matches` interpretation |
| `claim_modal_refund_click` | Step complete at `review`, then step open at `submit`                                             | `chain`                                 |
| `claim_modal_complete`     | `claim_modal_handoff`, then UI outcome `completed`                                                | `chain`                                 |
| `claim_modal_failed`       | `claim_modal_failure`, then UI outcome `failed`                                                   | `chain`, HTTP `status`                  |

The handoff contains `deposit_id`, `transaction_hash: string | null`, and `refund_transaction_hash`. The first is the authoritative join key; `transaction_hash` is the deposit's source transaction, and `refund_transaction_hash` is the submitted refund transaction. The searched hash, typed refund destination, deposit amount, chain, and HTTP status are not sent.

### Read the session envelope

Every funnel event now carries `session_id`, `modal`, `widget_version`, `timestamp`, and modal-specific `session_properties`. Deposit events additionally carry top-level `funding_method`, which is `null` before method selection. The snapshot is taken when the modal opens and stays fixed for that session.

* Deposit properties include enabled funding methods, wallet integration, fiat-method provenance, asset-migration providers and initial provider, gasless-wallet configuration, presentation, effective overlay-close configuration, prefill flags, and optional target dimensions.
* Withdraw properties include presentation, effective overlay-close configuration, prefill flags, and optional target dimensions. The target is the destination on which the form opened, including its source fallback; flags record whether your app supplied it.
* Claim properties include presentation, effective overlay-close configuration, and transaction-hash and refund-destination prefill flags.

`target_chain` is CAIP-2 and `target_token` is the on-chain identity. EVM and HyperCore hex values are lowercased; Solana mint case is preserved; `native` is allowed. Invalid target fields are omitted independently. Changing target props while the modal is open changes the flow but not this snapshot.

### Forward `POST /analytics/ingest-token`

The browser mints a short-lived attribution token on your proxy. Events go directly to Rhinestone and never traverse your proxy. `deposit-widget-proxy` forwards this route already; for a custom proxy, add it to the allowlist and return `Cache-Control: no-store`.

Skipping the route leaves sessions unattributed but does not stop collection or any modal flow. See [the analytics token route](/deposits/overview/widget/backend-setup#the-analytics-token-route).

### Allow the ingest origin in `connect-src`

If your app sets an explicit content security policy, add `https://v1.orchestrator.rhinestone.dev` to `connect-src`. The directive must also include your proxy origin and any configured `rpcUrls`; see [content security policy](/deposits/overview/widget/widget-configuration#content-security-policy).

A block surfaces only as `analytics_ingest_failure` with `reason: "network"`. Apps without an explicit policy need no change.

## 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/overview/widget/backend-setup#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 offer signed [recovery](/deposits/overview/widget/history-and-recovery)                                                                                                    |

See [required routes](/deposits/overview/widget/backend-setup#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/overview/widget/backend-setup#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/crypto/withdraw-funds#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/overview/widget/backend-setup), 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/overview/widget/backend-setup) 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/overview/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>`** and the `./claim` subpath — a standalone transaction-hash lookup
  that returned a failed or rejected deposit's funds. Removed in v0.16.0; use
  [history and recovery](/deposits/overview/widget/history-and-recovery) 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/overview/widget/widget-configuration#content-security-policy).
* **`HYPERCORE_RECIPIENT_NOT_EOA` is no longer emitted.** [HyperCore](/deposits/overview/widget/widget-configuration#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/onramps/payment-methods-and-availability#regional-behavior).
* **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 [callbacks and error handling](/deposits/overview/widget/callbacks-and-error-handling) 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/crypto/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>
