import { useState, useEffect, useCallback } from "react"; import { View, Text, TouchableOpacity, StyleSheet, Alert, KeyboardAvoidingView, Platform, ScrollView, Image, } from "react-native"; import { Redirect, router } from "expo-router"; import * as Linking from "expo-linking"; import Animated, { FadeIn, FadeInDown, FadeInUp, } from "react-native-reanimated"; import { Ionicons } from "@expo/vector-icons"; // Safely import Apple Authentication - may not be available in Expo Go let AppleAuthentication: typeof import("expo-apple-authentication") | null = null; let GoogleSignin: typeof import("@react-native-google-signin/google-signin").GoogleSignin | null = null; if (Platform.OS === "ios") { try { AppleAuthentication = require("expo-apple-authentication"); } catch { AppleAuthentication = null; } } if (Platform.OS === "android") { try { const googleModule = require("@react-native-google-signin/google-signin"); GoogleSignin = googleModule.GoogleSignin; if (GoogleSignin) { GoogleSignin.configure({ webClientId: "764866133492-e78o2rh3rdjc4rfj2j6r2j77b065kggm.apps.googleusercontent.com", }); } } catch { GoogleSignin = null; } } import { authClient } from "../../src/lib/authClient"; import { getAuthError } from "../../src/lib/errors"; import { Events } from "../../src/lib/analytics"; import { apiRequestWithAccessToken } from "../../src/lib/apiClient"; import { formatAppleProfileName } from "../../src/lib/appleProfileName"; import { persistAppleProfileName } from "../../src/lib/appleProfileNamePersistence"; import { profileSettingsApi } from "../../src/lib/profileSettingsApi"; import type { Profile } from "../../src/types"; import { useAuthStore } from "../../src/store"; import { GradientBackground, GlassCard, GlassButton, GlassInput } from "../../src/components/ui"; import { colors, spacing, radius, typography } from "../../src/theme"; type AuthMode = "magic-link" | "password"; interface PendingVerification { email: string; password: string; } export default function LoginScreen() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [isLoading, setIsLoading] = useState(false); const [isGuestLoading, setIsGuestLoading] = useState(false); const [isMagicLinkSent, setIsMagicLinkSent] = useState(false); const [pendingVerification, setPendingVerification] = useState(null); const [isCheckingVerification, setIsCheckingVerification] = useState(false); const [isResendingEmail, setIsResendingEmail] = useState(false); const [resendCooldown, setResendCooldown] = useState(0); const [authMode, setAuthMode] = useState("magic-link"); const [isSignUp, setIsSignUp] = useState(false); const [error, setError] = useState(null); const [isAppleAuthAvailable, setIsAppleAuthAvailable] = useState(false); const [isGoogleAuthAvailable, setIsGoogleAuthAvailable] = useState(false); const { isAuthenticated } = useAuthStore(); // Check Apple Auth availability asynchronously useEffect(() => { if (Platform.OS === "ios" && AppleAuthentication) { AppleAuthentication.isAvailableAsync().then((available) => { setIsAppleAuthAvailable(available); }).catch(() => { setIsAppleAuthAvailable(false); }); } }, []); // Check Google Auth availability useEffect(() => { if (Platform.OS === "android" && GoogleSignin) { setIsGoogleAuthAvailable(true); } }, []); // Cooldown timer for resend verification email useEffect(() => { if (resendCooldown <= 0) return; const timer = setInterval(() => { setResendCooldown((prev) => { if (prev <= 1) { clearInterval(timer); return 0; } return prev - 1; }); }, 1000); return () => clearInterval(timer); }, [resendCooldown]); // Resend verification email const resendVerificationEmail = useCallback(async () => { if (!pendingVerification || resendCooldown > 0) return; setIsResendingEmail(true); const { error: resendError } = await authClient.auth.resend({ type: 'signup', email: pendingVerification.email, }); setIsResendingEmail(false); if (resendError) { if (Platform.OS !== 'web') { Alert.alert("Error", getAuthError(resendError)); } } else { // Start 60 second cooldown setResendCooldown(60); if (Platform.OS !== 'web') { Alert.alert("Email Sent", "A new verification email has been sent. Please check your inbox."); } } }, [pendingVerification, resendCooldown]); // Check if user has verified their email const checkVerificationStatus = useCallback(async () => { if (!pendingVerification) return; setIsCheckingVerification(true); const { error: signInError } = await authClient.auth.signInWithPassword({ email: pendingVerification.email, password: pendingVerification.password, }); setIsCheckingVerification(false); if (signInError) { if (signInError.message.includes("Email not confirmed")) { // Still not verified - show a message if (Platform.OS !== 'web') { Alert.alert("Not yet verified", "Please check your email and click the verification link."); } } else { Alert.alert("Error", getAuthError(signInError)); } } // If successful, the auth state change will handle the redirect }, [pendingVerification]); // Listen for auth state changes (e.g., when user verifies in another tab/browser) useEffect(() => { const { data: { subscription } } = authClient.auth.onAuthStateChange((event, session) => { if (event === 'SIGNED_IN' && session) { // User verified and signed in - clear pending state setPendingVerification(null); } }); return () => subscription.unsubscribe(); }, []); const showError = (message: string) => { setError(message); // Also show native alert on mobile if (Platform.OS !== 'web') { Alert.alert("Error", message); } }; const clearError = () => setError(null); if (isAuthenticated) { return ; } const handleMagicLink = async () => { clearError(); if (!email.trim()) { showError("Please enter your email"); return; } setIsLoading(true); const { error: authError } = await authClient.auth.signInWithOtp({ email: email.trim(), options: { emailRedirectTo: Linking.createURL("/(auth)/login"), }, }); setIsLoading(false); if (authError) { showError(getAuthError(authError)); } else { setIsMagicLinkSent(true); Events.signIn("magic_link"); } }; const handlePasswordAuth = async () => { clearError(); if (!email.trim()) { showError("Please enter your email"); return; } if (!password) { showError("Please enter your password"); return; } if (isSignUp && password.length < 8) { showError("Password must be at least 8 characters"); return; } setIsLoading(true); if (isSignUp) { const { data, error: signUpError } = await authClient.auth.signUp({ email: email.trim(), password, options: { emailRedirectTo: Linking.createURL("/(auth)/login"), }, }); setIsLoading(false); if (signUpError) { showError(getAuthError(signUpError)); } else if (data.user && data.user.identities && data.user.identities.length === 0) { // User already exists - Supabase returns success but empty identities // for security (prevents email enumeration) showError("An account with this email already exists. Please sign in instead."); } else { // Show the verification pending screen setPendingVerification({ email: email.trim(), password, }); Events.signUp("password"); } } else { const { error: signInError } = await authClient.auth.signInWithPassword({ email: email.trim(), password, }); setIsLoading(false); if (!signInError) { Events.signIn("password"); } else if (signInError) { // Check if the error is due to unverified email if (signInError.message.includes("Email not confirmed")) { // Show the verification pending screen setPendingVerification({ email: email.trim(), password, }); } else { showError(getAuthError(signInError)); } } } }; const handleAppleSignIn = async () => { console.log("[Apple Sign In] Starting, isAppleAuthAvailable:", isAppleAuthAvailable, "AppleAuthentication:", !!AppleAuthentication); if (isAppleAuthAvailable && AppleAuthentication) { try { console.log("[Apple Sign In] Calling signInAsync..."); const credential = await AppleAuthentication.signInAsync({ requestedScopes: [ AppleAuthentication.AppleAuthenticationScope.FULL_NAME, AppleAuthentication.AppleAuthenticationScope.EMAIL, ], }); console.log("[Apple Sign In] Got credential:", !!credential, "identityToken:", !!credential?.identityToken); if (credential.identityToken) { const { data, error } = await authClient.auth.signInWithIdToken({ provider: "apple", token: credential.identityToken, }); if (error) { console.log("[Apple Sign In] Supabase error:", error.message); Alert.alert("Error", getAuthError(error)); } else { const appleProfileName = formatAppleProfileName(credential.fullName); const userId = data.user?.id; const accessToken = data.session?.access_token; if (appleProfileName && userId && accessToken) { const authStore = useAuthStore.getState(); authStore.setPendingAppleDisplayName({ userId, name: appleProfileName }); try { await persistAppleProfileName( { userId, accessToken }, appleProfileName, { getProfile: (token) => apiRequestWithAccessToken<{ profile: Profile }>("/v1/me", token), updateProfileName: (token, name) => profileSettingsApi.updateProfileWithAccessToken(token, { name }), }, ); await authStore.fetchUser({ userId, accessToken, isAnonymous: !!(data.user as { is_anonymous?: boolean } | null)?.is_anonymous, }); } catch { // The account session remains valid. Clear the subject-keyed // temporary name so onboarding safely falls back to its name step. console.warn("[Apple Sign In] Could not persist supplied name"); } finally { useAuthStore.getState().clearPendingAppleDisplayName(userId); } } else if (appleProfileName) { Alert.alert("Error", "Apple sign in did not return an authenticated session. Please try again."); } Events.signIn("apple"); } } else { Alert.alert("Error", "No identity token received from Apple"); } } catch (e: any) { console.log("[Apple Sign In] Caught error:", e.code, e.message); if (e.code === "ERR_REQUEST_CANCELED") { // User canceled - do nothing } else { Alert.alert("Error", "Apple sign in failed. Please try again."); } } } else { console.log("[Apple Sign In] Falling back to OAuth"); // Fallback to web OAuth when native module unavailable const { error } = await authClient.auth.signInWithOAuth({ provider: "apple", options: { redirectTo: Linking.createURL("/(auth)/login"), }, }); if (error) { Alert.alert("Error", getAuthError(error)); } } }; const handleGoogleSignIn = async () => { console.log("[Google Sign In] Starting, isGoogleAuthAvailable:", isGoogleAuthAvailable, "GoogleSignin:", !!GoogleSignin); if (isGoogleAuthAvailable && GoogleSignin) { try { console.log("[Google Sign In] Checking Play Services..."); await GoogleSignin.hasPlayServices(); console.log("[Google Sign In] Calling signIn..."); const response = await GoogleSignin.signIn(); console.log("[Google Sign In] Got response, type:", response.type); if (response.type === "success" && response.data.idToken) { console.log("[Google Sign In] Got idToken, signing in with Supabase..."); const { error } = await authClient.auth.signInWithIdToken({ provider: "google", token: response.data.idToken, }); if (error) { console.log("[Google Sign In] Supabase error:", error.message); Alert.alert("Error", getAuthError(error)); } else { Events.signIn("google"); } } else if (response.type === "cancelled") { console.log("[Google Sign In] User cancelled"); } else { Alert.alert("Error", "No identity token received from Google"); } } catch (e: any) { console.log("[Google Sign In] Caught error:", e.code, e.message); Alert.alert("Error", "Google sign in failed. Please try again."); } } }; const handleGuestSignIn = async () => { clearError(); setIsGuestLoading(true); const { error: authError } = await authClient.auth.signInAnonymously(); setIsGuestLoading(false); if (authError) { showError(getAuthError(authError)); return; } Events.signIn("guest"); }; if (isMagicLinkSent) { return ( Check your email We sent a magic link to{'\n'} {email} setIsMagicLinkSent(false)} style={{ marginTop: spacing.lg }} > Try a different email ); } // Email verification pending screen if (pendingVerification) { return ( Verify your email We sent a verification link to{'\n'} {pendingVerification.email} Click the link in your email to verify your account, then tap the button below. I've verified my email 0} style={{ marginTop: spacing.sm }} > {resendCooldown > 0 ? `Resend email (${resendCooldown}s)` : "Resend verification email"} { setPendingVerification(null); setEmail(pendingVerification.email); setPassword(""); setResendCooldown(0); }} style={{ marginTop: spacing.sm }} > Use a different email ); } return ( {/* Logo/Brand */} Sauci Grow closer together {/* Auth Card */} {/* Auth Mode Toggle */} setAuthMode("magic-link")} activeOpacity={0.7} > Magic Link setAuthMode("password")} activeOpacity={0.7} > Password {/* Error Message */} {error && ( {error} )} {/* Form Fields */} { clearError(); setEmail(text); }} keyboardType="email-address" autoCapitalize="none" autoCorrect={false} icon={} /> {authMode === "password" && ( <> { clearError(); setPassword(text); }} secureTextEntry autoCapitalize="none" icon={} /> {!isSignUp && ( router.push("/(auth)/forgot-password")} > Forgot password? )} )} {authMode === "magic-link" ? ( Continue with Magic Link ) : ( <> {isSignUp ? "Create Account" : "Sign In"} setIsSignUp(!isSignUp)} > {isSignUp ? "Already have an account? Sign In" : "Don't have an account? Sign Up"} )} {/* Social Sign In - iOS only */} {Platform.OS === "ios" && ( or continue with {isAppleAuthAvailable && AppleAuthentication ? ( ) : ( } style={styles.socialButton} > Apple )} )} {/* Social Sign In - Android (Google) */} {Platform.OS === "android" && isGoogleAuthAvailable && ( or continue with } style={styles.socialButton} fullWidth > Continue with Google )} } fullWidth > Continue without Login Save your account anytime in Settings. ); } const styles = StyleSheet.create({ container: { flex: 1, }, centeredContainer: { flex: 1, justifyContent: "center", alignItems: "center", }, centeredContent: { alignItems: "center", paddingHorizontal: spacing.lg, }, scrollContent: { flexGrow: 1, }, content: { flex: 1, paddingHorizontal: spacing.lg, paddingTop: 80, paddingBottom: spacing.xl, }, brandContainer: { alignItems: "center", marginBottom: spacing.xl, }, logoImage: { width: 100, height: 100, marginBottom: spacing.md, }, title: { ...typography.largeTitle, color: colors.text, marginBottom: spacing.xs, }, subtitle: { ...typography.body, color: colors.textSecondary, textAlign: "center", }, emailHighlight: { color: colors.primary, fontWeight: "600", }, verificationHint: { ...typography.subhead, color: colors.textSecondary, textAlign: "center", marginTop: spacing.md, paddingHorizontal: spacing.md, }, iconContainer: { width: 100, height: 100, borderRadius: 50, backgroundColor: colors.primaryLight, justifyContent: "center", alignItems: "center", marginBottom: spacing.lg, }, formContainer: { marginBottom: spacing.lg, }, modeToggleContainer: { flexDirection: "row", backgroundColor: colors.background, // Flat background borderRadius: radius.md, padding: 4, marginBottom: spacing.lg, borderWidth: 1, borderColor: colors.border, }, // ... dividerLine: { flex: 1, height: 1, backgroundColor: colors.border, }, modeToggle: { flex: 1, paddingVertical: 12, borderRadius: radius.sm, alignItems: "center", }, modeToggleActive: { backgroundColor: colors.primary, }, modeToggleText: { ...typography.subhead, color: colors.textTertiary, fontWeight: "600", }, modeToggleTextActive: { color: colors.text, }, errorContainer: { flexDirection: "row", alignItems: "center", backgroundColor: colors.errorLight, padding: spacing.md, borderRadius: radius.md, marginBottom: spacing.md, gap: spacing.sm, }, errorText: { ...typography.subhead, color: colors.error, flex: 1, }, form: { gap: spacing.sm, }, toggleSignUp: { marginTop: spacing.md, alignItems: "center", }, toggleSignUpText: { ...typography.subhead, color: colors.primary, }, forgotPassword: { alignSelf: "center", marginTop: -spacing.xs, marginBottom: spacing.xs, }, forgotPasswordText: { ...typography.subhead, color: colors.primary, }, socialContainer: { marginTop: spacing.md, }, divider: { flexDirection: "row", alignItems: "center", marginBottom: spacing.lg, }, dividerText: { ...typography.caption1, color: colors.textTertiary, paddingHorizontal: spacing.md, }, socialButtons: { flexDirection: "row", gap: spacing.md, }, socialButton: { flex: 1, }, appleButton: { flex: 1, height: 48, }, guestContainer: { marginTop: spacing.lg, alignItems: "center", gap: spacing.sm, }, guestHint: { ...typography.caption1, color: colors.textTertiary, textAlign: "center", }, });