Skip to content

feat(rn_cli_wallet): migrate to Expo (SDK 56) + prebuild/CNG + web for Maestro#555

Draft
ignaciosantise wants to merge 20 commits into
mainfrom
chore/expo-module-rn-cli-wallet
Draft

feat(rn_cli_wallet): migrate to Expo (SDK 56) + prebuild/CNG + web for Maestro#555
ignaciosantise wants to merge 20 commits into
mainfrom
chore/expo-module-rn-cli-wallet

Conversation

@ignaciosantise

Copy link
Copy Markdown
Collaborator

Summary

Incrementally migrates the production reference wallet wallets/rn_cli_wallet (multi-chain WalletKit + WC Pay, RN 0.85.3) from bare React Native CLI to Expo SDK 56, enabling expo prebuild (Continuous Native Generation) and a web build used to run Maestro Pay E2E tests in a browser.

Done in small, verifiable steps (see commit history). Native iOS/Android continue to build and run unchanged in behavior.

What's included

Expo migration

  • Add Expo modules (SDK 56), keeping bare native dirs initially; registerRootComponent entry.
  • Replace react-native-config with EXPO_PUBLIC_* env vars.
  • Declare Expo config plugins + native config; per-variant app icons; expo-system-ui.
  • APP_VARIANT dynamic config for build variants; Android release-signing config plugin.
  • Switch to Expo prebuild / CNG.

Web support (react-native-web)

  • Web bundle compiles; .web overrides for native-only modules (mmkv→localStorage, camera disabled, haptics/NFC no-ops, JS bottom-tabs).
  • Web IC/KYC form flow: opens in a new tab with callbackUrl, result relayed via BroadcastChannel (iframe blocked by X-Frame-Options; COOP severs window.opener).
  • web + web:export scripts.

