feat(mobile): light/dark theme toggle - #527
Merged
Merged
Conversation
Ports the web wallet's useTheme hook and ThemeToggle button to React Native, persisting the choice under the same veil_theme storage key. The web version applies its theme by setting data-theme on the document root and letting CSS variables restyle everything. React Native has neither a document nor cascading variables, so the palette becomes data: lib/theme.ts defines a ThemeColors record per theme and screens build their StyleSheets from it. The dark values are the ones the app already shipped with, so dark mode looks exactly as it did and only light mode is new. The active theme lives in module state read through useSyncExternalStore rather than a context provider, which keeps useTheme() callable anywhere with nothing to wrap the tree in — the same ergonomics as the web hook — while still driving every subscriber from one source of truth. Restyles the existing screens (home, swap, bulk payout) and the root layout, so the toggle changes the whole app rather than one component. The Stack's contentStyle carries the background so a route that is still loading never flashes the opposite theme. A tap applies immediately and persists in the background: a failed write costs the preference on next launch rather than the interaction now.
|
@Y33t-dev is attempting to deploy a commit to the miracle656's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
@Y33t-dev Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
Owner
# Conflicts: # frontend/mobile/README.md # frontend/mobile/package-lock.json # frontend/mobile/package.json
# Conflicts: # frontend/mobile/app/_layout.tsx
This was referenced Jul 30, 2026
Merged
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.
closes #490
Ports the web wallet's
useThemehook andThemeTogglebutton to React Native. The choice persists under the sameveil_themestorage key the web wallet uses, and the whole app restyles.Translating the web approach
frontend/wallet/hooks/useTheme.tsapplies its theme by settingdata-themeon the document root and letting CSS variables do the rest. Neither half of that exists in React Native — no document, no cascading variables — so the palette becomes data instead:Screens build their stylesheets from it:
The dark values are the ones the app already shipped with, lifted verbatim out of the existing screens. Dark mode is therefore pixel-identical to what is on
maintoday, and light mode is the only new surface.The token set is deliberately small — one per job (
textStrong,textPrimary,textSecondary,textMuted,textFaint, …) rather than one per screen — so adding a screen does not mean adding tokens.No provider
The active theme lives in module state read through
useSyncExternalStore, not a context provider. That keepsuseTheme()callable from anywhere with nothing to wrap the tree in, which is the ergonomic the web hook has, while still driving every subscriber from one source of truth. (On the web eachuseThemecall has its ownuseStateand they stay in sync only because they all write to the same DOM attribute — that trick has no equivalent here, so the store is explicit.)The snapshot handed to
useSyncExternalStoreis the theme name, not the colour object: snapshots are compared by identity, and a string compares by value.colorsis then derived from a stable module constant, so it keeps a stable identity for as long as the theme does and is safe as auseMemodependency.The toggle actually restyles the app
hooks/useTheme.tsandcomponents/ThemeToggle.tsxalone would be a switch that changes one button, so this also converts the existing screens:app/_layout.tsx— themedStatusBar, pluscontentStyleon the Stack so the background is painted behind every route. Without it a screen still loading, or shorter than the viewport, flashes the opposite theme.app/index.tsx,app/swap.tsx,app/bulk-payout.tsx— staticStyleSheet.createcalls becomecreateStyles(colors).After the change there is not a single hardcoded colour literal left in
app/. A test enforces the spirit of that: it asserts every colour role differs between the two themes, withonAccentas the one documented exception (white in both, because it sits on the accent fill rather than the background). A role accidentally given the same value in both would fail.swap.tsx'sRowhelper now takes the stylesheet as a prop rather than closing over a module-level one — defining it inside the component would remount its subtree on every theme change.Icons
The sun and moon are the web component's SVG paths, redrawn with
react-native-svg(pinned to the version Expo SDK 57 bundles, so it works in Expo Go without a custom dev client). Behaviour matches the web control: it shows the icon of the mode you would switch to.Failure handling
A tap applies immediately and persists in the background. If the write fails the user keeps the theme they just chose and loses only the preference on next launch — the opposite ordering would mean a storage hiccup makes a toggle appear broken. There is a test for that ordering.
Unreadable storage and corrupt stored values both fall back to dark rather than failing.
Files
lib/theme.ts— palette, active theme, persistence, subscription.hooks/useTheme.ts— React binding.components/ThemeToggle.tsx— the button.app/_layout.tsx,app/index.tsx,app/swap.tsx,app/bulk-payout.tsx— restyled.lib/__tests__/theme.test.ts— 18 tests.Notes for review
jest-expo. feat(mobile): encrypted wallet backup export #524 and feat(mobile): switch between testnet and mainnet at runtime #526 add the same harness independently, since all three need it and none depends on the others — whichever merges last can drop those lines.types: ["jest"]intsconfig.jsonis the same story; without it TypeScript 6 does not pick up@types/jestunder the Expo base config.systemoption would diverge from the web wallet's model rather than port it.app.jsonalready setsuserInterfaceStyle: "automatic", so seeding the initial theme fromuseColorScheme()would be a small follow-up if wanted.createStyles(colors)treatment once both have landed.Verification