feat(app): finish onboarding auth flow

This commit is contained in:
Timo Knuth
2026-07-06 14:32:20 +02:00
parent a1a3367ce4
commit 673ead2e3a
7 changed files with 708 additions and 817 deletions

View File

@@ -1,40 +1,80 @@
import React, { useEffect, useState } from 'react';
import {
View,
ActivityIndicator,
ImageBackground,
KeyboardAvoidingView,
Platform,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
KeyboardAvoidingView,
Platform,
ActivityIndicator,
ScrollView,
Image,
View,
} from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Ionicons } from '@expo/vector-icons';
import { router } 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 AsyncStorage from '@react-native-async-storage/async-storage';
import * as AppleAuthentication from 'expo-apple-authentication';
import Constants from 'expo-constants';
import { useSafeAnalytics } from '../../services/analytics';
import { PreAuthOnboardingService } from '../../services/preAuthOnboardingService';
import { Language } from '../../types';
const ONBOARDING_AUTH_BACKGROUND = {
light: '#fbfaf3',
dark: '#0a110b',
const HERO_IMAGE = require('../../assets/welcome_botanical_hero.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, t } = useApp();
const { isDarkMode, colorPalette, hydrateSession, getPendingPlant, language, t } = useApp();
const colors = useColors(isDarkMode, colorPalette);
const copy = getCopy(language);
const posthog = useSafeAnalytics();
const pendingPlant = getPendingPlant();
const screenBackground = isDarkMode
? ONBOARDING_AUTH_BACKGROUND.dark
: ONBOARDING_AUTH_BACKGROUND.light;
const isExpoGo = Constants.appOwnership === 'expo';
const [name, setName] = useState('');
const [email, setEmail] = useState('');
@@ -42,10 +82,14 @@ export default function SignupScreen() {
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<string | null>(null);
const isExpoGo = Constants.appOwnership === 'expo';
useEffect(() => {
posthog.capture('signup_screen_viewed', { context: 'onboarding' });
}, [posthog]);
useEffect(() => {
if (isExpoGo) {
@@ -65,6 +109,15 @@ export default function SignupScreen() {
};
}, [isExpoGo]);
const finishAuth = async (session: Awaited<ReturnType<typeof AuthService.signUp>>) => {
await hydrateSession(session);
if (session?.userId) {
await PreAuthOnboardingService.flushToProfile(session.userId).catch(() => {});
}
await AsyncStorage.setItem('greenlens_show_tour', 'true');
router.replace('/(tabs)');
};
const validate = (): string | null => {
if (!name.trim()) return t.errNameRequired;
if (!email.trim() || !email.includes('@')) return t.errEmailInvalid;
@@ -77,30 +130,21 @@ export default function SignupScreen() {
const validationError = validate();
if (validationError) {
setError(validationError);
setEmailExpanded(true);
return;
}
setLoading(true);
setError(null);
try {
const session = await AuthService.signUp(email, name, password);
await hydrateSession(session);
if (session?.userId) {
await PreAuthOnboardingService.flushToProfile(session.userId).catch(() => {});
}
// Flag setzen: Tour beim nächsten App-Öffnen anzeigen
await AsyncStorage.setItem('greenlens_show_tour', 'true');
router.replace('/onboarding/source');
await finishAuth(session);
} catch (e: any) {
if (e.message === 'EMAIL_TAKEN') {
setError(t.errEmailTaken);
} else if (e.message === 'BACKEND_URL_MISSING') {
setError(t.errNetworkError);
} else if (e.message === 'NETWORK_ERROR') {
} else if (e.message === 'BACKEND_URL_MISSING' || e.message === 'NETWORK_ERROR') {
setError(t.errNetworkError);
} else if (e.message === 'SERVER_ERROR') {
setError(t.errServerError);
} else if (e.message === 'AUTH_ERROR') {
setError(t.errAuthError);
} else {
setError(t.errAuthError);
}
@@ -135,24 +179,10 @@ export default function SignupScreen() {
email: credential.email,
name: fullName || undefined,
});
const billing = await hydrateSession(session);
if (session?.userId) {
await PreAuthOnboardingService.flushToProfile(session.userId).catch(() => {});
}
await AsyncStorage.setItem('greenlens_show_tour', 'true');
posthog.capture('apple_login_succeeded', { surface: 'signup' });
const isPro = billing?.entitlement?.plan === 'pro' && billing?.entitlement?.status === 'active';
if (session.isNewUser) {
router.replace('/onboarding/source');
} else {
// Same routing as login: existing non-pro accounts go to the paywall
// directly instead of bouncing through the root entitlement redirect.
router.replace(isPro || !billing ? '/(tabs)' : '/profile/billing');
}
await finishAuth(session);
} catch (e: any) {
if (e?.code === 'ERR_REQUEST_CANCELED') {
return;
}
if (e?.code === 'ERR_REQUEST_CANCELED') return;
posthog.capture('apple_login_failed', {
surface: 'signup',
error: e instanceof Error ? e.message : String(e),
@@ -167,222 +197,177 @@ export default function SignupScreen() {
return (
<KeyboardAvoidingView
style={[styles.flex, { backgroundColor: screenBackground }]}
style={[styles.flex, { backgroundColor: isDarkMode ? '#0a110b' : '#fbfaf3' }]}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<ScrollView
contentContainerStyle={styles.scroll}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity
style={[styles.backBtn, { backgroundColor: colors.surface, borderColor: colors.border }]}
onPress={() => router.back()}
>
<Ionicons name="arrow-back" size={20} color={colors.text} />
<ScrollView contentContainerStyle={styles.scroll} keyboardShouldPersistTaps="handled" showsVerticalScrollIndicator={false}>
<ImageBackground source={HERO_IMAGE} style={styles.hero} imageStyle={styles.heroImage}>
<View style={styles.heroOverlay} />
<TouchableOpacity style={styles.backBtn} onPress={() => router.back()} activeOpacity={0.8}>
<Ionicons name="arrow-back" size={20} color="#ffffff" />
</TouchableOpacity>
<Image
source={require('../../assets/icon.png')}
style={styles.logoIcon}
resizeMode="contain"
/>
<Text style={[styles.appName, { color: colors.text }]}>GreenLens</Text>
<Text style={[styles.subtitle, { color: colors.textSecondary }]}>
{t.createAccount}
</Text>
</View>
{/* Pending Plant Hint */}
{pendingPlant && (
<View style={[styles.pendingHint, { backgroundColor: `${colors.primarySoft}40`, borderColor: `${colors.primaryDark}40` }]}>
<Ionicons name="sparkles" size={18} color={colors.primaryDark} />
<Text style={[styles.pendingHintText, { color: colors.primaryDark }]}>
{t.pendingPlantHint.replace('{0}', pendingPlant.result.name)}
</Text>
<View style={styles.heroCopy}>
<Text style={styles.heroTitle}>{copy.headline}</Text>
<Text style={styles.heroSubline}>{copy.subline}</Text>
</View>
)}
</ImageBackground>
<View style={[styles.sheet, { backgroundColor: isDarkMode ? '#101a12' : '#fbfaf3' }]}>
{pendingPlant ? (
<View style={[styles.pendingHint, { backgroundColor: colors.primarySoft, borderColor: colors.border }]}>
<Ionicons name="sparkles" size={18} color={colors.primary} />
<Text style={[styles.pendingHintText, { color: colors.text }]}>
{copy.savePlantPrefix} {pendingPlant.result.name}
</Text>
</View>
) : null}
{/* Card */}
<View style={[styles.card, { backgroundColor: colors.surface, borderColor: colors.cardBorder, shadowColor: colors.cardShadow }]}>
{appleAvailable ? (
<AppleAuthentication.AppleAuthenticationButton
buttonType={AppleAuthentication.AppleAuthenticationButtonType.CONTINUE}
buttonStyle={isDarkMode
? AppleAuthentication.AppleAuthenticationButtonStyle.WHITE
: AppleAuthentication.AppleAuthenticationButtonStyle.BLACK}
cornerRadius={12}
cornerRadius={14}
style={styles.appleButton}
onPress={handleAppleSignIn}
/>
) : null}
{appleAvailable ? (
<View style={styles.dividerRowCompact}>
<View style={styles.dividerRow}>
<View style={[styles.dividerLine, { backgroundColor: colors.border }]} />
<Text style={[styles.dividerText, { color: colors.textMuted }]}>{t.orDivider}</Text>
<View style={[styles.dividerLine, { backgroundColor: colors.border }]} />
</View>
) : null}
{/* Name */}
<View style={styles.fieldGroup}>
<Text style={[styles.label, { color: colors.textSecondary }]}>Name</Text>
<View style={[styles.inputRow, { backgroundColor: colors.inputBg, borderColor: colors.inputBorder }]}>
<Ionicons name="person-outline" size={18} color={colors.textMuted} style={styles.inputIcon} />
<TextInput
style={[styles.input, { color: colors.text }]}
placeholder={t.namePlaceholder}
placeholderTextColor={colors.textMuted}
value={name}
onChangeText={setName}
autoCapitalize="words"
autoComplete="name"
returnKeyType="next"
/>
</View>
</View>
{!emailExpanded ? (
<TouchableOpacity
style={[styles.emailChoiceBtn, { backgroundColor: colors.surface, borderColor: colors.borderStrong }]}
onPress={() => setEmailExpanded(true)}
activeOpacity={0.84}
>
<Ionicons name="mail-outline" size={19} color={colors.primary} />
<Text style={[styles.emailChoiceText, { color: colors.text }]}>{copy.emailCta}</Text>
</TouchableOpacity>
) : (
<View style={styles.form}>
<View style={styles.fieldGroup}>
<Text style={[styles.label, { color: colors.textSecondary }]}>{copy.nameLabel}</Text>
<View style={[styles.inputRow, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<Ionicons name="person-outline" size={18} color={colors.textMuted} style={styles.inputIcon} />
<TextInput
style={[styles.input, { color: colors.text }]}
placeholder={t.namePlaceholder}
placeholderTextColor={colors.textMuted}
value={name}
onChangeText={setName}
autoCapitalize="words"
autoComplete="name"
returnKeyType="next"
/>
</View>
</View>
{/* Email */}
<View style={styles.fieldGroup}>
<Text style={[styles.label, { color: colors.textSecondary }]}>E-Mail</Text>
<View style={[styles.inputRow, { backgroundColor: colors.inputBg, borderColor: colors.inputBorder }]}>
<Ionicons name="mail-outline" size={18} color={colors.textMuted} style={styles.inputIcon} />
<TextInput
style={[styles.input, { color: colors.text }]}
placeholder={t.emailPlaceholder}
placeholderTextColor={colors.textMuted}
value={email}
onChangeText={setEmail}
autoCapitalize="none"
keyboardType="email-address"
autoComplete="email"
returnKeyType="next"
/>
</View>
</View>
<View style={styles.fieldGroup}>
<Text style={[styles.label, { color: colors.textSecondary }]}>{copy.emailLabel}</Text>
<View style={[styles.inputRow, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<Ionicons name="mail-outline" size={18} color={colors.textMuted} style={styles.inputIcon} />
<TextInput
style={[styles.input, { color: colors.text }]}
placeholder={t.emailPlaceholder}
placeholderTextColor={colors.textMuted}
value={email}
onChangeText={setEmail}
autoCapitalize="none"
keyboardType="email-address"
autoComplete="email"
returnKeyType="next"
/>
</View>
</View>
{/* Password */}
<View style={styles.fieldGroup}>
<Text style={[styles.label, { color: colors.textSecondary }]}>{t.passwordLabel}</Text>
<View style={[styles.inputRow, { backgroundColor: colors.inputBg, borderColor: colors.inputBorder }]}>
<Ionicons name="lock-closed-outline" size={18} color={colors.textMuted} style={styles.inputIcon} />
<TextInput
style={[styles.input, { color: colors.text }]}
placeholder={t.passwordPlaceholder}
placeholderTextColor={colors.textMuted}
value={password}
onChangeText={setPassword}
secureTextEntry={!showPassword}
autoComplete="new-password"
returnKeyType="next"
/>
<TouchableOpacity onPress={() => setShowPassword((v) => !v)} style={styles.eyeBtn}>
<Ionicons
name={showPassword ? 'eye-off-outline' : 'eye-outline'}
size={18}
color={colors.textMuted}
/>
</TouchableOpacity>
</View>
</View>
<View style={styles.fieldGroup}>
<Text style={[styles.label, { color: colors.textSecondary }]}>{t.passwordLabel}</Text>
<View style={[styles.inputRow, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<Ionicons name="lock-closed-outline" size={18} color={colors.textMuted} style={styles.inputIcon} />
<TextInput
style={[styles.input, { color: colors.text }]}
placeholder={t.passwordPlaceholder}
placeholderTextColor={colors.textMuted}
value={password}
onChangeText={setPassword}
secureTextEntry={!showPassword}
autoComplete="new-password"
returnKeyType="next"
/>
<TouchableOpacity onPress={() => setShowPassword((v) => !v)} style={styles.eyeBtn}>
<Ionicons name={showPassword ? 'eye-off-outline' : 'eye-outline'} size={18} color={colors.textMuted} />
</TouchableOpacity>
</View>
</View>
{/* Password Confirm */}
<View style={styles.fieldGroup}>
<Text style={[styles.label, { color: colors.textSecondary }]}>{t.confirmPasswordLabel}</Text>
<View style={[
styles.inputRow,
{
backgroundColor: colors.inputBg,
borderColor: passwordConfirm && password !== passwordConfirm ? colors.danger : colors.inputBorder,
},
]}>
<Ionicons name="lock-closed-outline" size={18} color={colors.textMuted} style={styles.inputIcon} />
<TextInput
style={[styles.input, { color: colors.text }]}
placeholder={t.confirmPasswordPlaceholder}
placeholderTextColor={colors.textMuted}
value={passwordConfirm}
onChangeText={setPasswordConfirm}
secureTextEntry={!showPasswordConfirm}
autoComplete="new-password"
returnKeyType="done"
onSubmitEditing={handleSignup}
/>
<TouchableOpacity onPress={() => setShowPasswordConfirm((v) => !v)} style={styles.eyeBtn}>
<Ionicons
name={showPasswordConfirm ? 'eye-off-outline' : 'eye-outline'}
size={18}
color={colors.textMuted}
/>
</TouchableOpacity>
</View>
</View>
{/* Password strength hint */}
{password.length > 0 && (
<View style={styles.strengthRow}>
{[1, 2, 3, 4].map((level) => (
<View
key={level}
style={[
styles.strengthBar,
{
backgroundColor:
password.length >= level * 3
? level <= 1
? colors.danger
: level === 2
? colors.warning
: colors.success
: colors.border,
},
]}
/>
))}
<Text style={[styles.strengthText, { color: colors.textMuted }]}>
{password.length < 4
? t.strengthTooShort
: password.length < 7
? t.strengthWeak
: password.length < 10
? t.strengthMedium
: t.strengthStrong}
</Text>
<View style={styles.fieldGroup}>
<Text style={[styles.label, { color: colors.textSecondary }]}>{t.confirmPasswordLabel}</Text>
<View style={[
styles.inputRow,
{
backgroundColor: colors.surface,
borderColor: passwordConfirm && password !== passwordConfirm ? colors.danger : colors.border,
},
]}>
<Ionicons name="lock-closed-outline" size={18} color={colors.textMuted} style={styles.inputIcon} />
<TextInput
style={[styles.input, { color: colors.text }]}
placeholder={t.confirmPasswordPlaceholder}
placeholderTextColor={colors.textMuted}
value={passwordConfirm}
onChangeText={setPasswordConfirm}
secureTextEntry={!showPasswordConfirm}
autoComplete="new-password"
returnKeyType="done"
onSubmitEditing={handleSignup}
/>
<TouchableOpacity onPress={() => setShowPasswordConfirm((v) => !v)} style={styles.eyeBtn}>
<Ionicons name={showPasswordConfirm ? 'eye-off-outline' : 'eye-outline'} size={18} color={colors.textMuted} />
</TouchableOpacity>
</View>
</View>
</View>
)}
{/* Error */}
{error && (
{error ? (
<View style={[styles.errorBox, { backgroundColor: colors.dangerSoft }]}>
<Ionicons name="alert-circle-outline" size={15} color={colors.danger} />
<Text style={[styles.errorText, { color: colors.danger }]}>{error}</Text>
<Text style={[styles.errorText, { color: colors.danger }]} selectable>{error}</Text>
</View>
)}
) : null}
{/* Signup Button */}
<TouchableOpacity
style={[styles.primaryBtn, { backgroundColor: colors.primary, opacity: loading ? 0.7 : 1 }]}
onPress={handleSignup}
activeOpacity={0.82}
disabled={loading}
>
{loading ? (
<ActivityIndicator color={colors.onPrimary} size="small" />
) : (
<Text style={[styles.primaryBtnText, { color: colors.onPrimary }]}>{t.onboardingRegister}</Text>
)}
{emailExpanded ? (
<TouchableOpacity
style={[styles.primaryBtn, { backgroundColor: colors.primary, opacity: loading ? 0.72 : 1 }]}
onPress={handleSignup}
activeOpacity={0.84}
disabled={loading}
>
{loading ? (
<ActivityIndicator color={colors.onPrimary} size="small" />
) : (
<Text style={[styles.primaryBtnText, { color: colors.onPrimary }]}>{copy.createCta}</Text>
)}
</TouchableOpacity>
) : null}
<TouchableOpacity style={styles.loginLink} onPress={() => router.replace('/auth/login')} activeOpacity={0.78}>
<Text style={[styles.loginLinkText, { color: colors.textSecondary }]}>
{copy.already}{' '}
<Text style={{ color: colors.primary, fontWeight: '800' }}>{copy.login}</Text>
</Text>
</TouchableOpacity>
</View>
{/* Login link */}
<TouchableOpacity style={styles.loginLink} onPress={() => router.replace('/auth/login')}>
<Text style={[styles.loginLinkText, { color: colors.textSecondary }]}>
{t.alreadyHaveAccount}{' '}
<Text style={{ color: colors.primary, fontWeight: '600' }}>{t.onboardingLogin}</Text>
</Text>
</TouchableOpacity>
<Text style={[styles.legal, { color: colors.textMuted }]}>{copy.legal}</Text>
</View>
</ScrollView>
</KeyboardAvoidingView>
);
@@ -390,160 +375,108 @@ export default function SignupScreen() {
const styles = StyleSheet.create({
flex: { flex: 1 },
scroll: {
flexGrow: 1,
justifyContent: 'center',
scroll: { flexGrow: 1 },
hero: {
minHeight: 330,
justifyContent: 'flex-end',
paddingHorizontal: 24,
paddingVertical: 48,
paddingTop: 56,
paddingBottom: 34,
},
header: {
alignItems: 'center',
marginBottom: 32,
heroImage: { resizeMode: 'cover' },
heroOverlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(5, 12, 7, 0.46)',
},
backBtn: {
position: 'absolute',
left: 0,
top: 0,
width: 40,
height: 40,
borderRadius: 20,
borderWidth: 1,
top: 54,
left: 22,
width: 42,
height: 42,
borderRadius: 21,
alignItems: 'center',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.28)',
},
logoIcon: {
width: 84,
height: 84,
borderRadius: 20,
marginBottom: 16,
heroCopy: { gap: 10 },
heroTitle: {
color: '#ffffff',
fontSize: 34,
lineHeight: 39,
fontWeight: '900',
},
appName: {
fontSize: 30,
fontWeight: '700',
letterSpacing: -0.5,
marginBottom: 6,
},
subtitle: {
fontSize: 15,
fontWeight: '400',
},
card: {
borderRadius: 20,
borderWidth: 1,
padding: 24,
gap: 14,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 1,
shadowRadius: 12,
elevation: 4,
},
appleButton: {
width: '100%',
height: 50,
marginBottom: 2,
},
dividerRowCompact: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
marginVertical: 2,
},
dividerLine: {
flex: 1,
height: 1,
},
dividerText: {
fontSize: 12,
fontWeight: '500',
},
fieldGroup: {
gap: 6,
},
label: {
fontSize: 13,
fontWeight: '500',
marginLeft: 2,
},
inputRow: {
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1,
borderRadius: 12,
paddingHorizontal: 14,
height: 50,
},
inputIcon: {
marginRight: 10,
},
input: {
flex: 1,
fontSize: 15,
height: 50,
},
eyeBtn: {
padding: 4,
marginLeft: 6,
},
strengthRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
marginTop: -4,
},
strengthBar: {
flex: 1,
height: 3,
borderRadius: 2,
},
strengthText: {
fontSize: 11,
marginLeft: 4,
width: 40,
},
errorBox: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
borderRadius: 10,
paddingHorizontal: 12,
paddingVertical: 10,
},
errorText: {
fontSize: 13,
flex: 1,
},
primaryBtn: {
height: 52,
borderRadius: 14,
justifyContent: 'center',
alignItems: 'center',
marginTop: 4,
},
primaryBtnText: {
heroSubline: {
color: 'rgba(255, 255, 255, 0.88)',
fontSize: 16,
fontWeight: '600',
lineHeight: 22,
fontWeight: '700',
},
loginLink: {
alignItems: 'center',
marginTop: 24,
paddingVertical: 8,
},
loginLinkText: {
fontSize: 15,
sheet: {
flex: 1,
marginTop: -24,
borderTopLeftRadius: 28,
borderTopRightRadius: 28,
paddingHorizontal: 22,
paddingTop: 24,
paddingBottom: 32,
gap: 14,
},
pendingHint: {
flexDirection: 'row',
alignItems: 'center',
padding: 16,
gap: 10,
borderRadius: 16,
borderWidth: 1,
marginBottom: 20,
gap: 12,
paddingHorizontal: 14,
paddingVertical: 12,
},
pendingHintText: {
flex: 1,
fontSize: 13,
fontWeight: '600',
lineHeight: 18,
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: {
height: 56,
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: {
height: 52,
borderWidth: 1,
borderRadius: 14,
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 14,
},
inputIcon: { marginRight: 10 },
input: { flex: 1, height: 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: {
height: 56,
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 },
});