Maestro on web

  • run-maestro-pay-tests.sh supports a URL App ID (rewrites flows appId:url:, plain maestro test over managed Chromium), and strips start/stopRecording (unsupported on web).
  • Make pay testIDs findable by Maestro web:
    • data-testid → id bridge (Maestro resolves id: with precedence id > aria-label > … > data-testid; RNW's aria-label was shadowing testIDs).
    • Wrap result icons' testID on a <View> (Maestro skips svg/img tags).

Test status (web, local)

Ran the full Pay suite against the web build: 2/12 pass today; the other 10 fail for reasons unrelated to the testID work this branch fixes:

  • pay_insufficient_funds, pay_cancel_from_review — validate the bridge + svg-wrap + recording-strip fixes end-to-end.
  • 🌐 Pay backend/network (BASE) down → generic HTTP error instead of expected result: cancelled, expired, single_deeplink.
  • 📋 Maestro-web copyTextFrom limitation (reads only an element's direct text node; RNW nests text): double_scan, single_nokyc, multiple_nokyc, usdt_polygon.
  • 🪪 Web KYC divergence (IC form opens in a new tab; form-internal assertions can't match): kyc_back_nav, cancel_from_kyc, multiple_kyc.

Native iOS/Android: built and launched via Expo prebuild; WalletKit init + core flows verified.

Follow-ups (out of scope here)

  • Update GitHub Actions secrets to EXPO_PUBLIC_* names; rotate any committed auth tokens.
  • For a fully green web suite: pay backend available, web-specific flow variants for the copyTextFrom cases, and a web KYC strategy.

🤖 Generated with Claude Code

ignaciosantise and others added 20 commits June 25, 2026 16:42
First incremental step toward migrating rn_cli_wallet from bare React
Native CLI to Expo (eventual goals: `expo prebuild` + web). Installs the
`expo` package and Expo modules autolinking into the existing committed
ios/ and android/ projects (no prebuild yet) and drives the app via the
Expo CLI. Targets Expo SDK 56 to match dapps/pos-app (same RN 0.85.3).

- package.json: add expo@56 + babel-preset-expo; main=index.js; scripts
  use expo run:ios / run:android / expo start (all local, no EAS)
- babel.config.js: babel-preset-expo (module-resolver aliases kept)
- metro.config.js: getSentryExpoConfig; shim Node `ws` -> empty; inject
  process.version polyfill via serializer.getPolyfills
- index.js: registerRootComponent (native module name -> "main")
- ios: AppDelegate -> ExpoAppDelegate/ExpoReactNativeFactory (deep-linking
  preserved), Podfile use_expo_modules!, deployment target 16.4
- android: expo autolinking (gradle, MainApplication/MainActivity),
  module name "main", versionCode 72
- polyfills.js: define process.version/browser before any module evals
  (hash-base -> readable-stream reads process.version.slice at load time)

Verified: builds and runs on iOS simulator and Android emulator; tsc and
lint clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… env vars

Removes the react-native-config native dependency in favor of Expo's
EXPO_PUBLIC_* convention (process.env, inlined into the JS bundle at build
time). This drops a native module (helps prebuild + web) and works on web.

- add src/utils/env.ts: typed ENV object reading process.env.EXPO_PUBLIC_*
- swap all 9 Config.ENV_* usages to ENV.* (App, WalletKitUtil, BalanceService,
  ERC20BalanceService, EIP155WalletUtil, PaymentTransactionUtil, TonLib,
  ScannerOptionsModal, PaymentOptionsModal/utils)
- .env.example: rename ENV_* -> EXPO_PUBLIC_*
- remove react-native-config from package.json; drop android dotenv.gradle
  apply + defaultEnvFile + proguard rule; pod install (RNCConfig removed)
- CI: walletkit-build-and-maestro action writes EXPO_PUBLIC_* into .env
- jest: switch to jest-expo preset (process.env -> expo/virtual/env needs it)
  + mock react-native-keyboard-controller; PaymentStore + others now pass
  (35 tests pass, up from 11)
- android versionCode 73

Verified: app builds/runs on iOS sim with balances loading (EXPO_PUBLIC_
values inlined); tsc + lint clean.

NOTE: the GitHub secrets that hold full .env blobs (env-file /
WALLETKIT_ENV_FILE) must be updated to use EXPO_PUBLIC_* names, and local
.env files need EXPO_PUBLIC_* entries.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ebuild prep)

Stages the app.json so `expo prebuild` can later regenerate the native
projects faithfully. No prebuild is run yet — the committed ios/ and android/
remain authoritative, and these plugins are inert until prebuild, so the
current build is unchanged.

Adds the 6 config plugins that ship with our native libs:
- react-native-bootsplash (assetsDir assets/bootsplash, EdgeToEdge)
- react-native-vision-camera (camera permission + code scanner)
- react-native-nfc-manager (NFC permission + NDEF entitlement)
- @zoontek/react-native-navigation-bar (no nav-bar contrast)
- react-native-bottom-tabs
- @sentry/react-native/expo (org walletconnect / project w3w-react-native)

Plus app-level native config mirroring the current projects:
- ios.associatedDomains (appkit-lab/lab.reown.com, *.pay.walletconnect.com)
- ios.infoPlist CFBundleDisplayName "React N. Wallet" + location string
- android.permissions POST_NOTIFICATIONS
- android.intentFilters (wc scheme; reown.com/rn_walletkit + pay.walletconnect.com app links)

Validated with `expo config --type introspect`: all plugins resolve and
produce the expected Info.plist / entitlements / AndroidManifest. (keyboard-
controller needs no plugin; it autolinks.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolves the `userInterfaceStyle: automatic` config warning (Android needs
expo-system-ui to honor it) and makes the app.json userInterfaceStyle setting
effective. Autolinks via expo-modules (ExpoSystemUI pod added); no committed
native source changes needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ts (prebuild prep)

Adds app.config.js (wrapping the static app.json) that selects the native
applicationId / bundleIdentifier by the APP_VARIANT env var:

  production (default) -> com.walletconnect.web3wallet.rnsample
  internal             -> ...rnsample.internal   (CI distribution)
  debug                -> ...rnsample.debug       (local dev)

This is the Expo-idiomatic replacement for the current 3 Xcode targets /
Android buildTypes, chosen to avoid a fragile iOS multi-target config plugin.
It is inert until we switch to `expo prebuild` — the committed ios/ and
android/ projects still drive today's builds via schemes / buildTypes, so CI
and the yarn scripts are unchanged for now.

Validated with `expo config`: APP_VARIANT={production,internal,debug} produces
the correct id suffixes and all 6 config plugins still resolve.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ing assets)

Extracts the existing app icons from the committed native projects into
assets/icons/<variant>/ and wires them into app.config.js per APP_VARIANT, so
`expo prebuild` reproduces the current per-variant icons:

- icon.png            <- iOS AppIcon[-Internal|-Debug] ~ios-marketing (1024)
- adaptive-foreground / -background / -monochrome.png
                      <- android src/{main,internal,debug}/res mipmap-xxxhdpi

Splash is already covered by the react-native-bootsplash plugin
(assets/bootsplash/), so no splash changes needed.

Inert until prebuild (committed native dirs still drive today's builds).
Validated with `expo config --type introspect`: all variants resolve with the
right icon paths and no missing-asset errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… prep)

Adds plugins/withAndroidReleaseSigning.js — an Expo config plugin that injects
a `release` signingConfig into the generated android/app/build.gradle, loading
credentials from android/secrets.properties and selecting the key by
APP_VARIANT:

  production -> WC_*_UPLOAD     internal -> WC_*_INTERNAL     debug -> WC_*_DEBUG

So a per-variant `expo prebuild` produces a release build signed with the right
key (replacing the current 3 hand-written Gradle signingConfigs/buildTypes).

Supporting pieces:
- scripts/setup-secrets.js + `postprebuild` hook: seeds android/secrets.properties
  from a repo-root secrets.properties.mock after prebuild wipes android/ (CI
  supplies the real file). Non-destructive (skips if the file exists).
- secrets.properties.mock at the wallet root (mock debug creds), since prebuild
  removes the in-android mock.
- wired the plugin into app.json plugins.

Inert until prebuild (committed android/ still drives builds). Verified: the
transform produces the correct per-variant signing keys + switches the release
build type to signingConfigs.release; `expo config` resolves all 7 plugins with
no introspect errors; lint clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…arity

- expo name -> 'React N. Wallet' (drives iOS display name + Android label;
  drops the redundant CFBundleDisplayName infoPlist override)
- ios.buildNumber 16, android.versionCode 73 (match current native)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ation)

ios/ and android/ are no longer committed — they are generated by `expo prebuild`
from app.json / app.config.js / plugins / assets. This completes the Expo
migration: native projects are now config-driven.

Variants (build internal + production on CI, debug locally):
- iOS: APP_VARIANT (app.config.js) sets bundle id + icon at prebuild time
  (production = base, internal = .internal, debug = .debug); single ReactNWallet scheme.
- Android: a Gradle `internal` buildType (plugins/withAndroidVariants.js, renamed
  from withAndroidReleaseSigning) applies the .internal suffix + internal/upload
  signingConfigs, so assembleInternal/assembleRelease work unchanged.

Changes:
- .gitignore: ignore /ios and /android; `git rm --cached` the committed projects
- plugins/withAndroidVariants.js: internal buildType + internal/upload signing
- app.config.js: iOS bundle id/icon via APP_VARIANT; Android package stays base
  (suffix handled by the Gradle buildType)
- app.json: name "React N. Wallet", ios.buildNumber, android.versionCode
- bootsplash assets regenerated in the Expo plugin layout (assets/bootsplash/{android,ios})
- scripts: add `prebuild`; ios/ios:internal/ios:prod use APP_VARIANT; android debug
  simplified; copy-sample-files only seeds .env (keystore/secrets come from prebuild
  + setup-secrets)
- CI: release-walletkit sets is-expo-project (both legs) + ios scheme ReactNWallet +
  app-variant; release-ios-base gains a backward-compatible app-variant input used in
  prebuild (android base untouched); walletkit-build-and-maestro action runs
  `expo prebuild` per leg (--no-install) and uses the ReactNWallet scheme
- AGENTS.md: document the CNG/prebuild workflow

Verified locally: `expo prebuild` regenerates faithful native config; Gradle
`assembleInternal --dry-run` succeeds (internal buildType + signingConfigs valid);
tsc/lint clean; both apps previously ran from prebuilt output. CI workflow changes
still need a real CI run to confirm.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First step toward running the wallet on web (for Maestro tests):
- add react-dom, react-native-web, @expo/metro-runtime
- add @lottiefiles/dotlottie-react (lottie-react-native's web peer)
- HomeTabNavigator.web.tsx: use JS @react-navigation/bottom-tabs (the native
  react-native-bottom-tabs imports RN internals unavailable on web)
- app.json: web config (metro bundler, single-page output)

`npx expo export --platform web` now succeeds (3718 modules). Runtime web
shims (mmkv->localStorage, web crypto entry, disable camera/NFC, webview) are
the next step — the bundle compiles but won't run in a browser yet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The web bundle now loads and renders the wallet in a browser (verified with
Playwright: app mounts, navigates to the Wallets tab, balances load). Intended
for Maestro web testing.

Web-only overrides (native modules have no web support):
- index.web.js + web-polyfills.js: web entry that skips react-native-quick-crypto
  (native JSI) and @walletconnect/react-native-compat (native); sets process.version
  + global Buffer before the web3 stack loads. main -> "index" so Metro picks
  index.web.js on web / index.js on native.
- metro.config.js web aliases: react-native-mmkv -> localStorage shim;
  react-native-quick-crypto -> browser Web Crypto shim (no crypto polyfill needed —
  the browser provides crypto); @craftzdog/react-native-buffer -> plain buffer.
- src/shims/{mmkv,quick-crypto}.web.ts
- HomeTabNavigator.web.tsx (JS @react-navigation/bottom-tabs vs native bottom-tabs)
- Scan/index.web.tsx (paste-URI flow; no camera)
- useNfc.web.ts + utils/haptics.web.ts (no-ops)
- SettingsStore: Appearance.setColorScheme?.() (not implemented on RN web)
- app.json web config (metro, single-page); deps: react-native-web, react-dom,
  @expo/metro-runtime, @lottiefiles/dotlottie-react

Remaining/known: balance fetch fails for some chains in-browser (CORS, same
non-fatal chain log as native); Settings/Connections/Pay-KYC (webview +
quick-base64) screens not yet web-verified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eb.tsx)

