Visual + performance revamp: theming, store split, pipeline, pagination#605
Open
jamespepper81 wants to merge 19 commits into
Open
Visual + performance revamp: theming, store split, pipeline, pagination#605jamespepper81 wants to merge 19 commits into
jamespepper81 wants to merge 19 commits into
Conversation
…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
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
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.
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
Changes Made
Theming & visuals
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.components/ScreenLoading.tsxreplaces four duplicated off-theme "Loading…" fallbacks.components/AppButton.tsx(Pressable + spring press + haptics) adopted on primary CTAs; newcomponents/PressableOpacity.tsxdrop-in replacesTouchableOpacityapp-wide.Alert.alertconfirmation/success is replaced bycomponents/TransactionReviewSheet.tsx(native formSheet with a review state and a copyable-txid success state); addedKeyboardAvoidingView. Settings/edit-wallet pickers converted to formSheet presentation.Performance
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-slicesuseWallet()hook has been removed so this can't regress.getWalletDatadedupes/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.FlatList); home carousel getsgetItemLayout.Animatedcode migrated to Reanimated (single animation system).Data correctness
/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
expo-secure-storewith a silent one-time migration.nativewind/zustanddeps.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
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
npm run lint)TypeScript compilation (
npx tsc --noEmit) also passes with zero errors.Security Considerations
__DEV__)PIN storage moved to the hardware-backed keystore.
Performance Impact
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-imageremains installed-but-unused (removal needs a prebuild) — documented indocs/REVAMP_NOTES.mdas the sole remaining deferred item.Checklist
docs/REVAMP_NOTES.md)Additional Notes
Architecture and the reasoning behind the deferred
expo-imageitem are documented indocs/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