import { Stack } from 'expo-router'; import { ThemeProvider, useAppTheme } from '../context/ThemeContext'; import { AuthProvider, useAuth } from '../context/AuthContext'; import { ToastProvider } from '../context/ToastContext'; import { RefreshProvider, useRefresh } from '../context/RefreshContext'; import { ConfigProvider, useAppConfig } from '../context/ConfigContext'; import { LanguageProvider } from '../context/LanguageContext'; import { View, Platform, StyleSheet, TouchableOpacity, Text, ActivityIndicator, StatusBar } from 'react-native'; import { Feather } from '@expo/vector-icons'; import { useEffect, useState } from 'react'; import * as SplashScreen from 'expo-splash-screen'; import { useFonts, Outfit_300Light, Outfit_400Regular, Outfit_500Medium, Outfit_600SemiBold, Outfit_700Bold, Outfit_800ExtraBold } from '@expo-google-fonts/outfit'; import { AnimatedSplash } from '../components/AnimatedSplash'; import { ErrorBoundary } from '../components/ErrorBoundary'; SplashScreen.preventAutoHideAsync(); export default function RootLayout() { const [fontsLoaded, fontError] = useFonts({ Outfit_300Light, Outfit_400Regular, Outfit_500Medium, Outfit_600SemiBold, Outfit_700Bold, Outfit_800ExtraBold, }); const [isAnimationComplete, setIsAnimationComplete] = useState(false); useEffect(() => { if (fontsLoaded || fontError) { SplashScreen.hideAsync(); } }, [fontsLoaded, fontError]); if (!fontsLoaded && !fontError) return ; return ( {!isAnimationComplete ? ( setIsAnimationComplete(true)} /> ) : ( )} ); } import { KillSwitchOverlay } from '../components/KillSwitchOverlay'; import { AnnouncementBanner } from '../components/AnnouncementBanner'; function RootLayoutContent() { // Safe hook call because it's now guaranteed to be rendered ONLY when providers are ready and animation is done const { colors } = useAppTheme(); const { config, isConnected, isSyncing, syncConfig } = useAppConfig(); const [announcementDismissed, setAnnouncementDismissed] = useState(false); useEffect(() => { if (isConnected) syncConfig(); }, [isConnected]); // Logic for Kill Switch & Maintenance const isKillSwitchActive = config?.control_center?.kill_switch_active || false; // Calculate if maintenance is currently active const isInMaintenanceWindow = () => { const start = config?.control_center?.maintenance_start_at; const end = config?.control_center?.maintenance_end_at; if (!start || !end) return false; const now = new Date().getTime(); const startTime = new Date(start).getTime(); const endTime = new Date(end).getTime(); return now >= startTime && now <= endTime; }; const shouldBlockAccess = isKillSwitchActive || isInMaintenanceWindow(); // Logic for Force Update — current version comes from synced config so admin can advertise the released build const currentAppVersion = config?.app_updates?.app_version || "2.0.0"; const minVersion = config?.app_updates?.min_app_version || "1.0.0"; const isUpdateRequired = () => { if (!minVersion) return false; const current = currentAppVersion.split('.').map(Number); const min = minVersion.split('.').map(Number); for (let i = 0; i < Math.max(current.length, min.length); i++) { const v1 = current[i] || 0; const v2 = min[i] || 0; if (v1 < v2) return true; if (v1 > v2) return false; } return false; }; const isUpdating = isUpdateRequired(); const killSwitchMessage = isKillSwitchActive ? config?.control_center?.kill_switch_message : (isInMaintenanceWindow() ? "System is currently undergoing scheduled maintenance." : ""); const updateMessage = `A mandatory update (v${minVersion}) is required to continue using the app. Please update from the store.`; return ( {/* 1. Global Announcement */} setAnnouncementDismissed(true)} /> {/* 2. Kill Switch / Maintenance Overlay */} {/* 3. Force Update Overlay */} {/* Offline Indicator */} {!isConnected && ( You are currently offline. )} {/* Sync Indicator */} {isSyncing && isConnected && ( )} ); } const styles = StyleSheet.create({ offlineBanner: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 8, gap: 8, zIndex: 999, }, offlineText: { color: '#FFF', fontSize: 12, fontFamily: 'Outfit_600SemiBold', }, syncIndicator: { position: 'absolute', top: Platform.OS === 'ios' ? 60 : 40, right: 20, zIndex: 1000, backgroundColor: 'rgba(0,0,0,0.05)', borderRadius: 20, padding: 4, } });