import React, {
createContext,
useContext,
useEffect,
useState,
useCallback,
useMemo,
type ReactNode,
} from 'react';
import { AppState, type AppStateStatus } from 'react-native';
import type { Session, User, AuthChangeEvent } from '@supabase/supabase-js';
import { supabase } from '../lib/supabase';
import { signIn as authSignIn, signOut as authSignOut, getSession } from '../lib/auth';
/**
* Auth context state interface
*/
interface AuthState {
user: User | null;
session: Session | null;
isLoading: boolean;
isInitialized: boolean;
error: string | null;
}
/**
* Auth context actions interface
*/
interface AuthActions {
signIn: (email: string, password: string) => Promise<boolean>;
signOut: () => Promise<void>;
clearError: () => void;
}
/**
* Combined auth context type
*/
type AuthContextType = AuthState & AuthActions;
/**
* Default auth state
*/
const defaultAuthState: AuthState = {
user: null,
session: null,
isLoading: true,
isInitialized: false,
error: null,
};
/**
* Auth context with default values
*/
const AuthContext = createContext<AuthContextType | undefined>(undefined);
/**
* Props for AuthProvider component
*/
interface AuthProviderProps {
children: ReactNode;
}
/**
* AuthProvider component that manages authentication state
* - Initializes auth state on mount
* - Listens to auth state changes
* - Auto-refreshes session when app comes to foreground
* - Provides auth actions via context
*/
export function AuthProvider({ children }: AuthProviderProps) {
const [state, setState] = useState<AuthState>(defaultAuthState);
/**
* Update auth state with partial updates
*/
const updateState = useCallback((updates: Partial<AuthState>) => {
setState((prev) => ({ ...prev, ...updates }));
}, []);
/**
* Initialize auth state by checking for existing session
*/
const initialize = useCallback(async () => {
try {
const { session, error } = await getSession();
if (error) {
console.warn('Auth initialization warning:', error);
}
updateState({
user: session?.user ?? null,
session,
isLoading: false,
isInitialized: true,
error: null,
});
} catch (error) {
console.error('Auth initialization error:', error);
updateState({
user: null,
session: null,
isLoading: false,
isInitialized: true,
error: error instanceof Error ? error.message : 'Failed to initialize auth',
});
}
}, [updateState]);
/**
* Sign in with email and password
*/
const signIn = useCallback(
async (email: string, password: string): Promise<boolean> => {
updateState({ isLoading: true, error: null });
const result = await authSignIn(email, password);
if (result.success && result.user && result.session) {
updateState({
user: result.user,
session: result.session,
isLoading: false,
error: null,
});
return true;
}
updateState({
isLoading: false,
error: result.error || 'Sign in failed',
});
return false;
},
[updateState]
);
/**
* Sign out the current user
*/
const signOut = useCallback(async () => {
updateState({ isLoading: true, error: null });
const result = await authSignOut();
if (result.success) {
updateState({
user: null,
session: null,
isLoading: false,
error: null,
});
} else {
updateState({
isLoading: false,
error: result.error || 'Sign out failed',
});
}
}, [updateState]);
/**
* Clear any auth errors
*/
const clearError = useCallback(() => {
updateState({ error: null });
}, [updateState]);
/**
* Handle app state changes (foreground/background)
* Refresh session when app comes to foreground
*/
useEffect(() => {
const handleAppStateChange = async (nextAppState: AppStateStatus) => {
if (nextAppState === 'active' && state.session) {
// App came to foreground, refresh the session
try {
const { data, error } = await supabase.auth.refreshSession();
if (error) {
console.warn('Session refresh warning:', error.message);
} else if (data.session) {
updateState({
session: data.session,
user: data.session.user,
});
}
} catch (error) {
console.error('Session refresh error:', error);
}
}
};
const subscription = AppState.addEventListener('change', handleAppStateChange);
return () => {
subscription.remove();
};
}, [state.session, updateState]);
/**
* Listen to Supabase auth state changes
*/
useEffect(() => {
const {
data: { subscription },
} = supabase.auth.onAuthStateChange(
(event: AuthChangeEvent, session: Session | null) => {
console.log('Auth state changed:', event);
switch (event) {
case 'SIGNED_IN':
updateState({
user: session?.user ?? null,
session,
isLoading: false,
error: null,
});
break;
case 'SIGNED_OUT':
updateState({
user: null,
session: null,
isLoading: false,
error: null,
});
break;
case 'TOKEN_REFRESHED':
updateState({
session,
user: session?.user ?? null,
});
break;
case 'USER_UPDATED':
updateState({
user: session?.user ?? null,
});
break;
case 'PASSWORD_RECOVERY':
case 'MFA_CHALLENGE_VERIFIED':
// Handle other events as needed
break;
default:
// INITIAL_SESSION and other events
break;
}
}
);
return () => {
subscription.unsubscribe();
};
}, [updateState]);
/**
* Initialize auth on mount
*/
useEffect(() => {
initialize();
}, [initialize]);
/**
* Memoized context value
*/
const value = useMemo<AuthContextType>(
() => ({
...state,
signIn,
signOut,
clearError,
}),
[state, signIn, signOut, clearError]
);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
/**
* Hook to access auth context
* Must be used within an AuthProvider
*/
export function useAuth(): AuthContextType {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
/**
* Hook to check if user is authenticated
*/
export function useIsAuthenticated(): boolean {
const { session } = useAuth();
return session !== null;
}
export type { AuthContextType, AuthState, AuthActions };