import React, { useEffect, useState } from 'react'; import { View } from 'react-native'; import { Stack, useRouter, useSegments } from 'expo-router'; import { PaperProvider, MD3DarkTheme, ActivityIndicator, Snackbar, Button, Text } from 'react-native-paper'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import * as LocalAuthentication from 'expo-local-authentication'; import { useAppStore } from '../src/store/appStore'; import { registerToastCallback } from '../src/utils/errorHandler'; /** * Thème Material Design 3 sombre personnalisé pour n8n Pilot. * L'orange (#FF6D3E) est la couleur d'accent principale — cohérente avec l'identité n8n. */ const APP_THEME = { ...MD3DarkTheme, colors: { ...MD3DarkTheme.colors, primary: '#FF6D3E', primaryContainer: '#5C1C00', secondary: '#FF9E7D', background: '#121212', surface: '#1E1E1E', surfaceVariant: '#2C2C2C', }, }; /** * Layout racine de l'application. * Responsabilités : * 1. Fournit les providers globaux (GestureHandler, SafeArea, Paper) * 2. Charge la configuration au démarrage depuis le secure store * 3. Redirige vers /setup si aucune configuration n'existe * 4. Enregistre le callback de toast pour l'errorHandler global * 5. Affiche le toast d'erreur global en overlay */ export default function RootLayout() { const router = useRouter(); const segments = useSegments(); const { config, preferences, isAuthenticated, loadConfig, setAuthenticated } = useAppStore(); const [isReady, setIsReady] = useState(false); const [toastMessage, setToastMessage] = useState(''); const [toastVisible, setToastVisible] = useState(false); /** * Enregistre le callback de toast dans l'errorHandler au premier montage. * Permet à toute la couche API de remonter des erreurs vers l'UI sans couplage direct. */ useEffect(() => { registerToastCallback((message) => { setToastMessage(message); setToastVisible(true); }); }, []); const runBiometricAuth = async (): Promise => { const result = await LocalAuthentication.authenticateAsync({ promptMessage: 'Authentifiez-vous pour accéder à n8n Pilot', cancelLabel: 'Annuler', }); return result.success; }; useEffect(() => { const init = async () => { await loadConfig(); const { preferences: prefs } = useAppStore.getState(); if (prefs.biometricEnabled) { const success = await runBiometricAuth(); setAuthenticated(success); } else { setAuthenticated(true); } setIsReady(true); }; init(); }, []); const handleUnlock = async (): Promise => { const success = await runBiometricAuth(); if (success) setAuthenticated(true); }; useEffect(() => { if (!isReady) return; if (preferences.biometricEnabled && !isAuthenticated) return; const inSetup = segments[0] === 'setup'; if (!config.isConfigured && !inSetup) { router.replace('/setup'); } else if (config.isConfigured && inSetup) { router.replace('/(tabs)'); } }, [isReady, config.isConfigured, isAuthenticated, preferences.biometricEnabled, segments]); if (!isReady) { return ( ); } if (preferences.biometricEnabled && !isAuthenticated) { return ( n8n Pilot Authentification biométrique requise ); } return ( {/* Toast global pour toutes les erreurs remontées par l'errorHandler */} setToastVisible(false)} duration={4000} style={{ backgroundColor: '#4A0000' }} > {toastMessage} ); }