feat(wallet): primitives + wallet tab rebuild (A+B)#4
Conversation
Needed for local Android build. Upstream lxmf 0.1.6 has versionCode/ versionName as literals in build.gradle.kts that break on some gradle setups. Postinstall script rewrites them to reflection calls.
Crash in MeshMap.tsx:387 on nodes tab — GestureDetector requires GestureHandlerRootView ancestor per react-native-gesture-handler install docs. Standard one-line wrap. Also prerequisite for incoming wallet-ui-port work (SlideToConfirm + PressSurface use GestureDetector).
npm install after 038364a added hasInstallScript: true flag to the lockfile. Committing so downstream installs reflect the postinstall hook without a dirty tree on every clone.
Additive layer on top of teammate's existing theme/useGlass system. No color or typography tokens added — his theme/colors.ts, theme/typography.ts, and hooks/useGlass own those. All primitives consume useTheme() + stateTokens (press washes, disabled opacity). Primitives ported from archive/hunter-v3-full, rewired to use mobile_app/theme/* (spacing[1..8] numeric scale, radii, fontSize, fontFamily, and anonme.sh palette via colors.primary/accent/success). - DepthButton: morphic gradient button, 5 variants × 5 tones, press compression + highlight/bottom-inset motion. Legacy 'purple' tone routes through colors.accent (neon-green) until stealth palette lands. - PressSurface: generic press wrapper with row/strip/card variants, press-in wash + spring release with caller-matched border radius. - IconButton: 36/44/52 hit targets, tint tones, plain + contained. - SlideToConfirm: pan-to-confirm slider with magnet zone, resistance, threshold haptic, and color interpolation cyan → neon on confirm. Phosphor ArrowRight swapped for Feather arrow-right. - NumericKeypad: amount input with token chip, fiat label, use-max chip. Phosphor Backspace/CaretDown swapped for Feather delete and chevron-down. SolanaIcon/USDCIcon replaced with TokenLogo primitive. - SegmentedControl: tone-able toggle (cyan/purple/stealth-via-neon). - Pill, Icon, TokenLogo: leaf primitives (Feather wrapper, inline SVG). Design-system layer: - haptics.ts: 10 tiers (tap/select/lightPress/mediumPress/confirm/ warning/error/dragStart/dragCross/releaseHeavy) backed by expo-haptics. - motion.ts + tokens/motion.ts: easing scales, duration tokens, press compression, spring presets (direct/settle/sheet/pressRelease/ iconRelease), travel amounts. - tokens/state.ts: depth (press insets, highlights) + feedback (press washes, disabled opacity, icon tint). Hooks + utils: - useHideBalance: React context + provider. NOTE: in-memory only until @react-native-async-storage/async-storage is added to deps. - blePermissions: Android 12+ runtime permission helpers (SCAN, CONNECT, ADVERTISE, FINE_LOCATION). Deliberately skipped: - GlassSurface primitive (his useGlass wins) - appTheme, tokens/foundation, tokens/semantic, tokens/component (rewired inline or replaced by useTheme()) - SwipeDismissView (never worked on Android) - devReset (depends on LocalWallet infra not yet reconciled with his wallet context) - sound.ts (sound system deferred; haptics carry interaction feedback) tsc --noEmit clean. Zero appTheme imports remaining. Pre-existing upstream/v3 errors in MeshMap/nodes/ThemeContext are teammate's lane.
Replaces the 200-line inline tabs-with-panels wallet with the Home
composition ported from archive/hunter-v3-full, rewired to teammate's
theme + data contexts.
Layout (top → bottom):
- HomeHero — his ANONMESH kicker + 'wallet' title + eye toggle + QR icon
- BalanceCard — $USD hero + SOL balance below, long-press to hide
- ActionRow — Send / Receive / Swap-Soon / Yield-Soon (4 tiles)
- NearbyPeersCard — peer count + avatar stack, taps to Nodes tab
- Recent section header
- RecentActivity — honest empty state ('no activity yet')
Data wiring:
- displayName + peers from LxmfContext (his)
- publicKey from WalletContext (his) — consumed by QRModal
- ASSETS + TOTAL_USD from his components/wallet/constants fixture
(matches current visible behavior; real balance wires in Phase 7)
- Hide state from useHideBalance (ours, now AsyncStorage-persistent)
Integrations:
- QR icon in HomeHero opens his existing QRModal via local state —
no new route needed, reuses his wallet/anonmesh QR view.
- NearbyPeersCard tap navigates to /(tabs)/nodes — his MeshMap owns
peer visualization; we don't duplicate.
- Send + Receive actions push to /send/recipient and /receive
(routes land in Commits C + D). Until then Expo Router shows
'Unmatched route' — acceptable stub.
Persistence:
- Added @react-native-async-storage/async-storage@2.2.0.
- useHideBalance hydrates from + persists to storage under
anonmesh:hide-balance key.
Root layout:
- HideBalanceProvider wraps inside WalletProvider so the eye toggle
state is available across the wallet tab without prop drilling.
No touches to onboarding, messages, settings, nodes, or teammate's
contexts. His SendPanel / ReceivePanel / SwapPanel / YieldPanel and
WalletTabs files remain on disk but are no longer imported; safe to
delete in a follow-up cleanup pass.
tsc --noEmit clean. Pre-existing upstream errors in MeshMap / nodes /
ThemeContext remain (teammate's lane, unrelated).
Addresses device review feedback: - BalanceCard: hide dots now 6 × 22px (HIDDEN_MASK) instead of 3 × 14px so the mask reads at the same weight as the amount itself. Glued the '$' sign into the amount line so it inherits the 46px hero size. Wrapped the hero in a subtle accent-glass surface with a cyan shadow glow (22% opacity) for visual weight and brand anchor. - HomeHero: no copy change; entrance animation added below. - NearbyPeersCard: peer-count flicker fixed via useMemo on filtered lists. State labels adapt: 'Mesh offline' / 'Scanning for peers…' / 'N peers nearby' with a subtitle showing all-time seen count. - RecentActivity: replaced honest empty state with 6-row mock fixture (MOCK_ACTIVITY under src/__fixtures__/) showing Settled / Handed to mesh / Queued on device statuses across SOL + USDC transfers. Rows use PressSurface with direction badges and tonal pills. Amount is masked to ••• when hide-balance is on. wallet.tsx: staggered FadeInDown entrance (420ms duration, 90ms step) across all 6 sections so the screen composes intentionally on mount. Separation rule: src/__fixtures__/mockActivity.ts imported only by RecentActivity in dev/demo. Prod tx history replaces it in Phase 7. tsc --noEmit clean.
Fixes the 'Unmatched route' state when tapping the Send action tile on the wallet tab. Four new routes under `app/send/`: - /send/recipient — paste / scan (stub) / dev-mock address fill - /send/amount — NumericKeypad with fiat equivalent, Use-max chip, balance-exceeded validation; reads SOL balance from his ASSETS fixture until the wallet layer emits real balance - /send/review — amount hero + To/Route/Fee detail rows + stealth toggle + SlideToConfirm - /send/success — big success check mark + status + reference with copy-to-clipboard, share, explorer link buttons Shared scaffolding in `components/send/SendScaffold.tsx` (back button, eyebrow, title, content, footer pattern). Stubbed for Phase 7: - Real tx broadcast — `ReviewCard.handleConfirm` currently generates a fake 44-char base58 tx id, waits 1.2s, then routes to success. Replace with `WalletAdapter.signTransaction` + `Connection.sendRawTransaction` when available. - Peer picker — archive included a nearby-peers list in RecipientPicker, but his `LxmfPeer` shape has no publicKey. Mesh handle → Solana address mapping is Phase 7 territory; for now only paste / scan-stub / dev-mock are offered. - QR scanner — archive used a QrScannerModal primitive not yet ported. Tap shows an Alert until Commit D delivers the scan surface. All screens use ported primitives (DepthButton, SlideToConfirm, NumericKeypad, IconButton, Pill, Icon) and consume teammate's `useTheme` + `useGlass` so colors stay on-brand. Send tile on wallet ActionRow now opens this flow — no more 'Unmatched route' red screen. tsc --noEmit clean.
Fixes 'Unmatched route' when tapping the Receive action tile. Also replaces HomeHero's legacy QRModal opener with navigation to the new /receive route, so the QR icon in the wallet header now leads to a proper stealth-capable surface instead of the small modal. Layout: - Header: 'Receive' title + X close button - SegmentedControl: Standard / Stealth (cyan / neon accent tone) - When Stealth selected: preview pill labeled 'Preview — stealth wiring lands in Phase 7' (honest staging) - Identity alias from LxmfContext.displayName or short wallet address - QR card (220px) with border colored by active mode — cyan for standard, neon accent for stealth - Supported-networks row: SOL + USDC TokenLogo marks - Non-selectable short address below QR - Action bar: Share + Copy with copy-pulse animation (cyan halo expands 0→1.12 scale then decays; icon morphs copy → check for 1.4s; haptics.confirm fires on copy) Data: - walletAddress from WalletContext.publicKey?.toBase58() - displayName from LxmfContext - Stealth address is still a preview placeholder (previewStealthAddress format); real derivation is Phase 7 and lives in worktrees/anon0mesh-fork-ui/lib/stealth/. QR rendering uses teammate's components/settings/QRCode.tsx which is a hash-based visual faux-QR — not actually scannable across devices. Real QR lib (react-native-qrcode-svg) can replace in a follow-up once scanning is a real path. HomeHero update: QR icon on the wallet header now navigates to /receive instead of opening the legacy QRModal. Modal remains in settings for his tab; no longer referenced from Home. tsc --noEmit clean.
…button
Addresses round-2 device review:
Root layout — gray bar at top of send/receive screens fixed by
registering each route with headerShown: false. Previously Expo
Router was using its native default header for the unregistered
routes. Also: send/success now has gestureEnabled: false so
swipe-back doesn't escape the receipt; receive opens as a modal with
slide-from-bottom animation.
BalanceCard — removed the visible border. Subtle cyan shadow glow
remains for depth. Cleaner, closer to how the hero read in archive.
NearbyPeersCard — 558-peers bug fixed. LxmfContext accumulates every
peer ever heard; most have stale lastSeen timestamps. Filter to
peers heard within the last 120 seconds (FRESH_WINDOW_SEC). Count
formats with toLocaleString for readability. Dropped the 'seen'
subtitle (it was the 558 number — misleading 'presence' signal).
ReceiveScreen — removed the 'Preview — stealth wiring lands in Phase 7'
pill. Stealth mode now just shows the segmented toggle + a discreet
'(stealth)' qualifier in the supported-networks row. QR card slimmed
down: no border, 8px padding, 200px QR instead of 220px — reads
cleaner and closer to archive weight. Action bar lifted away from
the bottom edge (paddingBottom 7 / paddingTop 3) so Copy ripple
doesn't get clipped by system nav.
ReviewCard — the To row now has a copy icon next to the address.
Tap copies full address + haptic tap.
RecipientPicker — 'Continue to amount' → 'Continue'. Simpler.
SuccessCard — showBack={false}. No back arrow on receipt; Done
button is the only exit. Combined with gestureEnabled: false in
layout, the transfer can't be dismissed mid-receipt by accident.
wallet.tsx — pull-to-refresh wired. 700ms stub + confirm haptic.
Real balance refetch lands in Phase 7.
tsc --noEmit clean.
Three major fixes in one commit, per device review:
1. Real devnet SOL transfer (was faked):
- New src/services/sendTransaction.ts with devnet Connection
('https://api.devnet.solana.com', 'confirmed' commitment)
- For local wallets: exportSecretKey → Keypair.fromSecretKey →
sign → sendRawTransaction. Biometric prompt fires inside
exportSecretKey. Secret is zeroed with .fill(0) immediately
after signing.
- MWA path throws 'not yet wired' — Seeker signing lands later.
- Hardcoded devnet; mainnet wiring is deliberately not in this
commit so dev builds can't accidentally drain real funds.
- ReviewCard.handleConfirm now calls sendSolTransfer; error surface
shows real RPC error messages instead of silently succeeding.
- Matches parity with archive LocalWalletAdapter.send behavior.
2. Token picker:
- New components/send/TokenPicker.tsx — modal bottom sheet with
SOL + USDC catalog entries showing balance + USD value + check
mark on selected.
- Wired to AmountKeypad via onPressCurrency — tapping the SOL chip
now opens the picker. Selecting a token resets amount to 0 and
updates the keypad's max decimals.
- Info row at bottom: 'Jupiter integration brings the full token
list' — honest about what's next.
- Non-SOL selections (USDC today, Jupiter tokens later) get a
simulated receipt path in ReviewCard so UX flow still demos.
Success screen can flag simulated via the 'simulated' param.
3. Dynamic decimal precision:
- NumericKeypad gains maxDecimals prop. Default 6. SOL passes 9,
USDC passes 6. Keypad now rejects decimal entries beyond the
token's configured precision (was hardcoded 2).
- MAX_DIGITS bumped 10 → 18 to cover SOL at 9 decimals plus large
integer parts.
tsc --noEmit clean.
…ow + swipe dismiss
Big polish bundle addressing device review round 3:
1. Real scannable QR on /receive:
- Installed react-native-qrcode-svg (pure JS, no native rebuild).
- Replaces the hash-based faux-QR with a real QR that cross-device
scanners can actually read.
- anonmesh logo embedded in the center (from assets/images/logos/
anonmesh_logo.png) with ecl='H' (high error correction) so the
logo doesn't break the scan.
- Color adapts: void-navy for standard, neon-dark for stealth.
2. Phantom-lite token-first send flow:
- Token picker moved from amount step to the TOP of the recipient
screen as a prominent tappable card ('Sending SOL · Solana').
Users see + choose the token BEFORE entering the address —
matches Phantom/Base pattern.
- Amount step no longer has its own token picker. Token flows
via /send/amount?symbol=SOL URL param. Display-only on amount,
so pre-chosen token context carries through.
- User can still backtrack to change token before confirming.
3. Swipe-to-dismiss on receive screen:
- New SwipeDismissHandle primitive — top-of-screen grab handle
that drives a shared translateY value via Gesture.Pan with
tuned thresholds (activeOffsetY: 8, failOffsetY: -10).
- Deliberate by design: only the handle captures the gesture, so
child ScrollViews + Pressables aren't blocked (fixes the
regression from the old SwipeDismissView primitive that killed
child taps).
- Release past 120px or 800px/s velocity animates out + calls
router.back(); otherwise springs back.
- Haptic tap on dismiss.
- Applied to /receive only for now. /send/recipient still uses
the X button; adding a handle there would conflict with the
native stack back gesture on Android.
4. MWA signing ported from archive:
- sendTransaction now branches on adapter.getMode(). Local keeps
the existing in-app signing via exportSecretKey → Keypair.sign.
- New MWA branch uses transact() from
@solana-mobile/mobile-wallet-adapter-protocol-web3js. Reauthorizes
using the cached 'anon_mwa_auth_token_v1' SecureStore token,
verifies session pubkey matches feePayer (MWA can return a
different selected account — archive flagged this as incident I5),
and submits via signAndSendTransactions.
- Devnet remains hardcoded. MWA identity: anonmesh / anonme.sh.
5. Peer count semantics fixed:
- Previously counted ALL online+fresh peers including TCP/Reticulum
hub connections, which explode to hundreds because g00n.cloud is
a public hub.
- Now 'peers nearby' filters specifically for via === 'ble' (real
physical proximity). When no BLE peers but hub is live, falls
back to 'Connected via hub · N reachable' so the user still sees
mesh activity without conflating it with proximity.
- Live pill stays green if either BLE or hub has fresh peers.
tsc --noEmit clean.
0b8e5b1 to
06afae4
Compare
06afae4 to
e21829b
Compare
e21829b to
715dc4f
Compare
There was a problem hiding this comment.
Pull request overview
Rebuilds the mobile wallet experience by introducing a small design-system layer (motion/state tokens + haptics), new wallet/send/receive primitives, and a rewritten Wallet tab with new Send/Receive routes—including real devnet SOL transfers and a QR-based receive screen—plus a gesture-handler root wrapping fix.
Changes:
- Added design-system tokens + haptics utilities and a suite of UI primitives used by the wallet/send/receive flows.
- Implemented new Send flow screens/components and a devnet SOL transfer service (local + MWA signing).
- Rewrote the Wallet tab UI (hero, actions, nearby peers, recent activity) and added a Receive modal screen.
Reviewed changes
Copilot reviewed 42 out of 43 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
| mobile_app/src/utils/blePermissions.ts | New Android BLE permission helper for scan/connect/advertise flows |
| mobile_app/src/services/sendTransaction.ts | Devnet SOL transfer implementation for local + MWA adapters |
| mobile_app/src/hooks/useHideBalance.tsx | AsyncStorage-backed hide-balance provider + hook |
| mobile_app/src/design-system/tokens/state.ts | New state tokens (press washes, disabled opacity, depth colors) |
| mobile_app/src/design-system/tokens/motion.ts | New motion tokens (easing, durations, springs) |
| mobile_app/src/design-system/tokens/index.ts | Barrel export for design-system tokens |
| mobile_app/src/design-system/motion.ts | Motion scale helpers + re-exported motion tokens |
| mobile_app/src/design-system/haptics.ts | Haptics wrapper with mute support and interaction tiers |
| mobile_app/src/fixtures/mockActivity.ts | Mock recent-activity fixture for wallet UI |
| mobile_app/scripts/fix-lxmf-gradle.mjs | Postinstall patch script for lxmf gradle config |
| mobile_app/package.json | Adds postinstall script + AsyncStorage + QR dependencies |
| mobile_app/package-lock.json | Lockfile updates for new dependencies and scripts |
| mobile_app/components/send/index.ts | Barrel export for send-flow components |
| mobile_app/components/send/TokenPicker.tsx | Token picker modal + hardcoded token catalog |
| mobile_app/components/send/SuccessCard.tsx | Send success/receipt UI with share/copy/explorer actions |
| mobile_app/components/send/SendScaffold.tsx | Shared scaffold/layout for send-flow screens |
| mobile_app/components/send/ReviewCard.tsx | Review screen + slide-to-confirm + SOL send invocation |
| mobile_app/components/send/RecipientPicker.tsx | Recipient entry screen with token-first selection |
| mobile_app/components/send/AmountKeypad.tsx | Amount entry screen with numeric keypad + max decimals |
| mobile_app/components/primitives/index.ts | Barrel export for new primitives |
| mobile_app/components/primitives/TokenLogo.tsx | Inline SVG token marks for SOL/USDC |
| mobile_app/components/primitives/SwipeDismissHandle.tsx | Gesture-handler based swipe-to-dismiss handle |
| mobile_app/components/primitives/SlideToConfirm.tsx | Slide-to-confirm control using reanimated + gestures |
| mobile_app/components/primitives/SegmentedControl.tsx | Animated segmented control primitive |
| mobile_app/components/primitives/PressSurface.tsx | Pressable surface primitive with press animations |
| mobile_app/components/primitives/Pill.tsx | Pill/badge primitive with tone mapping |
| mobile_app/components/primitives/NumericKeypad.tsx | Numeric keypad with max-decimals + “Use max” |
| mobile_app/components/primitives/IconButton.tsx | Animated icon button with tone/variant support |
| mobile_app/components/primitives/Icon.tsx | Unified icon component (Feather + custom SVG icons) |
| mobile_app/components/primitives/DepthButton.tsx | Gradient “depth” button with animated press states |
| mobile_app/components/home/index.ts | Barrel export for wallet-home components |
| mobile_app/components/home/RecentActivity.tsx | Recent activity list UI (currently mock-driven) |
| mobile_app/components/home/NearbyPeersCard.tsx | Nearby peers card driven by LxmfContext state |
| mobile_app/components/home/HomeHero.tsx | Wallet header with hide-balance + receive shortcut |
| mobile_app/components/home/BalanceCard.tsx | Balance hero card with long-press hide toggle |
| mobile_app/components/home/ActionRow.tsx | Wallet action tiles (Send/Receive + “Soon” stubs) |
| mobile_app/app/send/success.tsx | Route for send success screen |
| mobile_app/app/send/review.tsx | Route for send review screen |
| mobile_app/app/send/recipient.tsx | Route for send recipient screen |
| mobile_app/app/send/amount.tsx | Route for send amount screen |
| mobile_app/app/receive.tsx | Receive modal with QR + copy/share + swipe-to-dismiss |
| mobile_app/app/_layout.tsx | Root layout wraps Stack in GestureHandlerRootView + providers |
| mobile_app/app/(tabs)/wallet.tsx | Wallet tab rewrite with staggered entrances + pull-to-refresh stub |
Files not reviewed (1)
- mobile_app/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| withTiming(1, { duration: 180 }), | ||
| withTiming(1, { duration: 900 }), | ||
| withTiming(0, { duration: 280 }), | ||
| ); | ||
| setTimeout(() => setCopied(false), 1400); |
There was a problem hiding this comment.
setTimeout(() => setCopied(false), 1400) isn't cleared on unmount. If the user dismisses the modal quickly, this can call setCopied on an unmounted component. Consider storing the timeout id in a ref and clearing it in a useEffect cleanup.
| * MWA mode → not yet wired. Throws a clear error until the MWA | ||
| * signing flow is ported (Seeker Seed Vault path). | ||
| * |
There was a problem hiding this comment.
The header comment says the MWA signing flow is "not yet wired" and will throw, but the implementation below does perform transact → reauthorize → signAndSendTransactions. Consider updating the comment to match the actual behavior.
| if (result === PermissionsAndroid.RESULTS.GRANTED) continue; | ||
| if (result === PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN) { | ||
| anyNeverAsk = true; | ||
| } | ||
| return anyNeverAsk ? "never_ask_again" : "denied"; |
There was a problem hiding this comment.
requestBLEPermissions() returns on the first non-granted permission, so it may return "denied" even if a later permission is NEVER_ASK_AGAIN (and anyNeverAsk never gets a chance to reflect any permission). Consider scanning all results first, then returning never_ask_again if any permission is NEVER_ASK_AGAIN, otherwise denied.
| const lamports = Math.round(amountSOL * LAMPORTS_PER_SOL); | ||
| if (!Number.isFinite(lamports) || lamports <= 0) { | ||
| throw new Error("Invalid amount"); | ||
| } |
There was a problem hiding this comment.
Converting SOL to lamports via Math.round(amountSOL * LAMPORTS_PER_SOL) can round up and send more than the user entered (floating-point + rounding). Consider using a safer conversion (e.g., validate decimal precision and use Math.floor, or use a decimal/BN conversion) so the transfer amount is deterministic and never exceeds the requested amount.
| icon: "arrow-up-right", | ||
| tone: "primary", | ||
| // Route created in Commit C; until then Expo Router will show | ||
| // "Unmatched route" which is an acceptable stub for the port. | ||
| onPress: () => router.push("/send/recipient" as RouteHref), |
There was a problem hiding this comment.
The comment says the Send/Receive routes aren't created yet and Expo Router will show "Unmatched route", but this PR adds /send/* and /receive screens in app/. Consider updating/removing this comment so it doesn't mislead future readers.
| PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN, | ||
| PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT, | ||
| PermissionsAndroid.PERMISSIONS.BLUETOOTH_ADVERTISE, | ||
| PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION, | ||
| ]; |
There was a problem hiding this comment.
getAndroidPermissions() includes ACCESS_FINE_LOCATION for API 31+, but existing BLE permission requests in the app only ask for BLUETOOTH_SCAN/ADVERTISE/CONNECT on 31+ (no location). Requesting location here will trigger an unnecessary permission prompt and diverges from the current in-app behavior; consider dropping location for 31+ and keeping it only for <31.
| const auth = await mwaWallet.reauthorize({ | ||
| auth_token: cachedToken, | ||
| identity: APP_IDENTITY, | ||
| }); | ||
| const sessionPubkey = new PublicKey(Buffer.from(auth.accounts[0].address, "base64")); |
There was a problem hiding this comment.
MWA path assumes auth.accounts[0] exists and uses it without validation. If the wallet returns no accounts (or an empty list), this will throw a cryptic error. Also, reauthorize() may return a refreshed auth_token; consider persisting it back to SecureStore to keep the cache valid.
Prevent accidental commit of .env / .env.* variants at any level. - New root .gitignore: catches root-level env files + OS/editor cruft + backup files from repo reorganisation. - mobile_app/.gitignore: replaces narrow '.env*.local' (Next/Vite convention) with '.env' + '.env.*' + '!.env.example' (Expo default env filename is bare .env, which the old pattern missed). No content changes to any tracked source. Safety-only.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
feat(wallet): primitives + wallet tab rebuild (A+B)
* refactor(wallet): move dead Send/Swap/Yield panels out of barrel These four files (WalletTabs, SendPanel, SwapPanel, YieldPanel) are roadmap-preview surfaces with no live behavior — grep confirms zero consumers outside the barrel. The barrel re-export keeps them one casual import away from showing up in the wallet bento with theatre copy (MPC 3/3, JITO_RATE, dead Deposit/Withdraw). Removes the four exports from components/wallet/index.ts and adds a FUTURE header comment to each panel pointing back to AUDIT A6 / ROADMAP § 0.A.8. Files stay so the design isn't lost — re-export when each is wired and wrapped in <PreviewBadge>. * refactor(network): extract solanaConnection singleton Connection construction lived inside sendTransaction.ts, which made the singleton's existence a side-effect of importing the send service. Three hooks already reached past it. Pulls the RPC_URL resolution + new Connection into src/infrastructure/network/connection.ts; sendTransaction.ts now imports and re-exports it so existing callers don't churn. useNetworkMode + useWalletBalance read from the new module directly. ARCH P2 #1. * refactor: consolidate sliceNewEvents to single util The 'detect newly-prepended LXMF events given prev head + prev count' helper had drifted into three copies — LxmfContext (most defensive: handles old anchor falling off the end), MessagesScreen, useMessageNotifications. The notification + screen copies returned [] when the previous head disappeared from the (capped) buffer, silently dropping a window of events on restart. Pulls the LxmfContext semantics into src/utils/sliceNewEvents.ts and points all three callers at it. ARCH P2 #4. * refactor: consolidate relTime formatter WalletScreen + PendingCosigns each carried their own 'X ago' helper. PendingCosigns was missing the days branch so cosign requests over 24h stale would say '36h ago' instead of '1d ago'. Hoists the WalletScreen version (with the days branch) into src/utils/relTime.ts. Spec said WalletScreen + RecentActivity, but RecentActivity actually uses Intl.DateTimeFormat — the duplication is in PendingCosigns. Logged the drift in WORK_LOG. * refactor(network): promote useNetworkMode to context Five active call sites each got their own NetInfo subscription + a fresh MeshRpcAdapter — adapter has a stateful pending-request map and the rpcResponse routing useEffect ran in only one of them, so simultaneous mounts could mute responses for the others (T23). Lifts the entire hook logic into context/NetworkModeContext.tsx with a Provider mounted in app/_layout.tsx inside LxmfProvider (depends on it) and outside WalletBalanceProvider (consumer). src/hooks/useNetworkMode.ts is now a 4-line re-export shim so existing imports stay valid. ROADMAP § 2.6 / ARCH P2 #3.
* refactor(wallet): move dead Send/Swap/Yield panels out of barrel These four files (WalletTabs, SendPanel, SwapPanel, YieldPanel) are roadmap-preview surfaces with no live behavior — grep confirms zero consumers outside the barrel. The barrel re-export keeps them one casual import away from showing up in the wallet bento with theatre copy (MPC 3/3, JITO_RATE, dead Deposit/Withdraw). Removes the four exports from components/wallet/index.ts and adds a FUTURE header comment to each panel pointing back to AUDIT A6 / ROADMAP § 0.A.8. Files stay so the design isn't lost — re-export when each is wired and wrapped in <PreviewBadge>. * refactor(network): extract solanaConnection singleton Connection construction lived inside sendTransaction.ts, which made the singleton's existence a side-effect of importing the send service. Three hooks already reached past it. Pulls the RPC_URL resolution + new Connection into src/infrastructure/network/connection.ts; sendTransaction.ts now imports and re-exports it so existing callers don't churn. useNetworkMode + useWalletBalance read from the new module directly. ARCH P2 #1. * refactor: consolidate sliceNewEvents to single util The 'detect newly-prepended LXMF events given prev head + prev count' helper had drifted into three copies — LxmfContext (most defensive: handles old anchor falling off the end), MessagesScreen, useMessageNotifications. The notification + screen copies returned [] when the previous head disappeared from the (capped) buffer, silently dropping a window of events on restart. Pulls the LxmfContext semantics into src/utils/sliceNewEvents.ts and points all three callers at it. ARCH P2 #4. * refactor: consolidate relTime formatter WalletScreen + PendingCosigns each carried their own 'X ago' helper. PendingCosigns was missing the days branch so cosign requests over 24h stale would say '36h ago' instead of '1d ago'. Hoists the WalletScreen version (with the days branch) into src/utils/relTime.ts. Spec said WalletScreen + RecentActivity, but RecentActivity actually uses Intl.DateTimeFormat — the duplication is in PendingCosigns. Logged the drift in WORK_LOG. * refactor(network): promote useNetworkMode to context Five active call sites each got their own NetInfo subscription + a fresh MeshRpcAdapter — adapter has a stateful pending-request map and the rpcResponse routing useEffect ran in only one of them, so simultaneous mounts could mute responses for the others (T23). Lifts the entire hook logic into context/NetworkModeContext.tsx with a Provider mounted in app/_layout.tsx inside LxmfProvider (depends on it) and outside WalletBalanceProvider (consumer). src/hooks/useNetworkMode.ts is now a 4-line re-export shim so existing imports stay valid. ROADMAP § 2.6 / ARCH P2 #3.
Summary
Rebuilds the wallet tab with a custom primitive + haptic layer on top of the existing
theme/useGlasssystem, and wires a token-first send flow with real devnet SOL transfers (local + MWA signing). Receive gets a scannable QR with logo + swipe-to-dismiss. Also includes a gesture-handler root wrap fix that unblocks the nodes MeshMap crash.Lane agreement: wallet tab + send/receive routes + primitives + haptics. Zero touches to onboarding, messages, settings, nodes, or shared contexts.
Visual system: consumes the existing
theme/colors.ts,theme/typography.ts,theme/spacing.ts, andhooks/useGlass. No parallel color or typography system added.What ships
DepthButton,PressSurface,IconButton,SlideToConfirm,NumericKeypad,SegmentedControl,Pill,Icon,TokenLogo,SwipeDismissHandle.haptics.tswith 10 interaction tiers, motion tokens (press springs, durations, easing), state tokens (press washes, disabled opacity).app/(tabs)/wallet.tsx): hero + balance card (long-press hide, AsyncStorage persistent) + 4-tile action row (Send / Receive / Swap-Soon / Yield-Soon) + nearby-peers card + recent activity + pull-to-refresh + staggered entrance animations./send/recipient → /send/amount → /send/review → /send/success): token-first picker on recipient, dynamic per-token decimal precision (SOL=9, USDC=6), slide-to-confirm, devnet signing on confirm./receive): standard/stealth segmented toggle, real scannable QR (react-native-qrcode-svg) with logo center, copy/share action bar with pulse animation, swipe-down dismiss.src/services/sendTransaction.ts— local wallets sign in-app viaexportSecretKey → Keypair → sendRawTransaction(biometric-gated, zeroed after signing); MWA wallets usetransact → signAndSendTransactionswith session-pubkey match check. Devnet hardcoded for safety.<GestureHandlerRootView>now wraps the Stack, unblockingMeshMapwhich was crashing on the nodes tab.scripts/fix-lxmf-gradle.mjspostinstall patch for@magicred-1/react-native-lxmfgradle config.Dependencies added
@react-native-async-storage/async-storage@2.2.0(hide-balance persistence)react-native-qrcode-svg(scannable QR)Known deferred work
Honest list of follow-ups, in priority order:
@solana/spl-token+ associated token accounts) — USDC currently routes to a simulated receipt.getParsedTokenAccountsByOwner./receivestealth mode shows a preview string only.getFeeForMessage— Review shows a static~0.000005 SOLwhich matches the actual simple-transfer fee on devnet.On-chainpill./send/recipient(currently only on/receive).src/__fixtures__/mockActivity.tswith real tx history once the wallet layer emits it.Orphaned after this merge
The previous inline wallet tab panel files are no longer referenced anywhere; safe to delete in a follow-up cleanup commit if you want:
components/wallet/SendPanel.tsxcomponents/wallet/ReceivePanel.tsxcomponents/wallet/SwapPanel.tsxcomponents/wallet/YieldPanel.tsxcomponents/wallet/SwapRow.tsxcomponents/wallet/WalletTabs.tsxcomponents/wallet/AssetDot.tsxThe
ASSETS+TOTAL_USDconstants incomponents/wallet/constants.tsare still consumed by the newBalanceCardandTokenPickeruntil real balance wires.Test plan
npx tsc --noEmitclean (pre-existing errors inMeshMap/nodes/ThemeContextare unchanged; no new errors added)Sendtile: token chip (SOL) at top → picker opens → select USDC → chip updates with balance + max decimalsexplorer.solana.comsimulated: "1"query param)Receivetile or header QR icon: opens receive screen → QR scans from another device → Copy / Share work → grab-handle swipe down dismissesGestureDetectorcrash