import React, { createContext, useContext, useEffect, useState, useMemo, useCallback } from "react"; import { colorScheme } from "nativewind"; import AsyncStorage from "@react-native-async-storage/async-storage"; import { getStoragePrefix } from "../services/api-config"; export type ThemePreference = "light" | "dark" | "system"; function themeKey(): string { return `@${getStoragePrefix()}/theme-preference`; } function applyTheme(isDark: boolean) { colorScheme.set(isDark ? "dark" : "light"); if (typeof document !== "undefined") { if (isDark) { document.documentElement.classList.add("dark"); } else { document.documentElement.classList.remove("dark"); } } } interface ThemeContextValue { preference: ThemePreference; setPreference: (pref: ThemePreference) => void; } const ThemeContext = createContext({ preference: "system", setPreference: () => {}, }); export function ThemeProvider({ children }: { children: React.ReactNode }) { const [preference, setPreferenceState] = useState("system"); useEffect(() => { AsyncStorage.getItem(themeKey()) .then((stored) => { if (stored === "light" || stored === "dark" || stored === "system") { setPreferenceState(stored); } }) .catch(() => {}); }, []); const setPreference = useCallback((pref: ThemePreference) => { AsyncStorage.setItem(themeKey(), pref).catch(() => {}); setPreferenceState(pref); }, []); useEffect(() => { if (preference !== "system") { applyTheme(preference === "dark"); return; } if (typeof window !== "undefined" && typeof window.matchMedia === "function") { // Web system mode: react-native-css-interop's internal Appearance // listener uses the removed `MediaQueryList.addListener` API // (Chrome 121+) so it never fires. Drive it ourselves via // `addEventListener` instead. const mq = window.matchMedia("(prefers-color-scheme: dark)"); applyTheme(mq.matches); const handler = (e: MediaQueryListEvent) => applyTheme(e.matches); mq.addEventListener("change", handler); return () => mq.removeEventListener("change", handler); } // Native system mode: react-native-css-interop's Appearance listener // already tracks OS changes. Calling colorScheme.set("system") would // pass null to the Android native module which rejects non-nullable // parameters and crashes — leave it alone. return undefined; }, [preference]); const contextValue = useMemo(() => ({ preference, setPreference }), [preference, setPreference]); return {children}; } export function useTheme() { return useContext(ThemeContext); }