diff --git a/.changeset/spotty-foxes-fry.md b/.changeset/spotty-foxes-fry.md
new file mode 100644
index 00000000..4d0fcbc3
--- /dev/null
+++ b/.changeset/spotty-foxes-fry.md
@@ -0,0 +1,13 @@
+---
+'@reown/appkit-react-native': patch
+'@reown/appkit-ui-react-native': patch
+'@reown/appkit-bitcoin-react-native': patch
+'@reown/appkit-coinbase-react-native': patch
+'@reown/appkit-common-react-native': patch
+'@reown/appkit-core-react-native': patch
+'@reown/appkit-ethers-react-native': patch
+'@reown/appkit-solana-react-native': patch
+'@reown/appkit-wagmi-react-native': patch
+---
+
+chore: added useAppKitTheme hook
diff --git a/package.json b/package.json
index d9127d17..5f2bd7c8 100644
--- a/package.json
+++ b/package.json
@@ -26,7 +26,7 @@
"build:gallery": "turbo run build:gallery",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"prettier": "prettier --check .",
- "test": "turbo build && turbo run test --parallel",
+ "test": "turbo run test --parallel",
"clean": "turbo clean && rm -rf node_modules && (watchman watch-del-all || true)",
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\" --ignore-path .gitignore",
"playwright:test": "cd apps/native && yarn playwright:test",
diff --git a/packages/appkit/babel.config.js b/packages/appkit/babel.config.js
new file mode 100644
index 00000000..6403cbe5
--- /dev/null
+++ b/packages/appkit/babel.config.js
@@ -0,0 +1,3 @@
+module.exports = {
+ presets: ['module:metro-react-native-babel-preset']
+};
diff --git a/packages/appkit/jest-setup.ts b/packages/appkit/jest-setup.ts
new file mode 100644
index 00000000..58fc8174
--- /dev/null
+++ b/packages/appkit/jest-setup.ts
@@ -0,0 +1,2 @@
+// Import shared setup
+import '@shared-jest-setup';
diff --git a/packages/appkit/jest.config.ts b/packages/appkit/jest.config.ts
new file mode 100644
index 00000000..f2fb133e
--- /dev/null
+++ b/packages/appkit/jest.config.ts
@@ -0,0 +1,9 @@
+const appkitConfig = {
+ ...require('../../jest.config'),
+ setupFilesAfterEnv: ['./jest-setup.ts'],
+ // Override the moduleNameMapper to use the correct path from the package
+ moduleNameMapper: {
+ '^@shared-jest-setup$': '../../jest-shared-setup.ts'
+ }
+};
+module.exports = appkitConfig;
diff --git a/packages/appkit/package.json b/packages/appkit/package.json
index 2c5f7c45..d9a53621 100644
--- a/packages/appkit/package.json
+++ b/packages/appkit/package.json
@@ -9,6 +9,7 @@
"scripts": {
"build": "bob build",
"clean": "rm -rf lib",
+ "test": "jest",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx"
},
"files": [
diff --git a/packages/appkit/src/AppKitContext.tsx b/packages/appkit/src/AppKitContext.tsx
index 2d90df28..5f1d64c4 100644
--- a/packages/appkit/src/AppKitContext.tsx
+++ b/packages/appkit/src/AppKitContext.tsx
@@ -1,7 +1,7 @@
import React, { createContext, useContext, useMemo, type ReactNode } from 'react';
import { AppKit } from './AppKit';
-interface AppKitContextType {
+export interface AppKitContextType {
appKit: AppKit | null;
}
diff --git a/packages/appkit/src/__tests__/hooks/useAppKitTheme.test.tsx b/packages/appkit/src/__tests__/hooks/useAppKitTheme.test.tsx
new file mode 100644
index 00000000..e2340c65
--- /dev/null
+++ b/packages/appkit/src/__tests__/hooks/useAppKitTheme.test.tsx
@@ -0,0 +1,195 @@
+import { renderHook, act } from '@testing-library/react-native';
+import React from 'react';
+import { useAppKitTheme } from '../../hooks/useAppKitTheme';
+import { ThemeController } from '@reown/appkit-core-react-native';
+import { type AppKitContextType, AppKitContext } from '../../AppKitContext';
+import type { AppKit } from '../../AppKit';
+
+// Mock Appearance
+jest.mock('react-native', () => ({
+ Appearance: {
+ getColorScheme: jest.fn().mockReturnValue('light')
+ }
+}));
+
+// Mock valtio
+jest.mock('valtio', () => ({
+ useSnapshot: jest.fn(state => state)
+}));
+
+// Mock ThemeController
+jest.mock('@reown/appkit-core-react-native', () => ({
+ ThemeController: {
+ state: {
+ themeMode: undefined,
+ themeVariables: {}
+ },
+ setThemeMode: jest.fn(),
+ setThemeVariables: jest.fn()
+ }
+}));
+
+describe('useAppKitTheme', () => {
+ const mockAppKit = {} as AppKit;
+
+ const wrapper = ({ children }: { children: React.ReactNode }) => {
+ const contextValue: AppKitContextType = { appKit: mockAppKit };
+
+ return {children};
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ // Reset ThemeController state
+ ThemeController.state = {
+ themeMode: undefined,
+ themeVariables: {}
+ };
+ });
+
+ it('should throw error when used outside AppKitProvider', () => {
+ // Suppress console.error for this test
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
+
+ expect(() => {
+ renderHook(() => useAppKitTheme());
+ }).toThrow('AppKit instance is not yet available in context.');
+
+ consoleSpy.mockRestore();
+ });
+
+ it('should return initial theme state', () => {
+ const { result } = renderHook(() => useAppKitTheme(), { wrapper });
+
+ expect(result.current.themeMode).toBeUndefined();
+ expect(result.current.themeVariables).toStrictEqual({});
+ });
+
+ it('should return dark theme mode when set', () => {
+ ThemeController.state = {
+ themeMode: 'dark',
+ themeVariables: {}
+ };
+
+ const { result } = renderHook(() => useAppKitTheme(), { wrapper });
+
+ expect(result.current.themeMode).toBe('dark');
+ });
+
+ it('should return light theme mode when set', () => {
+ ThemeController.state = {
+ themeMode: 'light',
+ themeVariables: {}
+ };
+
+ const { result } = renderHook(() => useAppKitTheme(), { wrapper });
+
+ expect(result.current.themeMode).toBe('light');
+ });
+
+ it('should return theme variables when set', () => {
+ const themeVariables = { accent: '#00BB7F' };
+ ThemeController.state = {
+ themeMode: 'dark',
+ themeVariables
+ };
+
+ const { result } = renderHook(() => useAppKitTheme(), { wrapper });
+
+ expect(result.current.themeVariables).toEqual(themeVariables);
+ });
+
+ it('should call ThemeController.setThemeMode when setThemeMode is called', () => {
+ const { result } = renderHook(() => useAppKitTheme(), { wrapper });
+
+ act(() => {
+ result.current.setThemeMode('dark');
+ });
+
+ expect(ThemeController.setThemeMode).toHaveBeenCalledWith('dark');
+ });
+
+ it('should call ThemeController.setThemeMode with undefined', () => {
+ const { result } = renderHook(() => useAppKitTheme(), { wrapper });
+
+ act(() => {
+ result.current.setThemeMode(undefined);
+ });
+
+ expect(ThemeController.setThemeMode).toHaveBeenCalledWith(undefined);
+ });
+
+ it('should call ThemeController.setThemeVariables when setThemeVariables is called', () => {
+ const { result } = renderHook(() => useAppKitTheme(), { wrapper });
+ const themeVariables = { accent: '#FF5733' };
+
+ act(() => {
+ result.current.setThemeVariables(themeVariables);
+ });
+
+ expect(ThemeController.setThemeVariables).toHaveBeenCalledWith(themeVariables);
+ });
+
+ it('should call ThemeController.setThemeVariables with undefined', () => {
+ const { result } = renderHook(() => useAppKitTheme(), { wrapper });
+
+ act(() => {
+ result.current.setThemeVariables(undefined);
+ });
+
+ expect(ThemeController.setThemeVariables).toHaveBeenCalledWith(undefined);
+ });
+
+ it('should return stable function references', () => {
+ const { result } = renderHook(() => useAppKitTheme(), { wrapper });
+
+ const firstSetThemeMode = result.current.setThemeMode;
+ const firstSetThemeVariables = result.current.setThemeVariables;
+
+ // Functions should be stable (same reference)
+ expect(result.current.setThemeMode).toBe(firstSetThemeMode);
+ expect(result.current.setThemeVariables).toBe(firstSetThemeVariables);
+ });
+
+ it('should update theme mode and variables together', () => {
+ ThemeController.state = {
+ themeMode: 'dark',
+ themeVariables: { accent: '#00BB7F' }
+ };
+
+ const { result } = renderHook(() => useAppKitTheme(), { wrapper });
+
+ expect(result.current.themeMode).toBe('dark');
+ expect(result.current.themeVariables).toEqual({ accent: '#00BB7F' });
+ });
+
+ it('should handle multiple setThemeMode calls', () => {
+ const { result } = renderHook(() => useAppKitTheme(), { wrapper });
+
+ act(() => {
+ result.current.setThemeMode('dark');
+ result.current.setThemeMode('light');
+ result.current.setThemeMode(undefined);
+ });
+
+ expect(ThemeController.setThemeMode).toHaveBeenCalledTimes(3);
+ expect(ThemeController.setThemeMode).toHaveBeenNthCalledWith(1, 'dark');
+ expect(ThemeController.setThemeMode).toHaveBeenNthCalledWith(2, 'light');
+ expect(ThemeController.setThemeMode).toHaveBeenNthCalledWith(3, undefined);
+ });
+
+ it('should handle multiple setThemeVariables calls', () => {
+ const { result } = renderHook(() => useAppKitTheme(), { wrapper });
+ const variables1 = { accent: '#00BB7F' };
+ const variables2 = { accent: '#FF5733' };
+
+ act(() => {
+ result.current.setThemeVariables(variables1);
+ result.current.setThemeVariables(variables2);
+ });
+
+ expect(ThemeController.setThemeVariables).toHaveBeenCalledTimes(2);
+ expect(ThemeController.setThemeVariables).toHaveBeenNthCalledWith(1, variables1);
+ expect(ThemeController.setThemeVariables).toHaveBeenNthCalledWith(2, variables2);
+ });
+});
diff --git a/packages/appkit/src/hooks/useAccount.ts b/packages/appkit/src/hooks/useAccount.ts
index a3a30e87..02fd7ec9 100644
--- a/packages/appkit/src/hooks/useAccount.ts
+++ b/packages/appkit/src/hooks/useAccount.ts
@@ -6,8 +6,8 @@ import {
} from '@reown/appkit-core-react-native';
import { useMemo } from 'react';
import { useSnapshot } from 'valtio';
-import { useAppKit } from './useAppKit';
import type { AccountType, AppKitNetwork } from '@reown/appkit-common-react-native';
+import { useAppKitContext } from './useAppKitContext';
/**
* Represents a blockchain account with its associated metadata
@@ -64,7 +64,7 @@ export interface Account {
* @throws Will log errors via LogController if account parsing fails
*/
export function useAccount() {
- useAppKit(); // Use the hook for checks
+ useAppKitContext();
const {
activeAddress: address,
diff --git a/packages/appkit/src/hooks/useAppKit.ts b/packages/appkit/src/hooks/useAppKit.ts
index 910b26ed..2dff2dc1 100644
--- a/packages/appkit/src/hooks/useAppKit.ts
+++ b/packages/appkit/src/hooks/useAppKit.ts
@@ -1,27 +1,60 @@
-import { useContext, useMemo } from 'react';
+import { useMemo } from 'react';
import type { ChainNamespace } from '@reown/appkit-common-react-native';
import type { AppKit } from '../AppKit';
-import { AppKitContext } from '../AppKitContext';
+import { useAppKitContext } from './useAppKitContext';
+/**
+ * Interface representing the return value of the useAppKit hook
+ */
interface UseAppKitReturn {
+ /** Function to open the AppKit modal with optional view configuration */
open: AppKit['open'];
+ /** Function to close the AppKit modal */
close: AppKit['close'];
+ /** Function to disconnect the wallet, optionally scoped to a specific namespace */
disconnect: (namespace?: ChainNamespace) => void;
+ /** Function to switch to a different network */
switchNetwork: AppKit['switchNetwork'];
}
+/**
+ * Hook to access core AppKit functionality for controlling the modal
+ *
+ * @remarks
+ * This hook provides access to the main AppKit instance methods for opening/closing
+ * the modal, disconnecting wallets, and switching networks. All functions are memoized
+ * and properly bound to ensure stable references across renders.
+ *
+ * @returns {UseAppKitReturn} An object containing:
+ * - `open`: Opens the AppKit modal, optionally with a specific view
+ * - `close`: Closes the AppKit modal
+ * - `disconnect`: Disconnects the current wallet connection (optionally for a specific namespace)
+ * - `switchNetwork`: Switches to a different blockchain network
+ *
+ * @throws {Error} If used outside of an AppKitProvider
+ * @throws {Error} If AppKit instance is not available in context
+ *
+ * @example
+ * ```tsx
+ * function MyComponent() {
+ * const { open, close, disconnect, switchNetwork } = useAppKit();
+ *
+ * return (
+ *
+ *
+ * );
+ * }
+ * ```
+ */
export const useAppKit = (): UseAppKitReturn => {
- const context = useContext(AppKitContext);
-
- if (context === undefined) {
- throw new Error('useAppKit must be used within an AppKitProvider');
- }
-
- if (!context.appKit) {
- // This might happen if the provider is rendered before AppKit is initialized
- throw new Error('AppKit instance is not yet available in context.');
- }
+ const context = useAppKitContext();
const stableFunctions = useMemo(() => {
if (!context.appKit) {
diff --git a/packages/appkit/src/hooks/useAppKitContext.ts b/packages/appkit/src/hooks/useAppKitContext.ts
new file mode 100644
index 00000000..7116b9ef
--- /dev/null
+++ b/packages/appkit/src/hooks/useAppKitContext.ts
@@ -0,0 +1,43 @@
+import { useContext } from 'react';
+
+import { AppKitContext, type AppKitContextType } from '../AppKitContext';
+
+/**
+ * Hook to access the AppKit context
+ *
+ * @remarks
+ * This is an internal hook used by other AppKit hooks to ensure they're used within
+ * the AppKitProvider. You typically don't need to use this hook directly - use the
+ * higher-level hooks like `useAppKit`, `useAccount`, `useAppKitTheme`, etc. instead.
+ *
+ * @returns {AppKitContextType} The AppKit context containing the AppKit instance
+ *
+ * @throws {Error} If used outside of an AppKitProvider
+ * @throws {Error} If the AppKit instance is not yet available in context
+ *
+ * @internal
+ *
+ * @example
+ * ```tsx
+ * // This is typically used internally by other hooks
+ * function MyCustomHook() {
+ * const context = useAppKitContext();
+ * // Use context.appKit...
+ * }
+ * ```
+ */
+
+export const useAppKitContext = (): AppKitContextType => {
+ const context = useContext(AppKitContext);
+
+ if (context === undefined) {
+ throw new Error('useAppKitContext must be used within an AppKitProvider');
+ }
+
+ if (!context.appKit) {
+ // This might happen if the provider is rendered before AppKit is initialized
+ throw new Error('AppKit instance is not yet available in context.');
+ }
+
+ return context;
+};
diff --git a/packages/appkit/src/hooks/useAppKitEvents.ts b/packages/appkit/src/hooks/useAppKitEvents.ts
index d6e4b730..b7a5deec 100644
--- a/packages/appkit/src/hooks/useAppKitEvents.ts
+++ b/packages/appkit/src/hooks/useAppKitEvents.ts
@@ -2,10 +2,42 @@ import { useEffect } from 'react';
import { useSnapshot } from 'valtio';
import { EventsController, type EventsControllerState } from '@reown/appkit-core-react-native';
import { type EventName } from '@reown/appkit-common-react-native';
-import { useAppKit } from './useAppKit';
+import { useAppKitContext } from './useAppKitContext';
+/**
+ * Hook to subscribe to all AppKit events
+ *
+ * @remarks
+ * This hook provides reactive access to AppKit's event system, allowing you to
+ * monitor all events that occur within the AppKit lifecycle (connections, disconnections,
+ * network changes, etc.). The callback is invoked whenever a new event is emitted.
+ *
+ * @param callback - Optional callback function invoked when any event occurs
+ *
+ * @returns An object containing:
+ * - `data`: The most recent event data
+ * - `timestamp`: The timestamp of the most recent event
+ *
+ * @throws {Error} If used outside of an AppKitProvider
+ *
+ * @example
+ * ```tsx
+ * function MyComponent() {
+ * const { data, timestamp } = useAppKitEvents((event) => {
+ * console.log('Event occurred:', event.data.event);
+ * });
+ *
+ * return (
+ *
+ * Last event: {data?.event}
+ * Time: {timestamp}
+ *
+ * );
+ * }
+ * ```
+ */
export function useAppKitEvents(callback?: (newEvent: EventsControllerState) => void) {
- useAppKit(); // Use the hook for checks
+ useAppKitContext();
const { data, timestamp } = useSnapshot(EventsController.state);
useEffect(() => {
@@ -21,11 +53,39 @@ export function useAppKitEvents(callback?: (newEvent: EventsControllerState) =>
return { data, timestamp };
}
+/**
+ * Hook to subscribe to a specific AppKit event
+ *
+ * @remarks
+ * This hook allows you to listen for a specific event type rather than all events.
+ * It's more efficient than `useAppKitEvents` when you only care about a particular event.
+ *
+ * @param event - The specific event name to subscribe to (e.g., 'MODAL_OPEN', 'CONNECT_SUCCESS')
+ * @param callback - Callback function invoked when the specified event occurs
+ *
+ * @throws {Error} If used outside of an AppKitProvider
+ *
+ * @example
+ * ```tsx
+ * function MyComponent() {
+ * useAppKitEventSubscription('CONNECT_SUCCESS', (event) => {
+ * console.log('Wallet connected!', event.data);
+ * });
+ *
+ * useAppKitEventSubscription('DISCONNECT_SUCCESS', (event) => {
+ * console.log('Wallet disconnected!', event.data);
+ * });
+ *
+ * return {/ Your component /};
+ * }
+ * ```
+ */
+
export function useAppKitEventSubscription(
event: EventName,
callback: (newEvent: EventsControllerState) => void
) {
- useAppKit(); // Use the hook for checks
+ useAppKitContext();
useEffect(() => {
const unsubscribe = EventsController?.subscribeEvent(event, callback);
diff --git a/packages/appkit/src/hooks/useAppKitLogs.ts b/packages/appkit/src/hooks/useAppKitLogs.ts
index 2e5d175e..f03faba8 100644
--- a/packages/appkit/src/hooks/useAppKitLogs.ts
+++ b/packages/appkit/src/hooks/useAppKitLogs.ts
@@ -1,7 +1,7 @@
-import { useContext, useMemo } from 'react';
+import { useMemo } from 'react';
import { useSnapshot } from 'valtio';
import { LogController, type LogEntry, type LogLevel } from '@reown/appkit-core-react-native';
-import { AppKitContext } from '../AppKitContext';
+import { useAppKitContext } from './useAppKitContext';
export interface UseAppKitLogsReturn {
/**
@@ -65,15 +65,7 @@ export interface UseAppKitLogsReturn {
* ```
*/
export const useAppKitLogs = (): UseAppKitLogsReturn => {
- const context = useContext(AppKitContext);
-
- if (context === undefined) {
- throw new Error('useAppKitLogs must be used within an AppKitProvider');
- }
-
- if (!context.appKit) {
- throw new Error('AppKit instance is not yet available in context.');
- }
+ useAppKitContext();
const { logs } = useSnapshot(LogController.state);
diff --git a/packages/appkit/src/hooks/useAppKitState.ts b/packages/appkit/src/hooks/useAppKitState.ts
index 2539152e..c0e8fc97 100644
--- a/packages/appkit/src/hooks/useAppKitState.ts
+++ b/packages/appkit/src/hooks/useAppKitState.ts
@@ -2,10 +2,43 @@
import { useMemo } from 'react';
import { useSnapshot } from 'valtio';
import { ConnectionsController, ModalController } from '@reown/appkit-core-react-native';
-import { useAppKit } from './useAppKit';
+import { useAppKitContext } from './useAppKitContext';
+
+/**
+ * Hook to access the overall state of the AppKit modal and connection
+ *
+ * @remarks
+ * This hook provides a high-level view of the AppKit's current state, including
+ * whether the modal is open, if it's loading, connection status, and the active chain.
+ * It's useful for coordinating UI elements with the AppKit's state.
+ *
+ * @returns An object containing:
+ * - `isOpen`: Whether the AppKit modal is currently open
+ * - `isLoading`: Whether the AppKit is in a loading state
+ * - `isConnected`: Whether a wallet is currently connected
+ * - `chain`: The currently active blockchain network
+ *
+ * @throws {Error} If used outside of an AppKitProvider
+ *
+ * @example
+ * ```tsx
+ * function MyComponent() {
+ * const { isOpen, isLoading, isConnected, chain } = useAppKitState();
+ *
+ * return (
+ *
+ * Modal: {isOpen ? 'Open' : 'Closed'}
+ * Loading: {isLoading ? 'Yes' : 'No'}
+ * Connected: {isConnected ? 'Yes' : 'No'}
+ * {chain && Chain: {chain.name}}
+ *
+ * );
+ * }
+ * ```
+ */
export function useAppKitState() {
- useAppKit(); // Use the hook for checks
+ useAppKitContext();
const { activeAddress: address, connection, networks } = useSnapshot(ConnectionsController.state);
const { open, loading } = useSnapshot(ModalController.state);
diff --git a/packages/appkit/src/hooks/useAppKitTheme.ts b/packages/appkit/src/hooks/useAppKitTheme.ts
new file mode 100644
index 00000000..9e5b71a1
--- /dev/null
+++ b/packages/appkit/src/hooks/useAppKitTheme.ts
@@ -0,0 +1,81 @@
+import { useMemo } from 'react';
+import { useSnapshot } from 'valtio';
+import type { ThemeMode, ThemeVariables } from '@reown/appkit-common-react-native';
+import { ThemeController } from '@reown/appkit-core-react-native';
+import { useAppKitContext } from './useAppKitContext';
+
+/**
+ * Interface representing the result of the useAppKitTheme hook
+ */
+export interface UseAppKitThemeReturn {
+ /** The current theme mode ('dark' or 'light'), or undefined if using system default */
+ themeMode?: ThemeMode;
+ /** The current theme variables, currently only supports 'accent' color */
+ themeVariables: ThemeVariables;
+ /** Function to set the theme mode */
+ setThemeMode: (themeMode: ThemeMode | undefined) => void;
+ /** Function to set theme variables */
+ setThemeVariables: (themeVariables: ThemeVariables | undefined) => void;
+}
+
+/**
+ * Hook to control the visual appearance of the AppKit modal
+ *
+ * @remarks
+ * This hook provides access to the theme mode and theme variables, allowing you to
+ * customize the modal's appearance. Use this hook when you need to implement dark/light
+ * mode or match the modal's appearance with your application's theme.
+ *
+ * Currently, the only supported theme variable is `accent`, which controls the primary
+ * accent color used throughout the modal interface.
+ *
+ * @returns {UseAppKitThemeReturn} An object containing:
+ * - `themeMode`: The current theme mode ('dark' or 'light')
+ * - `themeVariables`: The current theme variables (only 'accent' is supported)
+ * - `setThemeMode`: Function to change the theme mode
+ * - `setThemeVariables`: Function to update theme variables
+ *
+ * @throws {Error} If used outside of an AppKitProvider
+ *
+ * @example
+ * ```typescript
+ * import { useAppKitTheme } from '@reown/appkit-react-native';
+ *
+ * function MyComponent() {
+ * const { themeMode, themeVariables, setThemeMode, setThemeVariables } = useAppKitTheme();
+ *
+ * // Set theme to dark mode
+ * setThemeMode('dark');
+ *
+ * // Customize the accent color
+ * setThemeVariables({
+ * accent: '#00BB7F'
+ * });
+ *
+ * return (
+ *
+ * Current theme: {themeMode}
+ * Accent color: {themeVariables?.accent}
+ *
+ * );
+ * }
+ * ```
+ */
+export function useAppKitTheme(): UseAppKitThemeReturn {
+ useAppKitContext();
+ const { themeMode, themeVariables } = useSnapshot(ThemeController.state);
+
+ const stableFunctions = useMemo(
+ () => ({
+ setThemeMode: ThemeController.setThemeMode.bind(ThemeController),
+ setThemeVariables: ThemeController.setThemeVariables.bind(ThemeController)
+ }),
+ []
+ );
+
+ return {
+ themeMode,
+ themeVariables,
+ ...stableFunctions
+ };
+}
diff --git a/packages/appkit/src/hooks/useProvider.ts b/packages/appkit/src/hooks/useProvider.ts
index 48664462..1aa9b2fa 100644
--- a/packages/appkit/src/hooks/useProvider.ts
+++ b/packages/appkit/src/hooks/useProvider.ts
@@ -3,6 +3,7 @@ import { useMemo } from 'react';
import { useSnapshot } from 'valtio';
import { ConnectionsController, LogController } from '@reown/appkit-core-react-native';
import type { Provider, ChainNamespace } from '@reown/appkit-common-react-native';
+import { useAppKitContext } from './useAppKitContext';
/**
* Interface representing the result of the useProvider hook
@@ -29,14 +30,12 @@ interface ProviderResult {
*
* if (provider) {
* // Use the provider for blockchain operations
- * const balance = await provider.request({
- * method: 'eth_getBalance',
- * params: [address, 'latest']
- * });
+ * const balance = await provider.request({...});
* }
* ```
*/
export function useProvider(): ProviderResult {
+ useAppKitContext();
const { connection } = useSnapshot(ConnectionsController.state);
const returnValue = useMemo(() => {
diff --git a/packages/appkit/src/hooks/useWalletInfo.ts b/packages/appkit/src/hooks/useWalletInfo.ts
index 4fabc86d..67844f29 100644
--- a/packages/appkit/src/hooks/useWalletInfo.ts
+++ b/packages/appkit/src/hooks/useWalletInfo.ts
@@ -1,10 +1,41 @@
import { useMemo } from 'react';
import { useSnapshot } from 'valtio';
import { ConnectionsController } from '@reown/appkit-core-react-native';
-import { useAppKit } from './useAppKit';
+import { useAppKitContext } from './useAppKitContext';
+/**
+ * Hook to access information about the currently connected wallet
+ *
+ * @remarks
+ * This hook provides access to metadata about the connected wallet, such as its name,
+ * icon, and other identifying information. It automatically subscribes to wallet info
+ * changes via valtio.
+ *
+ * @returns An object containing:
+ * - `walletInfo`: Metadata about the currently connected wallet (name, icon, etc.)
+ *
+ * @example
+ * ```tsx
+ * function MyComponent() {
+ * const { walletInfo } = useWalletInfo();
+ *
+ * return (
+ *
+ * {walletInfo && (
+ * <>
+ *
+ * {walletInfo.name}
+ * >
+ * )}
+ *
+ * );
+ * }
+ * ```
+ *
+ * @throws {Error} If used outside of an AppKitProvider
+ */
export function useWalletInfo() {
- useAppKit(); // Use the hook for checks
+ useAppKitContext();
const { walletInfo: walletInfoSnapshot } = useSnapshot(ConnectionsController.state);
const walletInfo = useMemo(() => ({ walletInfo: walletInfoSnapshot }), [walletInfoSnapshot]);
diff --git a/packages/appkit/src/index.ts b/packages/appkit/src/index.ts
index 96c88cd4..cef464c0 100644
--- a/packages/appkit/src/index.ts
+++ b/packages/appkit/src/index.ts
@@ -32,6 +32,7 @@ export type { AppKitConfig } from './types';
export { useAppKit } from './hooks/useAppKit';
export { useProvider } from './hooks/useProvider';
export { useAccount, type Account as UseAccountReturn } from './hooks/useAccount';
+export { useAppKitTheme, type UseAppKitThemeReturn } from './hooks/useAppKitTheme';
export { useWalletInfo } from './hooks/useWalletInfo';
export { useAppKitEvents, useAppKitEventSubscription } from './hooks/useAppKitEvents';
export { useAppKitState } from './hooks/useAppKitState';
diff --git a/packages/core/src/__tests__/controllers/ThemeController.test.ts b/packages/core/src/__tests__/controllers/ThemeController.test.ts
index 7285c8d7..c2d3e06d 100644
--- a/packages/core/src/__tests__/controllers/ThemeController.test.ts
+++ b/packages/core/src/__tests__/controllers/ThemeController.test.ts
@@ -1,19 +1,142 @@
+import { Appearance } from 'react-native';
import { ThemeController } from '../../index';
+// Mock react-native Appearance
+jest.mock('react-native', () => ({
+ Appearance: {
+ getColorScheme: jest.fn()
+ }
+}));
+
+const mockedAppearance = Appearance as jest.Mocked;
+
// -- Tests --------------------------------------------------------------------
describe('ThemeController', () => {
- it('should have valid default state', () => {
- expect(ThemeController.state).toEqual({
- themeMode: undefined,
- themeVariables: {}
+ beforeEach(() => {
+ // Reset state before each test
+ ThemeController.setThemeMode();
+ ThemeController.setDefaultThemeMode();
+ ThemeController.setThemeVariables();
+ jest.clearAllMocks();
+ });
+
+ describe('initial state', () => {
+ it('should have valid default state', () => {
+ expect(ThemeController.state.themeMode).toBeDefined();
+ expect(ThemeController.state.defaultThemeMode).toBeUndefined();
+ expect(ThemeController.state.themeVariables).toEqual({});
+ });
+ });
+
+ describe('setThemeMode', () => {
+ it('should set theme mode to light', () => {
+ ThemeController.setThemeMode('light');
+ expect(ThemeController.state.themeMode).toBe('light');
+ });
+
+ it('should set theme mode to dark', () => {
+ ThemeController.setThemeMode('dark');
+ expect(ThemeController.state.themeMode).toBe('dark');
+ });
+
+ it('should fall back to system theme when undefined and system is dark', () => {
+ mockedAppearance.getColorScheme.mockReturnValue('dark');
+ ThemeController.setThemeMode();
+ expect(ThemeController.state.themeMode).toBe('dark');
+ expect(mockedAppearance.getColorScheme).toHaveBeenCalled();
+ });
+
+ it('should fall back to system theme when undefined and system is light', () => {
+ mockedAppearance.getColorScheme.mockReturnValue('light');
+ ThemeController.setThemeMode();
+ expect(ThemeController.state.themeMode).toBe('light');
+ expect(mockedAppearance.getColorScheme).toHaveBeenCalled();
+ });
+
+ it('should default to light when system returns null', () => {
+ mockedAppearance.getColorScheme.mockReturnValue(null);
+ ThemeController.setThemeMode();
+ expect(ThemeController.state.themeMode).toBe('light');
+ });
+ });
+
+ describe('setDefaultThemeMode', () => {
+ it('should set default theme mode and apply it', () => {
+ ThemeController.setDefaultThemeMode('dark');
+ expect(ThemeController.state.defaultThemeMode).toBe('dark');
+ expect(ThemeController.state.themeMode).toBe('dark');
+ });
+
+ it('should set default theme mode to light and apply it', () => {
+ ThemeController.setDefaultThemeMode('light');
+ expect(ThemeController.state.defaultThemeMode).toBe('light');
+ expect(ThemeController.state.themeMode).toBe('light');
+ });
+
+ it('should set default theme mode to undefined and fall back to system', () => {
+ mockedAppearance.getColorScheme.mockReturnValue('dark');
+ ThemeController.setDefaultThemeMode();
+ expect(ThemeController.state.defaultThemeMode).toBeUndefined();
+ expect(ThemeController.state.themeMode).toBe('dark');
+ });
+ });
+
+ describe('setThemeVariables', () => {
+ it('should set theme variables', () => {
+ const variables = { accent: '#000000' };
+ ThemeController.setThemeVariables(variables);
+ expect(ThemeController.state.themeVariables).toEqual(variables);
+ });
+
+ it('should override existing theme variables', () => {
+ const initialVariables = { accent: '#000000' };
+ const newVariables = { accent: '#FFFFFF' };
+
+ ThemeController.setThemeVariables(initialVariables);
+ ThemeController.setThemeVariables(newVariables);
+
+ expect(ThemeController.state.themeVariables).toEqual({
+ accent: '#FFFFFF'
+ });
+ });
+
+ it('should reset theme variables when undefined', () => {
+ const variables = { accent: '#000000' };
+ ThemeController.setThemeVariables(variables);
+ expect(ThemeController.state.themeVariables).toEqual(variables);
+
+ ThemeController.setThemeVariables(undefined);
+ expect(ThemeController.state.themeVariables).toEqual({});
+ });
+
+ it('should handle empty object', () => {
+ ThemeController.setThemeVariables({});
+ expect(ThemeController.state.themeVariables).toEqual({});
+ });
+ });
+
+ describe('subscribe', () => {
+ it('should return an unsubscribe function', () => {
+ const callback = jest.fn();
+ const unsubscribe = ThemeController.subscribe(callback);
+
+ expect(typeof unsubscribe).toBe('function');
+
+ unsubscribe();
+ });
+
+ it('should have subscribe method defined', () => {
+ expect(ThemeController.subscribe).toBeDefined();
+ expect(typeof ThemeController.subscribe).toBe('function');
});
});
- it('should update state correctly when changing theme', () => {
- ThemeController.setThemeMode('light');
- expect(ThemeController.state).toEqual({
- themeMode: 'light',
- themeVariables: {}
+ describe('state immutability', () => {
+ it('should maintain state reference but update values', () => {
+ const stateRef = ThemeController.state;
+ ThemeController.setThemeMode('dark');
+ expect(ThemeController.state).toBe(stateRef);
+ expect(ThemeController.state.themeMode).toBe('dark');
});
});
});
diff --git a/packages/core/src/controllers/ThemeController.ts b/packages/core/src/controllers/ThemeController.ts
index 6ebceb21..c64a5507 100644
--- a/packages/core/src/controllers/ThemeController.ts
+++ b/packages/core/src/controllers/ThemeController.ts
@@ -6,7 +6,7 @@ import type { ThemeMode, ThemeVariables } from '@reown/appkit-common-react-nativ
export interface ThemeControllerState {
themeMode?: ThemeMode;
defaultThemeMode?: ThemeMode;
- themeVariables?: ThemeVariables;
+ themeVariables: ThemeVariables;
}
// -- State --------------------------------------------- //
@@ -24,22 +24,24 @@ export const ThemeController = {
return sub(state, () => callback(state));
},
- setThemeMode(themeMode: ThemeControllerState['themeMode']) {
+ setThemeMode(themeMode?: ThemeControllerState['themeMode']) {
if (!themeMode) {
- state.themeMode = Appearance.getColorScheme() as ThemeMode;
+ state.themeMode = (Appearance.getColorScheme() ?? 'light') as ThemeMode;
} else {
state.themeMode = themeMode;
}
},
- setDefaultThemeMode(themeMode: ThemeControllerState['defaultThemeMode']) {
+ setDefaultThemeMode(themeMode?: ThemeControllerState['defaultThemeMode']) {
state.defaultThemeMode = themeMode;
this.setThemeMode(themeMode);
},
- setThemeVariables(themeVariables: ThemeControllerState['themeVariables']) {
+ setThemeVariables(themeVariables?: ThemeControllerState['themeVariables']) {
if (!themeVariables) {
state.themeVariables = {};
+
+ return;
}
state.themeVariables = { ...state.themeVariables, ...themeVariables };