Network Config + react-native-core breaking changes - #55
Conversation
- expo-router file-based routing under src/app - all 18 screens with working navigation - v2 light theme + dark-ready theme system - reusable component library - domain/data/session seams for WDK (mock repository) - auto-lock, edge-to-edge config
- RepositoriesProvider DI boundary (mock now, WDK-ready) - QueryClient + typed query hooks + centralized keys - loading/error/retry states on all data screens - screens no longer import concrete repositories
- add WdkAppProvider at root with minimal Bitcoin wdkConfigs - import generated worklet bundle - log useWdkApp lifecycle (INITIALIZING -> NO_WALLET confirmed) - no screen/behaviour change; still on mock repository
- Replace mock data with live WDK hooks (balances, account, send) - Wire wallet create/import/unlock via useWalletManager - Add session/lock handling: WdkSessionGate + AutoLockOnBackground - Fix locked-vs-no-wallet detection (lock() reports NO_WALLET, not LOCKED; disambiguate via persisted wallets list) Verified on device: create → real balance/address → lock → unlock.
…which is encrypted first with the help of wdk-utils and then we are storing it to secure storage
… icon on wallet and receive screen
…ve acccounts get listed in accounts screen
Ports the pipeline already proven in tetherto/city-wallets-wl-app-mobile,
collapsed from its two-app matrix to this repo's single app: local EAS
builds on GitHub Actions runners, then `eas submit` to TestFlight and the
Play `internal` track. workflow_dispatch only, with buildTarget
(stores|android|ios) and pushTag inputs.
Three composite actions:
setup-eas — Node 22 plus a pinned eas-cli. Forces npm >= 11 because
npm 10 silently drops packages from git-dependency trees
and this project has three git deps (docs/ENVIRONMENT.md).
select-xcode — pins Xcode 26.3 on the macOS runner.
write-dotenv — materialises the EXPO_PUBLIC_* config to .env before the
build, since babel-preset-expo inlines those at bundle
time and eas build --local bundles in a copy of the tree.
Notable differences from the city-wallets original, all deliberate:
- No link-bare-addons step. react-native-bare-kit self-links via its
podspec prepare_command and a gradle `link` task; the upstream script
only exists to work around that repo's yarn workspaces.
- npm rather than yarn/corepack.
- Artifact location comes from `eas build --output` instead of
`find $GITHUB_WORKSPACE -name '*.aab'`, which would also match the
gradle intermediate at android/app/build/outputs/bundle/release/.
- .env values are written single-quoted with $ escaped, and the step fails
on a value it cannot encode. Expo parses .env with node:util.parseEnv and
then interpolates, so an unquoted # or an unescaped $word silently
truncates the value — verified against Expo's own parser. Empty secrets
are warned about and omitted rather than inlined as "".
- The 20 EXPO_PUBLIC_* secrets are mapped at job level, not on the `uses:`
step: a composite action's inner steps do not reliably see step-level env.
- concurrency queues instead of cancelling; a cancelled run can leave a
half-finished store submission behind.
Also adds .github/actionlint.yaml declaring the self-hosted runner label so
`actionlint` runs clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs/RELEASE.md covers how the pipeline is wired, the full secret inventory (including which values are base64 and the one that is raw JSON), the credentials.json shape — which exists only as a secret and so is not otherwise recorded anywhere — and the one-time setup still outstanding before the workflow can go green. Called out explicitly because each has a misleading failure mode: - The distribution provisioning profile must carry the iCloud entitlement with container iCloud.io.tether.wdkshowcase, which deliberately differs from the bundle id. - The release keystore's SHA-1 must be registered against the new package name, or Google Sign-In fails with DEVELOPER_ERROR (10) in release builds only. - The Play Developer API cannot create an app's first release, so one AAB must be uploaded by hand. Doing that with a locally built artifact consumes versionCode 1 and then collides with the EAS remote counter — the documented sequence avoids that by using the artifact from a CI run whose submit step failed. - The two *_NETWORK_LABEL variables are intentionally blank on mainnet, so a warning about them is not necessarily a missing secret. Includes the credential-free bundle test for confirming EXPO_PUBLIC_* values actually reach the bundle, and a troubleshooting table for the failure modes seen while building this. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…workflow ci: add Build and Publish workflow (TestFlight + Play internal), and fix EVM env inlining
… and edit network in app
- Switch both build jobs from the to the GitHub Environment so Apple distribution creds can be gated behind protection rules; clarify in the header that distribution is still TestFlight / Play internal, not a production listing - Filter eas-cli-local-build-plugin noise out of the Android and iOS build logs (pipefail keeps build failures propagating)
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|
| // .catch() here, not left to surface as an unhandled rejection — | ||
| // this fires from an AppState listener, nothing downstream is | ||
| // awaiting this call or positioned to catch a rejection itself. | ||
| lock().then(clearPasswordSession).catch((e) => { |
There was a problem hiding this comment.
Would suggest .finally(clearPasswordSession) here instead of catching on .then. If lock() fails the password now stays in memory while the app is in the background.
| { key: 'arbitrum', label: networkDisplayName('arbitrum') }, | ||
| { key: 'polygon', label: networkDisplayName('polygon') }, | ||
| { key: 'bitcoin', label: networkDisplayName('bitcoin') }, | ||
| ...ALL_NETWORKS.map((network) => ({ key: network as ChainFilter, label: networkDisplayName(network) })), |
There was a problem hiding this comment.
Could we type this filter from ALL_NETWORKS instead of casting into a hardcoded ChainFilter ?
jonathunne
left a comment
There was a problem hiding this comment.
A few things to fix before merge — see inline comments.
| // literally 'sepolia'; Tron isn't compiled into the worklet at all) — | ||
| // they weren't reachable, so they weren't carried forward. | ||
|
|
||
| function networkToChain(network: string): ChainId { |
There was a problem hiding this comment.
Return the network as-is (or type ChainId from the registry) instead of defaulting unknown ones to 'bitcoin'.
- AutoLockOnBackground: .then() -> .finally() so clearPasswordSession() still runs if lock() rejects (password was staying in memory on failure) - activity.tsx: derive ChainFilter from ALL_NETWORKS instead of casting - useWalletData: networkToChain() returns network as-is instead of silently defaulting unrecognized ones to 'bitcoin'
Two independent pieces of work, merged together into
developand raisedhere as one PR since they landed in the same window — not one change,
structured below so each can be reviewed on its own terms.
[wdk-react-native-core PR #77](Harden wallet lifecycle: remove implicit auto-init, enforce explicit identity boundary wdk-react-native-core#77).
labels, colors, icons, explorer links, indexer mapping) into one file,
in response to direct product feedback that adding or removing a
network required editing too many places.
Part 1 — Adapting to wdk-react-native-core PR #77
Reviewed the full PR (every breaking-changes note, both direction and
rationale) before making any change here, rather than adapting reactively
to whichever error surfaced first.
What actually breaks, and the fix for each
useWalletActions.tswm.setActiveWalletId(DEFAULT_WALLET_ID)removed — that function nolonger exists in the new API. Turns out it was already redundant before
this:
unlock(), called on the very next line, already sets the activeidentity correctly as part of its own real work, per PR #77's own
guidance that
unlock/createWallet/restoreWallet/switchWallet"manage identity correctly ... and should be used instead."
lock: (): void => wm.lock()→lock: (): Promise<void> => wm.lock().wm.lock()now returnsPromise<void>(previouslyvoid) and sharesan operation mutex with
unlock/createWallet/restoreWallet/switchWallet. The old signature silently dropped that promise —every caller needs to actually await this now.
AutoLockOnBackground.tsx— the one fix here I'd flag as genuinelyrisky if missed, not just a type mismatch. The background-detection
handler called
lock()and thenclearPasswordSession()immediatelyafter, without awaiting
lock()at all. With the new shared mutex, afast background→foreground→unlock cycle could hit
unlock()whilelock()was still mid-flight and holding it — a real, if narrow, racecondition, not a hypothetical one. Now sequenced as
lock().then(clearPasswordSession).catch(...).useWdkSession.tsretryremoved from this hook's return value, sinceuseWdkApp()nolonger exposes it at all.
hasStoredWalletdisambiguation logic (working around WDKpreviously reporting
NO_WALLETinstead ofLOCKEDright afterlock(), even when a wallet genuinely existed on disk) is kept,not removed — PR #77's own notes claim this exact bug is now fixed
upstream, but that hasn't been independently verified end-to-end on a
real device over multiple sessions yet. Removing a working safety net
on a changelog description alone seemed like a worse trade than a
little temporary redundancy. Left a comment noting exactly what to
simplify once this is confirmed.
index.tsxretryremoved from the destructureduseWdkSession()result, andthe error screen's
onRetryprop dropped entirely (it's optional onErrorState, so this cleanly removes the button rather than leave itwired to nothing). Deliberately not replaced with a fabricated retry
action — this screen renders when WDK's own app-level init fails,
before any specific lifecycle method like
unlock/createWalletwasever called, so there genuinely isn't one to re-call, unlike what PR
#77's own guidance assumes for other error cases.
Verified safe, no change needed
enableAutoInitialization/currentUserId/clearSensitiveDataOnBackgroundonWdkAppProvider— never usedanywhere in this app.
restoreWallet-retry-on-"already exists" path inimportWallet(the iOS-Keychain-survives-uninstall self-heal) — confirmed safe by
checking
WdkSessionGate.tsx: onboarding is only ever reachable withno wallet active, so PR #77's new "throws if a wallet is already
active" guard can't fire there.
activeWalletIdno longer persisting across app restarts — nothing inthis app relied on that; identity is always re-established explicitly
via
unlock(DEFAULT_WALLET_ID).cloud-provider.tsx's directuseWalletManager()usage(
getMnemonic/getSeedAndEntropyFromMnemonic/activeWalletId) —doesn't touch any changed lifecycle API.
Testing scaffolding included here — not a permanent fix, flagging clearly
Two changes exist purely to verify the above against a real, running
build of PR #77 before it merges upstream, and should be revisited once
it does:
package.json—@tetherto/wdk-react-native-corepoints atgithub:nulllpc/wdk-react-native-core-fork#npc/remove-auto-init-flag(the PR's own branch), not a published version. Should become a real
version bump once PR #77 merges and publishes.
tsconfig.json— apathsoverride for@tetherto/wdk-react-native-core, working around a confirmed bug inthat fork's own build config (
outDirset with norootDir, landingcompiled types at
dist/src/index.d.tsinstead of thedist/index.d.tsits own
package.jsonpoints to). Doesn't affect the app at runtime —Metro's resolution is more lenient than
tsc's — onlynpm run typecheckneeds it. Worth reporting upstream; remove oncefixed there.
Part 2 — One file to add, remove, or change a network
Direct product feedback: "Adding a network today looks like it involves
editing: worklet config, runtime config, assets, indexer mapping, address
maps, icons, etc. A small registry in the starter ... would make 'clone
and add or remove support for different chains' a bit easier."
The design
src/wdk/networks.tsis now the one file to edit. Per network, one entryholds: its runtime provider/bundler/paymaster config, its asset(s),
display label (env-aware where relevant, e.g. Bitcoin/Ethereum's
testnet-label handling), color, icon, block-explorer link, and the
indexer's own blockchain-name mapping. Everything else —
wdkConfigs,ASSETS/ASSET_MAP,networkDisplayName(),ALL_NETWORKS, per-screenfilter lists — is derived from this one array, not maintained separately.
Two things deliberately not folded in, both for real, verified
technical reasons, not just scope-narrowing:
wdk.config.js(the worklet-bundler's own config — which networkscompile into the native bundle) stays fully separate. It's read by an
external tool via Node's native
import(), at a point before any ofour own build tooling runs, and per explicit product-team direction:
"the worklet-side config would remain the same."
require()call arestill written out literally, one per network, inside its own entry —
never generated in a loop. This isn't residual complexity; it's the
same real constraint already hit once in this project's history:
babel-preset-expoonly inlinesprocess.env.EXPO_PUBLIC_*when thekey is a literal at compile time, and Metro only resolves
require()when the path is a literal at its call site. A previous version of
this exact config did build these dynamically in a loop, and it
shipped every EVM network with
provider: ''in a release build,completely silently, since dev mode papers over it. The header comment
on
networks.tsdocuments both rules with a ✅/❌ example, so thisisn't rediscovered the hard way a second time.
Migration
config.ts,assets.ts, andchains.tsare now thin (~9-line)re-export shims pointing at
networks.ts, explicitly labeled"Shim only — nothing to edit here." This was deliberate: it means the
ten existing files importing from them across the app needed zero
changes, keeping this diff scoped to the registry itself rather than
touching every consumer.
Verified, not assumed
(
networkColor,CHAIN_ICONS, the duplicated explorer-URL map in bothsend/success.tsxandtx/[id].tsx) before consolidating, rather thanguessing at call sites.
networkColormap carried two entries (sepolia,tron) thatdidn't correspond to any real internal network key this app actually
uses — confirmed unreachable, not carried forward.
AppAsset.fromConfigs()takes a plain array, so flattening assets outof each network's own entry required no change to that file at all.
Related, deferred follow-up (not in this PR)
Genuinely reduced this from four edit locations to two
(
wdk.config.js+networks.ts) rather than one — therootDir/literalconstraints above are real, not a missed opportunity. If
wdk-worklet-bundlerever supports loading a shared, typed config moduledirectly,
wdk.config.jscould potentially collapse into this too; notattempted here since it depends on that external tool's own capabilities,
which haven't been verified.
Verification
npm run typecheck— clean.background/foreground cycling (targeting the
AutoLockOnBackground.tsxrace specifically), lock → background → foreground → unlock, cloud
backup, and a full pass through Home/Send/Receive/Activity confirming
every network's label/color/icon/explorer link still renders correctly
post-refactor.
resolved separately during this work (not part of this diff, noted
here since they blocked testing): an
expo-fontdual-version conflictcausing an Android-only native crash, and an org-wide
legacy-peer-deps=truenpm setting preventingreact-native-bare-kit(a peer dependency) from installing at all.
Related