WellNuo/components/layout/RootLayout.web.tsx
Sergei 7105bb72f7 Stable Light version - App Store submission
WellNuo Lite architecture:
- Simplified navigation flow with NavigationController
- Profile editing with API sync (/auth/profile endpoint)
- OTP verification improvements
- ESP WiFi provisioning setup (espProvisioning.ts)
- E2E testing infrastructure (Playwright)
- Speech recognition hooks (web/native)
- Backend auth enhancements

This is the stable version submitted to App Store.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-12 20:28:18 -08:00

154 lines
5.7 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 { useColorScheme } from '@/hooks/use-color-scheme';
// 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 colorScheme = useColorScheme();
const { isAuthenticated, isInitializing, setToken } = useAuth();
const segments = useSegments();
const navigationState = useRootNavigationState();
// Track if initial redirect was done
const hasInitialRedirect = useRef(false);
useEffect(() => {
// Check for token in URL query params (Web only feature)
if (Platform.OS === 'web') {
const url = new URL(window.location.href);
const tokenParam = url.searchParams.get('token');
if (tokenParam) {
console.log('[Layout] Found token in URL, attempting login...');
// Need a way to set token in AuthContext.
// Since useAuth gives us setToken (assuming it does, or login logic), we can use it.
// If AuthContext doesn't expose setToken directly, we might need to modify AuthContext
// or use specific login method. Assuming 'api.setToken' works and then we refresh auth.
// actually useAuth usually exposes a way.
// Let's assume for a moment we can use the injected setToken (if I added it) or just use api.
// For now, let's try to set it via API and reload user?
// Actually, best is if AuthContext handles it.
// But let's look at AuthContext later. For now I will try to use the api directly to verify constraint.
const handleToken = async () => {
await setToken(tokenParam);
// Refresh the page to clear the token from URL
window.location.href = window.location.origin + window.location.pathname;
};
handleToken();
}
}
}, []);
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={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<View style={styles.webContainer}>
<View style={styles.mobileWrapper}>
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="(auth)" />
<Stack.Screen name="(tabs)" />
<Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
</Stack>
<StatusBar style="auto" />
</View>
</View>
</ThemeProvider>
);
}
export default function RootLayout() {
return (
<AuthProvider>
<BeneficiaryProvider>
<ToastProvider>
<RootLayoutNav />
</ToastProvider>
</BeneficiaryProvider>
</AuthProvider>
);
}
const styles = StyleSheet.create({
webContainer: {
flex: 1,
backgroundColor: '#e5e5e5', // Light gray background for desktop
alignItems: 'center',
justifyContent: 'center',
},
mobileWrapper: {
flex: 1,
width: '100%',
maxWidth: 430, // Mobile width constraint
backgroundColor: '#fff',
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 3.84,
elevation: 5,
overflow: 'hidden', // Clip content to rounded corners if we want
}
});