- Add 2-second timeout to profile fetch in getStoredUser() to ensure app startup < 3 seconds even with slow network. Falls back to cached user data on timeout. - Implement early scan termination in BLEManager when devices found. Scan now exits after 3 seconds once minimum devices are detected, instead of always waiting full 10 seconds. - Add PerformanceService for tracking app startup time, API response times, and BLE operation durations with threshold checking. - Integrate performance tracking in app/_layout.tsx to measure and log startup duration in dev mode. - Add comprehensive test suite for performance service and BLE scan optimizations. Performance targets: - App startup: < 3 seconds - BLE operations: < 10 seconds 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
158 lines
5.4 KiB
TypeScript
158 lines
5.4 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';
|
|
import { performanceService, PERFORMANCE_THRESHOLDS } from '@/services/performance';
|
|
|
|
// Mark app startup as early as possible
|
|
performanceService.markAppStart();
|
|
|
|
// 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(() => {});
|
|
|
|
// Track app startup performance
|
|
const startupDuration = performanceService.markAppReady();
|
|
if (__DEV__) {
|
|
const status = startupDuration <= PERFORMANCE_THRESHOLDS.appStartup ? '✓' : '✗';
|
|
console.log(`[Performance] App startup: ${startupDuration}ms ${status} (target: ${PERFORMANCE_THRESHOLDS.appStartup}ms)`);
|
|
}
|
|
}
|
|
|
|
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>
|
|
);
|
|
}
|