import React, { useEffect, useState } from 'react'; import { ActivityIndicator, ImageBackground, KeyboardAvoidingView, Platform, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View, useWindowDimensions, } 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: "Let's finish your setup!", subline: 'Erstelle einen Account, speichere deine Pflanzen und sichere dir 3 Gratis-Scans pro Monat.', emailCta: 'Mit E-Mail fortfahren', createCta: 'Account erstellen', already: 'Schon einen Account?', login: 'Anmelden', legal: 'Mit dem Fortfahren akzeptierst du Datenschutz und Nutzungsbedingungen.', nameLabel: 'Name', emailLabel: 'E-Mail', savePlantPrefix: 'Dein Scan wird nach der Registrierung gespeichert:', }; } if (language === 'es') { return { headline: "Let's finish your setup!", subline: 'Crea una cuenta para guardar tus plantas y recibir 3 escaneos gratis al mes.', emailCta: 'Continuar con email', createCta: 'Crear cuenta', already: 'Ya tienes cuenta?', login: 'Iniciar sesion', legal: 'Al continuar aceptas la Politica de privacidad y los Terminos.', nameLabel: 'Nombre', emailLabel: 'Email', savePlantPrefix: 'Guardaremos tu escaneo despues del registro:', }; } return { headline: "Let's finish your setup!", subline: 'Create an account to save your plants and 3 free scans per month.', emailCta: 'Continue with Email', createCta: 'Create Account', already: 'Already have an account?', login: 'Log in', legal: 'By continuing you agree to our Privacy Policy and Terms.', nameLabel: 'Name', emailLabel: 'Email', savePlantPrefix: 'Your scan will be saved after signup:', }; }; export default function SignupScreen() { const { isDarkMode, colorPalette, hydrateSession, getPendingPlant, language, t } = useApp(); const colors = useColors(isDarkMode, colorPalette); const copy = getCopy(language); const posthog = useSafeAnalytics(); const pendingPlant = getPendingPlant(); const params = useLocalSearchParams<{ returnTo?: string; topup?: string }>(); const isExpoGo = Constants.appOwnership === 'expo'; const { height } = useWindowDimensions(); const compact = height < 700; const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [passwordConfirm, setPasswordConfirm] = useState(''); const [showPassword, setShowPassword] = useState(false); const [showPasswordConfirm, setShowPasswordConfirm] = useState(false); const [emailExpanded, setEmailExpanded] = useState(false); const [appleAvailable, setAppleAvailable] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const heroHeight = emailExpanded ? (compact ? 200 : 240) : (compact ? 320 : 400); const appleButtonHeight = emailExpanded ? 44 : 56; const inputHeight = emailExpanded && compact ? 46 : 52; useEffect(() => { posthog.capture('signup_screen_viewed', { context: 'onboarding' }); }, [posthog]); 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(() => {}); } 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 validate = (): string | null => { if (!name.trim()) return t.errNameRequired; if (!email.trim() || !email.includes('@')) return t.errEmailInvalid; if (password.length < 6) return t.errPasswordShort; if (password !== passwordConfirm) return t.errPasswordMismatch; return null; }; const handleSignup = async () => { const validationError = validate(); if (validationError) { setError(validationError); setEmailExpanded(true); return; } setLoading(true); setError(null); try { const session = await AuthService.signUp(email, name, password); await finishAuth(session); } catch (e: any) { if (e.message === 'EMAIL_TAKEN') { setError(t.errEmailTaken); } else if (e.message === 'BACKEND_URL_MISSING' || e.message === 'NETWORK_ERROR') { setError(t.errNetworkError); } else if (e.message === 'SERVER_ERROR') { setError(t.errServerError); } else { setError(t.errAuthError); } } finally { setLoading(false); } }; const handleAppleSignIn = async () => { setLoading(true); setError(null); posthog.capture('apple_login_started', { surface: 'signup' }); 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: 'signup' }); await finishAuth(session); } catch (e: any) { if (e?.code === 'ERR_REQUEST_CANCELED') return; posthog.capture('apple_login_failed', { surface: 'signup', 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} {pendingPlant ? ( {copy.savePlantPrefix} {pendingPlant.result.name} ) : null} {appleAvailable ? ( ) : null} {appleAvailable ? ( {t.orDivider} ) : null} {!emailExpanded ? ( setEmailExpanded(true)} activeOpacity={0.84} > {copy.emailCta} ) : ( {copy.nameLabel} {copy.emailLabel} {t.passwordLabel} setShowPassword((v) => !v)} style={styles.eyeBtn}> {t.confirmPasswordLabel} setShowPasswordConfirm((v) => !v)} style={styles.eyeBtn}> )} {error ? ( {error} ) : null} {emailExpanded ? ( {loading ? ( ) : ( {copy.createCta} )} ) : null} router.replace({ pathname: '/auth/login', params: { returnTo: params.returnTo, topup: params.topup } })} activeOpacity={0.78}> {copy.already}{' '} {copy.login} {copy.legal} ); } const styles = StyleSheet.create({ flex: { flex: 1 }, scroll: { flexGrow: 1 }, hero: { minHeight: 330, justifyContent: 'flex-end', paddingHorizontal: 24, paddingTop: 56, paddingBottom: 34, }, heroImage: { resizeMode: 'cover' }, heroOverlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(5, 12, 7, 0.46)', }, 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: 34, lineHeight: 39, 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: 28, paddingBottom: 40, justifyContent: 'space-between', }, pendingHint: { flexDirection: 'row', alignItems: 'center', gap: 10, borderRadius: 16, borderWidth: 1, paddingHorizontal: 14, paddingVertical: 12, }, pendingHintText: { flex: 1, fontSize: 13, lineHeight: 18, fontWeight: '700' }, appleButton: { width: '100%', height: 56 }, dividerRow: { flexDirection: 'row', alignItems: 'center', gap: 12 }, dividerLine: { flex: 1, height: 1 }, dividerText: { fontSize: 12, fontWeight: '800' }, emailChoiceBtn: { minHeight: 56, paddingVertical: 14, borderRadius: 14, borderWidth: 1.5, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 10, }, emailChoiceText: { fontSize: 16, fontWeight: '800' }, form: { gap: 12 }, fieldGroup: { gap: 6 }, label: { fontSize: 13, fontWeight: '800', marginLeft: 2 }, inputRow: { minHeight: 52, borderWidth: 1, borderRadius: 14, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, }, inputIcon: { marginRight: 10 }, input: { flex: 1, minHeight: 52, fontSize: 15 }, eyeBtn: { padding: 5, marginLeft: 6 }, errorBox: { flexDirection: 'row', alignItems: 'center', gap: 7, borderRadius: 12, paddingHorizontal: 12, paddingVertical: 10, }, errorText: { flex: 1, fontSize: 13, lineHeight: 18, fontWeight: '700' }, primaryBtn: { minHeight: 56, paddingVertical: 14, borderRadius: 14, alignItems: 'center', justifyContent: 'center', }, primaryBtnText: { fontSize: 17, fontWeight: '900' }, loginLink: { alignItems: 'center', paddingTop: 4, paddingBottom: 2 }, loginLinkText: { fontSize: 15, fontWeight: '700' }, legal: { textAlign: 'center', fontSize: 11.5, lineHeight: 16, paddingHorizontal: 12 }, });