> ## 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.

# Mobile apps

> Run the deposit flow in a native app: our hosted page in a native sheet, with your app's wallet signing.

In a native app the modal is not a component you render. It is a page we host, presented in a native sheet, with your app's wallet driving every signature — so you add one dependency and pass the same props the web modal takes, instead of building a web view around a page you host yourself.

```mermaid actions={false} theme={null}
flowchart LR
    A[Your app] --> B[Native sheet]
    B --> C[deposit.rhinestone.dev]
    C --> D[Your proxy]
    D --> E[Rhinestone]
```

Nothing about the backend changes: the page calls [your proxy](/deposits/widget/backend), and your proxy holds the API key.

<Note>
  All three wrappers speak the same contract and take the same configuration. The
  walkthrough below is React Native; Swift and Kotlin mirror it.
</Note>

## Install

<CodeGroup>
  ```bash React Native theme={null}
  npm install @rhinestone/deposit-modal-react-native react-native-webview
  ```

  ```swift Swift theme={null}
  // Package.swift
  .package(
    url: "https://github.com/rhinestonewtf/deposit-modal-swift.git",
    from: "0.1.0"
  )
  ```

  ```kotlin Kotlin theme={null}
  // build.gradle.kts
  implementation("dev.rhinestone:deposit-modal:0.1.0")
  ```
</CodeGroup>

## Present the sheet

```tsx theme={null}
import { DepositSheet } from "@rhinestone/deposit-modal-react-native";
import * as WebBrowser from "expo-web-browser";

<DepositSheet
  visible={open}
  onDismiss={() => setOpen(false)}
  config={{
    mode: "deposit",
    backendUrl: "https://your-proxy.example/deposit",
    recipient: account,
    targetChain: 8453,
    targetToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  }}
  wallet={{
    state: walletState,
    request: ({ chainId, request }) => yourWallet.request(chainId, request),
    onConnectRequested: () => yourWalletPicker.open(),
  }}
  openUrl={({ url }) => WebBrowser.openBrowserAsync(url)}
  onDepositSettled={(deposit) => creditBalance(deposit)}
/>;
```

`config` is every [deposit modal](/deposits/widget/deposit-modal) prop that survives a JSON hop, under the same names, and it is not a mount-time value — change it and the page reconfigures in place, which is how appearance follows an app the user can re-theme mid-flow.

`mode` is `deposit` or `withdraw`. Any other value, `claim` included, is refused with an unsupported-mode error frame naming the mode, rather than a blank page.

<Warning>
  `targetToken` is an **address** on an EVM target, never a symbol. `"USDC"` is
  rejected at registration, and the sheet reports it as the deposit service being
  unavailable — which reads as an outage rather than a typo.
</Warning>

## What each prop turns on

Capabilities come from the handlers you pass, so the page never offers a row your app cannot serve.

| you pass          | the page gains                                                                                             |
| ----------------- | ---------------------------------------------------------------------------------------------------------- |
| `wallet`          | The connect row, and signing over CAIP-27                                                                  |
| `sendTransaction` | Withdraw                                                                                                   |
| `signRecovery`    | The recover action on a failed deposit in [history](/deposits/widget/history-and-recovery), in either mode |
| `openUrl`         | The card and exchange rows                                                                                 |

Omitting one is not a degraded flow, it is a smaller one — QR and manual transfer need no wallet at all.

### `openUrl` must present a browser over your app

`expo-web-browser`'s `openBrowserAsync`, or `react-native-inappbrowser-reborn`. Never `Linking.openURL`, which hands the user to whichever app claims the scheme, and never the web view itself, which would put a payment page on the same document as the bridge.

Answer when the browser is presented, not when it is dismissed — a dismissal-tied answer has to survive your activity being killed behind the browser. Nothing about the deposit depends on that answer: our own order-status route drives the tracker while the user is elsewhere.

Without `openUrl` the page offers no card row at all. That is deliberate — card verification routinely refuses an embedded web view, and a payment method that fails at the point of paying is worse than one never offered.

## Your proxy must allow our origin

Every backend call from a mobile integration carries `https://deposit.rhinestone.dev` as its `Origin`, not your app's domain. A proxy with an explicit CORS allow-list rejects all of them at preflight — the whole sheet dead, with no request in your logs and no error naming CORS. See [mobile integrations call from our origin](/deposits/widget/backend#mobile-integrations-call-from-our-origin-not-yours); it is a one-line change, and it belongs in your proxy **before** your first mobile build.

## Errors your handlers throw

Throw `userRejected()` when the user said no, `walletUnavailable()` when you could not reach the wallet, and `submissionUncertain()` when an app switch or a process death left you unable to say whether a transaction was broadcast.

The last one matters more than it looks. Without it you have to guess in one direction or the other, and the safe-*looking* guess — reporting failure — is the one that double-spends: the page re-offers its button and the user sends again. `submissionUncertain()` has copy that does not invite a retry, and anything else thrown from a sending handler is treated the same way for the same reason.

## A deposit that outlives the sheet

The OS kills a backgrounded web view routinely. While `DepositSheet` is mounted it polls the same deposit route the page uses and reports terminal deposits through `onDepositSettled`, including one that both started and finished while the page was dead.

Once your process is gone, nothing native is running to notice.

<Warning>
  A completion that lands while your app is backgrounded reaches you **on next
  resume**, not at the moment funds arrive. If you need to act at that instant —
  credit a balance, send a push — subscribe to
  [webhooks](/deposits/widget/backend#webhooks) on your own backend. That is true of
  the web modal too; it is only more visible here, because a mobile app is
  backgrounded routinely and a browser tab is not.
</Warning>

## Pinning the page

The wrapper points at the page tracking the npm `@latest` modal. `embedUrl={EMBED_URL_DEV}` points at the `@dev` snapshot instead — useful while you build against an unreleased change, and worth dropping before you ship.

The page adapts to the wrapper at runtime: both sides announce their protocol version and capabilities during the handshake, and the page serves whichever version it is talking to. That is what keeps a protocol *bug* fixable for an app already in the store, where you cannot ship a wrapper update on our schedule.

## App store review

Both stores accept a hosted funding flow inside a web view as a component of a larger app. Two rules carry that position, and breaking either inverts it:

* **A deposited balance must not unlock anything in your app.** Apple 3.1.1 names cryptocurrency wallets explicitly as a mechanism that may not unlock content or functionality.
* **Do not charge an app-level fee on top of a deposit.**

Per store, the things worth knowing before you submit:

* Apple requires an **organization** developer account for wallet functionality. An individual account cannot ship this at all.
* Google treats non-custodial wallets as out of scope of its cryptocurrency exchange policy, but the **Financial features declaration** is still mandatory.
* Do **not** enroll in Google Play's link-out or alternative billing programs for the on-ramp. Enrolling concedes that Play's billing policy applies and pulls reporting and service fees onto payments that never touched Play billing.
* Google requires the site owner's permission to present a site you do not own in a web view. Ask us for it — it covers our hosted page in your app.
* Give the reviewer a funded test account, say in the review notes that the card path deliberately opens the system browser, and name a country where the on-ramp is available. Region-dependent availability otherwise reads as a broken feature.

<Note>
  Store policies change and this is not legal advice. Check the current guidelines
  before you submit.
</Note>