react-native-webview is native-only, so on web the identity-collection (KYC)
form renders in an <iframe> (react-native-web renders to the DOM). Same props
and prefill/theme URL building as native; uses the browser's btoa instead of
react-native-quick-base64. Completion is received via a window 'message'
listener (origin-checked) instead of WebView.onMessage — same
{ type: 'IC_COMPLETE'|'IC_ERROR', success, error } payload.

Caveats (server/page side, not verifiable locally):
- the IC page must allow framing (X-Frame-Options / CSP frame-ancestors) for the
  wallet origin (e.g. localhost during dev), else the iframe won't load.
- the page must window.parent.postMessage the result on web (native targets
  window.ReactNativeWebView.postMessage).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
react-native-web renders the custom Button (accessibilityRole="button") as a
<button>, so the info-icon Button nested inside the row Button produced
<button> inside <button> — invalid HTML / hydration error on web. Use a
Pressable (renders as a plain div, valid inside the row button) for the
icon-right action and stopPropagation so tapping the icon doesn't also select
the option. Behavior unchanged on native (also fixes the odd button-in-button).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cked)

pay.walletconnect.com sends X-Frame-Options: DENY, so the IC form can't be
iframed on web. Rework CollectDataWebView.web.tsx to open the /collect form in a
new tab/window (top-level context, allowed) with a callbackUrl param pointing at
our origin + ?pay_ic_callback=1.

On completion the form redirects the popup to
{callbackUrl}?status=success&paymentId=... (or status=error&code&message).
index.web.js detects that marker in the popup, relays the result to the wallet
tab via window.opener.postMessage, and closes the popup; the wallet tab listens
and calls onComplete/onError.

NOTE: the form requires callbackUrl to be HTTPS (or a custom deeplink) — plain
http://localhost is rejected, so the return leg only works when the web app is
served over HTTPS (production / tunnel / --https). Popups require a user gesture,
hence the "Continue" button.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… opener)

The IC tab visits pay.walletconnect.com, whose Cross-Origin-Opener-Policy
severs window.opener — so on redirect back, window.opener was null and the
callback booted a fresh wallet in the tab instead of relaying to the original
tab. Switch to same-origin cross-tab messaging that COOP doesn't affect:

- index.web.js: the callback tab posts the result on a BroadcastChannel
  ('pay-ic-callback') + a localStorage fallback, then closes (no opener needed;
  shows a "you can close this tab" hint if close is blocked).
- CollectDataWebView.web.tsx: opens the form in a tab (window.open '_blank',
  no popup features) and listens on BroadcastChannel + storage + message,
  resolving onComplete/onError.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When MAESTRO_APP_ID/APP_ID is an http(s) URL, target the web build via
