Skip to content

Visual + performance revamp: theming, store split, pipeline, pagination#605

Open
jamespepper81 wants to merge 19 commits into
devfrom
claude/bitsleuth-wallet-audit-2xio0a
Open

Visual + performance revamp: theming, store split, pipeline, pagination#605
jamespepper81 wants to merge 19 commits into
devfrom
claude/bitsleuth-wallet-audit-2xio0a

Conversation

@jamespepper81

Copy link
Copy Markdown
Contributor

Description

A full audit-driven revamp of the wallet across three areas: visual polish, runtime performance, and a few security/correctness fixes surfaced along the way. 22 commits; TypeScript (npx tsc --noEmit) and ESLint (npm run lint) are clean with zero warnings.

Type of Change

  • 🎨 UI/UX improvement
  • ⚡ Performance improvement
  • 🔒 Security improvement
  • ♻️ Code refactoring (no functional changes)
  • 📚 Documentation update
  • 🐛 Bug fix (non-breaking change which fixes an issue)

Changes Made

Theming & visuals

  • New hooks/theme-store.tsx: light/dark/system modes (defaults to the device setting; existing saved choices preserved). Removes the manual-only toggle and the full NativeTabs remount on theme change.
  • Unified the app on the Bitcoin-orange brand: the legacy cyan/coral palette was removed from light-mode gradients, the error boundary, toasts, skeletons, and the default wallet color, all now derived from theme tokens. New components/ScreenLoading.tsx replaces four duplicated off-theme "Loading…" fallbacks.
  • New components/AppButton.tsx (Pressable + spring press + haptics) adopted on primary CTAs; new components/PressableOpacity.tsx drop-in replaces TouchableOpacity app-wide.
  • Send flow: the emoji Alert.alert confirmation/success is replaced by components/TransactionReviewSheet.tsx (native formSheet with a review state and a copyable-txid success state); added KeyboardAvoidingView. Settings/edit-wallet pickers converted to formSheet presentation.

Performance

  • Split the "God store" into churn-domain contexts (hooks/wallet-contexts.tsx) with narrow hooks (useWalletBalance, useWalletActions, …). Components subscribe only to what they render, so they no longer re-render on the 30s data polls. The all-slices useWallet() hook has been removed so this can't regress.
  • getWalletData dedupes/memoizes so the pipeline runs once per poll instead of 3–4×; per-address stats request dropped (balance comes from UTXOs) → 1 request/address; pull-to-refresh and cold-start no longer nuke healthy caches.
  • Balance chart made O(n) + memoized; transaction rows memoized; transaction/address/coin-control lists virtualized (FlatList); home carousel gets getItemLayout.
  • All legacy Animated code migrated to Reanimated (single animation system).

Data correctness

  • Confirmed transaction history is now paginated via Esplora /txs/chain/{last_seen_txid} (was truncated at ~25 confirmed per address), with an early-stop cache contract to keep re-scans cheap.

Security & hygiene

  • Unlock PIN moved from plaintext AsyncStorage to expo-secure-store with a silent one-time migration.
  • Wallet setup no longer falls back to the publicly-known "abandon…" test mnemonic on crypto-init failure (fails closed).
  • Removed a dev screen, 9 never-imported components, and unused nativewind/zustand deps.

Screenshots / Videos

Not included — this environment can't run the iOS/Android apps. Changes were verified via TypeScript, ESLint, and code review; UI screenshots should be captured before merge.

iOS

n/a in this environment

Android

n/a in this environment

Testing Performed

Manual Testing

  • Tested on iOS (version: )
  • Tested on Android (version: )
  • Tested wallet creation flow
  • Tested transaction sending/receiving
  • Tested with different wallet types
  • Tested biometric/PIN authentication

Device testing was not possible in this environment — please exercise these flows before merging. Verification here was static (typecheck + lint) plus trace-reads of the instrumented service logs for the data-pipeline and pagination paths.

Automated Testing

  • Linting passes (npm run lint)
  • All existing tests pass (no test suite in the repo)
  • Added new tests for new functionality (no test infra present)
  • Reviewed in both light and dark themes (code-level)

TypeScript compilation (npx tsc --noEmit) also passes with zero errors.

Security Considerations

  • No sensitive data is logged or exposed (verbose UTXO logging gated behind __DEV__)
  • Private keys/mnemonics remain client-side only
  • No new analytics or tracking added
  • Cryptographic operations reviewed (signing/transaction logic left unchanged; only state delivery and caching were refactored)
  • Input validation implemented (send-flow validation preserved as-is; mnemonic generation now fails closed)

PIN storage moved to the hardware-backed keystore.

Performance Impact

  • Performance improvement

