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: