From 4afffe46f0292ad1d1ccee330fac88a3eaa2a4bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoni=20Nu=C3=B1ez=20Romeu?= Date: Mon, 6 Jul 2026 11:36:01 +0200 Subject: [PATCH] feat: add biometric authentication support --- frontend-mobile/app/auth/login.tsx | 84 +++++++++++++++++++++++++- frontend-mobile/services/biometrics.ts | 34 +++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 frontend-mobile/services/biometrics.ts diff --git a/frontend-mobile/app/auth/login.tsx b/frontend-mobile/app/auth/login.tsx index dd24add..7d2dda1 100644 --- a/frontend-mobile/app/auth/login.tsx +++ b/frontend-mobile/app/auth/login.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { View, Text, @@ -10,8 +10,15 @@ import { Alert } from 'react-native'; import { useRouter } from 'expo-router'; +import { Ionicons } from '@expo/vector-icons'; import { useAuthStore } from '../../store/authStore'; import { colors, spacing, borderRadius } from '../../constants/theme'; +import { + isBiometricsAvailable, + authenticateWithBiometrics, + saveBiometricCredentials, + getBiometricUsername +} from '../../services/biometrics'; export default function LoginScreen() { const router = useRouter(); @@ -19,6 +26,25 @@ export default function LoginScreen() { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [isLoading, setIsLoading] = useState(false); + const [biometricsAvailable, setBiometricsAvailable] = useState(false); + const [biometricUsername, setBiometricUsername] = useState(null); + + useEffect(() => { + checkBiometrics(); + }, []); + + const checkBiometrics = async () => { + try { + const available = await isBiometricsAvailable(); + setBiometricsAvailable(available); + if (available) { + const savedUsername = await getBiometricUsername(); + setBiometricUsername(savedUsername); + } + } catch (error) { + console.error('Error checking biometrics:', error); + } + }; const handleLogin = async () => { if (!username || !password) { @@ -29,6 +55,10 @@ export default function LoginScreen() { setIsLoading(true); try { await login(username, password); + // Save username for biometric login + if (biometricsAvailable) { + await saveBiometricCredentials(username); + } router.replace('/(tabs)'); } catch (error) { Alert.alert('Error', 'Credenciales incorrectas'); @@ -37,6 +67,30 @@ export default function LoginScreen() { } }; + const handleBiometricLogin = async () => { + if (!biometricUsername) { + Alert.alert('Error', 'No hay credenciales biométricas guardadas'); + return; + } + + setIsLoading(true); + try { + const authenticated = await authenticateWithBiometrics(); + if (authenticated) { + // Use saved username with empty password for biometric login + // The backend should handle biometric authentication differently + await login(biometricUsername, ''); + router.replace('/(tabs)'); + } else { + Alert.alert('Error', 'Autenticación biométrica fallida'); + } + } catch (error) { + Alert.alert('Error', 'Error en la autenticación biométrica'); + } finally { + setIsLoading(false); + } + }; + return ( + {biometricsAvailable && biometricUsername && ( + + + Iniciar con biometría + + )} + router.push('/auth/register')} @@ -152,4 +217,21 @@ const styles = StyleSheet.create({ color: colors.primary, fontSize: 14, }, + biometricButton: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: colors.card, + borderRadius: borderRadius.md, + padding: spacing.md, + marginTop: spacing.md, + borderWidth: 1, + borderColor: colors.primary, + }, + biometricText: { + color: colors.primary, + fontSize: 16, + fontWeight: '600', + marginLeft: spacing.sm, + }, }); diff --git a/frontend-mobile/services/biometrics.ts b/frontend-mobile/services/biometrics.ts new file mode 100644 index 0000000..46a85ed --- /dev/null +++ b/frontend-mobile/services/biometrics.ts @@ -0,0 +1,34 @@ +import * as LocalAuthentication from 'expo-local-authentication'; +import * as SecureStore from 'expo-secure-store'; + +export async function isBiometricsAvailable(): Promise { + const compatible = await LocalAuthentication.hasHardwareAsync(); + const enrolled = await LocalAuthentication.isEnrolledAsync(); + return compatible && enrolled; +} + +export async function authenticateWithBiometrics(): Promise { + try { + const result = await LocalAuthentication.authenticateAsync({ + promptMessage: 'Inicia sesión con biometría', + cancelLabel: 'Cancelar', + disableDeviceFallback: false, + }); + return result.success; + } catch (error) { + console.error('Biometric authentication error:', error); + return false; + } +} + +export async function saveBiometricCredentials(username: string): Promise { + await SecureStore.setItemAsync('biometric_username', username); +} + +export async function getBiometricUsername(): Promise { + return await SecureStore.getItemAsync('biometric_username'); +} + +export async function clearBiometricCredentials(): Promise { + await SecureStore.deleteItemAsync('biometric_username'); +} \ No newline at end of file