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>
154 lines
5.7 KiB
TypeScript
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
|
|
}
|
|
});
|