Skip to content

fix(react-native-ui): accept offramp events only from the main frame (OFFRAMP-731) - #2293

Open
AngelPaella wants to merge 6 commits into
angelcalderaro/offramp-664-sign-messagefrom
angelcalderaro/offramp-731-rn-main-frame-nonce
Open

AngelPaella wants to merge 6 commits into
angelcalderaro/offramp-664-sign-messagefrom
angelcalderaro/offramp-731-rn-main-frame-nonce

Conversation

@AngelPaella

@AngelPaella AngelPaella commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Part of OFFRAMP-731. The issue stays open until every host ships.

Page side: Paella-Labs/crossbit-main#34023, which stamps the nonce. Until it ships, this gate accepts the page's events unchanged.

On Android, react-native-webview gives window.ReactNativeWebView.postMessage to every frame, and the message's url is either the sender's origin (WebMessageListener) or the main page's URL (legacy addJavascriptInterface). So isOfframpPage can't tell which frame posted. The offramp page hosts a cross-origin frame (Persona's capture widget during KYC), and on the legacy path its messages pass the check. A frame could then send crypto:send-transaction or crypto:sign-message and have the host's payer sign, or send a fake offramp:completed.

The first version of this PR injected a stamping wrapper into the main frame with injectJavaScript. It now uses a nonce that the page stamps itself, the same contract as Flutter:

  • Each mount creates a random nonce (16 bytes from react-native-get-random-values, as 32 lowercase hex) and loads <url>#mainFrameNonce=<nonce>. The query doesn't change. A new mount gets a new nonce; rerenders keep it.
  • The offramp page reads the fragment and stamps data.__mainFrameNonce on every event it sends (a crossbit-main change). A cross-origin frame can't read the page's fragment, so it can't stamp.
  • Once an event with the current nonce arrives, the component drops every event without it, or with another nonce, with a console.warn that names the event and no reply. This covers all events now, not only wallet requests, so a forged offramp:completed or offramp:ready is dropped too. The latch lasts for the mount, across onLoadStart and onLoad. The page keeps the fragment in its URL, so a reload keeps stamping.
  • Until the first stamped event, unstamped events pass, with one warning per mount, so page deploys that don't stamp keep working. The page sends ui:height.changed as soon as it renders, long before KYC adds the Persona frame.
  • The gate runs after isOfframpPage. Messages without a string event pass as before; no handler takes them.
  • No script is injected anymore, and the session check (sameSession.ts) is gone. A page the WebView ends up on without this mount's fragment can't stamp, so after the latch its events are dropped.
  • Pending wallet requests reset on offramp:ready (each new document sends it once), not on onLoadStart, as feat(offramp): payer.signMessage for in-iframe wallet ownership proof (OFFRAMP-664) #2266 already does on web. Before, a replaceState while a signature was pending dropped the wallet's reply, and the page waited 120 s for a transaction or forever for a message signature. This also fixes crypto:send-transaction as it is on main.

Stacked on #2266 (OFFRAMP-664), which adds crypto:sign-message. Flutter: Crossmint/crossmint-flutter-sdk#117.

Compatibility:

  • This host with a page that doesn't stamp yet: same as main plus the offramp:ready reset, with one warning per mount and no frame check.
  • Older hosts load no fragment, and the page sends them unchanged events.

What it leaves open, for OFFRAMP-731's contract-level fix:

Test plan

  • packages/client/ui/react-native, crossmint-embedded-offramp.test.tsx: the WebView loads the route with a 32-char lowercase hex nonce in the fragment and the query unchanged; a new mount gets a new nonce and rerenders keep it; stamped events are accepted and stamped wallet requests answered, with no warning; after a stamped event, unstamped crypto:send-transaction, crypto:sign-message and offramp:completed are dropped with one warning that names the event and leaves out the request data, and no reply; a wrong nonce is dropped after the latch and does not trip it; before any stamped event, unstamped events pass with a single warning; another page's message is dropped with no warning; messages without a string event pass before and after the latch; data that is null, a string or a number counts as unstamped without throwing; the latch survives onLoadStart and onLoad; no script is injected; pending send and sign replies still arrive after onLoadStart, and after offramp:ready an old reply is dropped while a new request works.
  • pnpm exec vitest run in packages/client/ui/react-native at 8a54f2e: 8 files, 101 tests passed.
  • tsc --noEmit in the same package: no errors in the touched files. The 5 errors it reports are in test files this PR doesn't touch (with-google-pay, with-device-signer, software-device-signer-key-storage).
  • Device check: the 2026-11-06 check (Release builds of apps/wallets/expo, react-native-webview 14.0.1, iOS 26.5 simulator, Android 16 emulator) ran on the old injected wrapper, before this rework. The nonce approach has not been device-checked yet. It needs the page change on staging.

Package updates

.changeset/offramp-main-frame-only.md: patch for @crossmint/client-sdk-react-native-ui. Its text now describes the fragment nonce and the latch.

…navigation and stamp only the own session (OFFRAMP-731)
…rchParams and warn when a load is not stamped (OFFRAMP-731)
@changeset-bot

changeset-bot Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8a54f2e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@crossmint/client-sdk-react-native-ui Patch
@crossmint/wallets-playground-expo Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@AngelPaella
AngelPaella marked this pull request as ready for review November 6, 2026 18:40
@greptile-apps

greptile-apps Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge within its stated interim scope.

Reviews (1) · Last reviewed commit: "fix(react-native-ui): reset offramp wall..." · Reviewed by Greptile

@AngelPaella AngelPaella changed the title fix(react-native-ui): accept offramp wallet requests only from the main frame (OFFRAMP-731) fix(react-native-ui): accept offramp events only from the main frame (OFFRAMP-731) Nov 9, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant