Skip to content

Commit 048885f

Browse files
Merge pull request #499 from reown-com/chore/use-theme-hook
chore: Added new useAppKitTheme hook
2 parents 2cdbf19 + 32e7408 commit 048885f

20 files changed

+673
-52
lines changed

.changeset/spotty-foxes-fry.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-ui-react-native': patch
4+
'@reown/appkit-bitcoin-react-native': patch
5+
'@reown/appkit-coinbase-react-native': patch
6+
'@reown/appkit-common-react-native': patch
7+
'@reown/appkit-core-react-native': patch
8+
'@reown/appkit-ethers-react-native': patch
9+
'@reown/appkit-solana-react-native': patch
10+
'@reown/appkit-wagmi-react-native': patch
11+
---
12+
13+
chore: added useAppKitTheme hook

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"build:gallery": "turbo run build:gallery",
2727
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
2828
"prettier": "prettier --check .",
29-
"test": "turbo build && turbo run test --parallel",
29+
"test": "turbo run test --parallel",
3030
"clean": "turbo clean && rm -rf node_modules && (watchman watch-del-all || true)",
3131
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\" --ignore-path .gitignore",
3232
"playwright:test": "cd apps/native && yarn playwright:test",

packages/appkit/babel.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
presets: ['module:metro-react-native-babel-preset']
3+
};

packages/appkit/jest-setup.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Import shared setup
2+
import '@shared-jest-setup';

packages/appkit/jest.config.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const appkitConfig = {
2+
...require('../../jest.config'),
3+
setupFilesAfterEnv: ['./jest-setup.ts'],
4+
// Override the moduleNameMapper to use the correct path from the package
5+
moduleNameMapper: {
6+
'^@shared-jest-setup$': '../../jest-shared-setup.ts'
7+
}
8+
};
9+
module.exports = appkitConfig;

packages/appkit/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"scripts": {
1010
"build": "bob build",
1111
"clean": "rm -rf lib",
12+
"test": "jest",
1213
"lint": "eslint . --ext .js,.jsx,.ts,.tsx"
1314
},
1415
"files": [

packages/appkit/src/AppKitContext.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { createContext, useContext, useMemo, type ReactNode } from 'react';
22
import { AppKit } from './AppKit';
33

4-
interface AppKitContextType {
4+
export interface AppKitContextType {
55
appKit: AppKit | null;
66
}
77

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import { renderHook, act } from '@testing-library/react-native';
2+
import React from 'react';
3+
import { useAppKitTheme } from '../../hooks/useAppKitTheme';
4+
import { ThemeController } from '@reown/appkit-core-react-native';
5+
import { type AppKitContextType, AppKitContext } from '../../AppKitContext';
6+
import type { AppKit } from '../../AppKit';
7+
8+
// Mock Appearance
9+
jest.mock('react-native', () => ({
10+
Appearance: {
11+
getColorScheme: jest.fn().mockReturnValue('light')
12+
}
13+
}));
14+
15+
// Mock valtio
16+
jest.mock('valtio', () => ({
17+
useSnapshot: jest.fn(state => state)
18+
}));
19+
20+
// Mock ThemeController
21+
jest.mock('@reown/appkit-core-react-native', () => ({
22+
ThemeController: {
23+
state: {
24+
themeMode: undefined,
25+
themeVariables: {}
26+
},
27+
setThemeMode: jest.fn(),
28+
setThemeVariables: jest.fn()
29+
}
30+
}));
31+
32+
describe('useAppKitTheme', () => {
33+
const mockAppKit = {} as AppKit;
34+
35+
const wrapper = ({ children }: { children: React.ReactNode }) => {
36+
const contextValue: AppKitContextType = { appKit: mockAppKit };
37+
38+
return <AppKitContext.Provider value={contextValue}>{children}</AppKitContext.Provider>;
39+
};
40+
41+
beforeEach(() => {
42+
jest.clearAllMocks();
43+
// Reset ThemeController state
44+
ThemeController.state = {
45+
themeMode: undefined,
46+
themeVariables: {}
47+
};
48+
});
49+
50+
it('should throw error when used outside AppKitProvider', () => {
51+
// Suppress console.error for this test
52+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
53+
54+
expect(() => {
55+
renderHook(() => useAppKitTheme());
56+
}).toThrow('AppKit instance is not yet available in context.');
57+
58+
consoleSpy.mockRestore();
59+
});
60+
61+
it('should return initial theme state', () => {
62+
const { result } = renderHook(() => useAppKitTheme(), { wrapper });
63+
64+
expect(result.current.themeMode).toBeUndefined();
65+
expect(result.current.themeVariables).toStrictEqual({});
66+
});
67+
68+
it('should return dark theme mode when set', () => {
69+
ThemeController.state = {
70+
themeMode: 'dark',
71+
themeVariables: {}
72+
};
73+
74+
const { result } = renderHook(() => useAppKitTheme(), { wrapper });
75+
76+
expect(result.current.themeMode).toBe('dark');
77+
});
78+
79+
it('should return light theme mode when set', () => {
80+
ThemeController.state = {
81+
themeMode: 'light',
82+
themeVariables: {}
83+
};
84+
85+
const { result } = renderHook(() => useAppKitTheme(), { wrapper });
86+
87+
expect(result.current.themeMode).toBe('light');
88+
});
89+
90+
it('should return theme variables when set', () => {
91+
const themeVariables = { accent: '#00BB7F' };
92+
ThemeController.state = {
93+
themeMode: 'dark',
94+
themeVariables
95+
};
96+
97+
const { result } = renderHook(() => useAppKitTheme(), { wrapper });
98+
99+
expect(result.current.themeVariables).toEqual(themeVariables);
100+
});
101+
102+
it('should call ThemeController.setThemeMode when setThemeMode is called', () => {
103+
const { result } = renderHook(() => useAppKitTheme(), { wrapper });
104+
105+
act(() => {
106+
result.current.setThemeMode('dark');
107+
});
108+
109+
expect(ThemeController.setThemeMode).toHaveBeenCalledWith('dark');
110+
});
111+
112+
it('should call ThemeController.setThemeMode with undefined', () => {
113+
const { result } = renderHook(() => useAppKitTheme(), { wrapper });
114+
115+
act(() => {
116+
result.current.setThemeMode(undefined);
117+
});
118+
119+
expect(ThemeController.setThemeMode).toHaveBeenCalledWith(undefined);
120+
});
121+
122+
it('should call ThemeController.setThemeVariables when setThemeVariables is called', () => {
123+
const { result } = renderHook(() => useAppKitTheme(), { wrapper });
124+
const themeVariables = { accent: '#FF5733' };
125+
126+
act(() => {
127+
result.current.setThemeVariables(themeVariables);
128+
});
129+
130+
expect(ThemeController.setThemeVariables).toHaveBeenCalledWith(themeVariables);
131+
});
132+
133+
it('should call ThemeController.setThemeVariables with undefined', () => {
134+
const { result } = renderHook(() => useAppKitTheme(), { wrapper });
135+
136+
act(() => {
137+
result.current.setThemeVariables(undefined);
138+
});
139+
140+
expect(ThemeController.setThemeVariables).toHaveBeenCalledWith(undefined);
141+
});
142+
143+
it('should return stable function references', () => {
144+
const { result } = renderHook(() => useAppKitTheme(), { wrapper });
145+
146+
const firstSetThemeMode = result.current.setThemeMode;
147+
const firstSetThemeVariables = result.current.setThemeVariables;
148+
149+
// Functions should be stable (same reference)
150+
expect(result.current.setThemeMode).toBe(firstSetThemeMode);
151+
expect(result.current.setThemeVariables).toBe(firstSetThemeVariables);
152+
});
153+
154+
it('should update theme mode and variables together', () => {
155+
ThemeController.state = {
156+
themeMode: 'dark',
157+
themeVariables: { accent: '#00BB7F' }
158+
};
159+
160+
const { result } = renderHook(() => useAppKitTheme(), { wrapper });
161+
162+
expect(result.current.themeMode).toBe('dark');
163+
expect(result.current.themeVariables).toEqual({ accent: '#00BB7F' });
164+
});
165+
166+
it('should handle multiple setThemeMode calls', () => {
167+
const { result } = renderHook(() => useAppKitTheme(), { wrapper });
168+
169+
act(() => {
170+
result.current.setThemeMode('dark');
171+
result.current.setThemeMode('light');
172+
result.current.setThemeMode(undefined);
173+
});
174+
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);
179+
});
180+
181+
it('should handle multiple setThemeVariables calls', () => {
182+
const { result } = renderHook(() => useAppKitTheme(), { wrapper });
183+
const variables1 = { accent: '#00BB7F' };
184+
const variables2 = { accent: '#FF5733' };
185+
186+
act(() => {
187+
result.current.setThemeVariables(variables1);
188+
result.current.setThemeVariables(variables2);
189+
});
190+
191+
expect(ThemeController.setThemeVariables).toHaveBeenCalledTimes(2);
192+
expect(ThemeController.setThemeVariables).toHaveBeenNthCalledWith(1, variables1);
193+
expect(ThemeController.setThemeVariables).toHaveBeenNthCalledWith(2, variables2);
194+
});
195+
});

packages/appkit/src/hooks/useAccount.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import {
66
} from '@reown/appkit-core-react-native';
77
import { useMemo } from 'react';
88
import { useSnapshot } from 'valtio';
9-
import { useAppKit } from './useAppKit';
109
import type { AccountType, AppKitNetwork } from '@reown/appkit-common-react-native';
10+
import { useAppKitContext } from './useAppKitContext';
1111

1212
/**
1313
* Represents a blockchain account with its associated metadata
@@ -64,7 +64,7 @@ export interface Account {
6464
* @throws Will log errors via LogController if account parsing fails
6565
*/
6666
export function useAccount() {
67-
useAppKit(); // Use the hook for checks
67+
useAppKitContext();
6868

6969
const {
7070
activeAddress: address,

packages/appkit/src/hooks/useAppKit.ts

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,60 @@
1-
import { useContext, useMemo } from 'react';
1+
import { useMemo } from 'react';
22
import type { ChainNamespace } from '@reown/appkit-common-react-native';
33

44
import type { AppKit } from '../AppKit';
5-
import { AppKitContext } from '../AppKitContext';
5+
import { useAppKitContext } from './useAppKitContext';
66

7+
/**
8+
* Interface representing the return value of the useAppKit hook
9+
*/
710
interface UseAppKitReturn {
11+
/** Function to open the AppKit modal with optional view configuration */
812
open: AppKit['open'];
13+
/** Function to close the AppKit modal */
914
close: AppKit['close'];
15+
/** Function to disconnect the wallet, optionally scoped to a specific namespace */
1016
disconnect: (namespace?: ChainNamespace) => void;
17+
/** Function to switch to a different network */
1118
switchNetwork: AppKit['switchNetwork'];
1219
}
1320

21+
/**
22+
* Hook to access core AppKit functionality for controlling the modal
23+
*
24+
* @remarks
25+
* This hook provides access to the main AppKit instance methods for opening/closing
26+
* the modal, disconnecting wallets, and switching networks. All functions are memoized
27+
* and properly bound to ensure stable references across renders.
28+
*
29+
* @returns {UseAppKitReturn} An object containing:
30+
* - `open`: Opens the AppKit modal, optionally with a specific view
31+
* - `close`: Closes the AppKit modal
32+
* - `disconnect`: Disconnects the current wallet connection (optionally for a specific namespace)
33+
* - `switchNetwork`: Switches to a different blockchain network
34+
*
35+
* @throws {Error} If used outside of an AppKitProvider
36+
* @throws {Error} If AppKit instance is not available in context
37+
*
38+
* @example
39+
* ```tsx
40+
* function MyComponent() {
41+
* const { open, close, disconnect, switchNetwork } = useAppKit();
42+
*
43+
* return (
44+
* <View>
45+
* <Button onPress={() => open()} title="Connect Wallet" />
46+
* <Button onPress={() => disconnect()} title="Disconnect" />
47+
* <Button
48+
* onPress={() => switchNetwork('eip155:1')}
49+
* title="Switch to Ethereum"
50+
* />
51+
* </View>
52+
* );
53+
* }
54+
* ```
55+
*/
1456
export const useAppKit = (): UseAppKitReturn => {
15-
const context = useContext(AppKitContext);
16-
17-
if (context === undefined) {
18-
throw new Error('useAppKit must be used within an AppKitProvider');
19-
}
20-
21-
if (!context.appKit) {
22-
// This might happen if the provider is rendered before AppKit is initialized
23-
throw new Error('AppKit instance is not yet available in context.');
24-
}
57+
const context = useAppKitContext();
2558

2659
const stableFunctions = useMemo(() => {
2760
if (!context.appKit) {

0 commit comments

Comments
 (0)