feat(rn_cli_wallet): migrate to Expo (SDK 56) + prebuild/CNG + web for Maestro#555
Draft
ignaciosantise wants to merge 20 commits into
Draft
feat(rn_cli_wallet): migrate to Expo (SDK 56) + prebuild/CNG + web for Maestro#555ignaciosantise wants to merge 20 commits into
ignaciosantise wants to merge 20 commits into
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Claude finished @ignaciosantise's task in 4m 35s —— View job PR Review in Progress
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

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, enablingexpo 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
registerRootComponententry.react-native-configwithEXPO_PUBLIC_*env vars.expo-system-ui.APP_VARIANTdynamic config for build variants; Android release-signing config plugin.Web support (react-native-web)
.weboverrides for native-only modules (mmkv→localStorage, camera disabled, haptics/NFC no-ops, JS bottom-tabs).callbackUrl, result relayed viaBroadcastChannel(iframe blocked by X-Frame-Options; COOP severswindow.opener).web+web:exportscripts.Maestro on web
run-maestro-pay-tests.shsupports a URL App ID (rewrites flowsappId:→url:, plainmaestro testover managed Chromium), and stripsstart/stopRecording(unsupported on web).data-testid → idbridge (Maestro resolvesid:with precedenceid > aria-label > … > data-testid; RNW'saria-labelwas shadowing testIDs).<View>(Maestro skipssvg/imgtags).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.cancelled,expired,single_deeplink.copyTextFromlimitation (reads only an element's direct text node; RNW nests text):double_scan,single_nokyc,multiple_nokyc,usdt_polygon.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)
EXPO_PUBLIC_*names; rotate any committed auth tokens.copyTextFromcases, and a web KYC strategy.🤖 Generated with Claude Code