Fewer re-renders (narrow store subscriptions), fewer network requests per poll (pipeline dedupe + dropped per-address stats call), virtualized long lists, and lighter refresh/cold-start cache behavior.

Breaking Changes

None user-facing. Internally, useWallet() is removed in favor of the narrow context hooks; all call sites were migrated in this PR.

Deployment Notes

No native/config changes; no prebuild required. expo-image remains installed-but-unused (removal needs a prebuild) — documented in docs/REVAMP_NOTES.md as the sole remaining deferred item.

Checklist

  • My code follows the project's code style guidelines
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation (docs/REVAMP_NOTES.md)
  • My changes generate no new warnings or errors
  • I have tested my changes on both iOS and Android (not possible in this environment)
  • I have checked that my changes don't introduce security vulnerabilities
  • I have updated the CHANGELOG.md (deferred — recommend a version-bump commit before release)
  • All files are in the correct directories per CONTRIBUTING.md guidelines
  • No secrets, API keys, or sensitive data are committed

Additional Notes

Architecture and the reasoning behind the deferred expo-image item are documented in docs/REVAMP_NOTES.md. Recommend capturing before/after screenshots and doing a device pass on the send flow and wallet import before merge.

🤖 Generated with Claude Code

https://claude.ai/code/session_013iiPpE9v7J8Ja9kEx8YG2K


Generated by Claude Code

claude added 18 commits July 2, 2026 19:56
…port

- New hooks/theme-store.tsx: light/dark/system modes, defaults to system,
  persists under the existing 'theme' key so saved choices carry over
- wallet-store re-exports theme/toggleTheme from the new context, so all
  existing useWallet() call sites keep working while theme-only consumers
  can subscribe without re-rendering on wallet data polls
- Tab bar no longer remounts on every theme object change (keys on isDark)
- Theme-only screens and hot components (Toast, SkeletonLoader) switched
  to useTheme(); Toast/SkeletonLoader hardcoded greys replaced with tokens
- Settings gains a 'Match System Appearance' toggle
- Remove dead theme helpers (triggerHapticFeedback, fun-style factories)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013iiPpE9v7J8Ja9kEx8YG2K
- GradientBackground light mode now derives from theme tokens (was a
  hardcoded coral/peach family unrelated to the theme)
- ErrorBoundary fallback uses the current brand palette
- New shared ScreenLoading component (theme-aware spinner) replaces four
  copy-pasted off-theme 'Loading...' fallbacks in the tab screens
- Default wallet color is Bitcoin orange; new 'bitcoin' palette entry
- WalletCard fallback gradient and destructive menu colors use tokens
- fee-settings congestion colors, generate-xpub privacy warning, and
  +not-found screen switched from hardcoded hex to theme tokens

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013iiPpE9v7J8Ja9kEx8YG2K
- getWalletData now dedupes in-flight calls per xpub and memoizes successful
  results for 25s, collapsing the 3 parallel pipeline runs per 30s poll into
  one; pull-to-refresh and wallet erase clear the memo explicitly
- Polling pauses while the app is backgrounded (refetchOnWindowFocus still
  catches up on foreground); address discovery poll relaxed to 60s
- BalanceChart history is computed in a single O(n) pass and memoized, so it
  no longer recomputes O(points x transactions) on every store update
- TransactionItem rows are memoized behind a thin store-subscribing wrapper
  and no longer re-render on every poll; entrance stagger capped at 8 rows
- Transaction history renders through a virtualized FlatList instead of
  ScrollView + map over every transaction
- Home wallet carousel gets getItemLayout and stable wallet-id keys
- Verbose UTXO-serializing logs in the send path gated behind __DEV__

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013iiPpE9v7J8Ja9kEx8YG2K
- New components/AppButton.tsx: Pressable with spring press-scale, haptic
  feedback, themed primary/secondary/outline/destructive variants built on
  createButtonStyle, plus loading and disabled states and an icon slot
- Adopted on the home Send/Receive quick actions, receive screen's Setup
  Wallet / New Address / Copy / Share buttons, and the send screen's
  Review & Send button; per-screen duplicate button styles removed

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013iiPpE9v7J8Ja9kEx8YG2K
- New components/TransactionReviewSheet.tsx: formSheet modal with a
  proper review state (amount + fiat, full monospace recipient, network
  fee, fee rate + time estimate, RBF badge, total, irreversibility
  warning) and a success state (animated checkmark, copyable txid,
  View Transaction / Done actions)
- Send screen opens the sheet instead of the emoji Alert; the biometric
  gate and handleSendTransaction sequence are unchanged, and the old
  Alert's form-clear/navigation callbacks moved to the sheet actions
