Skip to content

feat(examples): react native (webview) example with an eip-1193/6963 wallet bridge - #845

Open
gomesalexandre wants to merge 8 commits into
lifinance:mainfrom
gomesalexandre:feat_react_native_webview_example
Open

feat(examples): react native (webview) example with an eip-1193/6963 wallet bridge#845
gomesalexandre wants to merge 8 commits into
lifinance:mainfrom
gomesalexandre:feat_react_native_webview_example

Conversation

@gomesalexandre

@gomesalexandre gomesalexandre commented Aug 16, 2026

Copy link
Copy Markdown

issue

closes #565

tl;dr

this adds a react-native example under examples/ (nothing in packages/widget changes). It runs the widget inside a react-native-webview, and the host RN app's wallet serves it over an EIP-1193 provider that's injected before page load and announced via EIP-6963. From the widget's side the RN app is just another injected wallet: it shows up in the wallet list, and connect/sign round-trip over postMessage. The widget itself is untouched, that's the point of doing it this way.

Went the webview route on purpose (note below on why not a native port). @chybisov this is the "use it in a webview" path you suggested on #565, with the wallet plumbing done end to end so it works out of the box.

how it works

  • examples/react-native-webview-widget-page/: tiny vite page that renders <LiFiWidget> with providers: [EthereumProvider()]. Served from a real origin, never file:// (opaque origin breaks localStorage on Android).
  • examples/react-native-webview/: Expo host. injectedJavaScriptBeforeContentLoaded materialises an EIP-1193 provider in the page, announces it over 6963 with the host app's name/icon, and forwards every request() to the RN side over postMessage with id-correlated responses. RN routes to viem/wagmi (WalletConnect works identically, it's just another EIP-1193 source on the host).
  • 4001 on user-reject is preserved with the right shape, and chainChanged/accountsChanged are pushed back into the page. Without those the route execution machine hangs, or wagmi desyncs from the host chain.

EVM only here. Solana/Sui/Tron bridges are follow-ups (each is another provider family, keeps this reviewable).

receipts (iOS sim)

Drove the whole thing end to end.

1. Widget lists the RN app as a wallet. This is the money shot, it proves the 6963 announce lands:
wallet discovered

2. Connect crosses the bridge into a native RN approval (the request left the webview):
native approval

3. Connected, address chip shows:
connected

4. Live quote WETH to ETH, real li.quest routing through the bridged provider:
live quote

5. Reject path. Hit Reject on the native alert, widget recovers cleanly from "Connecting" back to the wallet list, which proves the 4001 round-trips right instead of hanging:
reject recovery

full swap, executed on an anvil mainnet fork, zero real funds

Wired an anvil --fork-url <mainnet> harness (dev-only, HARNESS flag). Wallet becomes anvil acct #0, and both the app's viem clients and the widget's own SDK reads (via sdkConfig.rpcUrls) point at the fork, so a same-chain swap executes for real against forked DEX state.

6. Widget reads the fork's balance (10,000 ETH, "/ 10,000"):
fork balance

7. Sign, broadcast, pending (anvil log shows the eth_sendRawTransaction):
swap pending

8. Swap successful, 0.5 ETH to 936.94 USDC. Fork USDC balance after: 936.544051, real slippage vs the quote, so real contract execution rather than a painted screen:
swap done

honest caveats

  • Embed, not port. Typography/theming is the widget's web theme inside a WebView. A native RN port is a 36.5k-line rewrite, I built a native prototype to measure it (~1.1k lines got ~3% of the way, and the form is the part that ports best). Happy to share that assessment, it's why webview is the right call here.
  • The widget's terminal status comes from li.quest /v1/status, which can't know about fork txs, so the harness mocks only that one endpoint in-page. Everything else is real.
  • WalletConnect to an external app works, but iOS WKWebView suspends JS on app-switch, which freezes mid-route polling. Documented, and the example keeps signing in-process.

state of the diff

16 files, no scaffolding: the receipt-driving automation rig and debug probes are stripped, the anvil harness ships off by default (ephemeral wallet out of the box, opt-in for local swaps), there's a README matching the other examples/ entries, biome is clean and both packages typecheck green.

gomesalexandre and others added 6 commits August 16, 2026 13:31
…bridge

The widget in a react-native-webview, with the host app's wallet serving it
through an EIP-1193 provider injected before page load and announced via
EIP-6963 - the widget's wallet menu lists the RN app like any injected
wallet, and every request() round-trips over postMessage with id
correlation, 4001 rejection fidelity and chainChanged/accountsChanged
pushed page-ward. Zero widget internals touched.

