- Create ThemeContext with light/dark/system mode support - Add DarkColors palette for dark mode UI - Extend Colors object with full dark theme variants - Update useThemeColor hook to use ThemeContext - Add useThemeColors, useResolvedTheme, useIsDarkMode hooks - Update RootLayout (native and web) with ThemeProvider - Add theme toggle UI in ProfileDrawer settings - Theme preference persisted to AsyncStorage - Add comprehensive tests for ThemeContext and hooks 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
132 lines
4.5 KiB
TypeScript
132 lines
4.5 KiB
TypeScript
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
|
|
import { Stack, router, useRootNavigationState, useSegments } from 'expo-router';
|
|
import * as SplashScreen from 'expo-splash-screen';
|
|
import { StatusBar } from 'expo-status-bar';
|
|
import { useEffect, useRef } from 'react';
|
|
// import 'react-native-reanimated'; // Often causes issues on web if not configured perfectly, optional here
|
|
import { Platform, StyleSheet, View } from 'react-native';
|
|
|
|
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
|
import { ToastProvider } from '@/components/ui/Toast';
|
|
import { AuthProvider, useAuth } from '@/contexts/AuthContext';
|
|
import { BeneficiaryProvider } from '@/contexts/BeneficiaryContext';
|
|
import { ThemeProvider as WellNuoThemeProvider, useTheme } from '@/contexts/ThemeContext';
|
|
import { Colors } from '@/constants/theme';
|
|
|
|
// Polyfill for $ to prevent ReferenceError: Cannot access '$' before initialization
|
|
// This is a workaround for some web-specific bundling issues in this project.
|
|
if (Platform.OS === 'web' && typeof window !== 'undefined') {
|
|
// @ts-ignore
|
|
if (typeof window.$ === 'undefined') {
|
|
// @ts-ignore
|
|
window.$ = undefined;
|
|
}
|
|
}
|
|
|
|
// Prevent auto-hide, ignore errors if splash not available
|
|
SplashScreen.preventAutoHideAsync().catch(() => { });
|
|
|
|
let splashHidden = false;
|
|
|
|
function RootLayoutNav() {
|
|
const { resolvedTheme, isDark } = useTheme();
|
|
const { isAuthenticated, isInitializing } = useAuth();
|
|
const segments = useSegments();
|
|
const navigationState = useRootNavigationState();
|
|
const themeColors = Colors[resolvedTheme];
|
|
|
|
// Track if initial redirect was done
|
|
const hasInitialRedirect = useRef(false);
|
|
|
|
// Note: Token URL login feature not yet implemented
|
|
// Would need api.setToken() method and AuthContext.refreshAuth() call
|
|
|
|
useEffect(() => {
|
|
if (!navigationState?.key) return;
|
|
if (isInitializing) return;
|
|
|
|
if (!splashHidden) {
|
|
splashHidden = true;
|
|
SplashScreen.hideAsync().catch(() => { });
|
|
}
|
|
|
|
const inAuthGroup = segments[0] === '(auth)';
|
|
|
|
if (!hasInitialRedirect.current) {
|
|
hasInitialRedirect.current = true;
|
|
|
|
// URL Param Auth Check (Simple version)
|
|
if (Platform.OS === 'web') {
|
|
const url = new URL(window.location.href);
|
|
const token = url.searchParams.get('token');
|
|
if (token) {
|
|
// If we have a token, we might be authenticated now (race condition with AuthProvider).
|
|
// If isAuthenticated is true, good. If not, maybe we need to wait.
|
|
// But for now, let's allow the flow.
|
|
}
|
|
}
|
|
|
|
if (!isAuthenticated && !inAuthGroup) {
|
|
router.replace('/(auth)/login');
|
|
return;
|
|
}
|
|
}
|
|
|
|
}, [isAuthenticated, isInitializing, navigationState?.key]);
|
|
|
|
if (isInitializing) {
|
|
return <LoadingSpinner fullScreen message="Loading..." />;
|
|
}
|
|
|
|
return (
|
|
<ThemeProvider value={resolvedTheme === 'dark' ? DarkTheme : DefaultTheme}>
|
|
<View style={[styles.webContainer, { backgroundColor: isDark ? '#1a1a2e' : '#e5e5e5' }]}>
|
|
<View style={[styles.mobileWrapper, { backgroundColor: themeColors.background }]}>
|
|
<Stack screenOptions={{ headerShown: false }}>
|
|
<Stack.Screen name="(auth)" />
|
|
<Stack.Screen name="(tabs)" />
|
|
<Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
|
|
</Stack>
|
|
<StatusBar style={isDark ? 'light' : 'dark'} />
|
|
</View>
|
|
</View>
|
|
</ThemeProvider>
|
|
);
|
|
}
|
|
|
|
export default function RootLayout() {
|
|
return (
|
|
<WellNuoThemeProvider>
|
|
<AuthProvider>
|
|
<BeneficiaryProvider>
|
|
<ToastProvider>
|
|
<RootLayoutNav />
|
|
</ToastProvider>
|
|
</BeneficiaryProvider>
|
|
</AuthProvider>
|
|
</WellNuoThemeProvider>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
webContainer: {
|
|
flex: 1,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
mobileWrapper: {
|
|
flex: 1,
|
|
width: '100%',
|
|
maxWidth: 430,
|
|
shadowColor: '#000',
|
|
shadowOffset: {
|
|
width: 0,
|
|
height: 2,
|
|
},
|
|
shadowOpacity: 0.25,
|
|
shadowRadius: 3.84,
|
|
elevation: 5,
|
|
overflow: 'hidden',
|
|
}
|
|
});
|