summaryrefslogblamecommitdiff
path: root/apps/mobile/contexts/AuthContext.tsx
blob: 6102433b9bf6539285d34b9f6e3e6cc7242c28c0 (plain) (tree)









































































































































































































































































































                                                                                       
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 };