WellNuo/app/_layout.tsx
Sergei 88bb6d7f8f Configure Root Layout with enhanced redirect logic
Implement comprehensive redirect logic in the Root Layout that:
- Redirects unauthenticated users to login screen
- Checks if authenticated users need to complete onboarding (firstName)
- Prevents redirect loops by only redirecting on initial mount
- Protects tabs from unauthorized access (redirects to login on logout)
- Lets auth screens handle their own navigation via NavigationController

Add comprehensive test suite with 8 tests covering:
- Initial redirect scenarios (authenticated/unauthenticated)
- Onboarding flow (missing firstName)
- Logout protection
- Loading states

Update jest.config.js to support expo-status-bar, expo-splash-screen,
react-native-reanimated, and react-native-worklets transforms.

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

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

147 lines
4.8 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';
import { StripeProvider } from '@stripe/stripe-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 { BLEProvider, useBLE } from '@/contexts/BLEContext';
import { useColorScheme } from '@/hooks/use-color-scheme';
import { setOnLogoutBLECleanupCallback } from '@/services/api';
// Stripe publishable key (test mode) - must match backend STRIPE_PUBLISHABLE_KEY
const STRIPE_PUBLISHABLE_KEY = 'pk_test_51P3kdqP0gvUw6M9C7ixPQHqbPcvga4G5kAYx1h6QXQAt1psbrC2rrmOojW0fTeQzaxD1Q9RKS3zZ23MCvjjZpWLi00eCFWRHMk';
// Prevent auto-hide, ignore errors if splash not available
SplashScreen.preventAutoHideAsync().catch(() => {});
// Track if splash screen was hidden to prevent double-hiding
let splashHidden = false;
function RootLayoutNav() {
const colorScheme = useColorScheme();
const { isAuthenticated, isInitializing, user } = useAuth();
const { cleanupBLE } = useBLE();
const segments = useSegments();
const navigationState = useRootNavigationState();
// Track if initial redirect was done
const hasInitialRedirect = useRef(false);
// Set up BLE cleanup callback for logout
// Use ref to ensure callback is always current and stable
const cleanupBLERef = useRef(cleanupBLE);
useEffect(() => {
cleanupBLERef.current = cleanupBLE;
}, [cleanupBLE]);
useEffect(() => {
// Set callback that calls the current ref (always up-to-date)
setOnLogoutBLECleanupCallback(() => cleanupBLERef.current());
// Cleanup: remove callback on unmount
return () => {
setOnLogoutBLECleanupCallback(null);
};
}, []); // Empty deps - set once, callback uses ref
useEffect(() => {
// Wait for navigation to be ready
if (!navigationState?.key) {
return;
}
// Wait for INITIAL auth check to complete
if (isInitializing) {
return;
}
// Hide splash screen safely (only once)
if (!splashHidden) {
splashHidden = true;
SplashScreen.hideAsync().catch(() => {});
}
const inAuthGroup = segments[0] === '(auth)';
const inTabsGroup = segments[0] === '(tabs)';
// INITIAL REDIRECT (only once after app starts):
if (!hasInitialRedirect.current) {
hasInitialRedirect.current = true;
// Not authenticated → redirect to login
if (!isAuthenticated && !inAuthGroup) {
router.replace('/(auth)/login');
return;
}
// Authenticated but in auth group → this means user just logged in
// Let the auth screens handle navigation via NavigationController
// They will call navigateAfterLogin() to determine the correct route
if (isAuthenticated && inAuthGroup) {
// Don't redirect - let auth screens finish their flow
return;
}
// Authenticated and in tabs group → user opened app while logged in
// Check if user should be in onboarding flow
if (isAuthenticated && inTabsGroup && user) {
// If user has no name, redirect to complete profile
if (!user.firstName) {
router.replace('/(auth)/enter-name');
return;
}
// Otherwise, stay on current tab - user is properly authenticated
}
}
// ONGOING PROTECTION:
// If user logs out, redirect to login
if (!isAuthenticated && inTabsGroup) {
router.replace('/(auth)/login');
return;
}
}, [isAuthenticated, isInitializing, navigationState?.key, user]);
// IMPORTANT: segments not in deps - we don't want to react to navigation changes
if (isInitializing) {
return <LoadingSpinner fullScreen message="Loading..." />;
}
return (
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<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" />
</ThemeProvider>
);
}
export default function RootLayout() {
return (
<StripeProvider
publishableKey={STRIPE_PUBLISHABLE_KEY}
merchantIdentifier="merchant.com.wellnuo.app"
>
<AuthProvider>
<BeneficiaryProvider>
<BLEProvider>
<ToastProvider>
<RootLayoutNav />
</ToastProvider>
</BLEProvider>
</BeneficiaryProvider>
</AuthProvider>
</StripeProvider>
);
}