From de63ac5270ecb233358ee3c6e889e18a0bf3d074 Mon Sep 17 00:00:00 2001 From: albyalex96 Date: Thu, 5 Mar 2026 20:02:39 +0100 Subject: [PATCH] Improved Localization in Auth Screen --- src/i18n/locales/en.json | 20 + src/i18n/locales/it.json | 30 +- src/screens/AuthScreen.tsx | 1725 +++++++++++++++++++++--------------- 3 files changed, 1053 insertions(+), 722 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 0f5c4bc5..c48ce7e6 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1531,5 +1531,25 @@ }, "ai_chat_screen":{ "loading":"Loading AI context..." + }, + "auth_screen":{ + "invalid_email_title":"Invalid Email", + "invalid_email_desc":"Enter a valid email address", + "password_short_title":"Password too short", + "password_short_desc":"Password must be at least 6 characters", + "password_not_match_title":"Password do not match", + "password_not_match_desc":"Password do not match", + "authentication_failed":"Authentication Failed", + "welcome_back":"Welcome back", + "create_account":"Create your account", + "sync_desc":"Sync your addons, progress and settings across devices", + "sign_in":"Sign In", + "password_min_placeholder":"Password (min 6 characters)", + "forgot_password":"Forgot password?", + "password_confirm_placeholder":"Confirm Password", + "dont_have_account":"Don't have an account?", + "already_have_account":"Already have an account?", + "sign_up":"Sign Up", + "continue_without_account":"Continue without an account" } } diff --git a/src/i18n/locales/it.json b/src/i18n/locales/it.json index 353916df..bb4d4839 100644 --- a/src/i18n/locales/it.json +++ b/src/i18n/locales/it.json @@ -15,7 +15,7 @@ "go_back": "Torna indietro", "settings": "Impostazioni", "close": "Chiudi", - "remove":"Rimuovi", + "remove": "Rimuovi", "enable": "Abilita", "disable": "Disabilita", "show_more": "Mostra altro", @@ -948,10 +948,10 @@ "confirm_remove_msg": "Sei sicuro di voler rimuovere la tua chiave API OpenRouter? Questo disabiliterà le funzioni di chat IA.", "success_removed": "Chiave API rimossa con successo", "error_remove": "Impossibile rimuovere la chiave API", - "model":"Modello", - "using":"Usando", - "free_routing":"(Routing automatico gratuito)", - "paid_routing":"Usa un ID di modello Openrouter (utile per piani a pagamento)." + "model": "Modello", + "using": "Usando", + "free_routing": "(Routing automatico gratuito)", + "paid_routing": "Usa un ID di modello Openrouter (utile per piani a pagamento)." }, "catalog_settings": { "title": "Cataloghi", @@ -1551,5 +1551,25 @@ }, "ai_chat_screen": { "loading": "Caricamento contesto AI in corso..." + }, + "auth_screen": { + "invalid_email_title": "Email non valida", + "invalid_email_desc": "Inserisci un indirizzo email valido", + "password_short_title": "Password troppo corta", + "password_short_desc": "La password deve contenere almeno 6 lettere", + "password_not_match_title": "Password non corrispondenti", + "password_not_match_desc": "Le Password non corrispondono", + "authentication_failed": "Autenticazione Fallita", + "welcome_back": "Bentornato", + "create_account": "Crea il tuo account", + "sync_desc": "Sincronizza i tuoi addon , progressi e impostazioni fra i tuoi dispositivi", + "sign_in": "Accedi", + "password_min_placeholder": "Password (min 6 lettere)", + "forgot_password": "Password dimenticata?", + "password_confirm_placeholder": "Conferma Password", + "dont_have_account": "Non hai un account?", + "already_have_account": "Hai già un account?", + "sign_up": "Registrati", + "continue_without_account": "Continua senza account" } } diff --git a/src/screens/AuthScreen.tsx b/src/screens/AuthScreen.tsx index 7c42cfb0..22c93709 100644 --- a/src/screens/AuthScreen.tsx +++ b/src/screens/AuthScreen.tsx @@ -1,5 +1,21 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; -import { View, TextInput, Text, TouchableOpacity, StyleSheet, ActivityIndicator, SafeAreaView, KeyboardAvoidingView, Platform, Animated, Easing, Keyboard, StatusBar, useWindowDimensions, Linking } from 'react-native'; +import { + View, + TextInput, + Text, + TouchableOpacity, + StyleSheet, + ActivityIndicator, + SafeAreaView, + KeyboardAvoidingView, + Platform, + Animated, + Easing, + Keyboard, + StatusBar, + useWindowDimensions, + Linking, +} from 'react-native'; import { mmkvStorage } from '../services/mmkvStorage'; import { LinearGradient } from 'expo-linear-gradient'; import { MaterialIcons } from '@expo/vector-icons'; @@ -9,762 +25,1037 @@ import { useNavigation, useRoute } from '@react-navigation/native'; import * as Haptics from 'expo-haptics'; import { useToast } from '../contexts/ToastContext'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useTranslation } from 'react-i18next'; const EMAIL_CONFIRMATION_REQUIRED_PREFIX = '__EMAIL_CONFIRMATION__'; const AUTH_BG_GRADIENT = ['#07090F', '#0D1020', '#140B24'] as const; const normalizeAuthErrorMessage = (input: string): string => { - const raw = (input || '').trim(); - if (!raw) return 'Authentication failed'; + const raw = (input || '').trim(); + if (!raw) return 'Authentication failed'; - let parsed: any = null; - if (raw.startsWith('{') && raw.endsWith('}')) { - try { - parsed = JSON.parse(raw); - } catch { - parsed = null; - } - } + let parsed: any = null; + if (raw.startsWith('{') && raw.endsWith('}')) { + try { + parsed = JSON.parse(raw); + } catch { + parsed = null; + } + } - const code = (parsed?.error_code || parsed?.code || '').toString().toLowerCase(); - const message = (parsed?.msg || parsed?.message || raw).toString(); + const code = (parsed?.error_code || parsed?.code || '') + .toString() + .toLowerCase(); + const message = (parsed?.msg || parsed?.message || raw).toString(); - if (code === 'invalid_credentials' || /invalid login credentials/i.test(message)) { - return 'Invalid email or password'; - } - if (code === 'email_not_confirmed' || /email not confirmed/i.test(message)) { - return 'Email not confirmed. Check your inbox or Spam/Junk folder, verify your account, then sign in.'; - } + if ( + code === 'invalid_credentials' || + /invalid login credentials/i.test(message) + ) { + return 'Invalid email or password'; + } + if (code === 'email_not_confirmed' || /email not confirmed/i.test(message)) { + return 'Email not confirmed. Check your inbox or Spam/Junk folder, verify your account, then sign in.'; + } - return message; + return message; }; const AuthScreen: React.FC = () => { - const { width, height } = useWindowDimensions(); - const isTablet = width >= 768; - const { currentTheme } = useTheme(); - const { signIn, signUp } = useAccount(); - const navigation = useNavigation(); - const route = useRoute(); - const fromOnboarding = !!route?.params?.fromOnboarding; - const insets = useSafeAreaInsets(); - const safeTopInset = Math.max(insets.top, Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : 0); - const backButtonTop = safeTopInset + 8; - const { showError, showSuccess } = useToast(); + const { width, height } = useWindowDimensions(); + const isTablet = width >= 768; + const { currentTheme } = useTheme(); + const { signIn, signUp } = useAccount(); + const navigation = useNavigation(); + const route = useRoute(); + const fromOnboarding = !!route?.params?.fromOnboarding; + const insets = useSafeAreaInsets(); + const safeTopInset = Math.max( + insets.top, + Platform.OS === 'android' ? StatusBar.currentHeight || 0 : 0, + ); + const backButtonTop = safeTopInset + 8; + const { showError, showSuccess } = useToast(); + const { t } = useTranslation(); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [showConfirm, setShowConfirm] = useState(false); + const [mode, setMode] = useState<'signin' | 'signup'>('signin'); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [confirmPassword, setConfirmPassword] = useState(''); - const [showPassword, setShowPassword] = useState(false); - const [showConfirm, setShowConfirm] = useState(false); - const [mode, setMode] = useState<'signin' | 'signup'>('signin'); - const [error, setError] = useState(null); - const [loading, setLoading] = useState(false); + // Subtle, performant animations + const introOpacity = useRef(new Animated.Value(0)).current; + const introTranslateY = useRef(new Animated.Value(10)).current; + const cardOpacity = useRef(new Animated.Value(0)).current; + const cardTranslateY = useRef(new Animated.Value(12)).current; + const ctaScale = useRef(new Animated.Value(1)).current; + const titleOpacity = useRef(new Animated.Value(1)).current; + const titleTranslateY = useRef(new Animated.Value(0)).current; + const ctaTextOpacity = useRef(new Animated.Value(1)).current; + const ctaTextTranslateY = useRef(new Animated.Value(0)).current; + const modeAnim = useRef(new Animated.Value(0)).current; // 0 = signin, 1 = signup + const [switchWidth, setSwitchWidth] = useState(0); + // Legacy local toast state removed in favor of global toast + const [keyboardHeight, setKeyboardHeight] = useState(0); - // Subtle, performant animations - const introOpacity = useRef(new Animated.Value(0)).current; - const introTranslateY = useRef(new Animated.Value(10)).current; - const cardOpacity = useRef(new Animated.Value(0)).current; - const cardTranslateY = useRef(new Animated.Value(12)).current; - const ctaScale = useRef(new Animated.Value(1)).current; - const titleOpacity = useRef(new Animated.Value(1)).current; - const titleTranslateY = useRef(new Animated.Value(0)).current; - const ctaTextOpacity = useRef(new Animated.Value(1)).current; - const ctaTextTranslateY = useRef(new Animated.Value(0)).current; - const modeAnim = useRef(new Animated.Value(0)).current; // 0 = signin, 1 = signup - const [switchWidth, setSwitchWidth] = useState(0); - // Legacy local toast state removed in favor of global toast - const [keyboardHeight, setKeyboardHeight] = useState(0); + useEffect(() => { + Animated.parallel([ + Animated.timing(introOpacity, { + toValue: 1, + duration: 300, + easing: Easing.out(Easing.cubic), + useNativeDriver: true, + }), + Animated.timing(introTranslateY, { + toValue: 0, + duration: 300, + easing: Easing.out(Easing.cubic), + useNativeDriver: true, + }), + Animated.timing(cardOpacity, { + toValue: 1, + duration: 360, + delay: 90, + easing: Easing.out(Easing.cubic), + useNativeDriver: true, + }), + Animated.timing(cardTranslateY, { + toValue: 0, + duration: 360, + delay: 90, + easing: Easing.out(Easing.cubic), + useNativeDriver: true, + }), + ]).start(); + }, [cardOpacity, cardTranslateY, introOpacity, introTranslateY]); - useEffect(() => { - Animated.parallel([ - Animated.timing(introOpacity, { - toValue: 1, - duration: 300, - easing: Easing.out(Easing.cubic), - useNativeDriver: true, - }), - Animated.timing(introTranslateY, { - toValue: 0, - duration: 300, - easing: Easing.out(Easing.cubic), - useNativeDriver: true, - }), - Animated.timing(cardOpacity, { - toValue: 1, - duration: 360, - delay: 90, - easing: Easing.out(Easing.cubic), - useNativeDriver: true, - }), - Animated.timing(cardTranslateY, { - toValue: 0, - duration: 360, - delay: 90, - easing: Easing.out(Easing.cubic), - useNativeDriver: true, - }), - ]).start(); - }, [cardOpacity, cardTranslateY, introOpacity, introTranslateY]); + // Animate on mode change + useEffect(() => { + Animated.timing(modeAnim, { + toValue: mode === 'signin' ? 0 : 1, + duration: 220, + easing: Easing.out(Easing.cubic), + useNativeDriver: true, + }).start(); - // Animate on mode change - useEffect(() => { - Animated.timing(modeAnim, { - toValue: mode === 'signin' ? 0 : 1, - duration: 220, - easing: Easing.out(Easing.cubic), - useNativeDriver: true, - }).start(); + Animated.sequence([ + Animated.parallel([ + Animated.timing(titleOpacity, { + toValue: 0, + duration: 120, + useNativeDriver: true, + }), + Animated.timing(titleTranslateY, { + toValue: -6, + duration: 120, + useNativeDriver: true, + }), + Animated.timing(ctaTextOpacity, { + toValue: 0, + duration: 120, + useNativeDriver: true, + }), + Animated.timing(ctaTextTranslateY, { + toValue: -4, + duration: 120, + useNativeDriver: true, + }), + ]), + Animated.parallel([ + Animated.timing(titleOpacity, { + toValue: 1, + duration: 180, + useNativeDriver: true, + }), + Animated.timing(titleTranslateY, { + toValue: 0, + duration: 180, + useNativeDriver: true, + }), + Animated.timing(ctaTextOpacity, { + toValue: 1, + duration: 180, + useNativeDriver: true, + }), + Animated.timing(ctaTextTranslateY, { + toValue: 0, + duration: 180, + useNativeDriver: true, + }), + ]), + ]).start(); + }, [ + mode, + ctaTextOpacity, + ctaTextTranslateY, + modeAnim, + titleOpacity, + titleTranslateY, + ]); - Animated.sequence([ - Animated.parallel([ - Animated.timing(titleOpacity, { toValue: 0, duration: 120, useNativeDriver: true }), - Animated.timing(titleTranslateY, { toValue: -6, duration: 120, useNativeDriver: true }), - Animated.timing(ctaTextOpacity, { toValue: 0, duration: 120, useNativeDriver: true }), - Animated.timing(ctaTextTranslateY, { toValue: -4, duration: 120, useNativeDriver: true }), - ]), - Animated.parallel([ - Animated.timing(titleOpacity, { toValue: 1, duration: 180, useNativeDriver: true }), - Animated.timing(titleTranslateY, { toValue: 0, duration: 180, useNativeDriver: true }), - Animated.timing(ctaTextOpacity, { toValue: 1, duration: 180, useNativeDriver: true }), - Animated.timing(ctaTextTranslateY, { toValue: 0, duration: 180, useNativeDriver: true }), - ]), - ]).start(); - }, [mode, ctaTextOpacity, ctaTextTranslateY, modeAnim, titleOpacity, titleTranslateY]); + // Hide/show header when keyboard toggles + useEffect(() => { + const showEvt = + Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow'; + const hideEvt = + Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide'; + const onShow = (e: any) => { + const kh = e?.endCoordinates?.height ?? 0; + setKeyboardHeight(kh); + }; + const onHide = () => { + setKeyboardHeight(0); + }; + const subShow = Keyboard.addListener(showEvt, onShow as any); + const subHide = Keyboard.addListener(hideEvt, onHide as any); + return () => { + subShow.remove(); + subHide.remove(); + }; + }, []); - // Hide/show header when keyboard toggles - useEffect(() => { - const showEvt = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow'; - const hideEvt = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide'; - const onShow = (e: any) => { - const kh = e?.endCoordinates?.height ?? 0; - setKeyboardHeight(kh); - }; - const onHide = () => { - setKeyboardHeight(0); - }; - const subShow = Keyboard.addListener(showEvt, onShow as any); - const subHide = Keyboard.addListener(hideEvt, onHide as any); - return () => { - subShow.remove(); - subHide.remove(); - }; - }, []); + const isEmailValid = useMemo(() => /\S+@\S+\.\S+/.test(email.trim()), [email]); + const isPasswordValid = useMemo(() => password.length >= 6, [password]); + const isConfirmValid = useMemo( + () => mode === 'signin' || confirmPassword.length >= 6, + [confirmPassword, mode], + ); + const passwordsMatch = useMemo( + () => mode === 'signin' || confirmPassword === password, + [confirmPassword, password, mode], + ); + const canSubmit = + isEmailValid && + isPasswordValid && + (mode === 'signin' || (isConfirmValid && passwordsMatch)); - const isEmailValid = useMemo(() => /\S+@\S+\.\S+/.test(email.trim()), [email]); - const isPasswordValid = useMemo(() => password.length >= 6, [password]); - const isConfirmValid = useMemo(() => (mode === 'signin') || confirmPassword.length >= 6, [confirmPassword, mode]); - const passwordsMatch = useMemo(() => (mode === 'signin') || confirmPassword === password, [confirmPassword, password, mode]); - const canSubmit = isEmailValid && isPasswordValid && (mode === 'signin' || (isConfirmValid && passwordsMatch)); + const handleSubmit = async () => { + if (loading) return; - const handleSubmit = async () => { - if (loading) return; + if (!isEmailValid) { + const msg = 'Enter a valid email address'; + setError(msg); + showError( + t('auth_screen.invalid_email_title'), + t('auth_screen.invalid_email_desc'), + ); + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch( + () => {}, + ); + return; + } + if (!isPasswordValid) { + const msg = 'Password must be at least 6 characters'; + setError(msg); + showError( + t('auth_screen.password_short_title'), + t('auth_screen.password_short_desc'), + ); + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch( + () => {}, + ); + return; + } + if (mode === 'signup' && !passwordsMatch) { + const msg = 'Passwords do not match'; + setError(msg); + showError(t('password_not_match_title'), t('password_not_match_desc')); + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch( + () => {}, + ); + return; + } + setLoading(true); + setError(null); + const err = + mode === 'signin' + ? await signIn(email.trim(), password) + : await signUp(email.trim(), password); + if (err) { + if ( + mode === 'signup' && + err.startsWith(EMAIL_CONFIRMATION_REQUIRED_PREFIX) + ) { + setError(null); + setMode('signin'); + setPassword(''); + setConfirmPassword(''); + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch( + () => {}, + ); + setLoading(false); + return; + } - if (!isEmailValid) { - const msg = 'Enter a valid email address'; - setError(msg); - showError('Invalid Email', 'Enter a valid email address'); - Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {}); - return; - } - if (!isPasswordValid) { - const msg = 'Password must be at least 6 characters'; - setError(msg); - showError('Password Too Short', 'Password must be at least 6 characters'); - Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {}); - return; - } - if (mode === 'signup' && !passwordsMatch) { - const msg = 'Passwords do not match'; - setError(msg); - showError('Passwords Don\'t Match', 'Passwords do not match'); - Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {}); - return; - } - setLoading(true); - setError(null); - const err = mode === 'signin' ? await signIn(email.trim(), password) : await signUp(email.trim(), password); - if (err) { - if (mode === 'signup' && err.startsWith(EMAIL_CONFIRMATION_REQUIRED_PREFIX)) { - setError(null); - setMode('signin'); - setPassword(''); - setConfirmPassword(''); - Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {}); - setLoading(false); - return; - } + const cleanError = normalizeAuthErrorMessage(err); + setError(cleanError); + showError(t('auth_screen.authentication_failed'), cleanError); + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch( + () => {}, + ); + } else { + const msg = + mode === 'signin' ? 'Logged in successfully' : 'Sign up successful'; + showSuccess(t('common.success'), msg); + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch( + () => {}, + ); - const cleanError = normalizeAuthErrorMessage(err); - setError(cleanError); - showError('Authentication Failed', cleanError); - Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {}); - } else { - const msg = mode === 'signin' ? 'Logged in successfully' : 'Sign up successful'; - showSuccess('Success', msg); - Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {}); + // Navigate to main tabs after successful authentication + navigation.reset({ + index: 0, + routes: [{ name: 'MainTabs' as never }], + } as any); + } + setLoading(false); + }; - // Navigate to main tabs after successful authentication - navigation.reset({ index: 0, routes: [{ name: 'MainTabs' as never }] } as any); - } - setLoading(false); - }; + const handleSkipAuth = async () => { + try { + await mmkvStorage.setItem('showLoginHintToastOnce', 'true'); + } catch {} + navigation.reset({ + index: 0, + routes: [{ name: 'MainTabs' as never }], + } as any); + }; - const handleSkipAuth = async () => { - try { - await mmkvStorage.setItem('showLoginHintToastOnce', 'true'); - } catch {} - navigation.reset({ index: 0, routes: [{ name: 'MainTabs' as never }] } as any); - }; + // showToast helper replaced with direct calls to toast.* API - // showToast helper replaced with direct calls to toast.* API + return ( + + - return ( - - + {/* Background Pattern (iOS only) */} + {Platform.OS !== 'android' && ( + + {Array.from({ length: 20 }).map((_, i) => ( + + ))} + + )} - {/* Background Pattern (iOS only) */} - {Platform.OS !== 'android' && ( - - {Array.from({ length: 20 }).map((_, i) => ( - - ))} - - )} + + {navigation.canGoBack() && ( + navigation.goBack()} + style={[styles.backButton, { top: backButtonTop }]} + hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} + > + + + )} - - {navigation.canGoBack() && ( - navigation.goBack()} - style={[styles.backButton, { top: backButtonTop }]} - hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} - > - - - )} + + 0 + ? { + justifyContent: 'flex-start', + paddingTop: Platform.OS === 'ios' ? 12 : safeTopInset + 8, + } + : null, + ]} + > + 0 ? styles.centerHeaderCompact : null, + { + opacity: introOpacity, + transform: [{ translateY: introTranslateY }], + }, + ]} + > + + {mode === 'signin' + ? t('auth_screen.welcome_back') + : t('auth_screen.create_account')} + + {keyboardHeight === 0 && ( + + {t('auth_screen.sync_desc')} + + )} + - - 0 - ? { - justifyContent: 'flex-start', - paddingTop: Platform.OS === 'ios' ? 12 : safeTopInset + 8, - } - : null, - ]} - > - 0 ? styles.centerHeaderCompact : null, - { - opacity: introOpacity, - transform: [{ translateY: introTranslateY }], - }, - ]} - > - - {mode === 'signin' ? 'Welcome back' : 'Create your account'} - - {keyboardHeight === 0 && ( - - Sync your addons, progress and settings across devices - - )} - + 0 ? styles.cardCompact : null, + { + backgroundColor: + Platform.OS === 'android' ? '#121212' : 'rgba(255,255,255,0.02)', + borderColor: + Platform.OS === 'android' ? '#1f1f1f' : 'rgba(255,255,255,0.06)', + ...(Platform.OS !== 'android' + ? { + shadowColor: currentTheme.colors.primary, + shadowOffset: { width: 0, height: 0 }, + shadowOpacity: 0.1, + shadowRadius: 20, + } + : {}), + opacity: cardOpacity, + transform: [{ translateY: cardTranslateY }], + }, + ]} + > + {/* Mode Toggle */} + setSwitchWidth(e.nativeEvent.layout.width)} + style={[ + styles.switchRow, + { + backgroundColor: + Platform.OS === 'android' ? '#1a1a1a' : 'rgba(255,255,255,0.04)', + }, + ]} + > + {/* Animated indicator */} + + setMode('signin')} + activeOpacity={0.8} + > + + {t('auth_screen.sign_in')} + + + setMode('signup')} + activeOpacity={0.8} + > + + Sign Up + + + - 0 ? styles.cardCompact : null, { - backgroundColor: Platform.OS === 'android' ? '#121212' : 'rgba(255,255,255,0.02)', - borderColor: Platform.OS === 'android' ? '#1f1f1f' : 'rgba(255,255,255,0.06)', - ...(Platform.OS !== 'android' ? { - shadowColor: currentTheme.colors.primary, - shadowOffset: { width: 0, height: 0 }, - shadowOpacity: 0.1, - shadowRadius: 20, - } : {}), - opacity: cardOpacity, - transform: [{ translateY: cardTranslateY }], - }]}> - - {/* Mode Toggle */} - setSwitchWidth(e.nativeEvent.layout.width)} - style={[styles.switchRow, { backgroundColor: Platform.OS === 'android' ? '#1a1a1a' : 'rgba(255,255,255,0.04)' }]} - > - {/* Animated indicator */} - - setMode('signin')} - activeOpacity={0.8} - > - - Sign In - - - setMode('signup')} - activeOpacity={0.8} - > - - Sign Up - - - + {/* Email Input */} + + + + + + + {Platform.OS !== 'android' && isEmailValid && ( + + )} + + - {/* Email Input */} - - - - - - - {Platform.OS !== 'android' && isEmailValid && ( - - )} - - + {/* Password Input */} + + + + + + + setShowPassword((p) => !p)} + style={styles.eyeButton} + > + + + {Platform.OS !== 'android' && isPasswordValid && ( + + )} + + - {/* Password Input */} - - - - - - - setShowPassword(p => !p)} style={styles.eyeButton}> - - - {Platform.OS !== 'android' && isPasswordValid && ( - - )} - - + {mode === 'signin' && ( + + Linking.openURL('https://nuvioapp.space/account/reset-password') + } + activeOpacity={0.75} + style={styles.forgotPasswordButton} + > + + {t('auth_screen.forgot_password')} + + + )} - {mode === 'signin' && ( - Linking.openURL('https://nuvioapp.space/account/reset-password')} - activeOpacity={0.75} - style={styles.forgotPasswordButton} - > - Forgot password? - - )} + {/* Confirm Password (signup only) */} + {mode === 'signup' && ( + + + + + + + setShowConfirm((p) => !p)} + style={styles.eyeButton} + > + + + {Platform.OS !== 'android' && passwordsMatch && isConfirmValid && ( + + )} + + + )} - {/* Confirm Password (signup only) */} - {mode === 'signup' && ( - - - - - - - setShowConfirm(p => !p)} style={styles.eyeButton}> - - - {Platform.OS !== 'android' && passwordsMatch && isConfirmValid && ( - - )} - - - )} + {/* Error */} + {!!error && ( + + + {error} + + )} - {/* Error */} - {!!error && ( - - - {error} - - )} + {/* Submit Button */} + + { + Animated.spring(ctaScale, { + toValue: 0.98, + useNativeDriver: true, + speed: 20, + bounciness: 0, + }).start(); + }} + onPressOut={() => { + Animated.spring(ctaScale, { + toValue: 1, + useNativeDriver: true, + speed: 20, + bounciness: 6, + }).start(); + }} + activeOpacity={0.85} + disabled={loading} + > + {loading ? ( + + ) : ( + + {mode === 'signin' + ? t('auth_screen.sign_in') + : t('auth_screen.create_account')} + + )} + + - {/* Submit Button */} - - { - Animated.spring(ctaScale, { - toValue: 0.98, - useNativeDriver: true, - speed: 20, - bounciness: 0, - }).start(); - }} - onPressOut={() => { - Animated.spring(ctaScale, { - toValue: 1, - useNativeDriver: true, - speed: 20, - bounciness: 6, - }).start(); - }} - activeOpacity={0.85} - disabled={loading} - > - {loading ? ( - - ) : ( - - {mode === 'signin' ? 'Sign In' : 'Create Account'} - - )} - - + {/* Switch Mode */} + setMode(mode === 'signin' ? 'signup' : 'signin')} + activeOpacity={0.7} + style={{ marginTop: 16 }} + > + + {mode === 'signin' + ? t('auth_screen.dont_have_account') + : t('auth_screen.already_have_account')} + + {mode === 'signin' + ? t('auth_screen.sign_up') + : t('auth_screen.sign_in')} + + + - {/* Switch Mode */} - setMode(mode === 'signin' ? 'signup' : 'signin')} - activeOpacity={0.7} - style={{ marginTop: 16 }} - > - - {mode === 'signin' ? "Don't have an account? " : 'Already have an account? '} - - {mode === 'signin' ? 'Sign up' : 'Sign in'} - - - - - {/* Skip sign in - more prominent when coming from onboarding */} - - - Continue without an account - - - - - - - {/* Toasts rendered globally in App root */} - - - ); + {/* Skip sign in - more prominent when coming from onboarding */} + + + {t('auth_screen.continue_without_account')} + + + + + + {/* Toasts rendered globally in App root */} + + + ); }; const styles = StyleSheet.create({ - backgroundPattern: { - ...StyleSheet.absoluteFillObject, - zIndex: 0, - }, - patternDot: { - position: 'absolute', - width: 2, - height: 2, - backgroundColor: '#fff', - borderRadius: 1, - }, - header: { - alignItems: 'center', - paddingBottom: 8, - }, - centerHeader: { - width: '100%', - alignItems: 'center', - marginBottom: 18, - paddingHorizontal: 20, - }, - centerHeaderTablet: { - maxWidth: 620, - }, - centerHeaderCompact: { - marginBottom: 10, - }, - logoContainer: { - position: 'relative', - alignItems: 'center', - justifyContent: 'center', - marginBottom: 16, - }, - logo: { - width: 180, - height: 54, - zIndex: 2, - }, - logoGlow: { - position: 'absolute', - width: 200, - height: 70, - backgroundColor: 'rgba(229, 9, 20, 0.1)', - borderRadius: 35, - zIndex: 1, - }, - heading: { - fontSize: 28, - fontWeight: '800', - letterSpacing: -0.5, - marginBottom: 4, - }, - subheading: { - fontSize: 13, - lineHeight: 18, - textAlign: 'center', - paddingHorizontal: 20, - marginTop: 1, - }, - centerContainer: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - paddingHorizontal: 20, - paddingTop: 0, - paddingBottom: 28, - }, - centerContainerTablet: { - paddingHorizontal: 36, - }, - card: { - width: '100%', - maxWidth: 400, - padding: 24, - borderRadius: 20, - borderWidth: 1, - elevation: 20, - }, - cardTablet: { - maxWidth: 560, - }, - cardCompact: { - padding: 18, - borderRadius: 16, - }, - switchRow: { - flexDirection: 'row', - borderRadius: 14, - padding: 3, - marginBottom: 24, - position: 'relative', - overflow: 'hidden', - }, - switchIndicator: { - position: 'absolute', - top: 3, - bottom: 3, - left: 3, - borderRadius: 12, - }, - switchButton: { - flex: 1, - paddingVertical: 12, - borderRadius: 12, - alignItems: 'center', - elevation: 0, - }, - switchText: { - fontWeight: '700', - fontSize: 15, - letterSpacing: 0.2, - }, - inputContainer: { - marginBottom: 16, - }, - inputRow: { - height: 56, - flexDirection: 'row', - alignItems: 'center', - borderRadius: 14, - paddingHorizontal: 4, - }, - iconContainer: { - width: 40, - height: 40, - borderRadius: 10, - alignItems: 'center', - justifyContent: 'center', - marginRight: 12, - }, - input: { - flex: 1, - fontSize: 16, - paddingVertical: 0, - fontWeight: '500', - }, - eyeButton: { - width: 40, - height: 40, - alignItems: 'center', - justifyContent: 'center', - marginRight: 4, - }, - errorRow: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: 16, - paddingHorizontal: 4, - }, - errorText: { - color: '#ff6b6b', - marginLeft: 8, - fontSize: 13, - fontWeight: '500', - }, - ctaButton: { - height: 56, - borderRadius: 14, - alignItems: 'center', - justifyContent: 'center', - marginTop: 8, - elevation: 0, - }, - ctaText: { - color: '#fff', - fontWeight: '700', - fontSize: 16, - letterSpacing: 0.3, - }, - backButton: { - position: 'absolute', - left: 16, - zIndex: 20, - }, - switchModeText: { - textAlign: 'center', - fontSize: 14, - fontWeight: '500', - }, - forgotPasswordButton: { - alignSelf: 'flex-end', - marginTop: -6, - marginBottom: 12, - }, - forgotPasswordText: { - fontSize: 13, - fontWeight: '600', - }, + backgroundPattern: { + ...StyleSheet.absoluteFillObject, + zIndex: 0, + }, + patternDot: { + position: 'absolute', + width: 2, + height: 2, + backgroundColor: '#fff', + borderRadius: 1, + }, + header: { + alignItems: 'center', + paddingBottom: 8, + }, + centerHeader: { + width: '100%', + alignItems: 'center', + marginBottom: 18, + paddingHorizontal: 20, + }, + centerHeaderTablet: { + maxWidth: 620, + }, + centerHeaderCompact: { + marginBottom: 10, + }, + logoContainer: { + position: 'relative', + alignItems: 'center', + justifyContent: 'center', + marginBottom: 16, + }, + logo: { + width: 180, + height: 54, + zIndex: 2, + }, + logoGlow: { + position: 'absolute', + width: 200, + height: 70, + backgroundColor: 'rgba(229, 9, 20, 0.1)', + borderRadius: 35, + zIndex: 1, + }, + heading: { + fontSize: 28, + fontWeight: '800', + letterSpacing: -0.5, + marginBottom: 4, + }, + subheading: { + fontSize: 13, + lineHeight: 18, + textAlign: 'center', + paddingHorizontal: 20, + marginTop: 1, + }, + centerContainer: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: 20, + paddingTop: 0, + paddingBottom: 28, + }, + centerContainerTablet: { + paddingHorizontal: 36, + }, + card: { + width: '100%', + maxWidth: 400, + padding: 24, + borderRadius: 20, + borderWidth: 1, + elevation: 20, + }, + cardTablet: { + maxWidth: 560, + }, + cardCompact: { + padding: 18, + borderRadius: 16, + }, + switchRow: { + flexDirection: 'row', + borderRadius: 14, + padding: 3, + marginBottom: 24, + position: 'relative', + overflow: 'hidden', + }, + switchIndicator: { + position: 'absolute', + top: 3, + bottom: 3, + left: 3, + borderRadius: 12, + }, + switchButton: { + flex: 1, + paddingVertical: 12, + borderRadius: 12, + alignItems: 'center', + elevation: 0, + }, + switchText: { + fontWeight: '700', + fontSize: 15, + letterSpacing: 0.2, + }, + inputContainer: { + marginBottom: 16, + }, + inputRow: { + height: 56, + flexDirection: 'row', + alignItems: 'center', + borderRadius: 14, + paddingHorizontal: 4, + }, + iconContainer: { + width: 40, + height: 40, + borderRadius: 10, + alignItems: 'center', + justifyContent: 'center', + marginRight: 12, + }, + input: { + flex: 1, + fontSize: 16, + paddingVertical: 0, + fontWeight: '500', + }, + eyeButton: { + width: 40, + height: 40, + alignItems: 'center', + justifyContent: 'center', + marginRight: 4, + }, + errorRow: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 16, + paddingHorizontal: 4, + }, + errorText: { + color: '#ff6b6b', + marginLeft: 8, + fontSize: 13, + fontWeight: '500', + }, + ctaButton: { + height: 56, + borderRadius: 14, + alignItems: 'center', + justifyContent: 'center', + marginTop: 8, + elevation: 0, + }, + ctaText: { + color: '#fff', + fontWeight: '700', + fontSize: 16, + letterSpacing: 0.3, + }, + backButton: { + position: 'absolute', + left: 16, + zIndex: 20, + }, + switchModeText: { + textAlign: 'center', + fontSize: 14, + fontWeight: '500', + }, + forgotPasswordButton: { + alignSelf: 'flex-end', + marginTop: -6, + marginBottom: 12, + }, + forgotPasswordText: { + fontSize: 13, + fontWeight: '600', + }, }); export default AuthScreen;