- Confirm is disabled while broadcasting, preventing double-submits
- Send form is now wrapped in KeyboardAvoidingView so the keyboard no
  longer covers the amount and custom-fee inputs
- SuccessAnimation (previously unused) is wired in with the theme's
  success color

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013iiPpE9v7J8Ja9kEx8YG2K
- Settings currency / auto-lock / wallet pickers converted from
  transparent JS slide-up modals to presentationStyle formSheet with
  themed backgrounds, theme-token dividers, and selection haptics
- Home edit-wallet modal simplified to a single formSheet layout
  (removes the duplicated iOS-26/fallback branches) using AppButton
  for Cancel/Save
- manage-wallets edit modal converted the same way

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013iiPpE9v7J8Ja9kEx8YG2K
Security:
- New services/secure-pin-service.ts stores the unlock PIN in the
  hardware-backed keystore instead of plaintext AsyncStorage, with a
  silent one-time migration of any existing PIN; all three PIN readers
  (auto-lock store, PIN verification screen, security test service) now
  go through it, and wallet erase deletes the secure copy
- wallet-setup no longer falls back to the publicly-known 'abandon...'
  test mnemonic when crypto fails to load - mnemonic generation and
  validation now fail closed and block wallet creation

Hygiene:
- Delete dev screen app/mfa-test.tsx from the route tree
- Delete 9 orphaned components that were never imported (BitSleuthButton,
  BitSleuthCard, PremiumButton, AnimatedNumber, AnimatedPressable,
  OptimizedTouchableOpacity, LoadingAnimation, ConfettiCelebration,
  EmojiReaction); SuccessAnimation was kept and is now in use
- Remove unused nativewind and zustand dependencies
  (react-native-confetti-cannon stays: wallet-setup uses it directly)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013iiPpE9v7J8Ja9kEx8YG2K
- docs/REVAMP_NOTES.md: theme architecture, color rules, getWalletData
  memo contract, PIN migration behavior, and the deferred-work list
- Add feedback entries to frontend-design and react-native-skills
  SKILL_MEMORY files per the repo's skills feedback loop

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013iiPpE9v7J8Ja9kEx8YG2K
- wallet-addresses and coin-control now render through FlatList (stable
  keys, initialNumToRender/windowSize/removeClippedSubviews) with their
  summary/quick-action blocks as list headers and the educational cards
  as footers, instead of mapping every row inside a ScrollView
- wallet-addresses copy/refresh feedback switched from Alert to the
  shared toast
- Receive screen informational messages (rate-limit wait, generation
  failure, unexpected error) now use the toast, and a success toast
  confirms new address generation; the gap-limit education alert stays
  as an alert since it needs the full explanation read

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013iiPpE9v7J8Ja9kEx8YG2K
The store body (queries, mutations, state - unchanged) now runs inside a
hand-rolled WalletProvider that publishes each of its nine memoized slices
plus a small meta slice through its own context (hooks/wallet-contexts.tsx).
New narrow hooks (useWallets, useWalletBalance, useWalletTransactions,
useWalletUtxos, useWalletSettings, useWalletActions, useCoinControl, ...)
let components subscribe to only the data they render.

useWallet() remains as a compatibility hook with the exact legacy shape and
outside-provider behavior (returns undefined for the screens that gate on
it), so all existing call sites are untouched and re-render exactly as
before; converting consumers to the narrow hooks is the follow-up commit.

@nkzw/create-context-hook stays: auto-lock-store still uses it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013iiPpE9v7J8Ja9kEx8YG2K
- WalletSelector, WalletCard, QRScanner, PIN screens, settings, coin-control,
  send, home, and TransactionItem's wrapper now subscribe via the narrow
  context hooks (and useTheme) instead of the all-slices useWallet(), so
  components that render no polled data stay idle across the 30s refreshes
  and the send form no longer re-renders mid-typing on unrelated churn
- Wrapper availability gates read only the low-churn WalletsContext
- balanceData no longer embeds the priceQuery object in its memo value/deps
  (it gave BalanceContext a new identity on every store render);
  address-details reads bitcoinPrice instead of priceQuery.data

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013iiPpE9v7J8Ja9kEx8YG2K
- getWalletData no longer issues /address/{addr} stats per used address:
  the wallet balance is computed from UTXOs and address activity comes
  from the txs response (a cache hit from discovery), so the per-address
  wire cost drops from 2 requests to 1. The addresses screen keeps its
  chain_stats calls, which are exact even for >50-tx addresses.
- Pull-to-refresh now clears only the load-bearing caches for the current
  wallet (per-address caches + discovery metadata) instead of also wiping
  the global transaction-body cache, other wallets' data, and a list of
  malformed cache keys. Confirmed tx bodies are immutable and stay.
