feat: add biometric authentication support
This commit is contained in:
@@ -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<string | null>(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 (
|
||||
<KeyboardAvoidingView
|
||||
style={styles.container}
|
||||
@@ -81,6 +135,17 @@ export default function LoginScreen() {
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{biometricsAvailable && biometricUsername && (
|
||||
<TouchableOpacity
|
||||
style={[styles.biometricButton, isLoading && styles.buttonDisabled]}
|
||||
onPress={handleBiometricLogin}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Ionicons name="finger-print" size={24} color={colors.primary} />
|
||||
<Text style={styles.biometricText}>Iniciar con biometría</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.linkButton}
|
||||
onPress={() => 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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import * as LocalAuthentication from 'expo-local-authentication';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
|
||||
export async function isBiometricsAvailable(): Promise<boolean> {
|
||||
const compatible = await LocalAuthentication.hasHardwareAsync();
|
||||
const enrolled = await LocalAuthentication.isEnrolledAsync();
|
||||
return compatible && enrolled;
|
||||
}
|
||||
|
||||
export async function authenticateWithBiometrics(): Promise<boolean> {
|
||||
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<void> {
|
||||
await SecureStore.setItemAsync('biometric_username', username);
|
||||
}
|
||||
|
||||
export async function getBiometricUsername(): Promise<string | null> {
|
||||
return await SecureStore.getItemAsync('biometric_username');
|
||||
}
|
||||
|
||||
export async function clearBiometricCredentials(): Promise<void> {
|
||||
await SecureStore.deleteItemAsync('biometric_username');
|
||||
}
|
||||
Reference in New Issue
Block a user