import React, { createContext, useContext, useState, useEffect, useCallback, useRef } from 'react'; import { router } from 'expo-router'; import { authService } from '../services/authService'; import { updateSupabaseAuth, setSupabaseToken } from '../lib/supabaseClient'; import { setupGlobalFetchInterceptor, setupTokenObservers } from '../utils/fetchInterceptor'; import { tokenManager, TokenState } from '../services/tokenManager'; import { initializeRevenueCatConditionally, identifyRevenueCatUser, resetRevenueCatUser, } from '~/features/subscription/revenueCatManager'; // User type type User = { id: string; email: string; role: string; name: string; }; // Define type for the Auth context type AuthContextType = { isAuthenticated: boolean; user: User | null; loading: boolean; showPasswordResetModal: boolean; setShowPasswordResetModal: (show: boolean) => void; authModeOverride: string | null; setAuthModeOverride: (mode: string | null) => void; setUser: (userData: { id: string; email: string; role: string }) => void; signIn: (email: string, password: string) => Promise<{ success: boolean; error?: string }>; signUp: (email: string, password: string) => Promise<{ success: boolean; error?: string; needsVerification?: boolean }>; signInWithGoogle: () => Promise<{ success: boolean; error?: string }>; signOut: () => Promise; }; // Create context with default values const AuthContext = createContext({ isAuthenticated: false, user: null, loading: false, showPasswordResetModal: false, setShowPasswordResetModal: () => {}, authModeOverride: null, setAuthModeOverride: () => {}, setUser: () => {}, signIn: async () => ({ success: false }), signUp: async () => ({ success: false }), signInWithGoogle: async () => ({ success: false }), signOut: async () => {}, }); // Hook for easy access to the Auth context export const useAuth = () => useContext(AuthContext); // Auth Provider component export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [isAuthenticated, setIsAuthenticated] = useState(false); const [loading, setLoading] = useState(true); const [user, setUser] = useState(null); const [isSigningOut, setIsSigningOut] = useState(false); const [showPasswordResetModal, setShowPasswordResetModal] = useState(false); const [authModeOverride, setAuthModeOverride] = useState(null); // Use a ref to track the latest auth state for the token observer const authStateRef = useRef({ isAuthenticated, user }); // Update the ref whenever auth state changes useEffect(() => { authStateRef.current = { isAuthenticated, user }; }, [isAuthenticated, user]); // Update user data when it changes const updateUserData = useCallback((userData: { id: string; email: string; role: string }) => { console.debug('Auth: updateUserData called with:', userData); if (userData) { console.debug('Auth: Setting isAuthenticated to true and updating user'); setIsAuthenticated(true); // Add name field derived from email const userWithName = { ...userData, name: userData.email.split('@')[0] }; setUser(userWithName); console.debug('Auth: User data updated successfully'); } else { console.debug('Auth: updateUserData called with null/undefined userData'); } }, []); // Load auth state on startup useEffect(() => { let unsubscribe: (() => void) | null = null; const initAuth = async () => { try { // Set up the global fetch interceptor FIRST - this is critical for handling 401s during auth check setupGlobalFetchInterceptor(); // Set up token manager observers for Supabase integration setupTokenObservers(); // Register the token refresh callback BEFORE any auth operations authService.onTokenRefresh = updateUserData; // Subscribe to token state changes for UI updates unsubscribe = tokenManager.subscribe((state: TokenState, token?: string) => { console.debug('Auth: Token state changed to', state); console.debug('Auth: Current auth state - isAuthenticated:', authStateRef.current.isAuthenticated, 'user:', !!authStateRef.current.user); if (state === TokenState.VALID && token) { // We have a valid token, try to get user data console.debug('Auth: Token is VALID, attempting to get user data...'); authService.getUserFromToken().then(userData => { console.debug('Auth: getUserFromToken result:', userData); if (userData) { console.debug('Auth: Calling updateUserData with:', userData); updateUserData(userData); // Force a re-check of auth state after update setTimeout(() => { console.debug('Auth: After updateUserData - isAuthenticated:', authStateRef.current.isAuthenticated, 'user:', !!authStateRef.current.user); }, 100); } else { console.debug('Auth: getUserFromToken returned null/undefined'); } }).catch(error => { console.debug('Error getting user data from token:', error); }); } else if (state === TokenState.EXPIRED_OFFLINE) { // Token expired while offline - preserve auth state, don't logout console.debug('Auth: Token expired offline, preserving auth state'); // User can continue using the app offline, token will refresh when connection returns return; // Don't trigger logout } else if (state === TokenState.EXPIRED && !isSigningOut) { // Only handle token expiration if we're not actively signing out console.debug('Auth: Token expired, handling expiration. isSigningOut:', isSigningOut); handleTokenExpiration().catch(error => { console.debug('Error during token expiration handling:', error); }); } }); // Check if user is authenticated (uses local validation to avoid race conditions) const authenticated = await authService.isAuthenticated(); if (authenticated) { // Get user information from token const userData = await authService.getUserFromToken(); if (userData) { updateUserData(userData); // Update Supabase auth state only if we have a valid current token // to avoid triggering unnecessary token refresh during startup try { const currentToken = await authService.getAppToken(); if (currentToken && authService.isTokenValidLocally(currentToken)) { // Only update Supabase if we have a valid token to avoid refresh cycles console.debug('Updating Supabase auth with existing valid token'); await setSupabaseToken(currentToken); } } catch (error) { console.debug('Error setting Supabase token during startup:', error); } // Initialize RevenueCat conditionally for existing authenticated users try { await initializeRevenueCatConditionally(); await identifyRevenueCatUser(userData.id); } catch (error) { console.error('Error with RevenueCat initialization for existing user:', error); // Don't block startup if initialization fails } } else { // If user data couldn't be retrieved, sign out await handleSignOut(); } } else { // If not authenticated, make sure we're on the login screen if (router.canGoBack()) { router.replace('/(public)/login'); } } } catch (error) { console.error('Error initializing auth state:', error); // On error, make sure we're signed out and on login screen await handleSignOut(); } finally { setLoading(false); } }; initAuth(); // Cleanup the callback and subscription on unmount return () => { authService.onTokenRefresh = null; if (unsubscribe) { unsubscribe(); } }; }, []); // Empty dependencies - we use refs to access latest values // Handle sign in const handleSignIn = async (email: string, password: string) => { // Set loading state immediately setLoading(true); try { // Authenticate with the API const result = await authService.signIn(email, password); if (result.success) { // Reset password reset modal on successful login setShowPasswordResetModal(false); try { // Get user information from token const userData = await authService.getUserFromToken(); if (userData) { updateUserData(userData); // Update Supabase auth state await updateSupabaseAuth(); // Initialize RevenueCat conditionally based on B2B settings try { await initializeRevenueCatConditionally(); await identifyRevenueCatUser(userData.id); } catch (error) { console.error('Error with RevenueCat initialization:', error); // Don't block auth flow if initialization fails } // Use replace to prevent swiping back to login // Small delay to ensure auth state is updated setTimeout(() => { router.replace('/(protected)/(tabs)/'); }, 100); } } catch (err) { console.error('Error getting user data after sign in:', err); // Keep loading false but return the success result // This will allow navigation to proceed even if we couldn't get user data } } // Check if it's a password reset required error if (!result.success && result.error === 'FIREBASE_USER_PASSWORD_RESET_REQUIRED') { console.log('AuthContext: Setting showPasswordResetModal to true'); setShowPasswordResetModal(true); } return result; } catch (error) { console.error('Error in handleSignIn:', error); return { success: false, error: 'An unexpected error occurred' }; } finally { // Always set loading to false when done, regardless of success or failure setLoading(false); } }; // Handle sign up const handleSignUp = async (email: string, password: string) => { // Set loading state immediately setLoading(true); try { // Register with the API const result = await authService.signUp(email, password); if (result.success) { // Check if email verification is needed if (result.needsVerification) { // Don't authenticate user, just return success with verification flag return result; } try { // Get user information from token (only if no verification needed) const userData = await authService.getUserFromToken(); if (userData) { updateUserData(userData); // Update Supabase auth state await updateSupabaseAuth(); // Initialize RevenueCat conditionally based on B2B settings try { await initializeRevenueCatConditionally(); await identifyRevenueCatUser(userData.id); } catch (error) { console.error('Error with RevenueCat initialization:', error); // Don't block auth flow if initialization fails } // Use replace to prevent swiping back to login // Small delay to ensure auth state is updated setTimeout(() => { router.replace('/(protected)/(tabs)/'); }, 100); } } catch (err) { console.error('Error getting user data after sign up:', err); // Keep loading false but return the success result } } return result; } catch (error) { console.error('Error in handleSignUp:', error); return { success: false, error: 'An unexpected error occurred' }; } finally { // Always set loading to false when done, regardless of success or failure setLoading(false); } }; // Handle sign out const handleSignOut = async () => { // Prevent multiple simultaneous logout calls if (isSigningOut) { console.debug('Auth: Sign out already in progress, skipping'); return; } setIsSigningOut(true); try { // Track sign out event before clearing user data // Use TokenManager to clear tokens and state await tokenManager.clearTokens(); // Also call authService signOut for any server-side cleanup await authService.signOut(); setIsAuthenticated(false); setUser(null); // Update Supabase auth state await updateSupabaseAuth(); // Reset RevenueCat user to anonymous try { await resetRevenueCatUser(); } catch (error) { console.error('Error resetting RevenueCat user:', error); // Don't block sign out if reset fails } // Don't navigate here - let the root layout handle navigation based on auth state // This prevents the infinite redirect loop on Android } catch (error) { console.error('Error signing out:', error); } finally { setIsSigningOut(false); } }; // Handle token expiration - try to refresh before logging out const handleTokenExpiration = async () => { console.debug('Auth: Handling token expiration'); // Check if we're offline first const { hasStableConnection, isDeviceConnected } = await import('~/features/errorHandling/utils/networkErrorUtils'); const isOnline = await isDeviceConnected(); if (!isOnline) { console.debug('Auth: Device offline, preserving auth state'); return; // Don't logout when offline } // Check if connection is stable before attempting critical operations const isStable = await hasStableConnection(); if (!isStable) { console.debug('Auth: Connection unstable during token expiration, deferring action'); // Schedule a retry after network stabilizes setTimeout(() => { // Re-check token state after delay const currentState = tokenManager.getState(); if (currentState === TokenState.EXPIRED) { handleTokenExpiration(); } }, 3000); return; } try { // Check if we can attempt a refresh (has valid refresh token) const canRefresh = await tokenManager.canAttemptRefresh(); if (canRefresh) { console.debug('Auth: Refresh token available, attempting refresh before logout'); // Try to get a valid token (this will trigger refresh if needed) const validToken = await tokenManager.getValidToken(); if (validToken) { console.debug('Auth: Token refresh successful, staying logged in'); // Token refresh succeeded, the TokenManager will notify us of the new valid state // Update user data to ensure UI is synchronized const userData = await authService.getUserFromToken(); if (userData) { updateUserData(userData); } return; } else { console.debug('Auth: Token refresh failed despite having refresh token'); } } else { console.debug('Auth: No refresh token available'); } // No refresh token or refresh failed, proceed with logout console.debug('Auth: Proceeding with logout due to expired tokens'); await handleSignOut(); } catch (error) { console.debug('Error during token expiration handling:', error); // If anything fails, fall back to logout to ensure security await handleSignOut(); } }; // Handle Google Sign-In const handleGoogleSignIn = async () => { setLoading(true); try { // The actual Google Sign-In is handled in the GoogleSignInButton component // Here we just handle the post-authentication flow try { // Get user information from token const userData = await authService.getUserFromToken(); if (userData) { updateUserData(userData); // Update Supabase auth state await updateSupabaseAuth(); // Identify user with RevenueCat try { await identifyRevenueCatUser(userData.id); } catch (rcError) { console.error('Error identifying user with RevenueCat:', rcError); // Don't block auth flow if RevenueCat identification fails } // Track Google sign in event identify(userData.id, { email: userData.email, role: userData.role, }); track('user_signed_in', { method: 'google', }); // Use replace to prevent swiping back to login // Small delay to ensure auth state is updated setTimeout(() => { router.replace('/(protected)/(tabs)/'); }, 100); return { success: true }; } } catch (err) { console.error('Error getting user data after Google Sign-In:', err); } return { success: false, error: 'No user data available after Google Sign-In' }; } catch (error) { console.error('Error in handleGoogleSignIn:', error); return { success: false, error: 'An unexpected error occurred during Google Sign-In' }; } finally { setLoading(false); } }; return ( {children} ); };