Skip to content

Commit 269638e

Browse files
Merge pull request #504 from reown-com/fix/theme
fix: improved theme logic
2 parents f69b234 + 5714a28 commit 269638e

File tree

7 files changed

+141
-97
lines changed

7 files changed

+141
-97
lines changed

.changeset/late-queens-talk.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@reown/appkit-react-native': patch
3+
'@reown/appkit-bitcoin-react-native': patch
4+
'@reown/appkit-coinbase-react-native': patch
5+
'@reown/appkit-common-react-native': patch
6+
'@reown/appkit-core-react-native': patch
7+
'@reown/appkit-ethers-react-native': patch
8+
'@reown/appkit-solana-react-native': patch
9+
'@reown/appkit-ui-react-native': patch
10+
'@reown/appkit-wagmi-react-native': patch
11+
---
12+
13+
fix: refactors the theme management logic to introduce a clearer separation between system theme and user-defined default theme

packages/appkit/src/__tests__/hooks/useAppKitTheme.test.tsx

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ jest.mock('valtio', () => ({
2121
jest.mock('@reown/appkit-core-react-native', () => ({
2222
ThemeController: {
2323
state: {
24-
themeMode: undefined,
24+
themeMode: 'light',
2525
themeVariables: {}
2626
},
27-
setThemeMode: jest.fn(),
27+
setDefaultThemeMode: jest.fn(),
2828
setThemeVariables: jest.fn()
2929
}
3030
}));
@@ -42,7 +42,7 @@ describe('useAppKitTheme', () => {
4242
jest.clearAllMocks();
4343
// Reset ThemeController state
4444
ThemeController.state = {
45-
themeMode: undefined,
45+
themeMode: 'light',
4646
themeVariables: {}
4747
};
4848
});
@@ -61,7 +61,7 @@ describe('useAppKitTheme', () => {
6161
it('should return initial theme state', () => {
6262
const { result } = renderHook(() => useAppKitTheme(), { wrapper });
6363

64-
expect(result.current.themeMode).toBeUndefined();
64+
expect(result.current.themeMode).toBe('light');
6565
expect(result.current.themeVariables).toStrictEqual({});
6666
});
6767

@@ -99,24 +99,24 @@ describe('useAppKitTheme', () => {
9999
expect(result.current.themeVariables).toEqual(themeVariables);
100100
});
101101

102-
it('should call ThemeController.setThemeMode when setThemeMode is called', () => {
102+
it('should call ThemeController.setDefaultThemeMode when setThemeMode is called', () => {
103103
const { result } = renderHook(() => useAppKitTheme(), { wrapper });
104104

105105
act(() => {
106106
result.current.setThemeMode('dark');
107107
});
108108

109-
expect(ThemeController.setThemeMode).toHaveBeenCalledWith('dark');
109+
expect(ThemeController.setDefaultThemeMode).toHaveBeenCalledWith('dark');
110110
});
111111

112-
it('should call ThemeController.setThemeMode with undefined', () => {
112+
it('should call ThemeController.setDefaultThemeMode with undefined', () => {
113113
const { result } = renderHook(() => useAppKitTheme(), { wrapper });
114114

115115
act(() => {
116116
result.current.setThemeMode(undefined);
117117
});
118118

119-
expect(ThemeController.setThemeMode).toHaveBeenCalledWith(undefined);
119+
expect(ThemeController.setDefaultThemeMode).toHaveBeenCalledWith(undefined);
120120
});
121121

122122
it('should call ThemeController.setThemeVariables when setThemeVariables is called', () => {
@@ -172,10 +172,10 @@ describe('useAppKitTheme', () => {
172172
result.current.setThemeMode(undefined);
173173
});
174174

175-
expect(ThemeController.setThemeMode).toHaveBeenCalledTimes(3);
176-
expect(ThemeController.setThemeMode).toHaveBeenNthCalledWith(1, 'dark');
177-
expect(ThemeController.setThemeMode).toHaveBeenNthCalledWith(2, 'light');
178-
expect(ThemeController.setThemeMode).toHaveBeenNthCalledWith(3, undefined);
175+
expect(ThemeController.setDefaultThemeMode).toHaveBeenCalledTimes(3);
176+
expect(ThemeController.setDefaultThemeMode).toHaveBeenNthCalledWith(1, 'dark');
177+
expect(ThemeController.setDefaultThemeMode).toHaveBeenNthCalledWith(2, 'light');
178+
expect(ThemeController.setDefaultThemeMode).toHaveBeenNthCalledWith(3, undefined);
179179
});
180180

181181
it('should handle multiple setThemeVariables calls', () => {

packages/appkit/src/hooks/useAppKitTheme.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import { useAppKitContext } from './useAppKitContext';
88
* Interface representing the result of the useAppKitTheme hook
99
*/
1010
export interface UseAppKitThemeReturn {
11-
/** The current theme mode ('dark' or 'light'), or undefined if using system default */
12-
themeMode?: ThemeMode;
11+
/** The current theme mode ('dark' or 'light') */
12+
themeMode: ThemeMode;
1313
/** The current theme variables, currently only supports 'accent' color */
1414
themeVariables: ThemeVariables;
1515
/** Function to set the theme mode */
@@ -63,11 +63,12 @@ export interface UseAppKitThemeReturn {
6363
*/
6464
export function useAppKitTheme(): UseAppKitThemeReturn {
6565
useAppKitContext();
66+
6667
const { themeMode, themeVariables } = useSnapshot(ThemeController.state);
6768

6869
const stableFunctions = useMemo(
6970
() => ({
70-
setThemeMode: ThemeController.setThemeMode.bind(ThemeController),
71+
setThemeMode: ThemeController.setDefaultThemeMode.bind(ThemeController),
7172
setThemeVariables: ThemeController.setThemeVariables.bind(ThemeController)
7273
}),
7374
[]

packages/appkit/src/modal/w3m-modal/index.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export function AppKit() {
2323
const { bottom, top } = useSafeAreaInsets();
2424
const { close } = useInternalAppKit();
2525
const { open } = useSnapshot(ModalController.state);
26-
const { themeMode, themeVariables, defaultThemeMode } = useSnapshot(ThemeController.state);
26+
const { themeMode, themeVariables } = useSnapshot(ThemeController.state);
2727
const { projectId } = useSnapshot(OptionsController.state);
2828

2929
const handleBackPress = () => {
@@ -35,10 +35,8 @@ export function AppKit() {
3535
};
3636

3737
useEffect(() => {
38-
if (theme && !defaultThemeMode) {
39-
ThemeController.setThemeMode(theme);
40-
}
41-
}, [theme, defaultThemeMode]);
38+
ThemeController.setSystemThemeMode(theme ?? undefined);
39+
}, [theme]);
4240

4341
const prefetch = useCallback(async () => {
4442
await ApiController.prefetch();
Lines changed: 92 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,112 @@
1-
import { Appearance } from 'react-native';
21
import { ThemeController } from '../../index';
32

4-
// Mock react-native Appearance
5-
jest.mock('react-native', () => ({
6-
Appearance: {
7-
getColorScheme: jest.fn()
8-
}
9-
}));
10-
11-
const mockedAppearance = Appearance as jest.Mocked<typeof Appearance>;
12-
133
// -- Tests --------------------------------------------------------------------
144
describe('ThemeController', () => {
155
beforeEach(() => {
16-
// Reset state before each test
17-
ThemeController.setThemeMode();
18-
ThemeController.setDefaultThemeMode();
19-
ThemeController.setThemeVariables();
6+
// Reset state
7+
ThemeController.setDefaultThemeMode(undefined);
8+
ThemeController.setSystemThemeMode();
9+
ThemeController.setThemeVariables(undefined);
2010
jest.clearAllMocks();
2111
});
2212

2313
describe('initial state', () => {
2414
it('should have valid default state', () => {
25-
expect(ThemeController.state.themeMode).toBeDefined();
26-
expect(ThemeController.state.defaultThemeMode).toBeUndefined();
27-
expect(ThemeController.state.themeVariables).toEqual({});
15+
const state = ThemeController.state;
16+
expect(state.themeMode).toBeDefined();
17+
expect(state.defaultThemeMode).toBeUndefined();
18+
expect(state.themeVariables).toEqual({});
2819
});
2920
});
3021

31-
describe('setThemeMode', () => {
32-
it('should set theme mode to light', () => {
33-
ThemeController.setThemeMode('light');
34-
expect(ThemeController.state.themeMode).toBe('light');
22+
describe('setDefaultThemeMode', () => {
23+
it('should set default theme mode to light', () => {
24+
ThemeController.setDefaultThemeMode('light');
25+
const state = ThemeController.state;
26+
expect(state.defaultThemeMode).toBe('light');
3527
});
3628

37-
it('should set theme mode to dark', () => {
38-
ThemeController.setThemeMode('dark');
39-
expect(ThemeController.state.themeMode).toBe('dark');
29+
it('should set default theme mode to dark', () => {
30+
ThemeController.setDefaultThemeMode('dark');
31+
const state = ThemeController.state;
32+
expect(state.defaultThemeMode).toBe('dark');
4033
});
4134

42-
it('should fall back to system theme when undefined and system is dark', () => {
43-
mockedAppearance.getColorScheme.mockReturnValue('dark');
44-
ThemeController.setThemeMode();
45-
expect(ThemeController.state.themeMode).toBe('dark');
46-
expect(mockedAppearance.getColorScheme).toHaveBeenCalled();
35+
it('should clear default theme mode when set to undefined', () => {
36+
ThemeController.setDefaultThemeMode('dark');
37+
ThemeController.setDefaultThemeMode(undefined);
38+
const state = ThemeController.state;
39+
expect(state.defaultThemeMode).toBeUndefined();
4740
});
4841

49-
it('should fall back to system theme when undefined and system is light', () => {
50-
mockedAppearance.getColorScheme.mockReturnValue('light');
51-
ThemeController.setThemeMode();
52-
expect(ThemeController.state.themeMode).toBe('light');
53-
expect(mockedAppearance.getColorScheme).toHaveBeenCalled();
42+
it('should update default theme mode from light to dark', () => {
43+
ThemeController.setDefaultThemeMode('light');
44+
ThemeController.setDefaultThemeMode('dark');
45+
const state = ThemeController.state;
46+
expect(state.defaultThemeMode).toBe('dark');
5447
});
48+
});
5549

56-
it('should default to light when system returns null', () => {
57-
mockedAppearance.getColorScheme.mockReturnValue(null);
58-
ThemeController.setThemeMode();
59-
expect(ThemeController.state.themeMode).toBe('light');
50+
describe('setSystemThemeMode', () => {
51+
it('should set system theme mode to dark', () => {
52+
ThemeController.setSystemThemeMode('dark');
53+
const state = ThemeController.state;
54+
expect(state.systemThemeMode).toBe('dark');
55+
});
56+
57+
it('should set system theme mode to light', () => {
58+
ThemeController.setSystemThemeMode('light');
59+
const state = ThemeController.state;
60+
expect(state.systemThemeMode).toBe('light');
61+
});
62+
63+
it('should default to light when called without arguments', () => {
64+
ThemeController.setSystemThemeMode();
65+
const state = ThemeController.state;
66+
expect(state.systemThemeMode).toBe('light');
67+
});
68+
69+
it('should update system theme mode from light to dark', () => {
70+
ThemeController.setSystemThemeMode('light');
71+
ThemeController.setSystemThemeMode('dark');
72+
const state = ThemeController.state;
73+
expect(state.systemThemeMode).toBe('dark');
6074
});
6175
});
6276

63-
describe('setDefaultThemeMode', () => {
64-
it('should set default theme mode and apply it', () => {
77+
describe('theme mode precedence', () => {
78+
it('should have both system and default theme modes set independently', () => {
79+
ThemeController.setSystemThemeMode('light');
6580
ThemeController.setDefaultThemeMode('dark');
66-
expect(ThemeController.state.defaultThemeMode).toBe('dark');
67-
expect(ThemeController.state.themeMode).toBe('dark');
81+
const state = ThemeController.state;
82+
expect(state.systemThemeMode).toBe('light');
83+
expect(state.defaultThemeMode).toBe('dark');
6884
});
6985

70-
it('should set default theme mode to light and apply it', () => {
86+
it('should allow default theme mode to be cleared while system remains', () => {
87+
ThemeController.setSystemThemeMode('dark');
7188
ThemeController.setDefaultThemeMode('light');
72-
expect(ThemeController.state.defaultThemeMode).toBe('light');
89+
ThemeController.setDefaultThemeMode(undefined);
90+
const state = ThemeController.state;
91+
expect(state.systemThemeMode).toBe('dark');
92+
expect(state.defaultThemeMode).toBeUndefined();
93+
});
94+
it('should derive themeMode with correct priority: defaultThemeMode > systemThemeMode > light', () => {
95+
// Initially, with no values set, should default to 'light'
96+
ThemeController.setDefaultThemeMode(undefined);
97+
ThemeController.setSystemThemeMode();
7398
expect(ThemeController.state.themeMode).toBe('light');
74-
});
7599

76-
it('should set default theme mode to undefined and fall back to system', () => {
77-
mockedAppearance.getColorScheme.mockReturnValue('dark');
78-
ThemeController.setDefaultThemeMode();
79-
expect(ThemeController.state.defaultThemeMode).toBeUndefined();
100+
// When only systemThemeMode is set, themeMode should follow it
101+
ThemeController.setSystemThemeMode('dark');
102+
expect(ThemeController.state.themeMode).toBe('dark');
103+
104+
// When defaultThemeMode is set, it takes priority over systemThemeMode
105+
ThemeController.setDefaultThemeMode('light');
106+
expect(ThemeController.state.themeMode).toBe('light');
107+
108+
// When defaultThemeMode is cleared, falls back to systemThemeMode
109+
ThemeController.setDefaultThemeMode(undefined);
80110
expect(ThemeController.state.themeMode).toBe('dark');
81111
});
82112
});
@@ -85,7 +115,8 @@ describe('ThemeController', () => {
85115
it('should set theme variables', () => {
86116
const variables = { accent: '#000000' };
87117
ThemeController.setThemeVariables(variables);
88-
expect(ThemeController.state.themeVariables).toEqual(variables);
118+
const state = ThemeController.state;
119+
expect(state.themeVariables).toEqual(variables);
89120
});
90121

91122
it('should override existing theme variables', () => {
@@ -94,24 +125,28 @@ describe('ThemeController', () => {
94125

95126
ThemeController.setThemeVariables(initialVariables);
96127
ThemeController.setThemeVariables(newVariables);
128+
const state = ThemeController.state;
97129

98-
expect(ThemeController.state.themeVariables).toEqual({
130+
expect(state.themeVariables).toEqual({
99131
accent: '#FFFFFF'
100132
});
101133
});
102134

103135
it('should reset theme variables when undefined', () => {
104136
const variables = { accent: '#000000' };
105137
ThemeController.setThemeVariables(variables);
106-
expect(ThemeController.state.themeVariables).toEqual(variables);
138+
let state = ThemeController.state;
139+
expect(state.themeVariables).toEqual(variables);
107140

108141
ThemeController.setThemeVariables(undefined);
109-
expect(ThemeController.state.themeVariables).toEqual({});
142+
state = ThemeController.state;
143+
expect(state.themeVariables).toEqual({});
110144
});
111145

112146
it('should handle empty object', () => {
113147
ThemeController.setThemeVariables({});
114-
expect(ThemeController.state.themeVariables).toEqual({});
148+
const state = ThemeController.state;
149+
expect(state.themeVariables).toEqual({});
115150
});
116151
});
117152

@@ -134,9 +169,10 @@ describe('ThemeController', () => {
134169
describe('state immutability', () => {
135170
it('should maintain state reference but update values', () => {
136171
const stateRef = ThemeController.state;
137-
ThemeController.setThemeMode('dark');
172+
ThemeController.setDefaultThemeMode('dark');
138173
expect(ThemeController.state).toBe(stateRef);
139-
expect(ThemeController.state.themeMode).toBe('dark');
174+
const state = ThemeController.state;
175+
expect(state.defaultThemeMode).toBe('dark');
140176
});
141177
});
142178
});

0 commit comments

Comments
 (0)