new_react_frontend #9

Merged
Ichitux merged 24 commits from new_react_frontend into main 2026-07-06 13:10:11 +00:00
2 changed files with 117 additions and 1 deletions
Showing only changes of commit 4afffe46f0 - Show all commits
+83 -1
View File
@@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { import {
View, View,
Text, Text,
@@ -10,8 +10,15 @@ import {
Alert Alert
} from 'react-native'; } from 'react-native';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { useAuthStore } from '../../store/authStore'; import { useAuthStore } from '../../store/authStore';
import { colors, spacing, borderRadius } from '../../constants/theme'; import { colors, spacing, borderRadius } from '../../constants/theme';
import {
isBiometricsAvailable,
authenticateWithBiometrics,
saveBiometricCredentials,
getBiometricUsername
} from '../../services/biometrics';
export default function LoginScreen() { export default function LoginScreen() {
const router = useRouter(); const router = useRouter();
@@ -19,6 +26,25 @@ export default function LoginScreen() {
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [isLoading, setIsLoading] = useState(false); 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 () => { const handleLogin = async () => {
if (!username || !password) { if (!username || !password) {
@@ -29,6 +55,10 @@ export default function LoginScreen() {
setIsLoading(true); setIsLoading(true);
try { try {
await login(username, password); await login(username, password);
// Save username for biometric login
if (biometricsAvailable) {
await saveBiometricCredentials(username);
}
router.replace('/(tabs)'); router.replace('/(tabs)');
} catch (error) { } catch (error) {
Alert.alert('Error', 'Credenciales incorrectas'); 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 ( return (
<KeyboardAvoidingView <KeyboardAvoidingView
style={styles.container} style={styles.container}
@@ -81,6 +135,17 @@ export default function LoginScreen() {
</Text> </Text>
</TouchableOpacity> </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 <TouchableOpacity
style={styles.linkButton} style={styles.linkButton}
onPress={() => router.push('/auth/register')} onPress={() => router.push('/auth/register')}
@@ -152,4 +217,21 @@ const styles = StyleSheet.create({
color: colors.primary, color: colors.primary,
fontSize: 14, 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,
},
}); });
+34
View File
@@ -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');
}