npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rhinestone/deposit-modal-react-native

v0.2.0

Published

React Native wrapper for the Rhinestone deposit modal

Readme

@rhinestone/deposit-modal-react-native

The Rhinestone deposit flow in a React Native app: a native sheet holding our hosted page, with your app's wallet driving every signature.

bun add @rhinestone/deposit-modal-react-native react-native-webview
import { DepositSheet } from "@rhinestone/deposit-modal-react-native";

<DepositSheet
  visible={open}
  onDismiss={() => setOpen(false)}
  config={{
    mode: "deposit",
    backendUrl: "https://your-proxy.example/deposit",
    recipient: account,
    targetChain: 8453,
    // An address, never a symbol — an EVM target rejects "USDC".
    targetToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  }}
  wallet={{
    state: walletState,
    request: ({ chainId, request }) => yourWallet.request(chainId, request),
    onConnectRequested: () => yourWalletPicker.open(),
  }}
  openUrl={({ url }) => WebBrowser.openBrowserAsync(url)}
/>;

Before your first build

Your proxy must allow https://deposit.rhinestone.dev. Every backend call from a mobile integration originates from our hosted page, not from your app's domain, so a proxy with an explicit CORS allow-list rejects all of them at preflight — the whole sheet dead, with nothing naming the cause. A proxy using a permissive default is unaffected. Dev builds use https://dev.deposit.rhinestone.dev.

The two hosted pages are release channels

EMBED_URL tracks the npm @latest modal, EMBED_URL_DEV tracks the @dev snapshot. Neither takes configuration — the only difference is which bundle you get, so pin embedUrl={EMBED_URL_DEV} while building against an unreleased change and drop it before you ship.

The page adapts to your wrapper at runtime: both sides announce their protocol version and capabilities during the handshake, which is what keeps a protocol bug fixable for an app already in the store.

What each prop turns on

config is every prop the web modal takes that survives a JSON hop, with the same names. It is not a mount-time value — change it and the page reconfigures in place, which is how appearance follows an app that can switch it mid-flow.

| you pass | the page gains | |---|---| | wallet | The connect row, and signing over CAIP-27 | | sendTransaction | Withdraw | | signRecovery | Claim | | openUrl | The card and exchange rows |

Omitting one is not a degraded flow, it is a smaller one. Without openUrl the page offers no card row at all, because 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.

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 a different app, and never the web view itself, which is the arrangement this exists to end. Answer when the browser is presented, not when it is dismissed — a dismissal-tied answer has to survive your activity being killed behind Custom Tabs.

Nothing about the deposit depends on that answer. Our own order-status route drives the tracker, so the page is idle while the user is elsewhere.

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.

That last one matters more than it looks. Without it a host has to lie in one direction or the other, and the safe-looking lie — reporting failure — is the one that double-spends: the page re-offers its button and the user sends again. Anything else you throw from a sending handler is treated as uncertain for the same reason.

A deposit that outlives the page

The OS kills a backgrounded web view routinely, and a user who backgrounds the app mid-settlement would otherwise return to a blank sheet with the deposit long since complete. While the component is mounted it polls the same /deposits route the page uses and reports terminal deposits through onDepositSettled, including one that both started and finished while the page was dead. Once the process is gone this stops; the modal's history panel covers the reopen.

The sheet is the height of the flow

The page publishes what it currently needs and the sheet follows it, so a one-row screen is not presented at the height of the whole deposit flow. Drag the grabber up for full height; a page that publishes no height gets full height to begin with.

Nothing to pass — presentation="fullScreen" opts out, and an integrator who ignores all of this is unaffected.

Dismissal is not always yours to grant

The page publishes a dismissal policy, and at a few moments it is blocked: a wallet request outstanding, a submission in flight, a payment session held by a third party. Android's hardware back and the sheet's own close both route through the page first, and a dismissal it cannot accept now is honoured when the lock lifts rather than refused.

A lock is never open-ended — every request the page blocks on carries its own deadline — so this cannot produce a sheet the user is stuck in.

The swipe goes through the same policy. The sheet is drawn here rather than presented as a pageSheet, whose interactive swipe React Native gives no way to refuse — so a drag on the grabber and a tap outside are ours to route, and a locked screen cannot be swiped away mid-signature. presentation="fullScreen" remains available, but it is no longer the way to protect a signature.

Development

bun install
bun run typecheck
bun run test
bun run build

src/host.ts is the contract; it has no React and no react-native-webview in it, so the whole bridge is testable in Node against src/test/page-double.ts, which runs the injected script rather than reading the frame out of a mock. src/DepositSheet.tsx is what is left over: presentation, origin pinning, the back gesture, and the poll.

The sheet is covered too, against a page double living in a real window (src/test/page-window.ts): React Native and the web view are mocked, the bridge is not, so a component test still crosses the nonce, the encoder and the correlation table. That is where the session lifetime is pinned down — a session that must survive a parent's re-render, a reload allowance that must not survive the session, and a watch that has to follow a config it was not born with.