- Cold-start cache wipe now triggers only on app-version change; the old
  any->5-min-gap wipe was redundant because every affected cache already
  self-expires within 5 minutes (FRESH_LAUNCH_THRESHOLD_MS removed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013iiPpE9v7J8Ja9kEx8YG2K
- use-tab-animation rewritten on shared values (also fixes the entrance
  replaying on every re-render while focused, since the focus effect is
  now properly memoized); the four tab screens wrap content in
  Reanimated's Animated.View
- Receive screen's refresh spin uses withRepeat/withTiming
- SplashScreen sequence rebuilt on withSpring/withTiming/withDelay with
  deterministic JS-side completion timing
- PriceChart tooltip fades via a shared value; PanResponder unchanged

The app now runs a single animation system (Reanimated); no component
imports the legacy react-native Animated API anymore.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013iiPpE9v7J8Ja9kEx8YG2K
- New components/PressableOpacity.tsx: drop-in Pressable replacement that
  mirrors TouchableOpacity's activeOpacity feedback (repo skill 9.3)
- All 32 files with plain TouchableOpacity usages converted mechanically;
  the only remaining TouchableOpacity references are the three
  createAnimatedComponent(TouchableOpacity) bases (settings logout,
  WalletCard, TransactionItem) which already have custom Reanimated
  press feedback
- AppButton adopted for the fee-bump Bump Fee CTA (with proper loading
  state), recovery-phrase Reveal button, and biometric-setup Continue
  button; their bespoke button styles removed

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013iiPpE9v7J8Ja9kEx8YG2K
- REVAMP_NOTES: store architecture (narrow hooks preferred, useWallet
  compat), revised pipeline/refresh/cold-start behavior, Reanimated-only
  animation rule, PressableOpacity convention; deferred list pruned to
  what actually remains (useWallet stage-3 call sites, txs pagination
  gap, expo-image)
- SKILL_MEMORY entries for react-native-skills and frontend-design

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013iiPpE9v7J8Ja9kEx8YG2K
All remaining screens and components (fee-bump, fee/wallet settings,
transaction details/explorer/history, addresses, manage-wallets,
wallet-setup, recovery phrase, xpub, pin-setup, address-details,
LiquidGlassView, FeedbackPopup, PriceChart) now subscribe through the
narrow context hooks. LiquidGlassView is notable: it wraps cards across
home and settings and was still re-rendering on every poll via the
all-slices hook.

useWallet() and the WalletStore type are deleted - the narrow hooks in
hooks/wallet-contexts.tsx are now the only store API, so new code cannot
silently reintroduce app-wide re-render fanout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013iiPpE9v7J8Ja9kEx8YG2K
Addresses with more than ~25 confirmed transactions previously showed a
truncated list: /address/{addr}/txs returns one page and the app never
walked /txs/chain/{last_seen_txid} continuation pages.

- New getAddressTransactionsPaginated in esplora-service: fetches page 1
  in a new raw mode (no cached-body merge - the continuation cursor must
  come from the actual API page, and raw requests get their own dedupe
  key so a concurrent merged request can't corrupt it), then walks up to
  MAX_TX_CHAIN_PAGES_PER_ADDRESS chain pages sequentially through the
  rate-limited queue
- Efficiency contract: a fresh combined txid-list cache serves with zero
  requests; after TTL expiry only page 1 plus at most one chain page is
  fetched before the early-stop (a full page of already-cached txs means
  deeper history was covered by a previous scan, since confirmed history
  only prepends); deeper history is reassembled from the permanent
  confirmed-body cache via a TTL-ignoring txid-list read
- Chain pages cache their tx bodies permanently and the immutable page
  for the session; mid-pagination failures return a partial result
  (strictly better than the old always-truncated behavior)
- getWalletData's per-address loop and address-details use the paginated
  fetch; discovery and the addresses screen stay single-page by design
- Merged wallet list cap raised from 50 to 300 (history is virtualized)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013iiPpE9v7J8Ja9kEx8YG2K
- REVAMP_NOTES: narrow hooks are the only store API; paginated history
  contract (raw page-1 mode, early-stop, display limit); deferred list
  is down to expo-image only
- SKILL_MEMORY entry on cursor-vs-cache-merge pagination learning

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013iiPpE9v7J8Ja9kEx8YG2K
Comment thread services/wallet-service.ts Fixed
The per-address loop uses getAddressTransactionsPaginated now; the plain
getAddressTransactions import was left over and flagged by code review.
getAddressTransactions stays exported from esplora-service (still used by
address-details and the test script).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013iiPpE9v7J8Ja9kEx8YG2K
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.

2 participants