From 7d078de2fb99eedbec13d16eba42a8ce1d3843c7 Mon Sep 17 00:00:00 2001
From: nacho <25931366+ignaciosantise@users.noreply.github.com>
Date: Wed, 5 Nov 2025 15:39:24 -0300
Subject: [PATCH 01/13] chore: added useAppKitTheme hook
---
.changeset/spotty-foxes-fry.md | 13 ++++
packages/appkit/src/hooks/useAppKitTheme.ts | 81 +++++++++++++++++++++
2 files changed, 94 insertions(+)
create mode 100644 .changeset/spotty-foxes-fry.md
create mode 100644 packages/appkit/src/hooks/useAppKitTheme.ts
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/packages/appkit/src/hooks/useAppKitTheme.ts b/packages/appkit/src/hooks/useAppKitTheme.ts
new file mode 100644
index 00000000..d944c2d4
--- /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 { useAppKit } from './useAppKit';
+
+/**
+ * Interface representing the result of the useAppKitTheme hook
+ */
+interface UseAppKitThemeReturn {
+ /** The current theme mode ('dark' or 'light'), or undefined if using system default */
+ themeMode: ThemeMode | undefined;
+ /** The current theme variables, currently only supports 'accent' color */
+ themeVariables: ThemeVariables | undefined;
+ /** 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 {
+ useAppKit(); // Use the hook for checks
+ const { themeMode, themeVariables } = useSnapshot(ThemeController.state);
+
+ const stableFunctions = useMemo(
+ () => ({
+ setThemeMode: ThemeController.setThemeMode.bind(ThemeController),
+ setThemeVariables: ThemeController.setThemeVariables.bind(ThemeController)
+ }),
+ []
+ );
+
+ return {
+ themeMode,
+ themeVariables,
+ ...stableFunctions
+ };
+}
From 43d157998f64f241b9dc27357c3770fb92dc5f62 Mon Sep 17 00:00:00 2001
From: nacho <25931366+ignaciosantise@users.noreply.github.com>
Date: Wed, 5 Nov 2025 15:56:55 -0300
Subject: [PATCH 02/13] chore: export hook
---
packages/appkit/src/hooks/useAppKitTheme.ts | 2 +-
packages/appkit/src/index.ts | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/packages/appkit/src/hooks/useAppKitTheme.ts b/packages/appkit/src/hooks/useAppKitTheme.ts
index d944c2d4..6d4712f0 100644
--- a/packages/appkit/src/hooks/useAppKitTheme.ts
+++ b/packages/appkit/src/hooks/useAppKitTheme.ts
@@ -7,7 +7,7 @@ import { useAppKit } from './useAppKit';
/**
* Interface representing the result of the useAppKitTheme hook
*/
-interface UseAppKitThemeReturn {
+export interface UseAppKitThemeReturn {
/** The current theme mode ('dark' or 'light'), or undefined if using system default */
themeMode: ThemeMode | undefined;
/** The current theme variables, currently only supports 'accent' color */
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';
From 61a4164356a51d70f1dcf93cf8f6e41197abfc01 Mon Sep 17 00:00:00 2001
From: nacho <25931366+ignaciosantise@users.noreply.github.com>
Date: Wed, 5 Nov 2025 16:12:27 -0300
Subject: [PATCH 03/13] chore: added JSDocs to hooks, improved context check
---
packages/appkit/src/AppKitContext.tsx | 2 +-
packages/appkit/src/hooks/useAccount.ts | 4 +-
packages/appkit/src/hooks/useAppKit.ts | 57 ++++++++++++----
packages/appkit/src/hooks/useAppKitContext.ts | 43 ++++++++++++
packages/appkit/src/hooks/useAppKitEvents.ts | 66 ++++++++++++++++++-
packages/appkit/src/hooks/useAppKitLogs.ts | 14 +---
packages/appkit/src/hooks/useAppKitState.ts | 37 ++++++++++-
packages/appkit/src/hooks/useAppKitTheme.ts | 4 +-
packages/appkit/src/hooks/useProvider.ts | 7 +-
packages/appkit/src/hooks/useWalletInfo.ts | 35 +++++++++-
.../core/src/controllers/ThemeController.ts | 6 +-
11 files changed, 234 insertions(+), 41 deletions(-)
create mode 100644 packages/appkit/src/hooks/useAppKitContext.ts
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/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..a0ca1665
--- /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('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.');
+ }
+
+ 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
index 6d4712f0..52e5b8b4 100644
--- a/packages/appkit/src/hooks/useAppKitTheme.ts
+++ b/packages/appkit/src/hooks/useAppKitTheme.ts
@@ -2,7 +2,7 @@ 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 { useAppKit } from './useAppKit';
+import { useAppKitContext } from './useAppKitContext';
/**
* Interface representing the result of the useAppKitTheme hook
@@ -62,7 +62,7 @@ export interface UseAppKitThemeReturn {
* ```
*/
export function useAppKitTheme(): UseAppKitThemeReturn {
- useAppKit(); // Use the hook for checks
+ useAppKitContext();
const { themeMode, themeVariables } = useSnapshot(ThemeController.state);
const stableFunctions = useMemo(
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/core/src/controllers/ThemeController.ts b/packages/core/src/controllers/ThemeController.ts
index 6ebceb21..511f808d 100644
--- a/packages/core/src/controllers/ThemeController.ts
+++ b/packages/core/src/controllers/ThemeController.ts
@@ -13,7 +13,7 @@ export interface ThemeControllerState {
const state = proxy({
themeMode: undefined,
defaultThemeMode: undefined,
- themeVariables: {}
+ themeVariables: undefined
});
// -- Controller ---------------------------------------- //
@@ -39,7 +39,9 @@ export const ThemeController = {
setThemeVariables(themeVariables: ThemeControllerState['themeVariables']) {
if (!themeVariables) {
- state.themeVariables = {};
+ state.themeVariables = undefined;
+
+ return;
}
state.themeVariables = { ...state.themeVariables, ...themeVariables };
From d98d04efedad55e575af6cf36ea0a0936210086b Mon Sep 17 00:00:00 2001
From: nacho <25931366+ignaciosantise@users.noreply.github.com>
Date: Wed, 5 Nov 2025 16:23:45 -0300
Subject: [PATCH 04/13] chore: added useAppKitTheme test
---
packages/appkit/babel.config.js | 3 +
packages/appkit/jest-setup.ts | 2 +
packages/appkit/jest.config.ts | 9 +
packages/appkit/package.json | 1 +
.../__tests__/hooks/useAppKitTheme.test.tsx | 188 ++++++++++++++++++
.../controllers/ThemeController.test.ts | 4 +-
6 files changed, 205 insertions(+), 2 deletions(-)
create mode 100644 packages/appkit/babel.config.js
create mode 100644 packages/appkit/jest-setup.ts
create mode 100644 packages/appkit/jest.config.ts
create mode 100644 packages/appkit/src/__tests__/hooks/useAppKitTheme.test.tsx
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/__tests__/hooks/useAppKitTheme.test.tsx b/packages/appkit/src/__tests__/hooks/useAppKitTheme.test.tsx
new file mode 100644
index 00000000..ae97ee16
--- /dev/null
+++ b/packages/appkit/src/__tests__/hooks/useAppKitTheme.test.tsx
@@ -0,0 +1,188 @@
+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 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).toEqual({});
+ });
+
+ 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: undefined,
+ 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/core/src/__tests__/controllers/ThemeController.test.ts b/packages/core/src/__tests__/controllers/ThemeController.test.ts
index 7285c8d7..ce4b772e 100644
--- a/packages/core/src/__tests__/controllers/ThemeController.test.ts
+++ b/packages/core/src/__tests__/controllers/ThemeController.test.ts
@@ -5,7 +5,7 @@ describe('ThemeController', () => {
it('should have valid default state', () => {
expect(ThemeController.state).toEqual({
themeMode: undefined,
- themeVariables: {}
+ themeVariables: undefined
});
});
@@ -13,7 +13,7 @@ describe('ThemeController', () => {
ThemeController.setThemeMode('light');
expect(ThemeController.state).toEqual({
themeMode: 'light',
- themeVariables: {}
+ themeVariables: undefined
});
});
});
From cbaf886dc95d93d4f6ab1ac8fb8077d3b33fbcef Mon Sep 17 00:00:00 2001
From: nacho <25931366+ignaciosantise@users.noreply.github.com>
Date: Wed, 5 Nov 2025 16:44:15 -0300
Subject: [PATCH 05/13] chore: fix test
---
packages/appkit/src/__tests__/hooks/useAppKitTheme.test.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/appkit/src/__tests__/hooks/useAppKitTheme.test.tsx b/packages/appkit/src/__tests__/hooks/useAppKitTheme.test.tsx
index ae97ee16..f0bd04e0 100644
--- a/packages/appkit/src/__tests__/hooks/useAppKitTheme.test.tsx
+++ b/packages/appkit/src/__tests__/hooks/useAppKitTheme.test.tsx
@@ -15,7 +15,7 @@ jest.mock('@reown/appkit-core-react-native', () => ({
ThemeController: {
state: {
themeMode: undefined,
- themeVariables: {}
+ themeVariables: undefined
},
setThemeMode: jest.fn(),
setThemeVariables: jest.fn()
From 73cf2e0716363cfd07c763dadae96ddf0ecb0aab Mon Sep 17 00:00:00 2001
From: nacho <25931366+ignaciosantise@users.noreply.github.com>
Date: Thu, 13 Nov 2025 16:44:48 -0300
Subject: [PATCH 06/13] chore: changed border radius of logo area
---
packages/appkit/src/partials/w3m-connecting-qrcode/index.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/appkit/src/partials/w3m-connecting-qrcode/index.tsx b/packages/appkit/src/partials/w3m-connecting-qrcode/index.tsx
index fc5aca8c..e436d9be 100644
--- a/packages/appkit/src/partials/w3m-connecting-qrcode/index.tsx
+++ b/packages/appkit/src/partials/w3m-connecting-qrcode/index.tsx
@@ -21,7 +21,7 @@ import { ReownButton } from './components/ReownButton';
import { useWindowDimensions } from 'react-native';
const LOGO_SIZE = 60;
-const LOGO_BORDER_RADIUS = 20;
+const LOGO_BORDER_RADIUS = 12;
export function ConnectingQrCode() {
const { height, width } = useWindowDimensions();
From da44cdddaf332e3534ca9fe773115a6dfbfd3223 Mon Sep 17 00:00:00 2001
From: nacho <25931366+ignaciosantise@users.noreply.github.com>
Date: Fri, 14 Nov 2025 10:55:12 -0300
Subject: [PATCH 07/13] chore: fixed useAppKitTheme tests
---
package.json | 2 +-
.../appkit/src/__tests__/hooks/useAppKitTheme.test.tsx | 8 ++++----
2 files changed, 5 insertions(+), 5 deletions(-)
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/src/__tests__/hooks/useAppKitTheme.test.tsx b/packages/appkit/src/__tests__/hooks/useAppKitTheme.test.tsx
index f0bd04e0..e6bfc6bb 100644
--- a/packages/appkit/src/__tests__/hooks/useAppKitTheme.test.tsx
+++ b/packages/appkit/src/__tests__/hooks/useAppKitTheme.test.tsx
@@ -36,7 +36,7 @@ describe('useAppKitTheme', () => {
// Reset ThemeController state
ThemeController.state = {
themeMode: undefined,
- themeVariables: {}
+ themeVariables: undefined
};
});
@@ -55,13 +55,13 @@ describe('useAppKitTheme', () => {
const { result } = renderHook(() => useAppKitTheme(), { wrapper });
expect(result.current.themeMode).toBeUndefined();
- expect(result.current.themeVariables).toEqual({});
+ expect(result.current.themeVariables).toBeUndefined();
});
it('should return dark theme mode when set', () => {
ThemeController.state = {
themeMode: 'dark',
- themeVariables: {}
+ themeVariables: undefined
};
const { result } = renderHook(() => useAppKitTheme(), { wrapper });
@@ -72,7 +72,7 @@ describe('useAppKitTheme', () => {
it('should return light theme mode when set', () => {
ThemeController.state = {
themeMode: 'light',
- themeVariables: {}
+ themeVariables: undefined
};
const { result } = renderHook(() => useAppKitTheme(), { wrapper });
From 67efd2cb0c7ff0082f7ef8a2f9590b1a90dae40a Mon Sep 17 00:00:00 2001
From: nacho <25931366+ignaciosantise@users.noreply.github.com>
Date: Fri, 14 Nov 2025 11:12:47 -0300
Subject: [PATCH 08/13] chore: made themeVariables default value be an empty
object again
---
.../__tests__/hooks/useAppKitTheme.test.tsx | 16 ++++++-------
packages/appkit/src/hooks/useAppKitTheme.ts | 4 ++--
.../core/src/controllers/ThemeController.ts | 23 +++++++++++--------
3 files changed, 23 insertions(+), 20 deletions(-)
diff --git a/packages/appkit/src/__tests__/hooks/useAppKitTheme.test.tsx b/packages/appkit/src/__tests__/hooks/useAppKitTheme.test.tsx
index e6bfc6bb..af06361e 100644
--- a/packages/appkit/src/__tests__/hooks/useAppKitTheme.test.tsx
+++ b/packages/appkit/src/__tests__/hooks/useAppKitTheme.test.tsx
@@ -14,8 +14,8 @@ jest.mock('valtio', () => ({
jest.mock('@reown/appkit-core-react-native', () => ({
ThemeController: {
state: {
- themeMode: undefined,
- themeVariables: undefined
+ themeMode: 'dark',
+ themeVariables: {}
},
setThemeMode: jest.fn(),
setThemeVariables: jest.fn()
@@ -35,8 +35,8 @@ describe('useAppKitTheme', () => {
jest.clearAllMocks();
// Reset ThemeController state
ThemeController.state = {
- themeMode: undefined,
- themeVariables: undefined
+ themeMode: 'dark',
+ themeVariables: {}
};
});
@@ -55,13 +55,13 @@ describe('useAppKitTheme', () => {
const { result } = renderHook(() => useAppKitTheme(), { wrapper });
expect(result.current.themeMode).toBeUndefined();
- expect(result.current.themeVariables).toBeUndefined();
+ expect(result.current.themeVariables).toBe({});
});
it('should return dark theme mode when set', () => {
ThemeController.state = {
themeMode: 'dark',
- themeVariables: undefined
+ themeVariables: {}
};
const { result } = renderHook(() => useAppKitTheme(), { wrapper });
@@ -72,7 +72,7 @@ describe('useAppKitTheme', () => {
it('should return light theme mode when set', () => {
ThemeController.state = {
themeMode: 'light',
- themeVariables: undefined
+ themeVariables: {}
};
const { result } = renderHook(() => useAppKitTheme(), { wrapper });
@@ -83,7 +83,7 @@ describe('useAppKitTheme', () => {
it('should return theme variables when set', () => {
const themeVariables = { accent: '#00BB7F' };
ThemeController.state = {
- themeMode: undefined,
+ themeMode: 'dark',
themeVariables
};
diff --git a/packages/appkit/src/hooks/useAppKitTheme.ts b/packages/appkit/src/hooks/useAppKitTheme.ts
index 52e5b8b4..96059fce 100644
--- a/packages/appkit/src/hooks/useAppKitTheme.ts
+++ b/packages/appkit/src/hooks/useAppKitTheme.ts
@@ -9,9 +9,9 @@ import { useAppKitContext } from './useAppKitContext';
*/
export interface UseAppKitThemeReturn {
/** The current theme mode ('dark' or 'light'), or undefined if using system default */
- themeMode: ThemeMode | undefined;
+ themeMode: ThemeMode;
/** The current theme variables, currently only supports 'accent' color */
- themeVariables: ThemeVariables | undefined;
+ themeVariables: ThemeVariables;
/** Function to set the theme mode */
setThemeMode: (themeMode: ThemeMode | undefined) => void;
/** Function to set theme variables */
diff --git a/packages/core/src/controllers/ThemeController.ts b/packages/core/src/controllers/ThemeController.ts
index 511f808d..a1bfacff 100644
--- a/packages/core/src/controllers/ThemeController.ts
+++ b/packages/core/src/controllers/ThemeController.ts
@@ -2,18 +2,20 @@ import { Appearance } from 'react-native';
import { proxy, subscribe as sub } from 'valtio';
import type { ThemeMode, ThemeVariables } from '@reown/appkit-common-react-native';
+const systemThemeMode = Appearance.getColorScheme() as ThemeMode;
+
// -- Types --------------------------------------------- //
export interface ThemeControllerState {
- themeMode?: ThemeMode;
+ themeMode: ThemeMode;
defaultThemeMode?: ThemeMode;
- themeVariables?: ThemeVariables;
+ themeVariables: ThemeVariables;
}
// -- State --------------------------------------------- //
const state = proxy({
- themeMode: undefined,
+ themeMode: systemThemeMode,
defaultThemeMode: undefined,
- themeVariables: undefined
+ themeVariables: {}
});
// -- Controller ---------------------------------------- //
@@ -24,7 +26,7 @@ export const ThemeController = {
return sub(state, () => callback(state));
},
- setThemeMode(themeMode: ThemeControllerState['themeMode']) {
+ setThemeMode(themeMode?: ThemeControllerState['themeMode']) {
if (!themeMode) {
state.themeMode = Appearance.getColorScheme() as ThemeMode;
} else {
@@ -32,14 +34,15 @@ export const ThemeController = {
}
},
- setDefaultThemeMode(themeMode: ThemeControllerState['defaultThemeMode']) {
- state.defaultThemeMode = themeMode;
- this.setThemeMode(themeMode);
+ setDefaultThemeMode(themeMode?: ThemeControllerState['defaultThemeMode']) {
+ const _systemThemeMode = Appearance.getColorScheme() as ThemeMode;
+ state.defaultThemeMode = themeMode ?? _systemThemeMode;
+ this.setThemeMode(themeMode ?? _systemThemeMode);
},
- setThemeVariables(themeVariables: ThemeControllerState['themeVariables']) {
+ setThemeVariables(themeVariables?: ThemeControllerState['themeVariables']) {
if (!themeVariables) {
- state.themeVariables = undefined;
+ state.themeVariables = {};
return;
}
From ab4eec5d157ab65558c8e8d758709d2f4bb0a21e Mon Sep 17 00:00:00 2001
From: nacho <25931366+ignaciosantise@users.noreply.github.com>
Date: Fri, 14 Nov 2025 11:14:39 -0300
Subject: [PATCH 09/13] chore: changed useAppKitContext error message
---
packages/appkit/src/hooks/useAppKitContext.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/appkit/src/hooks/useAppKitContext.ts b/packages/appkit/src/hooks/useAppKitContext.ts
index a0ca1665..7116b9ef 100644
--- a/packages/appkit/src/hooks/useAppKitContext.ts
+++ b/packages/appkit/src/hooks/useAppKitContext.ts
@@ -31,7 +31,7 @@ export const useAppKitContext = (): AppKitContextType => {
const context = useContext(AppKitContext);
if (context === undefined) {
- throw new Error('useAppKit must be used within an AppKitProvider');
+ throw new Error('useAppKitContext must be used within an AppKitProvider');
}
if (!context.appKit) {
From b9bdeed1b2d0db1b8576ebd1d85b2469e78ca5a1 Mon Sep 17 00:00:00 2001
From: nacho <25931366+ignaciosantise@users.noreply.github.com>
Date: Fri, 14 Nov 2025 11:50:59 -0300
Subject: [PATCH 10/13] chore: added themecontroller tests
---
.../__tests__/hooks/useAppKitTheme.test.tsx | 13 +-
packages/appkit/src/hooks/useAppKitTheme.ts | 2 +-
.../controllers/ThemeController.test.ts | 141 ++++++++++++++++--
.../core/src/controllers/ThemeController.ts | 13 +-
4 files changed, 148 insertions(+), 21 deletions(-)
diff --git a/packages/appkit/src/__tests__/hooks/useAppKitTheme.test.tsx b/packages/appkit/src/__tests__/hooks/useAppKitTheme.test.tsx
index af06361e..e2340c65 100644
--- a/packages/appkit/src/__tests__/hooks/useAppKitTheme.test.tsx
+++ b/packages/appkit/src/__tests__/hooks/useAppKitTheme.test.tsx
@@ -5,6 +5,13 @@ 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)
@@ -14,7 +21,7 @@ jest.mock('valtio', () => ({
jest.mock('@reown/appkit-core-react-native', () => ({
ThemeController: {
state: {
- themeMode: 'dark',
+ themeMode: undefined,
themeVariables: {}
},
setThemeMode: jest.fn(),
@@ -35,7 +42,7 @@ describe('useAppKitTheme', () => {
jest.clearAllMocks();
// Reset ThemeController state
ThemeController.state = {
- themeMode: 'dark',
+ themeMode: undefined,
themeVariables: {}
};
});
@@ -55,7 +62,7 @@ describe('useAppKitTheme', () => {
const { result } = renderHook(() => useAppKitTheme(), { wrapper });
expect(result.current.themeMode).toBeUndefined();
- expect(result.current.themeVariables).toBe({});
+ expect(result.current.themeVariables).toStrictEqual({});
});
it('should return dark theme mode when set', () => {
diff --git a/packages/appkit/src/hooks/useAppKitTheme.ts b/packages/appkit/src/hooks/useAppKitTheme.ts
index 96059fce..9e5b71a1 100644
--- a/packages/appkit/src/hooks/useAppKitTheme.ts
+++ b/packages/appkit/src/hooks/useAppKitTheme.ts
@@ -9,7 +9,7 @@ import { useAppKitContext } from './useAppKitContext';
*/
export interface UseAppKitThemeReturn {
/** The current theme mode ('dark' or 'light'), or undefined if using system default */
- themeMode: ThemeMode;
+ themeMode?: ThemeMode;
/** The current theme variables, currently only supports 'accent' color */
themeVariables: ThemeVariables;
/** Function to set the theme mode */
diff --git a/packages/core/src/__tests__/controllers/ThemeController.test.ts b/packages/core/src/__tests__/controllers/ThemeController.test.ts
index ce4b772e..383b69a9 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: undefined
+ beforeEach(() => {
+ // Reset state before each test
+ ThemeController.setThemeMode(undefined);
+ ThemeController.setDefaultThemeMode(undefined);
+ ThemeController.setThemeVariables(undefined);
+ 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(undefined);
+ 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(undefined);
+ 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(undefined);
+ 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(undefined);
+ 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: undefined
+ 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 a1bfacff..c64a5507 100644
--- a/packages/core/src/controllers/ThemeController.ts
+++ b/packages/core/src/controllers/ThemeController.ts
@@ -2,18 +2,16 @@ import { Appearance } from 'react-native';
import { proxy, subscribe as sub } from 'valtio';
import type { ThemeMode, ThemeVariables } from '@reown/appkit-common-react-native';
-const systemThemeMode = Appearance.getColorScheme() as ThemeMode;
-
// -- Types --------------------------------------------- //
export interface ThemeControllerState {
- themeMode: ThemeMode;
+ themeMode?: ThemeMode;
defaultThemeMode?: ThemeMode;
themeVariables: ThemeVariables;
}
// -- State --------------------------------------------- //
const state = proxy({
- themeMode: systemThemeMode,
+ themeMode: undefined,
defaultThemeMode: undefined,
themeVariables: {}
});
@@ -28,16 +26,15 @@ export const ThemeController = {
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']) {
- const _systemThemeMode = Appearance.getColorScheme() as ThemeMode;
- state.defaultThemeMode = themeMode ?? _systemThemeMode;
- this.setThemeMode(themeMode ?? _systemThemeMode);
+ state.defaultThemeMode = themeMode;
+ this.setThemeMode(themeMode);
},
setThemeVariables(themeVariables?: ThemeControllerState['themeVariables']) {
From d4601cedfab19de946f826240cf201bc7c1dd769 Mon Sep 17 00:00:00 2001
From: nacho <25931366+ignaciosantise@users.noreply.github.com>
Date: Fri, 14 Nov 2025 11:54:25 -0300
Subject: [PATCH 11/13] chore: removed unnecesary undefined
---
.../__tests__/controllers/ThemeController.test.ts | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/packages/core/src/__tests__/controllers/ThemeController.test.ts b/packages/core/src/__tests__/controllers/ThemeController.test.ts
index 383b69a9..c2d3e06d 100644
--- a/packages/core/src/__tests__/controllers/ThemeController.test.ts
+++ b/packages/core/src/__tests__/controllers/ThemeController.test.ts
@@ -14,9 +14,9 @@ const mockedAppearance = Appearance as jest.Mocked;
describe('ThemeController', () => {
beforeEach(() => {
// Reset state before each test
- ThemeController.setThemeMode(undefined);
- ThemeController.setDefaultThemeMode(undefined);
- ThemeController.setThemeVariables(undefined);
+ ThemeController.setThemeMode();
+ ThemeController.setDefaultThemeMode();
+ ThemeController.setThemeVariables();
jest.clearAllMocks();
});
@@ -41,21 +41,21 @@ describe('ThemeController', () => {
it('should fall back to system theme when undefined and system is dark', () => {
mockedAppearance.getColorScheme.mockReturnValue('dark');
- ThemeController.setThemeMode(undefined);
+ 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(undefined);
+ 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(undefined);
+ ThemeController.setThemeMode();
expect(ThemeController.state.themeMode).toBe('light');
});
});
@@ -75,7 +75,7 @@ describe('ThemeController', () => {
it('should set default theme mode to undefined and fall back to system', () => {
mockedAppearance.getColorScheme.mockReturnValue('dark');
- ThemeController.setDefaultThemeMode(undefined);
+ ThemeController.setDefaultThemeMode();
expect(ThemeController.state.defaultThemeMode).toBeUndefined();
expect(ThemeController.state.themeMode).toBe('dark');
});
From a731d156f5e91f3218f105b690f4a7814301ea51 Mon Sep 17 00:00:00 2001
From: nacho <25931366+ignaciosantise@users.noreply.github.com>
Date: Fri, 14 Nov 2025 12:03:48 -0300
Subject: [PATCH 12/13] chore: correctly clean themeVariables if called with
setThemeVariables({})
---
packages/core/src/controllers/ThemeController.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/core/src/controllers/ThemeController.ts b/packages/core/src/controllers/ThemeController.ts
index c64a5507..142c904e 100644
--- a/packages/core/src/controllers/ThemeController.ts
+++ b/packages/core/src/controllers/ThemeController.ts
@@ -38,7 +38,7 @@ export const ThemeController = {
},
setThemeVariables(themeVariables?: ThemeControllerState['themeVariables']) {
- if (!themeVariables) {
+ if (Object.keys(themeVariables ?? {}).length === 0) {
state.themeVariables = {};
return;
From 32e74084ec0ae76a386b6e671a482d83b517636d Mon Sep 17 00:00:00 2001
From: nacho <25931366+ignaciosantise@users.noreply.github.com>
Date: Fri, 14 Nov 2025 12:12:11 -0300
Subject: [PATCH 13/13] chore: setThemeVariables change
---
packages/core/src/controllers/ThemeController.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/core/src/controllers/ThemeController.ts b/packages/core/src/controllers/ThemeController.ts
index 142c904e..c64a5507 100644
--- a/packages/core/src/controllers/ThemeController.ts
+++ b/packages/core/src/controllers/ThemeController.ts
@@ -38,7 +38,7 @@ export const ThemeController = {
},
setThemeVariables(themeVariables?: ThemeControllerState['themeVariables']) {
- if (Object.keys(themeVariables ?? {}).length === 0) {
+ if (!themeVariables) {
state.themeVariables = {};
return;