diff --git a/packages/walletkit-ui/package.json b/packages/walletkit-ui/package.json
index e8ebc64..848a09d 100644
--- a/packages/walletkit-ui/package.json
+++ b/packages/walletkit-ui/package.json
@@ -21,6 +21,7 @@
"devDependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
+ "@testing-library/react-hooks": "^8.0.1",
"jest-environment-jsdom": "^29.3.1"
},
"scripts": {
diff --git a/packages/walletkit-ui/src/contexts/ThemeProvider.test.tsx b/packages/walletkit-ui/src/contexts/ThemeProvider.test.tsx
new file mode 100644
index 0000000..b453ffd
--- /dev/null
+++ b/packages/walletkit-ui/src/contexts/ThemeProvider.test.tsx
@@ -0,0 +1,84 @@
+/**
+ * @jest-environment jsdom
+ */
+
+import { render } from "@testing-library/react";
+import { renderHook } from "@testing-library/react-hooks";
+import React from "react";
+
+import { ThemeProvider, useTheme, useThemeContext } from "./ThemeProvider";
+
+const consoleLog = jest.spyOn(console, "log").mockImplementation(jest.fn);
+const consoleError = jest.spyOn(console, "error").mockImplementation(jest.fn);
+const logger = { error: () => consoleError, info: () => consoleLog };
+
+describe("useTheme hook test", () => {
+ it("should pass when theme is not set", async () => {
+ const desiredTheme = "dark";
+ const api = {
+ set: jest.fn(),
+ get: async () => null,
+ };
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useTheme({
+ api,
+ colorScheme: desiredTheme,
+ logger,
+ })
+ );
+ await waitForNextUpdate();
+ expect(result.current.theme).toBe(desiredTheme);
+ expect(result.current.isThemeLoaded).toBe(true);
+ });
+
+ it("should pass when theme is already set", async () => {
+ const desiredTheme = "dark";
+ const api = {
+ set: jest.fn(),
+ get: async () => desiredTheme,
+ };
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useTheme({ api, colorScheme: "light", logger })
+ );
+ await waitForNextUpdate();
+ expect(result.current.theme).toBe(desiredTheme);
+ expect(result.current.isThemeLoaded).toBe(true);
+ });
+
+ it("should pass when theme is not set and colorScheme is not defined", async () => {
+ const api = {
+ set: jest.fn(),
+ get: async () => null,
+ };
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useTheme({ api, logger })
+ );
+ await waitForNextUpdate();
+ expect(result.current.theme).toBe("light");
+ expect(result.current.isThemeLoaded).toBe(true);
+ });
+});
+
+describe("ThemeProvider Context test", () => {
+ it("should match snapshot", () => {
+ function ThemeProviderComponent(): JSX.Element {
+ const { isLight, theme } = useThemeContext();
+ return (
+
+ {isLight.toString()}
+ {theme}
+
+ );
+ }
+ const api = {
+ set: jest.fn(),
+ get: async () => "light",
+ };
+ const rendered = render(
+
+
+
+ );
+ expect(rendered).toMatchSnapshot();
+ });
+});
diff --git a/packages/walletkit-ui/src/contexts/ThemeProvider.tsx b/packages/walletkit-ui/src/contexts/ThemeProvider.tsx
new file mode 100644
index 0000000..70f6f4e
--- /dev/null
+++ b/packages/walletkit-ui/src/contexts/ThemeProvider.tsx
@@ -0,0 +1,95 @@
+import React, {
+ createContext,
+ PropsWithChildren,
+ useContext,
+ useEffect,
+ useState,
+} from "react";
+
+import { BaseLogger } from "./logger";
+
+interface ThemeLoader {
+ theme: NonNullable;
+ isThemeLoaded: boolean;
+}
+
+type ColorSchemeName = "light" | "dark" | null | undefined;
+
+interface ThemeContextI {
+ api: {
+ get: () => Promise;
+ set: (language: NonNullable) => Promise;
+ };
+ logger: BaseLogger;
+ colorScheme?: ColorSchemeName;
+}
+
+export function useTheme({
+ api,
+ colorScheme,
+ logger,
+}: ThemeContextI): ThemeLoader {
+ const [isThemeLoaded, setIsThemeLoaded] = useState(false);
+ const [theme, setTheme] = useState>("light");
+
+ useEffect(() => {
+ api
+ .get()
+ .then((t) => {
+ let currentTheme: NonNullable = "light";
+ if (t !== null && t !== undefined) {
+ currentTheme = t as NonNullable;
+ } else if (colorScheme !== null && colorScheme !== undefined) {
+ currentTheme = colorScheme;
+ }
+ setTheme(currentTheme);
+ })
+ .catch(logger?.error)
+ .finally(() => setIsThemeLoaded(true));
+ }, []);
+
+ return {
+ isThemeLoaded,
+ theme,
+ };
+}
+
+interface Theme {
+ theme: NonNullable;
+ setTheme: (theme: NonNullable) => void;
+ isLight: boolean;
+}
+
+const ThemeContext = createContext(undefined as any);
+
+export function useThemeContext(): Theme {
+ return useContext(ThemeContext);
+}
+
+export interface ThemeProviderProps extends PropsWithChildren<{}> {
+ logger: BaseLogger;
+}
+
+export function ThemeProvider(
+ props: ThemeContextI & React.PropsWithChildren
+): JSX.Element | null {
+ const { children, api, colorScheme, logger } = props;
+ const { theme } = useTheme({ api, colorScheme, logger });
+ const [currentTheme, setTheme] =
+ useState>(theme);
+
+ useEffect(() => {
+ setTheme(theme);
+ }, [theme]);
+
+ // eslint-disable-next-line react/jsx-no-constructed-context-values
+ const context: Theme = {
+ theme: currentTheme,
+ setTheme,
+ isLight: currentTheme === "light",
+ };
+
+ return (
+ {children}
+ );
+}
diff --git a/packages/walletkit-ui/src/contexts/__snapshots__/ThemeProvider.test.tsx.snap b/packages/walletkit-ui/src/contexts/__snapshots__/ThemeProvider.test.tsx.snap
new file mode 100644
index 0000000..534a97a
--- /dev/null
+++ b/packages/walletkit-ui/src/contexts/__snapshots__/ThemeProvider.test.tsx.snap
@@ -0,0 +1,80 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ThemeProvider Context test should match snapshot 1`] = `
+{
+ "asFragment": [Function],
+ "baseElement":
+
+
+
+ true
+
+
+ light
+
+
+
+ ,
+ "container":
+
+
+ true
+
+
+ light
+
+
+
,
+ "debug": [Function],
+ "findAllByAltText": [Function],
+ "findAllByDisplayValue": [Function],
+ "findAllByLabelText": [Function],
+ "findAllByPlaceholderText": [Function],
+ "findAllByRole": [Function],
+ "findAllByTestId": [Function],
+ "findAllByText": [Function],
+ "findAllByTitle": [Function],
+ "findByAltText": [Function],
+ "findByDisplayValue": [Function],
+ "findByLabelText": [Function],
+ "findByPlaceholderText": [Function],
+ "findByRole": [Function],
+ "findByTestId": [Function],
+ "findByText": [Function],
+ "findByTitle": [Function],
+ "getAllByAltText": [Function],
+ "getAllByDisplayValue": [Function],
+ "getAllByLabelText": [Function],
+ "getAllByPlaceholderText": [Function],
+ "getAllByRole": [Function],
+ "getAllByTestId": [Function],
+ "getAllByText": [Function],
+ "getAllByTitle": [Function],
+ "getByAltText": [Function],
+ "getByDisplayValue": [Function],
+ "getByLabelText": [Function],
+ "getByPlaceholderText": [Function],
+ "getByRole": [Function],
+ "getByTestId": [Function],
+ "getByText": [Function],
+ "getByTitle": [Function],
+ "queryAllByAltText": [Function],
+ "queryAllByDisplayValue": [Function],
+ "queryAllByLabelText": [Function],
+ "queryAllByPlaceholderText": [Function],
+ "queryAllByRole": [Function],
+ "queryAllByTestId": [Function],
+ "queryAllByText": [Function],
+ "queryAllByTitle": [Function],
+ "queryByAltText": [Function],
+ "queryByDisplayValue": [Function],
+ "queryByLabelText": [Function],
+ "queryByPlaceholderText": [Function],
+ "queryByRole": [Function],
+ "queryByTestId": [Function],
+ "queryByText": [Function],
+ "queryByTitle": [Function],
+ "rerender": [Function],
+ "unmount": [Function],
+}
+`;
diff --git a/packages/walletkit-ui/src/contexts/index.ts b/packages/walletkit-ui/src/contexts/index.ts
index 14c432e..76006cf 100644
--- a/packages/walletkit-ui/src/contexts/index.ts
+++ b/packages/walletkit-ui/src/contexts/index.ts
@@ -1,2 +1,3 @@
export * from "./NetworkContext";
+export * from "./ThemeProvider";
export * from "./WhaleContext";
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ccce6a5..104dd6e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -31,6 +31,7 @@ importers:
'@birthdayresearch/sticky-jest': ^0.3.2
'@testing-library/jest-dom': ^5.16.5
'@testing-library/react': ^13.4.0
+ '@testing-library/react-hooks': ^8.0.1
'@waveshq/standard-defichain-jellyfishsdk': ^0.19.0
'@waveshq/standard-web': ^0.19.0
'@waveshq/standard-web-linter': ^0.19.0
@@ -53,6 +54,7 @@ importers:
devDependencies:
'@testing-library/jest-dom': 5.16.5
'@testing-library/react': 13.4.0_biqbaboplfbrettd7655fr4n2y
+ '@testing-library/react-hooks': 8.0.1_biqbaboplfbrettd7655fr4n2y
jest-environment-jsdom: 29.3.1
packages:
@@ -1124,6 +1126,28 @@ packages:
redent: 3.0.0
dev: true
+ /@testing-library/react-hooks/8.0.1_biqbaboplfbrettd7655fr4n2y:
+ resolution: {integrity: sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==}
+ engines: {node: '>=12'}
+ peerDependencies:
+ '@types/react': ^16.9.0 || ^17.0.0
+ react: ^16.9.0 || ^17.0.0
+ react-dom: ^16.9.0 || ^17.0.0
+ react-test-renderer: ^16.9.0 || ^17.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ react-dom:
+ optional: true
+ react-test-renderer:
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.20.6
+ react: 18.2.0
+ react-dom: 18.2.0_react@18.2.0
+ react-error-boundary: 3.1.4_react@18.2.0
+ dev: true
+
/@testing-library/react/13.4.0_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw==}
engines: {node: '>=12'}
@@ -5110,6 +5134,16 @@ packages:
react: 18.2.0
scheduler: 0.23.0
+ /react-error-boundary/3.1.4_react@18.2.0:
+ resolution: {integrity: sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==}
+ engines: {node: '>=10', npm: '>=6'}
+ peerDependencies:
+ react: '>=16.13.1'
+ dependencies:
+ '@babel/runtime': 7.20.6
+ react: 18.2.0
+ dev: true
+
/react-icons/4.7.1_react@18.2.0:
resolution: {integrity: sha512-yHd3oKGMgm7zxo3EA7H2n7vxSoiGmHk5t6Ou4bXsfcgWyhfDKMpyKfhHR6Bjnn63c+YXBLBPUql9H4wPJM6sXw==}
peerDependencies: