Core BLE system: - BLEManager: Real BLE device scanning and connection (iOS/Android) - MockBLEManager: Simulator-safe mock for development - BLEContext: React context for BLE state management - BLEProvider: Added to app/_layout.tsx Bluetooth permissions: - iOS: NSBluetoothAlwaysUsageDescription, NSBluetoothPeripheralUsageDescription - Android: BLUETOOTH, BLUETOOTH_ADMIN, BLUETOOTH_CONNECT, BLUETOOTH_SCAN, ACCESS_FINE_LOCATION Dependencies: - react-native-ble-plx@3.5.0 - expo-device@8.0.10 - react-native-base64@0.2.2 Simulator support: - Auto-detects iOS simulator via expo-device - Falls back to MockBLEManager with fake devices - No crashes or permission errors in development
109 lines
3.7 KiB
TypeScript
109 lines
3.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';
|
|
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 } from '@/contexts/BLEContext';
|
|
import { useColorScheme } from '@/hooks/use-color-scheme';
|
|
|
|
// 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 } = useAuth();
|
|
const segments = useSegments();
|
|
const navigationState = useRootNavigationState();
|
|
|
|
// Track if initial redirect was done
|
|
const hasInitialRedirect = useRef(false);
|
|
|
|
useEffect(() => {
|
|
// Wait for navigation to be ready
|
|
if (!navigationState?.key) {
|
|
console.log('[Layout] Navigation not ready yet');
|
|
return;
|
|
}
|
|
|
|
// Wait for INITIAL auth check to complete
|
|
if (isInitializing) {
|
|
console.log('[Layout] Still initializing auth state...');
|
|
return;
|
|
}
|
|
|
|
// Hide splash screen safely (only once)
|
|
if (!splashHidden) {
|
|
splashHidden = true;
|
|
SplashScreen.hideAsync().catch(() => {});
|
|
}
|
|
|
|
const inAuthGroup = segments[0] === '(auth)';
|
|
|
|
console.log('[Layout] Auth check:', { isAuthenticated, inAuthGroup, hasInitialRedirect: hasInitialRedirect.current });
|
|
|
|
// INITIAL REDIRECT (only once after app starts):
|
|
// - If not authenticated and not in auth → go to login
|
|
if (!hasInitialRedirect.current) {
|
|
hasInitialRedirect.current = true;
|
|
|
|
if (!isAuthenticated && !inAuthGroup) {
|
|
console.log('[Layout] Initial redirect → login');
|
|
router.replace('/(auth)/login');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// If not authenticated, do NOTHING - let the auth screens handle their own navigation
|
|
|
|
}, [isAuthenticated, isInitializing, navigationState?.key]);
|
|
// IMPORTANT: isLoading NOT in deps - we don't want to react to loading state changes
|
|
// segments also 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>
|
|
);
|
|
}
|