import React, { useEffect, useState } from 'react'; import { ActivityIndicator, Alert, ImageBackground, KeyboardAvoidingView, Platform, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View, } from 'react-native'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { Ionicons } from '@expo/vector-icons'; import { router, useLocalSearchParams } from 'expo-router'; import * as AppleAuthentication from 'expo-apple-authentication'; import Constants from 'expo-constants'; import { useApp } from '../../context/AppContext'; import { useColors } from '../../constants/Colors'; import { AuthService } from '../../services/authService'; import { useSafeAnalytics } from '../../services/analytics'; import { PreAuthOnboardingService } from '../../services/preAuthOnboardingService'; import { Language } from '../../types'; const HERO_IMAGE = require('../../assets/welcome_hero_vertical.png'); const getCopy = (language: Language) => { if (language === 'de') { return { headline: 'Welcome back!', subline: 'Melde dich an und mache mit deiner Pflanzenpflege weiter.', loginCta: 'Anmelden', forgot: 'Passwort vergessen?', forgotTitle: 'Passwort zuruecksetzen', forgotBody: 'Ein Reset-Link ist noch nicht in der App verfuegbar. Bitte nutze aktuell deine gespeicherten Login-Daten.', newHere: 'Neu hier?', create: 'Account erstellen', emailLabel: 'E-Mail', }; } if (language === 'es') { return { headline: 'Welcome back!', subline: 'Inicia sesion para continuar con el cuidado de tus plantas.', loginCta: 'Iniciar sesion', forgot: 'Olvidaste tu contrasena?', forgotTitle: 'Restablecer contrasena', forgotBody: 'El enlace de restablecimiento aun no esta disponible en la app. Usa tus datos guardados por ahora.', newHere: 'Nuevo aqui?', create: 'Crear cuenta', emailLabel: 'Email', }; } return { headline: 'Welcome back!', subline: 'Log in to keep scanning, saving and caring for your plants.', loginCta: 'Log in', forgot: 'Forgot password?', forgotTitle: 'Reset password', forgotBody: 'Password reset is not available in the app yet. Please use your saved login details for now.', newHere: 'New here?', create: 'Create account', emailLabel: 'Email', }; }; export default function LoginScreen() { const { isDarkMode, colorPalette, hydrateSession, language, t } = useApp(); const colors = useColors(isDarkMode, colorPalette); const copy = getCopy(language); const posthog = useSafeAnalytics(); const params = useLocalSearchParams<{ returnTo?: string; topup?: string }>(); const isExpoGo = Constants.appOwnership === 'expo'; const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [showPassword, setShowPassword] = useState(false); const [appleAvailable, setAppleAvailable] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { if (isExpoGo) { setAppleAvailable(false); return; } let mounted = true; AppleAuthentication.isAvailableAsync() .then((available) => { if (mounted) setAppleAvailable(available); }) .catch(() => { if (mounted) setAppleAvailable(false); }); return () => { mounted = false; }; }, [isExpoGo]); const finishAuth = async (session: Awaited>) => { await hydrateSession(session); if (session?.userId) { await PreAuthOnboardingService.flushToProfile(session.userId).catch(() => {}); } if (session.isNewUser) { await AsyncStorage.setItem('greenlens_show_tour', 'true'); } const returnTo = Array.isArray(params.returnTo) ? params.returnTo[0] : params.returnTo; const topup = Array.isArray(params.topup) ? params.topup[0] : params.topup; if (returnTo === 'billing') { router.replace({ pathname: '/profile/billing', params: topup ? { topup } : undefined }); return; } if (returnTo === 'paywall') { router.replace('/profile/billing?view=paywall&context=out_of_credits'); return; } router.replace('/(tabs)'); }; const handleLogin = async () => { if (!email.trim() || !password) { setError(t.errFillAllFields); return; } setLoading(true); setError(null); try { const session = await AuthService.login(email, password); await finishAuth(session); } catch (e: any) { if (e.message === 'USER_NOT_FOUND') { setError(t.errUserNotFound); } else if (e.message === 'WRONG_PASSWORD') { setError(t.errWrongPassword); } else if (e.message === 'BACKEND_URL_MISSING' || e.message === 'NETWORK_ERROR') { setError(t.errNetworkError); } else { setError(t.errLoginFailed); } } finally { setLoading(false); } }; const handleAppleSignIn = async () => { setLoading(true); setError(null); posthog.capture('apple_login_started', { surface: 'login' }); try { const credential = await AppleAuthentication.signInAsync({ requestedScopes: [ AppleAuthentication.AppleAuthenticationScope.FULL_NAME, AppleAuthentication.AppleAuthenticationScope.EMAIL, ], }); if (!credential.identityToken) { throw new Error('APPLE_AUTH_INVALID'); } const fullName = [ credential.fullName?.givenName, credential.fullName?.familyName, ].filter(Boolean).join(' '); const session = await AuthService.signInWithApple({ identityToken: credential.identityToken, appleUser: credential.user, email: credential.email, name: fullName || undefined, }); posthog.capture('apple_login_succeeded', { surface: 'login' }); await finishAuth(session); } catch (e: any) { if (e?.code === 'ERR_REQUEST_CANCELED') return; posthog.capture('apple_login_failed', { surface: 'login', error: e instanceof Error ? e.message : String(e), }); setError(e?.message === 'APPLE_BACKEND_UNAVAILABLE' ? 'Apple Login ist auf dem Backend noch nicht aktiviert. Bitte Backend neu starten oder deployen.' : t.errAuthError); } finally { setLoading(false); } }; return ( router.back()} activeOpacity={0.8}> {copy.headline} {copy.subline} {appleAvailable ? ( ) : null} {appleAvailable ? ( {t.orDivider} ) : null} {copy.emailLabel} {t.passwordLabel} setShowPassword((v) => !v)} style={styles.eyeBtn}> Alert.alert(copy.forgotTitle, copy.forgotBody)} activeOpacity={0.78} > {copy.forgot} {error ? ( {error} ) : null} {loading ? ( ) : ( {copy.loginCta} )} router.replace({ pathname: '/auth/signup', params: { returnTo: params.returnTo, topup: params.topup } })} activeOpacity={0.78}> {copy.newHere}{' '} {copy.create} ); } const styles = StyleSheet.create({ flex: { flex: 1 }, scroll: { flexGrow: 1 }, hero: { minHeight: 290, justifyContent: 'flex-end', paddingHorizontal: 24, paddingTop: 56, paddingBottom: 32, }, heroImage: { resizeMode: 'cover' }, heroOverlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(5, 12, 7, 0.48)', }, backBtn: { position: 'absolute', top: 54, left: 22, width: 42, height: 42, borderRadius: 21, alignItems: 'center', justifyContent: 'center', backgroundColor: 'rgba(0, 0, 0, 0.28)', zIndex: 10, elevation: 10, }, heroCopy: { gap: 10 }, heroTitle: { color: '#ffffff', fontSize: 36, lineHeight: 41, fontWeight: '900', }, heroSubline: { color: 'rgba(255, 255, 255, 0.88)', fontSize: 16, lineHeight: 22, fontWeight: '700', }, sheet: { flex: 1, marginTop: -24, borderTopLeftRadius: 28, borderTopRightRadius: 28, paddingHorizontal: 22, paddingTop: 24, paddingBottom: 34, gap: 14, }, appleButton: { width: '100%', height: 56 }, dividerRow: { flexDirection: 'row', alignItems: 'center', gap: 12 }, dividerLine: { flex: 1, height: 1 }, dividerText: { fontSize: 12, fontWeight: '800' }, form: { gap: 12 }, fieldGroup: { gap: 6 }, label: { fontSize: 13, fontWeight: '800', marginLeft: 2 }, inputRow: { height: 54, borderWidth: 1, borderRadius: 14, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, }, inputIcon: { marginRight: 10 }, input: { flex: 1, height: 54, fontSize: 15 }, eyeBtn: { padding: 5, marginLeft: 6 }, forgotBtn: { alignItems: 'flex-end', marginTop: -4 }, forgotText: { fontSize: 14, fontWeight: '800' }, errorBox: { flexDirection: 'row', alignItems: 'center', gap: 7, borderRadius: 12, paddingHorizontal: 12, paddingVertical: 10, }, errorText: { flex: 1, fontSize: 13, lineHeight: 18, fontWeight: '700' }, primaryBtn: { height: 56, borderRadius: 14, alignItems: 'center', justifyContent: 'center', }, primaryBtnText: { fontSize: 17, fontWeight: '900' }, signupLink: { alignItems: 'center', paddingTop: 8 }, signupLinkText: { fontSize: 15, fontWeight: '700' }, });