'maestro -p web test --headless' (point it at the web app served over HTTPS,
e.g. yarn web + an https proxy). Native bundle-id runs are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per Maestro docs, web is triggered by a `url:` flow header (not `appId:`) and
runs via plain `maestro test` (auto-detects web, managed Chromium) — NOT
`-p web`/`--headless`/start-device (those reported "0 devices" / a web
device-locale crash on maestro 2.0.10).

So in web mode (URL App ID) the run script now copies the flows to a temp dir,
rewrites the `appId: ${APP_ID}` header to `url: ${APP_ID}`, and runs plain
`maestro test`. Verified: a url-based smoke flow launches Chromium against
http://localhost:8081 and resolves our testIDs (button-scan).

Caveats: serve the web app over HTTP (Maestro's managed Chromium rejects local
mkcert HTTPS, so the https-only IC/KYC callback can't complete under local web);
shared pay flows still use native-oriented commands (clearState, launchApp/
stopApp appId, permissions, startRecording) that may need web adaptation —
verify once run with real WPAY secrets in .env.maestro.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Maestro's web driver resolves `id:` to a "resource-id" with precedence
id > aria-label > name > title > htmlFor > data-testid. react-native-web
maps accessibilityLabel -> aria-label, which shadowed our testIDs (e.g.
pay options had aria-label="base"), so `id: pay-option-0` never matched.
It also skips svg/img tags entirely (result icons) and can't record.

- index.web.js: MutationObserver bridge mirroring data-testid -> DOM id
  (id is checked first), so id: selectors resolve to testIDs regardless
  of any aria-label.
- ResultView.tsx: wrap result icons' testID on a <View> (renders a div)
  instead of the svg/Lottie, which Maestro skips. Native unaffected.
- run-maestro-pay-tests.sh: strip start/stopRecording from the web flow
  copy (Maestro web doesn't support recording; it fails flows ~2s in).

Validated on web: pay_insufficient_funds and pay_cancel_from_review pass.

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

vercel Bot commented Jun 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
deposit-app-demo Error Error Jun 26, 2026 1:42am
pos-demo Ready Ready Preview, Comment Jun 26, 2026 1:42am

Request Review

@claude

claude Bot commented Jun 26, 2026

Copy link
Copy Markdown

Claude finished @ignaciosantise's task in 4m 35s —— View job


PR Review in Progress

  • Gather context and read changed files
  • Check PR size
  • Review code quality, security, and correctness
  • Run license compliance subagent — ✅ No issues found
  • Run breaking changes subagent
  • Run data classification subagent
  • Consolidate findings and post final review

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