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>
This commit is contained in:
2026-05-21 18:34:56 +02:00
parent 00b2c5db85
commit 9c68e7a83f
7 changed files with 81 additions and 22 deletions
+52 -10
View File
@@ -1,9 +1,10 @@
import React, { useEffect, useState } from 'react';
import { View } from 'react-native';
import { Stack, useRouter, useSegments } from 'expo-router';
import { PaperProvider, MD3DarkTheme, ActivityIndicator, Snackbar } from 'react-native-paper';
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';
@@ -36,7 +37,7 @@ const APP_THEME = {
export default function RootLayout() {
const router = useRouter();
const segments = useSegments();
const { config, loadConfig } = useAppStore();
const { config, preferences, isAuthenticated, loadConfig, setAuthenticated } = useAppStore();
const [isReady, setIsReady] = useState(false);
const [toastMessage, setToastMessage] = useState('');
const [toastVisible, setToastVisible] = useState(false);
@@ -52,25 +53,44 @@ export default function RootLayout() {
});
}, []);
/** Charge la configuration depuis le secure store puis marque l'app comme prête */
const runBiometricAuth = async (): Promise<boolean> => {
const result = await LocalAuthentication.authenticateAsync({
promptMessage: 'Authentifiez-vous pour accéder à n8n Pilot',
cancelLabel: 'Annuler',
});
return result.success;
};
useEffect(() => {
loadConfig().finally(() => setIsReady(true));
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();
}, []);
/**
* Gère la redirection initiale selon l'état de configuration.
* - Pas configuré et hors /setup → redirige vers /setup
* - Configuré et dans /setup → redirige vers les tabs
*/
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, segments]);
}, [isReady, config.isConfigured, isAuthenticated, preferences.biometricEnabled, segments]);
if (!isReady) {
return (
@@ -80,6 +100,28 @@ export default function RootLayout() {
);
}
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>