| import React, { |
| createContext, |
| useContext, |
| useEffect, |
| useState, |
| ReactNode, |
| } from "react"; |
| import { Session, User } from "@supabase/supabase-js"; |
| import { CONFIG_ERROR, supabase } from "../config/supabase"; |
| import { config } from "../config/env"; |
| import { |
| GoogleSignin, |
| statusCodes, |
| isErrorWithCode, |
| } from "../lib/googleAuthSafe"; |
| import { Alert } from "react-native"; |
|
|
| interface AuthContextType { |
| user: User | null; |
| session: Session | null; |
| loading: boolean; |
| isDevMode: boolean; |
| configError: string | null; |
| signInWithGoogle: () => Promise<void>; |
| continueWithDevMode: () => void; |
| signOut: () => Promise<void>; |
| } |
|
|
| const AuthContext = createContext<AuthContextType | undefined>(undefined); |
|
|
| interface AuthProviderProps { |
| children: ReactNode; |
| } |
|
|
| export function AuthProvider({ children }: AuthProviderProps) { |
| const [user, setUser] = useState<User | null>(null); |
| const [session, setSession] = useState<Session | null>(null); |
| const [loading, setLoading] = useState(true); |
| const [isDevMode, setIsDevMode] = useState(false); |
| const [configError] = useState<string | null>(CONFIG_ERROR); |
|
|
| useEffect(() => { |
| if (!supabase) { |
| setLoading(false); |
| return; |
| } |
|
|
| try { |
| GoogleSignin.configure({ |
| webClientId: config.GOOGLE_CLIENT_ID, |
| scopes: ["email", "profile"], |
| offlineAccess: true, |
| }); |
| } catch (e: any) { |
| if (e.message?.includes("RNGoogleSignin")) { |
| console.warn( |
| "Google Sign-In not supported in Expo Go. Use a development build or 'Dev Mode'.", |
| ); |
| } else { |
| console.error("Google Sign-In config error:", e); |
| } |
| } |
|
|
| supabase.auth.getSession().then(({ data: { session } }) => { |
| setSession(session); |
| setUser(session?.user ?? null); |
| setLoading(false); |
| }); |
|
|
| const { |
| data: { subscription }, |
| } = supabase.auth.onAuthStateChange((_event, session) => { |
| setSession(session); |
| setUser(session?.user ?? null); |
| setLoading(false); |
| }); |
|
|
| return () => { |
| subscription.unsubscribe(); |
| }; |
| }, []); |
|
|
| const signInWithGoogle = async () => { |
| try { |
| if (!supabase) { |
| Alert.alert("Configuration Error", "Supabase is not configured."); |
| return; |
| } |
|
|
| setLoading(true); |
|
|
| |
| try { |
| await GoogleSignin.hasPlayServices(); |
| } catch (e: any) { |
| if (e.message?.includes("RNGoogleSignin")) { |
| Alert.alert( |
| "Expo Go Detected", |
| "Native Google Sign-In is not supported in Expo Go. Please use 'Dev Mode' or build a development client.", |
| ); |
| setLoading(false); |
| return; |
| } |
| throw e; |
| } |
|
|
| |
| const userInfo = await GoogleSignin.signIn(); |
|
|
| |
| if (userInfo.data?.idToken) { |
| const { data, error } = await supabase.auth.signInWithIdToken({ |
| provider: "google", |
| token: userInfo.data.idToken, |
| }); |
|
|
| if (error) throw error; |
|
|
| |
| if (data.session) { |
| setSession(data.session); |
| setUser(data.session.user); |
| } |
| } else { |
| throw new Error("No ID token returned from Google Sign-In"); |
| } |
| } catch (error: any) { |
| if (isErrorWithCode(error)) { |
| switch (error.code) { |
| case statusCodes.SIGN_IN_CANCELLED: |
| console.log("User cancelled the login flow"); |
| break; |
| case statusCodes.IN_PROGRESS: |
| console.log("Sign in is in progress"); |
| break; |
| case statusCodes.PLAY_SERVICES_NOT_AVAILABLE: |
| Alert.alert( |
| "Error", |
| "Google Play Services not available or outdated.", |
| ); |
| break; |
| default: |
| console.error("Google Sign-In Error:", error); |
| Alert.alert("Google Sign-In Error", error.message); |
| } |
| } else { |
| console.error("An error occurred:", error); |
| Alert.alert("Sign-In Failed", error.message || "Unknown error"); |
| } |
| } finally { |
| setLoading(false); |
| } |
| }; |
|
|
| const continueWithDevMode = () => { |
| const devUser: User = { |
| id: "dev-user-123", |
| email: "dev@citytracker.local", |
| app_metadata: {}, |
| user_metadata: { full_name: "Dev User" }, |
| aud: "authenticated", |
| created_at: new Date().toISOString(), |
| }; |
| setUser(devUser); |
| setIsDevMode(true); |
| setLoading(false); |
| }; |
|
|
| const signOut = async () => { |
| try { |
| setLoading(true); |
|
|
| |
| try { |
| await GoogleSignin.signOut(); |
| } catch (e) { |
| console.warn("Google Sign-Out error (ignoring):", e); |
| } |
|
|
| if (!isDevMode && supabase) { |
| await supabase.auth.signOut(); |
| } |
| setUser(null); |
| setSession(null); |
| setIsDevMode(false); |
| } catch (error) { |
| console.error("Sign out error:", error); |
| } finally { |
| setLoading(false); |
| } |
| }; |
|
|
| return ( |
| <AuthContext.Provider |
| value={{ |
| user, |
| session, |
| loading, |
| isDevMode, |
| configError, |
| signInWithGoogle, |
| continueWithDevMode, |
| signOut, |
| }} |
| > |
| {children} |
| </AuthContext.Provider> |
| ); |
| } |
|
|
| export function useAuth() { |
| const context = useContext(AuthContext); |
| if (context === undefined) { |
| throw new Error("useAuth must be used within an AuthProvider"); |
| } |
| return context; |
| } |
|
|