> ## 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 — v0.14.0 replaces the analytics event contracts and adds an analytics token route and a connect-src requirement; v0.12.0 requires GET /chains on your proxy and drops showHistoryButton; v0.9.0 brought managed accounts, the withdraw callback, and renamed props.

## 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/widget/status-tracking#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/widget/status-tracking#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/widget/status-tracking#deposit-analytics).

### Replace withdraw events

Both v0.13.x amount-screen events are removed. See the [current withdraw events](/deposits/widget/status-tracking#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/widget/status-tracking#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/widget/backend#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/widget/deposit-modal#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/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>
