Restructure with Turborepo
Run Tests on Branches / Backend Tests (push) Successful in 3m38s
Run Tests on Branches / Frontend Tests (push) Successful in 3m28s

This commit is contained in:
Antoni Nuñez Romeu
2026-07-06 15:51:53 +02:00
parent f66cafbbc3
commit 190b3d163d
277 changed files with 53253 additions and 0 deletions
+237
View File
@@ -0,0 +1,237 @@
import React, { useState, useEffect } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
KeyboardAvoidingView,
Platform,
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();
const { login } = useAuthStore();
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) {
Alert.alert('Error', 'Por favor ingresa usuario y contraseña');
return;
}
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');
} finally {
setIsLoading(false);
}
};
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}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<View style={styles.form}>
<Text style={styles.title}>Iniciar Sesión</Text>
<Text style={styles.subtitle}>Ingresa tus credenciales para continuar</Text>
<View style={styles.inputContainer}>
<Text style={styles.label}>Usuario</Text>
<TextInput
style={styles.input}
value={username}
onChangeText={setUsername}
placeholder="Tu usuario"
placeholderTextColor={colors.textSecondary}
autoCapitalize="none"
autoCorrect={false}
/>
</View>
<View style={styles.inputContainer}>
<Text style={styles.label}>Contraseña</Text>
<TextInput
style={styles.input}
value={password}
onChangeText={setPassword}
placeholder="Tu contraseña"
placeholderTextColor={colors.textSecondary}
secureTextEntry
/>
</View>
<TouchableOpacity
style={[styles.button, isLoading && styles.buttonDisabled]}
onPress={handleLogin}
disabled={isLoading}
>
<Text style={styles.buttonText}>
{isLoading ? 'Ingresando...' : 'Iniciar Sesión'}
</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')}
>
<Text style={styles.linkText}>¿No tienes cuenta? Regístrate</Text>
</TouchableOpacity>
</View>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
form: {
flex: 1,
justifyContent: 'center',
padding: spacing.xl,
},
title: {
fontSize: 28,
fontWeight: 'bold',
color: colors.text,
marginBottom: spacing.sm,
},
subtitle: {
fontSize: 16,
color: colors.textSecondary,
marginBottom: spacing.xl,
},
inputContainer: {
marginBottom: spacing.md,
},
label: {
fontSize: 14,
fontWeight: '500',
color: colors.text,
marginBottom: spacing.xs,
},
input: {
backgroundColor: colors.card,
borderRadius: borderRadius.md,
padding: spacing.md,
fontSize: 16,
color: colors.text,
},
button: {
backgroundColor: colors.primary,
borderRadius: borderRadius.md,
padding: spacing.md,
alignItems: 'center',
marginTop: spacing.md,
},
buttonDisabled: {
opacity: 0.6,
},
buttonText: {
color: colors.textInverse,
fontSize: 16,
fontWeight: '600',
},
linkButton: {
marginTop: spacing.lg,
alignItems: 'center',
},
linkText: {
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,
},
});
+172
View File
@@ -0,0 +1,172 @@
import React, { useState } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
KeyboardAvoidingView,
Platform,
Alert
} from 'react-native';
import { useRouter } from 'expo-router';
import { register } from '../../services/auth';
import { colors, spacing, borderRadius } from '../../constants/theme';
export default function RegisterScreen() {
const router = useRouter();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleRegister = async () => {
if (!username || !password || !confirmPassword) {
Alert.alert('Error', 'Por favor completa todos los campos');
return;
}
if (password !== confirmPassword) {
Alert.alert('Error', 'Las contraseñas no coinciden');
return;
}
setIsLoading(true);
try {
await register(username, password);
router.replace('/(tabs)');
} catch (error) {
Alert.alert('Error', 'No se pudo crear la cuenta');
} finally {
setIsLoading(false);
}
};
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<View style={styles.form}>
<Text style={styles.title}>Crear Cuenta</Text>
<Text style={styles.subtitle}>Regístrate para empezar</Text>
<View style={styles.inputContainer}>
<Text style={styles.label}>Usuario</Text>
<TextInput
style={styles.input}
value={username}
onChangeText={setUsername}
placeholder="Elige un usuario"
placeholderTextColor={colors.textSecondary}
autoCapitalize="none"
autoCorrect={false}
/>
</View>
<View style={styles.inputContainer}>
<Text style={styles.label}>Contraseña</Text>
<TextInput
style={styles.input}
value={password}
onChangeText={setPassword}
placeholder="Elige una contraseña"
placeholderTextColor={colors.textSecondary}
secureTextEntry
/>
</View>
<View style={styles.inputContainer}>
<Text style={styles.label}>Confirmar Contraseña</Text>
<TextInput
style={styles.input}
value={confirmPassword}
onChangeText={setConfirmPassword}
placeholder="Repite la contraseña"
placeholderTextColor={colors.textSecondary}
secureTextEntry
/>
</View>
<TouchableOpacity
style={[styles.button, isLoading && styles.buttonDisabled]}
onPress={handleRegister}
disabled={isLoading}
>
<Text style={styles.buttonText}>
{isLoading ? 'Creando cuenta...' : 'Crear Cuenta'}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.linkButton}
onPress={() => router.back()}
>
<Text style={styles.linkText}>¿Ya tienes cuenta? Inicia sesión</Text>
</TouchableOpacity>
</View>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
form: {
flex: 1,
justifyContent: 'center',
padding: spacing.xl,
},
title: {
fontSize: 28,
fontWeight: 'bold',
color: colors.text,
marginBottom: spacing.sm,
},
subtitle: {
fontSize: 16,
color: colors.textSecondary,
marginBottom: spacing.xl,
},
inputContainer: {
marginBottom: spacing.md,
},
label: {
fontSize: 14,
fontWeight: '500',
color: colors.text,
marginBottom: spacing.xs,
},
input: {
backgroundColor: colors.card,
borderRadius: borderRadius.md,
padding: spacing.md,
fontSize: 16,
color: colors.text,
},
button: {
backgroundColor: colors.primary,
borderRadius: borderRadius.md,
padding: spacing.md,
alignItems: 'center',
marginTop: spacing.md,
},
buttonDisabled: {
opacity: 0.6,
},
buttonText: {
color: colors.textInverse,
fontSize: 16,
fontWeight: '600',
},
linkButton: {
marginTop: spacing.lg,
alignItems: 'center',
},
linkText: {
color: colors.primary,
fontSize: 14,
},
});