import { createContext, FC, PropsWithChildren, useCallback, useContext, useEffect, useMemo, } from "react" import { StyleProp, useColorScheme } from "react-native" import { DarkTheme as NavDarkTheme, DefaultTheme as NavDefaultTheme, Theme as NavTheme, } from "@react-navigation/native" import { useMMKVString } from "react-native-mmkv" import { storage } from "@/utils/storage" import { setImperativeTheming } from "./context.utils" import { darkTheme, lightTheme } from "./theme" import type { AllowedStylesT, ImmutableThemeContextModeT, Theme, ThemeContextModeT, ThemedFnT, ThemedStyle, } from "./types" export type ThemeContextType = { navigationTheme: NavTheme setThemeContextOverride: (newTheme: ThemeContextModeT) => void theme: Theme themeContext: ImmutableThemeContextModeT themed: ThemedFnT } export const ThemeContext = createContext(null) export interface ThemeProviderProps { initialContext?: ThemeContextModeT } /** * The ThemeProvider is the heart and soul of the design token system. It provides a context wrapper * for your entire app to consume the design tokens as well as global functionality like the app's theme. * * To get started, you want to wrap your entire app's JSX hierarchy in `ThemeProvider` * and then use the `useAppTheme()` hook to access the theme context. * * Documentation: https://docs.infinite.red/ignite-cli/boilerplate/app/theme/Theming/ */ export const ThemeProvider: FC> = ({ children, initialContext, }) => { // The operating system theme: const systemColorScheme = useColorScheme() // Our saved theme context: can be "light", "dark", or undefined (system theme) const [themeScheme, setThemeScheme] = useMMKVString("ignite.themeScheme", storage) /** * This function is used to set the theme context and is exported from the useAppTheme() hook. * - setThemeContextOverride("dark") sets the app theme to dark no matter what the system theme is. * - setThemeContextOverride("light") sets the app theme to light no matter what the system theme is. * - setThemeContextOverride(undefined) the app will follow the operating system theme. */ const setThemeContextOverride = useCallback( (newTheme: ThemeContextModeT) => { setThemeScheme(newTheme) }, [setThemeScheme], ) /** * initialContext is the theme context passed in from the app.tsx file and always takes precedence. * themeScheme is the value from MMKV. If undefined, we fall back to the system theme * systemColorScheme is the value from the device. If undefined, we fall back to "light" */ const themeContext: ImmutableThemeContextModeT = useMemo(() => { const t = initialContext || themeScheme || (!!systemColorScheme ? systemColorScheme : "light") return t === "dark" ? "dark" : "light" }, [initialContext, themeScheme, systemColorScheme]) const navigationTheme: NavTheme = useMemo(() => { switch (themeContext) { case "dark": return NavDarkTheme default: return NavDefaultTheme } }, [themeContext]) const theme: Theme = useMemo(() => { switch (themeContext) { case "dark": return darkTheme default: return lightTheme } }, [themeContext]) useEffect(() => { setImperativeTheming(theme) }, [theme]) const themed = useCallback( (styleOrStyleFn: AllowedStylesT) => { const flatStyles = [styleOrStyleFn].flat(3) as (ThemedStyle | StyleProp)[] const stylesArray = flatStyles.map((f) => { if (typeof f === "function") { return (f as ThemedStyle)(theme) } else { return f } }) // Flatten the array of styles into a single object return Object.assign({}, ...stylesArray) as T }, [theme], ) const value = { navigationTheme, theme, themeContext, setThemeContextOverride, themed, } return {children} } /** * This is the primary hook that you will use to access the theme context in your components. * Documentation: https://docs.infinite.red/ignite-cli/boilerplate/app/theme/useAppTheme.tsx/ */ export const useAppTheme = () => { const context = useContext(ThemeContext) if (!context) { throw new Error("useAppTheme must be used within an ThemeProvider") } return context }