Files
billisdead 9c68e7a83f fix: biometric auth, workflow error propagation, execution logs
- Biometric: persist preference to SecureStore so it survives restarts;
  actually call LocalAuthentication.authenticateAsync() at startup and
  block navigation behind a locked screen until the user authenticates
- Workflow run: remove silent error swallowing in triggerWorkflow so
  failures surface as toasts; success snackbar only shown on API success
- Execution logs: add includeData=true to fetchExecutionById so n8n
  returns node-level data and error messages instead of an empty object

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 18:34:56 +02:00

166 lines
5.6 KiB
TypeScript

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<boolean> => {
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<void> => {
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 (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: '#121212' }}>
<ActivityIndicator color="#FF6D3E" size="large" />
</View>
);
}
if (preferences.biometricEnabled && !isAuthenticated) {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaProvider>
<PaperProvider theme={APP_THEME}>
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: '#121212', paddingHorizontal: 32 }}>
<Text variant="headlineMedium" style={{ color: '#FF6D3E', marginBottom: 12 }}>
n8n Pilot
</Text>
<Text variant="bodyMedium" style={{ color: '#FFFFFF', marginBottom: 32, textAlign: 'center' }}>
Authentification biométrique requise
</Text>
<Button mode="contained" onPress={handleUnlock} icon="fingerprint">
Déverrouiller
</Button>
</View>
</PaperProvider>
</SafeAreaProvider>
</GestureHandlerRootView>
);
}
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaProvider>
<PaperProvider theme={APP_THEME}>
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="(tabs)" />
<Stack.Screen
name="setup"
options={{
headerShown: true,
headerTitle: 'Configuration',
headerStyle: { backgroundColor: '#1E1E1E' },
headerTintColor: '#FF6D3E',
}}
/>
<Stack.Screen
name="execution/[id]"
options={{
headerShown: true,
headerTitle: "Logs d'exécution",
headerStyle: { backgroundColor: '#1E1E1E' },
headerTintColor: '#FF6D3E',
presentation: 'card',
}}
/>
</Stack>
{/* Toast global pour toutes les erreurs remontées par l'errorHandler */}
<Snackbar
visible={toastVisible}
onDismiss={() => setToastVisible(false)}
duration={4000}
style={{ backgroundColor: '#4A0000' }}
>
{toastMessage}
</Snackbar>
</PaperProvider>
</SafeAreaProvider>
</GestureHandlerRootView>
);
}