import React, { useState, useCallback, useRef } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
KeyboardAvoidingView,
Platform,
ScrollView,
ActivityIndicator,
Linking,
type TextInput as TextInputType,
} from 'react-native';
import { useColorScheme } from 'react-native';
import { router } from 'expo-router';
import { Colors } from '../../constants/Colors';
import { useAuthStore } from '../../stores/authStore';
import { config } from '../../lib/config';
/**
* Email validation regex
*/
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
/**
* Login screen component
* Handles user authentication with email and password
*/
export default function LoginScreen() {
const colorScheme = useColorScheme() ?? 'dark';
const colors = Colors[colorScheme];
// Form state
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [emailError, setEmailError] = useState<string | null>(null);
const [passwordError, setPasswordError] = useState<string | null>(null);
// Refs for input focus
const passwordInputRef = useRef<TextInputType>(null);
// Auth store
const signIn = useAuthStore((state) => state.signIn);
const isLoading = useAuthStore((state) => state.isLoading);
const authError = useAuthStore((state) => state.error);
const clearError = useAuthStore((state) => state.clearError);
/**
* Validate email format
*/
const validateEmail = useCallback((value: string): boolean => {
if (!value.trim()) {
setEmailError('Email is required');
return false;
}
if (!EMAIL_REGEX.test(value.trim())) {
setEmailError('Please enter a valid email address');
return false;
}
setEmailError(null);
return true;
}, []);
/**
* Validate password
*/
const validatePassword = useCallback((value: string): boolean => {
if (!value) {
setPasswordError('Password is required');
return false;
}
if (value.length < 6) {
setPasswordError('Password must be at least 6 characters');
return false;
}
setPasswordError(null);
return true;
}, []);
/**
* Handle email change
*/
const handleEmailChange = useCallback(
(value: string) => {
setEmail(value);
if (emailError) {
setEmailError(null);
}
if (authError) {
clearError();
}
},
[emailError, authError, clearError]
);
/**
* Handle password change
*/
const handlePasswordChange = useCallback(
(value: string) => {
setPassword(value);
if (passwordError) {
setPasswordError(null);
}
if (authError) {
clearError();
}
},
[passwordError, authError, clearError]
);
/**
* Handle login button press
*/
const handleLogin = useCallback(async () => {
// Clear previous errors
clearError();
// Validate inputs
const isEmailValid = validateEmail(email);
const isPasswordValid = validatePassword(password);
if (!isEmailValid || !isPasswordValid) {
return;
}
// Attempt sign in
const success = await signIn(email, password);
if (success) {
// Navigate to main app
router.replace('/(tabs)');
}
}, [email, password, signIn, validateEmail, validatePassword, clearError]);
/**
* Handle forgot password link press
*/
const handleForgotPassword = useCallback(() => {
Linking.openURL(config.forgotPasswordUrl);
}, []);
/**
* Handle email submit (move focus to password)
*/
const handleEmailSubmit = useCallback(() => {
passwordInputRef.current?.focus();
}, []);
/**
* Get combined error message
*/
const displayError = authError || emailError || passwordError;
return (
<KeyboardAvoidingView
style={[styles.container, { backgroundColor: colors.background }]}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
<View style={styles.headerContainer}>
<Text style={[styles.title, { color: colors.text }]}>Welcome to Makima</Text>
<Text style={[styles.subtitle, { color: colors.textSecondary }]}>
Sign in to continue
</Text>
</View>
<View style={styles.formContainer}>
{/* Email Input */}
<View style={styles.inputContainer}>
<Text style={[styles.label, { color: colors.text }]}>Email</Text>
<TextInput
style={[
styles.input,
{
backgroundColor: colors.input,
borderColor: emailError ? colors.error : colors.inputBorder,
color: colors.text,
},
]}
placeholder="Enter your email"
placeholderTextColor={colors.inputPlaceholder}
value={email}
onChangeText={handleEmailChange}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
autoComplete="email"
returnKeyType="next"
onSubmitEditing={handleEmailSubmit}
editable={!isLoading}
testID="email-input"
/>
{emailError && (
<Text style={[styles.fieldError, { color: colors.error }]}>{emailError}</Text>
)}
</View>
{/* Password Input */}
<View style={styles.inputContainer}>
<Text style={[styles.label, { color: colors.text }]}>Password</Text>
<TextInput
ref={passwordInputRef}
style={[
styles.input,
{
backgroundColor: colors.input,
borderColor: passwordError ? colors.error : colors.inputBorder,
color: colors.text,
},
]}
placeholder="Enter your password"
placeholderTextColor={colors.inputPlaceholder}
value={password}
onChangeText={handlePasswordChange}
secureTextEntry
autoCapitalize="none"
autoCorrect={false}
autoComplete="password"
returnKeyType="done"
onSubmitEditing={handleLogin}
editable={!isLoading}
testID="password-input"
/>
{passwordError && (
<Text style={[styles.fieldError, { color: colors.error }]}>{passwordError}</Text>
)}
</View>
{/* Auth Error */}
{authError && (
<View style={[styles.errorContainer, { backgroundColor: colors.error + '20' }]}>
<Text style={[styles.errorText, { color: colors.error }]}>{authError}</Text>
</View>
)}
{/* Login Button */}
<TouchableOpacity
style={[
styles.button,
{
backgroundColor: isLoading ? colors.buttonDisabled : colors.buttonPrimary,
},
]}
onPress={handleLogin}
disabled={isLoading}
activeOpacity={0.8}
testID="login-button"
>
{isLoading ? (
<ActivityIndicator color={colors.buttonPrimaryText} size="small" />
) : (
<Text style={[styles.buttonText, { color: colors.buttonPrimaryText }]}>
Sign In
</Text>
)}
</TouchableOpacity>
{/* Forgot Password Link */}
<TouchableOpacity
style={styles.forgotPasswordContainer}
onPress={handleForgotPassword}
disabled={isLoading}
testID="forgot-password-link"
>
<Text style={[styles.forgotPasswordText, { color: colors.link }]}>
Forgot Password?
</Text>
</TouchableOpacity>
</View>
{/* Footer */}
<View style={styles.footerContainer}>
<Text style={[styles.footerText, { color: colors.textSecondary }]}>
By signing in, you agree to our{' '}
<Text
style={[styles.linkText, { color: colors.link }]}
onPress={() => Linking.openURL(config.termsOfServiceUrl)}
>
Terms of Service
</Text>
{' and '}
<Text
style={[styles.linkText, { color: colors.link }]}
onPress={() => Linking.openURL(config.privacyPolicyUrl)}
>
Privacy Policy
</Text>
</Text>
</View>
</ScrollView>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
scrollContent: {
flexGrow: 1,
justifyContent: 'center',
paddingHorizontal: 24,
paddingVertical: 40,
},
headerContainer: {
alignItems: 'center',
marginBottom: 40,
},
title: {
fontSize: 28,
fontWeight: 'bold',
marginBottom: 8,
},
subtitle: {
fontSize: 16,
},
formContainer: {
width: '100%',
},
inputContainer: {
marginBottom: 20,
},
label: {
fontSize: 14,
fontWeight: '600',
marginBottom: 8,
},
input: {
height: 50,
borderWidth: 1,
borderRadius: 12,
paddingHorizontal: 16,
fontSize: 16,
},
fieldError: {
fontSize: 12,
marginTop: 4,
},
errorContainer: {
padding: 12,
borderRadius: 8,
marginBottom: 20,
},
errorText: {
fontSize: 14,
textAlign: 'center',
},
button: {
height: 50,
borderRadius: 12,
justifyContent: 'center',
alignItems: 'center',
marginTop: 8,
},
buttonText: {
fontSize: 16,
fontWeight: '600',
},
forgotPasswordContainer: {
alignItems: 'center',
marginTop: 20,
},
forgotPasswordText: {
fontSize: 14,
},
footerContainer: {
marginTop: 40,
alignItems: 'center',
},
footerText: {
fontSize: 12,
textAlign: 'center',
lineHeight: 18,
},
linkText: {
textDecorationLine: 'underline',
},
});