import { create } from 'zustand'; import { api, setOnUnauthorizedCallback } from '@/services/api'; import type { ApiError, User } from '@/types'; /** * Auth Store State * Manages authentication state using Zustand */ interface AuthState { user: User | null; isLoading: boolean; isInitializing: boolean; isAuthenticated: boolean; error: ApiError | null; } /** * Auth Store Actions * All auth-related operations */ interface AuthActions { // Authentication flow checkEmail: (email: string) => Promise<{ exists: boolean; name?: string | null }>; requestOtp: (email: string) => Promise<{ success: boolean; skipOtp: boolean }>; verifyOtp: (email: string, code: string) => Promise; logout: () => Promise; // State management clearError: () => void; refreshAuth: () => Promise; updateUser: (updates: Partial) => void; // Internal helpers _checkAuth: () => Promise; _setUnauthorizedCallback: () => void; } /** * Combined Auth Store Type */ type AuthStore = AuthState & AuthActions; /** * Initial state for auth store */ const initialState: AuthState = { user: null, isLoading: false, isInitializing: true, isAuthenticated: false, error: null, }; /** * Zustand Auth Store * * Replaces AuthContext with a more performant and flexible state management solution. * * @example * ```tsx * // Using in components * import { useAuthStore } from '@/stores/authStore'; * * function MyComponent() { * const { user, isAuthenticated, logout } = useAuthStore(); * * return ( * * {isAuthenticated ? ( * Welcome {user?.firstName} * ) : ( * Please login * )} * * ); * } * ``` */ export const useAuthStore = create((set, get) => ({ ...initialState, /** * Internal: Check authentication status on app start * Called automatically when store initializes */ _checkAuth: async () => { try { const isAuth = await api.isAuthenticated(); if (isAuth) { const user = await api.getStoredUser(); set({ user, isLoading: false, isInitializing: false, isAuthenticated: !!user, error: null, }); } else { set({ user: null, isLoading: false, isInitializing: false, isAuthenticated: false, error: null, }); } } catch { set({ user: null, isLoading: false, isInitializing: false, isAuthenticated: false, error: { message: 'Failed to check authentication' }, }); } }, /** * Internal: Setup callback for 401 unauthorized responses * Called automatically when store initializes */ _setUnauthorizedCallback: () => { setOnUnauthorizedCallback(() => { api.logout().then(() => { set({ user: null, isLoading: false, isInitializing: false, isAuthenticated: false, error: { message: 'Session expired. Please login again.' }, }); }); }); }, /** * Check if email exists in database * Step 1 of OTP flow */ checkEmail: async (email: string) => { set({ isLoading: true, error: null }); try { const response = await api.checkEmail(email); if (response.ok && response.data) { set({ isLoading: false }); return { exists: response.data.exists, name: response.data.name }; } // API failed - assume new user set({ isLoading: false }); return { exists: false }; } catch { set({ isLoading: false, error: { message: 'Network error. Please check your connection.' }, }); return { exists: false }; } }, /** * Request OTP code via email * Step 2 of OTP flow */ requestOtp: async (email: string) => { set({ isLoading: true, error: null }); try { const response = await api.requestOTP(email); if (response.ok) { set({ isLoading: false }); return { success: true, skipOtp: false }; } // API failed set({ isLoading: false, error: { message: 'Failed to send verification code. Please try again.' }, }); return { success: false, skipOtp: false }; } catch { set({ isLoading: false, error: { message: 'Network error. Please check your connection.' }, }); return { success: false, skipOtp: false }; } }, /** * Verify OTP code and authenticate user * Step 3 of OTP flow - completes authentication */ verifyOtp: async (email: string, code: string) => { set({ isLoading: true, error: null }); try { const verifyResponse = await api.verifyOTP(email, code); if (verifyResponse.ok && verifyResponse.data) { const user: User = { user_id: verifyResponse.data.user.id, email: email, firstName: verifyResponse.data.user.first_name || null, lastName: verifyResponse.data.user.last_name || null, max_role: 'USER', privileges: '', }; set({ user, isLoading: false, isInitializing: false, isAuthenticated: true, error: null, }); return true; } // Wrong OTP code set({ isLoading: false, error: { message: 'Invalid verification code. Please try again.' }, }); return false; } catch (error) { set({ isLoading: false, error: { message: error instanceof Error ? error.message : 'Verification failed' }, }); return false; } }, /** * Logout user and clear all auth state * Calls api.logout() which clears SecureStore tokens */ logout: async () => { set({ isLoading: true }); try { await api.logout(); } finally { set({ user: null, isLoading: false, isInitializing: false, isAuthenticated: false, error: null, }); } }, /** * Clear error state * Used to dismiss error messages */ clearError: () => { set({ error: null }); }, /** * Refresh authentication state from storage * Re-checks tokens and user data */ refreshAuth: async () => { await get()._checkAuth(); }, /** * Update user profile in state * Only updates local state - does NOT call API * Use api.updateProfile() separately to persist changes */ updateUser: (updates: Partial) => { const currentUser = get().user; if (!currentUser) return; set({ user: { ...currentUser, ...updates }, }); }, })); /** * Initialize auth store * Call this once at app startup (in _layout.tsx or App.tsx) * * @example * ```tsx * // In app/_layout.tsx * import { initAuthStore } from '@/stores/authStore'; * * export default function RootLayout() { * useEffect(() => { * initAuthStore(); * }, []); * } * ``` */ export function initAuthStore() { const store = useAuthStore.getState(); store._setUnauthorizedCallback(); store._checkAuth(); } /** * Selector hooks for optimized re-renders * Use these instead of full store when you only need specific values */ export const useAuthUser = () => useAuthStore((state) => state.user); export const useIsAuthenticated = () => useAuthStore((state) => state.isAuthenticated); export const useAuthLoading = () => useAuthStore((state) => state.isLoading); export const useAuthError = () => useAuthStore((state) => state.error);