/** * BUS-Tickets - Sign In Screen * Supports Email, Phone OTP, and OAuth (Google, Facebook, Apple) * Copyright (c) 2024-2026 IT Enterprise */ import { useState, useEffect } from 'react'; import { View, Text, TextInput, TouchableOpacity, ScrollView, StyleSheet, KeyboardAvoidingView, Platform, Alert, ActivityIndicator, } from 'react-native'; import { useRouter } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; import { useTheme } from '@/contexts/ThemeContext'; import { useAuth } from '@/contexts/AuthContext'; import { useConfig } from '@/contexts/ConfigContext'; import { useLocale } from '@/contexts/LocaleContext'; import { oAuthService, OAuthProvider } from '@/services/OAuthService'; type AuthMethod = 'email' | 'phone' | 'magic'; export default function SignInScreen() { const router = useRouter(); const { colors } = useTheme(); const { signIn, signInWithOAuth, signInWithOTP, requestOTP, isLoading } = useAuth(); const { config } = useConfig(); const { t } = useLocale(); const [authMethod, setAuthMethod] = useState('email'); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [phone, setPhone] = useState(''); const [otpCode, setOtpCode] = useState(''); const [showOtpInput, setShowOtpInput] = useState(false); const [showPassword, setShowPassword] = useState(false); const [magicLinkSent, setMagicLinkSent] = useState(false); const [oauthLoading, setOauthLoading] = useState(null); // Configure OAuth providers from app config useEffect(() => { if (config.authProviders) { oAuthService.configureFromAppConfig(config.authProviders); } }, [config.authProviders]); const handleEmailSignIn = async () => { if (!email || !password) { Alert.alert(t.common.error, t.auth.invalidCredentials); return; } try { await signIn(email, password); router.back(); } catch (error) { Alert.alert(t.common.error, t.auth.invalidCredentials); } }; const handleRequestOTP = async () => { if (!phone) { Alert.alert(t.common.error, t.auth.invalidCredentials); return; } try { await requestOTP(undefined, phone); setShowOtpInput(true); } catch (error) { Alert.alert(t.common.error, t.auth.networkError); } }; const handleVerifyOTP = async () => { if (!otpCode) { Alert.alert(t.common.error, t.auth.enterCode); return; } try { await signInWithOTP(phone, otpCode); router.back(); } catch (error) { Alert.alert(t.common.error, t.auth.invalidCredentials); } }; const handleMagicLink = async () => { if (!email) { Alert.alert(t.common.error, t.auth.invalidCredentials); return; } try { await requestOTP(email); setMagicLinkSent(true); } catch (error) { Alert.alert(t.common.error, t.auth.networkError); } }; const handleOAuthSignIn = async (provider: OAuthProvider) => { setOauthLoading(provider); try { const result = await oAuthService.signIn(provider); if (result.success && (result.idToken || result.accessToken)) { // Send to backend for verification await signInWithOAuth(provider, result.idToken || result.accessToken!); router.back(); } else { if (result.error !== 'User cancelled authentication') { Alert.alert(t.common.error, result.error || t.auth.networkError); } } } catch (error) { console.error('OAuth error:', error); Alert.alert(t.common.error, t.auth.networkError); } finally { setOauthLoading(null); } }; const styles = createStyles(colors); const enabledOAuthProviders = (config.authProviders || []).filter((p) => p.enabled); const getProviderIcon = (providerId: string) => { switch (providerId) { case 'google': return 'logo-google'; case 'facebook': return 'logo-facebook'; case 'apple': return 'logo-apple'; default: return 'log-in-outline'; } }; const getProviderColor = (providerId: string) => { switch (providerId) { case 'google': return '#4285F4'; case 'facebook': return '#1877F2'; case 'apple': return colors.text; default: return colors.primary; } }; return ( {/* Header */} {t.auth.signInTitle} {config.instanceName || 'BUS-Tickets'} {/* OAuth Buttons */} {enabledOAuthProviders.length > 0 && ( {enabledOAuthProviders.map((provider) => ( handleOAuthSignIn(provider.id as OAuthProvider)} disabled={!!oauthLoading} > {oauthLoading === provider.id ? ( ) : ( <> {t.auth.orContinueWith} {provider.name} )} ))} )} {/* Divider */} {enabledOAuthProviders.length > 0 && ( {t.auth.orContinueWith.split(' ')[0]} )} {/* Auth Method Tabs */} setAuthMethod('email')} > {t.auth.email} setAuthMethod('phone')} > OTP setAuthMethod('magic')} > Link {/* Email Form */} {authMethod === 'email' && ( setShowPassword(!showPassword)}> {t.auth.forgotPassword} {isLoading ? ( ) : ( {t.auth.signInButton} )} )} {/* Phone OTP Form */} {authMethod === 'phone' && ( {showOtpInput && ( )} {isLoading ? ( ) : ( {showOtpInput ? t.auth.verifyButton : t.auth.sendMagicLink.replace('link', 'OTP')} )} {showOtpInput && ( {t.auth.resendCode} )} )} {/* Magic Link Form */} {authMethod === 'magic' && ( {!magicLinkSent ? ( <> {t.auth.checkEmail} {isLoading ? ( ) : ( {t.auth.sendMagicLink} )} ) : ( {t.auth.magicLinkSent} {t.auth.checkEmail} setMagicLinkSent(false)} > {t.auth.resendCode} )} )} {/* Footer */} {t.auth.noAccount}{' '} {t.auth.signUpButton} ); } const createStyles = (colors: any) => StyleSheet.create({ container: { flex: 1, backgroundColor: colors.background, }, scrollView: { flex: 1, }, content: { padding: 24, }, header: { marginBottom: 32, }, title: { fontSize: 28, fontWeight: 'bold', color: colors.text, }, subtitle: { fontSize: 16, color: colors.textSecondary, marginTop: 8, }, oauthSection: { gap: 12, }, oauthButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 12, backgroundColor: colors.card, borderRadius: 12, padding: 16, borderWidth: 1, borderColor: colors.border, }, oauthButtonLoading: { opacity: 0.7, }, oauthButtonText: { fontSize: 16, color: colors.text, fontWeight: '500', }, dividerContainer: { flexDirection: 'row', alignItems: 'center', marginVertical: 24, }, divider: { flex: 1, height: 1, backgroundColor: colors.border, }, dividerText: { fontSize: 14, color: colors.textSecondary, marginHorizontal: 16, }, authMethodTabs: { flexDirection: 'row', backgroundColor: colors.card, borderRadius: 12, padding: 4, marginBottom: 24, }, authMethodTab: { flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 6, paddingVertical: 12, borderRadius: 8, }, authMethodTabActive: { backgroundColor: colors.background, }, authMethodTabText: { fontSize: 13, color: colors.textSecondary, }, authMethodTabTextActive: { color: colors.primary, fontWeight: '600', }, form: { gap: 16, }, inputContainer: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.card, borderRadius: 12, paddingHorizontal: 16, paddingVertical: 12, borderWidth: 1, borderColor: colors.border, }, input: { flex: 1, fontSize: 16, color: colors.text, marginLeft: 12, }, forgotPassword: { alignSelf: 'flex-end', }, forgotPasswordText: { fontSize: 14, color: colors.primary, }, signInButton: { backgroundColor: colors.primary, borderRadius: 12, padding: 16, alignItems: 'center', marginTop: 8, }, signInButtonDisabled: { opacity: 0.6, }, signInButtonText: { fontSize: 18, fontWeight: '600', color: '#fff', }, resendButton: { alignItems: 'center', padding: 12, }, resendButtonText: { fontSize: 14, color: colors.primary, }, infoBox: { flexDirection: 'row', alignItems: 'center', gap: 12, backgroundColor: colors.primary + '15', borderRadius: 12, padding: 16, }, infoText: { flex: 1, fontSize: 14, color: colors.text, lineHeight: 20, }, successBox: { alignItems: 'center', padding: 32, }, successTitle: { fontSize: 20, fontWeight: '600', color: colors.text, marginTop: 16, }, successText: { fontSize: 14, color: colors.textSecondary, textAlign: 'center', marginTop: 8, }, footer: { marginTop: 32, alignItems: 'center', }, footerText: { fontSize: 14, color: colors.textSecondary, }, footerLink: { color: colors.primary, fontWeight: '600', }, });