Ryanhub - file viewer
filename: app/Login.tsx
branch: master
back to repo
import { View, Text, StyleSheet, TextInput, ActivityIndicator, Button, KeyboardAvoidingView, Animated, TouchableOpacity } from 'react-native'
import React, { useState, useRef } from 'react'
import { NavigationProp } from '@react-navigation/native'
import { FIREBASE_AUTH } from '../FirebaseConfig'
import { createUserWithEmailAndPassword, signInWithEmailAndPassword } from 'firebase/auth'
import AsyncStorage from '@react-native-async-storage/async-storage';
import { doc, setDoc } from 'firebase/firestore';
import { FIREBASE_DB } from '../FirebaseConfig';
import { useAppTheme } from '../hooks/colorScheme';

const Login = ({ navigation }: { navigation: NavigationProp<any> }) => {
    const colors = useAppTheme();
    const [email, setEmail] = useState('')
    const [password, setPassword] = useState('')
    const [loading, setLoading] = useState(false)
    const [error, setError] = useState('')
    const fadeAnim = useRef(new Animated.Value(0)).current;
    const auth = FIREBASE_AUTH

    const showError = (message: string) => {
        setError(message);
        fadeAnim.setValue(0);
        Animated.timing(fadeAnim, {
            toValue: 1,
            duration: 200,
            useNativeDriver: true,
        }).start();
    };

    const handleEmailChange = (text: string) => {
        setEmail(text);
        if (error) setError('');
    };

    const handlePasswordChange = (text: string) => {
        setPassword(text);
        if (error) setError('');
    };

    const getDetailedErrorMessage = (error: any): string => {
        const errorCode = error.code?.replace('auth/', '') || 'internal-error';
        
        const errorMessages: { [key: string]: string } = {
            'invalid-email': 'Please enter a valid email address format.',
            'user-disabled': 'This account has been disabled. Please contact support for assistance.',
            'user-not-found': 'No account found with this email. Please create a new account.',
            'wrong-password': 'Incorrect password. Please try again or reset your password.',
            'too-many-requests': 'Access temporarily blocked due to many failed attempts. Please try again later.',
            'network-request-failed': 'Connection failed. Please check your internet and try again.',
            'email-already-in-use': 'This email is already registered. Please try signing in instead.',
            'weak-password': 'Password should be at least 6 characters long.',
            'operation-not-allowed': 'This login method is not enabled. Please use another method.',
            'account-exists-with-different-credential': 'An account already exists with this email using a different sign-in method.',
            'invalid-credential': 'Your login credentials are incorrect. Please try again.',
            'user-mismatch': 'The provided credentials do not match the previous sign-in.',
            'invalid-verification-code': 'Invalid verification code. Please try again.',
            'invalid-verification-id': 'Invalid verification ID. Please request a new code.',
            'timeout': 'The request has timed out. Please try again.',
            'web-storage-unsupported': 'This browser does not support web storage. Please enable cookies.',
            'internal-error': 'An unexpected error occurred. Please try again.',
        };

        return errorMessages[errorCode] || 
               'Something went wrong. Please try again or contact support if the problem persists.';
    };

    const handleFirebaseError = (error: any) => {
        const message = getDetailedErrorMessage(error);
        showError(message);
    };

    const signIn = async () => {
        if (!email.trim() || !password.trim()) {
            showError('Please fill in all fields');
            return;
        }
        setLoading(true);
        try {
            const userCredential = await signInWithEmailAndPassword(auth, email, password)
            await AsyncStorage.setItem('email', email)
            await AsyncStorage.setItem('password', password)
            await AsyncStorage.setItem('isLoggedOut', 'false')
        } catch (error: any) {
            handleFirebaseError(error);
            console.error(error)
        } finally {
            setLoading(false)
        }

        // set default goals if there are none already
        if (!(await AsyncStorage.getItem('userGoals'))) {
            
            const defaultGoals = {
                protein: '150',
                carbs: '150',
                fats: '100',
                dailyCalories: '2100',
            
                transFat: '2',
                saturatedFat: '20',
                polyunsaturatedFat: '17',
                monounsaturatedFat: '44',
            
                netCarbs: '53',
                sugar: '25',
                fiber: '28',
            
                cholesterol: '300',
                sodium: '2300',
                calcium: '1000',
                magnesium: '400',
                phosphorus: '700',
                potassium: '3400',
            
                iron: '18',
                copper: '900',
                zinc: '11',
                manganese: '2.3',
                selenium: '55',
            
                vitaminA: '900',
                vitaminD: '20',
                vitaminE: '15',
                vitaminK: '120',
                
                vitaminC: '90',
                vitaminB1: '1.2',
                vitaminB12: '2.4',
                vitaminB2: '1.3',
                vitaminB3: '16',
                vitaminB5: '5',
                vitaminB6: '1.7',
                folate: '400',
            };
            await AsyncStorage.setItem('userGoals', JSON.stringify(defaultGoals));
        }
    }

    const signUp = async () => {
        navigation.navigate('Onboarding')
    }

    React.useEffect(() => {
        const tryAutoLogin = async () => {
            const isLoggedOut = await AsyncStorage.getItem('isLoggedOut')
            if (isLoggedOut === 'true') {
                // Just load the stored email but don't auto-login
                const storedEmail = await AsyncStorage.getItem('email')
                if (storedEmail) setEmail(storedEmail)
                return
            }

            const storedEmail = await AsyncStorage.getItem('email')
            const storedPassword = await AsyncStorage.getItem('password')
            
            if (storedEmail && storedPassword) {
                setEmail(storedEmail)
                setPassword(storedPassword)
                // Attempt auto-login
                setLoading(true)
                try {
                    await signInWithEmailAndPassword(auth, storedEmail, storedPassword)
                } catch (error) {
                    console.log("Auto-login failed, requiring manual login")
                } finally {
                    setLoading(false)
                }
            }
        }
        tryAutoLogin()
    }, [])

    return (
        <View style={[styles.container, {backgroundColor: colors.background}]}>
            <KeyboardAvoidingView behavior='padding' style={styles.loginContainer}>
                <View style={styles.headerContainer}>
                    <Text style={[styles.title, {color: colors.text, textAlign: 'center'}]}>
                        Sign in to track your nutrition
                    </Text>
                </View>
                
                <View style={styles.inputContainer}>
                    <View style={styles.inputWrapper}>
                        <Text style={[styles.inputLabel, {color: colors.text}]}>Email</Text>
                        <TextInput 
                            style={[styles.input, {
                                color: colors.text, 
                                backgroundColor: colors.boxes,
                                borderColor: error ? colors.accent : colors.boxes,
                                borderWidth: error ? 2 : 1,
                            }]}
                            placeholder="Enter your email"
                            placeholderTextColor={colors.text + '40'}
                            value={email}
                            onChangeText={handleEmailChange}
                            keyboardType='email-address'
                            autoCapitalize='none'
                        />
                    </View>
                    <View style={styles.inputWrapper}>
                        <Text style={[styles.inputLabel, {color: colors.text}]}>Password</Text>
                        <TextInput 
                            style={[styles.input, {
                                color: colors.text, 
                                backgroundColor: colors.boxes,
                                borderColor: error ? colors.accent : colors.boxes,
                                borderWidth: error ? 2 : 1,
                            }]}
                            placeholder="Enter your password"
                            placeholderTextColor={colors.text + '40'}
                            value={password}
                            onChangeText={handlePasswordChange}
                            secureTextEntry
                        />
                    </View>
                </View>

                <View style={styles.errorOuterContainer}>
                    <Animated.View style={[styles.errorContainer, { 
                        opacity: fadeAnim,
                        backgroundColor: error ? colors.accent + '15' : 'transparent',
                        transform: [{
                            translateY: fadeAnim.interpolate({
                                inputRange: [0, 1],
                                outputRange: [5, 0]
                            })
                        }]
                    }]}>
                        {error ? (
                            <View style={styles.errorWrapper}>
                                <Text style={[styles.errorText, { color: colors.accent }]} numberOfLines={3}>
                                    {error}
                                </Text>
                            </View>
                        ) : null}
                    </Animated.View>
                </View>

                <View style={styles.buttonContainer}>
                    {loading ? (
                        <ActivityIndicator size="large" color={colors.accent} />
                    ) : (
                        <>
                            <TouchableOpacity 
                                style={[styles.primaryButton, { backgroundColor: colors.accent }]}
                                onPress={signIn}
                                activeOpacity={0.8}
                            >
                                <Text style={[styles.primaryButtonText, { color: colors.background }]}>
                                    Sign In
                                </Text>
                            </TouchableOpacity>
                            <TouchableOpacity 
                                style={[styles.secondaryButton, { backgroundColor: colors.boxes }]}
                                onPress={signUp}
                                activeOpacity={0.8}
                            >
                                <Text style={[styles.secondaryButtonText, { color: colors.accent }]}>
                                    Create New Account
                                </Text>
                            </TouchableOpacity>
                        </>
                    )}
                </View>
            </KeyboardAvoidingView>
        </View>
    )
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
    },
    loginContainer: {
        width: '100%',
        padding: 30,
        alignItems: 'center',
        gap: 25,
    },
    headerContainer: {
        width: '100%',
        alignItems: 'center',
        marginBottom: 20,
    },
    title: {
        fontSize: 28,
        fontWeight: 'bold',
        marginBottom: 10,
    },
    subtitle: {
        fontSize: 16,
        textAlign: 'center',
    },
    inputContainer: {
        width: '100%',
        gap: 20,
    },
    inputWrapper: {
        width: '100%',
    },
    inputLabel: {
        fontSize: 16,
        fontWeight: '600',
        marginBottom: 8,
        marginLeft: 4,
    },
    input: {
        width: '100%',
        padding: 16,
        borderRadius: 12,
        fontSize: 16,
        shadowColor: '#000',
        shadowOffset: {
            width: 0,
            height: 2,
        },
        shadowOpacity: 0.05,
        shadowRadius: 4,
        elevation: 2,
    },
    errorOuterContainer: {
        width: '100%',
        minHeight: 70, // Increased from 60
        maxHeight: 100, // Added max height
        marginVertical: 8,
        justifyContent: 'center', // Added to center content
    },
    errorContainer: {
        width: '100%',
        borderRadius: 12, // Increased from 8
        padding: 12, // Increased from 8
        justifyContent: 'center',
        alignItems: 'center',
    },
    errorWrapper: {
        width: '100%',
        paddingHorizontal: 16, // Increased from 8
    },
    errorText: {
        fontSize: 14,
        fontWeight: '500',
        textAlign: 'center',
        lineHeight: 20,
        flexWrap: 'wrap', // Added to ensure text wraps
    },
    buttonContainer: {
        width: '100%',
        gap: 12,
        marginTop: 10,
    },
    primaryButton: {
        width: '100%',
        padding: 16,
        borderRadius: 12,
        alignItems: 'center',
        shadowColor: '#000',
        shadowOffset: {
            width: 0,
            height: 2,
        },
        shadowOpacity: 0.1,
        shadowRadius: 8,
        elevation: 5,
    },
    primaryButtonText: {
        fontSize: 16,
        fontWeight: 'bold',
    },
    secondaryButton: {
        width: '100%',
        padding: 16,
        borderRadius: 12,
        alignItems: 'center',
        shadowColor: '#000',
        shadowOffset: {
            width: 0,
            height: 2,
        },
        shadowOpacity: 0.05,
        shadowRadius: 4,
        elevation: 2,
    },
    secondaryButtonText: {
        fontSize: 16,
        fontWeight: '600',
    },
})

export default Login