Two packages per the examples/* workspace glob: the vite page rendering
the widget, and the Expo host with the bridge (injected provider, viem
wallet host, Alert-based approvals).

Note: WebView<object> in the app is a workaround for react-native-webview
declaring WebView<P = undefined> extends Component<WebViewProps & P>,
which TS6 collapses to never the moment ref forces inference of P.
Candidate upstream fix on their side is P = object (or splitting the
default), tracked separately.
- page passes providers: [EthereumProvider()] - without it the v4 widget
  mounts no wallet stack at all ('No widget providers specified') and the
  wallet menu is permanently empty. This was the missing piece between
  'bridge announces correctly' and 'widget lists the wallet'.
- react-native-get-random-values first at app entry: Hermes has no
  crypto.getRandomValues and viem dies without it
- WebView<object> generic pin (react-native-webview P=undefined collapses
  to never under TS6 when ref forces inference)
- vite on 5174 (5173 squatted by a local ssh forward), LAN ip for device
- TEMP: debug console-forwarding + probes in the injected script and
  App.tsx, stripped before the PR

Verified on iOS sim end to end: widget renders, wallet menu lists
'LI.FI RN Example (Installed)' via the injected 6963 announcement,
connect round-trips through the native approval alert, address chip
shows, live quote WETH(mainnet)->ETH(Boba) at 0.1 WETH with gas/ETA,
balance check correctly flags insufficient funds, disconnect works.
… sim

WKWebView content is invisible to the iOS a11y tree, so coordinate taps
were the only way to drive the widget from test tooling - flaky and
unscriptable. Dev builds now poll a host-side command server and run
injected JS in the WebView, so tests click widget elements by
selector/text and read DOM state deterministically.

threading fix: the command server must be ThreadingHTTPServer - a
single-threaded one deadlocks (await blocks the loop, app can't poll).

Verified: scripted connect flow (click 'Connect wallet' -> click wallet
row -> native alert), and the reject path - after Reject the widget
recovers from Connecting back to the wallet list, proving the 4001
error shape round-trips.
…o real funds

HARNESS mode: wallet becomes anvil account #0, all chain-1 traffic (the
app's viem clients AND the widget's SDK reads via sdkConfig.rpcUrls query
config) goes to a local mainnet fork. Same-chain swaps execute for real
against forked DEX state - signed by the bridge, broadcast, mined - so
every execution state is reachable and receipted without a cent moving.

Verified end to end on the sim: connect as 0xf39F..., widget reads the
fork's 10,000 ETH balance, live quote 0.5 ETH -> 936.94 USDC, Review ->
Sign -> broadcast (anvil log: eth_sendRawTransaction) -> mined -> 'Swap
successful'. Fork balance after: 936.544051 USDC - real slippage vs the
quote, proving real contract execution.

Two harness facts documented for the matrix run:
- old anvil needs a basefee nudge (anvil_setNextBlockBaseFeePerGas + evm_mine)
  or txs sit pending; run with --base-fee 1 to avoid
- the widget's terminal status comes from li.quest /v1/status, which cannot
  know fork txs; the harness mocks exactly that one endpoint in-page (fetch
  wrapper) and nothing else
The ios/ dir is prebuild output (Podfile.lock, Xcode project) - regenerated
by expo run:ios. Every other example in the repo tracks only source.
Strip the receipt-driving scaffolding and conform to house style so the
example reads like the other examples/ entries, not a WIP branch:

- remove the DOM automation rig (scripts/automation-server.py + devAutomation.ts)
  and the onLoadEnd/console-forwarding debug probes
- default the anvil swap harness OFF (ephemeral wallet out of the box);
  document it as opt-in in the README
- add a README matching the other examples' structure
- use the exported BRIDGE_SOURCE const instead of a duplicated string literal
- biome clean, both packages typecheck green
- untrack .expo/ cache that had leaked in

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Aug 16, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 8870da5

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

Comment thread examples/react-native-webview/App.tsx Outdated
Comment on lines +34 to +35
const ANVIL_ACCOUNT_0_KEY =
'0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' as const

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exposed secret in examples/react-native-webview/App.tsx - high severity
Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

Reply @AikidoSec ignore: [REASON] to ignore this issue.
More Info

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch in principle, but no real secret here: that was anvil's well-known account #0, a public test account that only ever holds funds on a local fork. A raw 32-byte key literal trips secret scanners regardless, though, so I've removed it in fcabf2f - the key is now derived from anvil's public dev mnemonic at runtime, so no key literal ships in the example. The derived account is still anvil #0 (0xf39Fd6…2266).

gomesalexandre and others added 2 commits August 16, 2026 18:00
…eral

Aikido flagged the hardcoded anvil account #0 private key as an exposed
secret. It's anvil's public test account (holds nothing outside a fork), but a
raw 32-byte key literal trips every secret scanner regardless. Derive it from
anvil's well-known dev mnemonic at runtime instead, so no key literal ships in
the example. Verified the derived account is still anvil #0 (0xf39Fd6…2266).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The WebView<object> workaround and its TypeScript-6 comment were unnecessary
on react-native-webview 14 - a plain <WebView> with useRef<ComponentRef<typeof
WebView>> typechecks cleanly (verified: postMessage on the ref resolves, tsc
--noEmit green). Drops a confusing artifact and a claim that doesn't reproduce
on the pinned version.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

the widget doesn't work in react-native

1 participant