UTA DevHub
Guides

Authentication Flow Implementation Guide

Implementing core authentication using TanStack Query, Zustand, Axios interceptors, and Secure Storage.

GUIDE-05: Authentication Flow Implementation Guide

Overview

This guide provides practical patterns for implementing secure authentication flows (login, register, logout, session management) in React Native applications, aligned with our core architecture (DOC-01). It leverages TanStack Query for API interactions, Zustand for client-side auth state, an Axios interceptor for seamless token refresh, and secureStorage (DOC-04) for persistent, secure token storage.

🚀 Production-Ready Implementation Available

Want to skip the manual setup? Check out our Authentication Domain for a complete, copy-paste ready solution with:

  • TypeScript authentication hooks and services
  • Secure token management with automatic refresh
  • Authentication context and protected routes
  • Comprehensive testing patterns

Perfect for getting authentication working quickly in your project!

🚀 Production-Ready Implementation Available

Want to skip the manual setup? Check out our Authentication Domain for a complete, copy-paste ready solution with:

  • TypeScript authentication hooks and services
  • Secure token management with automatic refresh
  • Authentication context and protected routes
  • Comprehensive testing patterns

Perfect for getting authentication working quickly in your project!

When To Use

Use these patterns when implementing:

  • Email/Password authentication (Login, Register).
  • Token-based session management with automatic refresh.
  • Secure persistence of authentication tokens across app launches.
  • Integration of authentication state with TanStack Query (GUIDE-01) and Zustand (GUIDE-02).
  • Basic structure for handling authentication-related API calls.
  • Protected routes based on authentication status.

Note: Social Login and Biometric Login are covered in GUIDE-07 and GUIDE-08, respectively.

🚀 Production-Ready Implementation Available

Want to skip the manual setup? Check out our Authentication Domain for a complete, copy-paste ready solution with TypeScript hooks, secure token management, authentication context, and testing patterns.

Implementation Patterns

1. Sequence Diagram (Login Flow)

(Token refresh happens transparently within the API Client interceptor on subsequent API calls if needed)

2. Authentication Service

Defines functions for interacting with authentication-related backend endpoints (DOC-03).

// src/core/api/services/authService.ts
import { apiClient } from '../apiClient';
import { API_ENDPOINTS } from '../constants';
import type {
  User,
  LoginCredentials,
  LoginResponse,
  RegisterData,
  RefreshTokenRequest,
  RefreshTokenResponse // Assuming a response type for refresh
} from '../types/authTypes'; // Adjust path as needed
 
export const authService = {
  login: async (credentials: LoginCredentials): Promise<LoginResponse> => {
    const { data } = await apiClient.post(API_ENDPOINTS.LOGIN, credentials);
    return data;
  },
 
  register: async (userData: RegisterData): Promise<LoginResponse> => {
    const { data } = await apiClient.post(API_ENDPOINTS.REGISTER, userData);
    return data;
  },
 
  logout: async (): Promise<void> => {
    // Backend endpoint to invalidate session/token if applicable
    await apiClient.post(API_ENDPOINTS.LOGOUT);
    // Note: Local state clearing happens in useAuth hook
  },
 
  refreshToken: async (request: RefreshTokenRequest): Promise<RefreshTokenResponse> => {
    // This call might bypass interceptors depending on setup
    // Ensure it doesn't trigger the refresh logic itself if called by the interceptor
    const { data } = await apiClient.post(API_ENDPOINTS.REFRESH_TOKEN, request);
    return data; // Expect { token: string, refreshToken: string }
  },
 
  // Fetching current user data - uses the active auth token from apiClient header
  getCurrentUser: async (): Promise<User> => {
     const { data } = await apiClient.get(API_ENDPOINTS.CURRENT_USER);
     return data;
  },
 
  // --- Password Management (Optional) --- 
  forgotPassword: async (email: string): Promise<void> => {
    await apiClient.post(API_ENDPOINTS.FORGOT_PASSWORD, { email });
  },
 
  resetPassword: async (token: string, password: string): Promise<void> => {
    await apiClient.post(API_ENDPOINTS.RESET_PASSWORD, { token, password });
  },
};

3. Zustand Authentication Store

Manage authentication state using Zustand.

// src/features/auth/state/useAuthStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
import type { User } from '@/core/api/types'; // Adjust path
 
interface AuthState {
  token: string | null;
  refreshToken: string | null;
  isLoggedIn: boolean;
  user: User | null;
  
  // Actions
  setTokens: (tokens: { token: string; refreshToken: string }) => void;
  setUser: (user: User) => void;
  clearAuthState: () => void;
}
 
export const useAuthStore = create<AuthState>(
  persist(
    (set) => ({
      // Initial state
      token: null,
      refreshToken: null,
      isLoggedIn: false,
      user: null,
      
      // Actions
      setTokens: ({ token, refreshToken }) => set({ 
        token, 
        refreshToken, 
        isLoggedIn: true 
      }),
      setUser: (user) => set({ user }),
      clearAuthState: () => set({ 
        token: null, 
        refreshToken: null, 
        isLoggedIn: false, 
        user: null 
      }),
    }),
    {
      name: 'auth-storage',
      storage: createJSONStorage(() => AsyncStorage),
    }
  )
);
 
// Selector hooks - create custom hooks for common state selections
export const useAuthToken = () => useAuthStore((state) => state.token);
export const useRefreshToken = () => useAuthStore((state) => state.refreshToken);
export const useIsLoggedIn = () => useAuthStore((state) => state.isLoggedIn);
export const useUser = () => useAuthStore((state) => state.user);

4. Authentication Hook (useAuth)

Combines Zustand state access with TanStack Query mutations for a unified auth interface.

// src/features/auth/hooks/useAuth.ts
import { useCallback } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { authService } from '@/core/api/services/authService'; // Adjust path
import { apiClient } from '@/core/api/apiClient'; // Adjust path
import { secureStorage } from '@/core/security/storage/secureStorage'; // Adjust path
import { queryKeys } from '@/core/query/queryKeys'; // Adjust path
import { useAuthStore, useAuthToken, useRefreshToken, useIsLoggedIn, useUser } from '../state/useAuthStore'; // Adjust path
 
export const useAuth = () => {
  const queryClient = useQueryClient();
  
  // Get state and actions from Zustand store
  const setTokens = useAuthStore((state) => state.setTokens);
  const setUser = useAuthStore((state) => state.setUser);
  const clearAuthState = useAuthStore((state) => state.clearAuthState);
  
  // Auth state from Zustand store
  const token = useAuthToken();
  const refreshToken = useRefreshToken();
  const isLoggedIn = useIsLoggedIn();
  const user = useUser();
 
  // Login mutation (using TanStack Query)
  const loginMutation = useMutation({
    mutationFn: authService.login,
    onSuccess: async (data) => {
      // Update auth state in Zustand store
      setTokens({ token: data.token, refreshToken: data.refreshToken });
      setUser(data.user);
      
      // Update API client with the new token
      apiClient.setAuthHeader(data.token);
      
      // Store tokens securely for persistence
      await secureStorage.setItem('auth_token', data.token);
      await secureStorage.setItem('refresh_token', data.refreshToken);
      
      // Update the query cache with the new user data
      queryClient.setQueryData(queryKeys.users.current(), data.user);
    },
    onError: (error) => {
      console.error('Login failed:', error);
      // Error handling ideally done in the component calling mutate
    },
  });
 
  // Register Mutation
  const registerMutation = useMutation({
    mutationFn: (userData: RegisterData) => authService.register(userData),
    onSuccess: handleAuthSuccess, // Reuse the same success logic
     onError: (error) => {
      console.error('Registration failed:', error);
    },
  });
 
  // Logout Function
  const logout = useCallback(async () => {
    console.log('Executing logout...');
    try {
      // Optional: Call backend logout endpoint.
 
      // Remove auth header from API client
      apiClient.clearAuthHeader();
 
      // Clear tokens from secure storage
      await secureStorage.removeItem('auth_token');
      await secureStorage.removeItem('refresh_token');
 
      // Clear all queries from cache to ensure no stale data
      queryClient.clear();
    },
    // Even if the server-side logout fails, proceed with client-side cleanup
    onError: async (error) => {
      console.error('Logout error:', error);
      // Proceed with client-side cleanup anyway
      clearAuthState();
      apiClient.clearAuthHeader();
      await secureStorage.removeItem('auth_token');
      await secureStorage.removeItem('refresh_token');
      queryClient.clear();
    },
  });
 
  // Token refresh logic is handled by the API client interceptor.
 
  return {
    // State
    isLoggedIn,
    token,
    refreshToken, // Expose if needed for manual refresh trigger (e.g., on specific errors)
    user,
 
    // Actions (Mutations)
    login: loginMutation.mutate, // Use this in components
    loginAsync: loginMutation.mutateAsync, // For promise-based usage
    register: registerMutation.mutate,
    registerAsync: registerMutation.mutateAsync,
 
    // Status from Mutations
    isLoggingIn: loginMutation.isLoading,
    loginError: loginMutation.error,
    isRegistering: registerMutation.isLoading,
    registerError: registerMutation.error,
 
    // Logout Action
    logout,
  };
};

5. Secure Token Storage (secureStorage)

Uses expo-secure-store for native platforms. (Ensure alignment with DOC-04).

// src/core/security/storage/secureStorage.ts
import * as SecureStore from 'expo-secure-store';
// Consider platform checks if web support needed
 
export const secureStorage = {
  async setItem(key: string, value: string): Promise<void> {
    try {
        await SecureStore.setItemAsync(key, value);
    } catch (error) {
        console.error(`[SecureStorage] Error setting item ${key}:`, error);
        // Handle error appropriately (e.g., log, analytics)
    }
  },
 
  // Use this for highly sensitive items requiring biometrics (as per **DOC-04**)
  async setSecureItem(key: string, value: string): Promise<void> {
    try {
        await SecureStore.setItemAsync(key, value, {
            requireAuthentication: true, // Require biometrics/passcode
            authenticationPrompt: 'Authenticate to save sensitive data'
        });
    } catch (error) {
        console.error(`[SecureStorage] Error setting SECURE item ${key}:`, error);
    }
  },
 
  async getItem(key: string): Promise<string | null> {
    try {
        return await SecureStore.getItemAsync(key);
    } catch (error) {
        console.error(`[SecureStorage] Error getting item ${key}:`, error);
        return null;
    }
  },
 
  async removeItem(key: string): Promise<void> {
    try {
        await SecureStore.deleteItemAsync(key);
    } catch (error) {
        console.error(`[SecureStorage] Error removing item ${key}:`, error);
    }
  }
};

6. Auth Provider (AuthProvider)

Responsible for initializing the auth state from secure storage on app startup.

// src/features/auth/providers/AuthProvider.tsx
import React, { useEffect, useState, useCallback } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { secureStorage } from '@/core/security/storage/secureStorage'; // Adjust path (**DOC-04**)
import { useAuthStore } from '../state/useAuthStore'; // Adjust path
import { apiClient } from '@/core/api/apiClient'; // Adjust path (**DOC-03**)
import { queryKeys } from '@/core/query/queryKeys'; // Adjust path (**DOC-03**)
import { authService } from '@/core/api/services/authService'; // Adjust path
import { isTokenExpired } from '@/core/auth/utils/tokenUtils'; // Adjust path, assumes JWT helper
import LoadingScreen from '@/features/core/screens/LoadingScreen'; // Adjust path for your loading screen
 
// This provider focuses on initializing state from storage.
// Token refresh is primarily handled by the API Client Interceptor.
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const queryClient = useQueryClient();
  const setTokens = useAuthStore((state) => state.setTokens);
  const setUser = useAuthStore((state) => state.setUser);
  const clearAuthState = useAuthStore((state) => state.clearAuthState);
  const [isInitializing, setIsInitializing] = useState(true);
 
  // Function to handle full logout locally
  const handleLocalLogout = useCallback(async () => {
      dispatch(clearAuthState());
      apiClient.removeAuthHeader();
      await secureStorage.removeItem('auth_token');
      await secureStorage.removeItem('refresh_token');
      queryClient.clear();
      console.log('[AuthProvider] Local logout performed.');
  }, [dispatch, queryClient]);
 
  // Function to attempt token refresh (used during init if token is expired)
  const attemptTokenRefreshOnInit = useCallback(async (storedRefreshToken: string) => {
    try {
      console.log('[AuthProvider] Attempting token refresh during initialization...');
      const response = await authService.refreshToken({ refreshToken: storedRefreshToken });
 
      // Update Redux state
      dispatch(setTokens({ token: response.token, refreshToken: response.refreshToken }));
      // Set API header
      apiClient.setAuthHeader(response.token);
      // Update secure storage
      await secureStorage.setItem('auth_token', response.token);
      await secureStorage.setItem('refresh_token', response.refreshToken);
 
      // Fetch and cache user data after successful refresh
      try {
         const userData = await authService.getCurrentUser(); // Uses the new token implicitly
         dispatch(setUser(userData));
         queryClient.setQueryData(queryKeys.users.current(), userData);
         console.log('[AuthProvider] Token refresh and user fetch successful during init.');
      } catch (userFetchError) {
         console.error('[AuthProvider] Failed to fetch user after token refresh:', userFetchError);
         // Logout if user data is critical and fetch fails
         await handleLocalLogout();
         return false;
      }
      return true;
    } catch (error) {
      console.error('[AuthProvider] Initialization: Token refresh failed:', error);
      await handleLocalLogout(); // Logout if refresh fails
      return false;
    }
  }, [dispatch, queryClient, handleLocalLogout]);
 
  // Initialize auth state from secure storage on app start
  useEffect(() => {
    const initAuth = async () => {
      setIsInitializing(true);
      console.log('[AuthProvider] Initializing authentication state...');
      let storedToken: string | null = null;
      let storedRefreshToken: string | null = null;
 
      try {
        // Read tokens from secure storage
        [storedToken, storedRefreshToken] = await Promise.all([
          secureStorage.getItem('auth_token'),
          secureStorage.getItem('refresh_token'),
        ]);
 
        if (storedToken && storedRefreshToken) {
          // Check if the access token is still valid
          if (!isTokenExpired(storedToken)) {
            console.log('[AuthProvider] Valid token found in storage.');
            // Restore state: Token is still valid
            dispatch(setTokens({ token: storedToken, refreshToken: storedRefreshToken }));
            apiClient.setAuthHeader(storedToken);
 
             // Attempt to fetch user data (might hit cache or network)
            try {
               const userData = await queryClient.fetchQuery({
                   queryKey: queryKeys.users.current(),
                   queryFn: authService.getCurrentUser,
                   staleTime: 5 * 60 * 1000 // e.g., user data is fresh for 5 mins
               });
               dispatch(setUser(userData));
               console.log('[AuthProvider] User data restored/fetched.');
            } catch (userFetchError) {
                 console.error('[AuthProvider] Failed to fetch initial user data (token likely valid):", userFetchError);
                 // Decide if this is critical. Maybe the user can continue without fresh user data temporarily?
                 // await handleLocalLogout(); // Uncomment if app cannot function without user data
            }
          } else {
            console.log('[AuthProvider] Expired token found, attempting refresh...');
            // Token expired, try to refresh using the refresh token
            await attemptTokenRefreshOnInit(storedRefreshToken);
            // Success/failure (including logout) is handled within attemptTokenRefreshOnInit
          }
        } else {
          console.log('[AuthProvider] No tokens found in storage. Ensuring logged out state.');
          // No tokens found, ensure clean logged-out state
          await handleLocalLogout();
        }
      } catch (error) {
        console.error('[AuthProvider] Error during authentication initialization:', error);
        await handleLocalLogout(); // Logout on any unexpected error during init
      } finally {
        console.log('[AuthProvider] Initialization complete.');
        setIsInitializing(false);
      }
    };
 
    initAuth();
    // Dependencies ensure this runs only once on mount
  }, [dispatch, queryClient, attemptTokenRefreshOnInit, handleLocalLogout]);
 
  // Render loading screen while initializing
  if (isInitializing) {
    return <LoadingScreen message="Initializing session..." />;
  }
 
  // Render children once initialization is complete
  return <>{children}</>;
};

7. API Client with Token Handling Interceptor

Handles automatic token injection into requests and triggers refresh on 401 errors.

// src/core/api/apiClient.ts
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
import { store } from '@/core/store'; // Adjust path
import { API_BASE_URL, API_ENDPOINTS } from './constants'; // Ensure API_ENDPOINTS is imported
import { secureStorage } from '@/core/security/storage/secureStorage'; // Adjust path (**DOC-04**)
import { authService } from './services/authService'; // Adjust path
import { setTokens, clearAuthState } from '@/features/auth/state/authSlice'; // Adjust path, import clearAuthState
 
// Create API client instance
export const apiClient = axios.create({
  baseURL: API_BASE_URL,
  timeout: 15000, // Example timeout: 15 seconds
  headers: {
    'Content-Type': 'application/json',
  },
});
 
// Method to dynamically set Authorization header
apiClient.setAuthHeader = (token: string) => {
  apiClient.defaults.headers.common['Authorization'] = `Bearer ${token}`;
  console.log('[ApiClient] Auth header set.');
};
 
// Method to remove Authorization header
apiClient.removeAuthHeader = () => {
  delete apiClient.defaults.headers.common['Authorization'];
  console.log('[ApiClient] Auth header removed.');
};
 
 
// --- Token Refresh Logic State --- 
let isRefreshing = false;
// Queue for requests awaiting token refresh
let failedQueue: Array<{ resolve: (value: unknown) => void; reject: (reason?: any) => void }> = [];
 
// Function to process the queue after refresh attempt
const processQueue = (error: AxiosError | null, token: string | null = null) => {
  failedQueue.forEach(prom => {
    if (error) {
      prom.reject(error); // Reject queued request if refresh failed
    } else {
      prom.resolve(token); // Resolve queued request with new token
    }
  });
  failedQueue = []; // Clear the queue
};
 
// --- Request Interceptor: Adds Auth Token
// Injects the current token from Zustand store into outgoing requests.
apiClient.interceptors.request.use(
  (config) => {
    if (config.headers && !config.headers['Authorization']) {
      const token = useAuthStore.getState().token; // Get token from Zustand store for **every request**
      if (token) {
        config.headers['Authorization'] = `Bearer ${token}`;
      }
    }
    return config;
  },
  (error) => Promise.reject(error)
);
 
 
// --- Response Interceptor ---
// Handles 401 errors by attempting token refresh.
apiClient.interceptors.response.use(
  (response) => response, // Pass through successful responses
  async (error: AxiosError) => {
    const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
 
    // Check for 401 Unauthorized error
    if (error.response?.status === 401 && originalRequest && !originalRequest._retry) {
      console.log('[ApiClient] Received 401 error.');
 
      if (isRefreshing) {
        // If refresh is already in progress, queue this request
        console.log('[ApiClient] Token refresh in progress, queuing request:', originalRequest.url);
        return new Promise((resolve, reject) => {
          failedQueue.push({ resolve, reject });
        })
          .then(newToken => {
            // Retry the original request with the NEW token from successful refresh
            console.log('[ApiClient] Retrying queued request with new token:', originalRequest.url);
            if (originalRequest.headers) {
                originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
            }
            return apiClient(originalRequest); // Re-run the request
          })
          .catch(err => {
            console.log('[ApiClient] Queued request failed after refresh attempt.');
            return Promise.reject(err); // Propagate error if refresh failed
          });
      }
 
      // --- Start Token Refresh Process --- 
      console.log('[ApiClient] Initiating token refresh...');
      originalRequest._retry = true; // Mark request as retried
      isRefreshing = true;
 
      // Get refresh token (try Zustand first, then secure storage as fallback)
      let currentRefreshToken = useAuthStore.getState().refreshToken;
 
      if (!currentRefreshToken) {
          console.log('[ApiClient] Refresh token not in Zustand store, checking secure storage...');
          currentRefreshToken = await secureStorage.getItem('refresh_token');
      }
 
      // If no refresh token is found anywhere, logout immediately
      if (!currentRefreshToken) {
        console.error('[ApiClient] Refresh Error: No refresh token available. Logging out.');
        isRefreshing = false;
        store.dispatch(clearAuthState()); // Dispatch logout action
        apiClient.removeAuthHeader(); // Ensure header is cleared
        processQueue(new AxiosError('No refresh token available', '401'), null); // Reject queued requests
        return Promise.reject(error); // Reject original request
      }
 
      try {
        // Attempt to refresh the token via authService
        console.log('[ApiClient] Calling refreshToken API...');
        const refreshResponse = await authService.refreshToken({ refreshToken: currentRefreshToken });
        const { token: newToken, refreshToken: newRefreshToken } = refreshResponse;
        console.log('[ApiClient] Token refresh successful.');
 
        // --- Update State and Storage --- 
        useAuthStore.getState().setTokens({ token: newToken, refreshToken: newRefreshToken });
        await secureStorage.setItem('auth_token', newToken);
        await secureStorage.setItem('refresh_token', newRefreshToken);
        apiClient.setAuthHeader(newToken); // Update default header for subsequent requests
 
        // Update the header of the original request being retried
        if (originalRequest.headers) {
            originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
        }
 
        // Process the queue with the new token (resolves promises for queued requests)
        processQueue(null, newToken);
 
        // Retry the original request with the new token
        console.log('[ApiClient] Retrying original request with new token:', originalRequest.url);
        return apiClient(originalRequest);
 
      } catch (refreshError: any) {
        console.error('[ApiClient] Token refresh API call failed:', refreshError);
        // --- Handle Refresh Failure --- 
        // Clear tokens and logout
        useAuthStore.getState().clearAuthState();
        apiClient.removeAuthHeader();
        await secureStorage.removeItem('auth_token');
        await secureStorage.removeItem('refresh_token');
 
        // Reject the queue
        processQueue(refreshError as AxiosError, null);
 
        // Reject the original request and any subsequent ones in the queue
        return Promise.reject(refreshError);
      } finally {
        isRefreshing = false; // Reset refreshing state
        console.log('[ApiClient] Token refresh process finished.');
      }
    }
 
    // For non-401 errors or requests already retried, just reject
    return Promise.reject(error);
  }
);

8. Protected Route Components (Conceptual)

Guards access to routes based on authentication status.

// src/navigation/guards/AuthGuard.tsx
import React from 'react';
import { useAuthStore } from '@/features/auth/state/useAuthStore'; // Import Zustand store
import { Navigate, useLocation } from 'react-router-dom'; // Example using React Router DOM
// For React Navigation, the logic is usually handled in the navigator setup (see below)
 
interface AuthGuardProps {
  children: React.ReactNode;
}
 
// This component is more relevant for web React Router setups.
// For React Navigation, see the navigator example.
export const AuthGuard: React.FC<AuthGuardProps> = ({ children }) => {
  const isLoggedIn = useAuthStore((state) => state.isLoggedIn);
  const location = useLocation();
 
  if (!isLoggedIn) {
    // Redirect to login, saving the intended destination
    console.log('[AuthGuard] Not logged in, redirecting to /login');
    return <Navigate to="/login" state={{ from: location }} replace />;
  }
 
  // User is logged in, render the protected content
  return <>{children}</>;
};
 
// --- React Navigation Approach --- 
// src/navigation/RootNavigator.tsx
import { useAuthStore } from '@/features/auth/state/useAuthStore';
import AuthStack from './AuthStack'; // Navigator for Login, Register etc.
import AppStack from './AppStack';   // Navigator for authenticated screens
import LoadingScreen from '@/features/core/screens/LoadingScreen';
import { useAuthInitialization } from '@/core/auth/hooks/useAuthInitialization'; // Conceptual hook for init status
 
const RootNavigator = () => {
   const isLoggedIn = useAuthStore((state) => state.isLoggedIn);
   const { isInitializing } = useAuthInitialization(); // Get status from AuthProvider or similar
 
   if (isInitializing) {
       return <LoadingScreen />;
   }
 
   // Render the appropriate navigator based on login state
   return isLoggedIn ? <AppStack /> : <AuthStack />;
}
 
// In your main App.tsx or NavigationContainer setup:
<NavigationContainer>
    <RootNavigator />
</NavigationContainer>
*/

Common Challenges

When a user opens the app via a deep link (e.g., myapp://reset-password/abc), ensure correct routing based on auth status.

// src/navigation/linking.ts (Conceptual Example for React Navigation)
import { getStateFromPath } from '@react-navigation/native';
import { useAuthStore } from '@/features/auth/state/useAuthStore'; // Zustand store access
 
const PROTECTED_STACK = 'AppStack'; // Name of your authenticated navigator
const AUTH_STACK = 'AuthStack';     // Name of your unauthenticated navigator
const LOGIN_SCREEN = 'Login';      // Name of your login screen
 
const linking = {
  prefixes: ['myapp://', 'https://myapp.com'],
  config: {
    screens: {
      [AUTH_STACK]: {
        screens: {
           Login: 'login',
           Register: 'register',
           ForgotPassword: 'forgot-password',
           ResetPassword: 'reset-password/:token',
        }
      },
      [PROTECTED_STACK]: {
        screens: {
          Dashboard: 'dashboard',
          Profile: 'profile',
          // ... other authenticated screens
        },
      },
      NotFound: '*' // Catch-all route
    },
  },
 
  // Custom function to adjust initial navigation state based on auth
  getStateFromPath: (path, options) => {
    // Default navigation state determination
    const defaultState = getStateFromPath(path, options);
 
    // Check auth status directly from the Zustand store
    const isLoggedIn = useAuthStore.getState().isLoggedIn;
 
    // Determine the target stack/screen from the path
    const targetRoute = defaultState?.routes[0];
    const targetStackName = targetRoute?.name;
    const isProtectedTarget = targetStackName === PROTECTED_STACK;
    const isAuthTarget = targetStackName === AUTH_STACK;
 
    console.log(`[Linking] Path: "${path}", LoggedIn: ${isLoggedIn}, TargetStack: ${targetStackName}`);
 
    // --- Routing Logic --- 
    // 1. Trying to access protected route while logged out?
    if (isProtectedTarget && !isLoggedIn) {
      console.log(`[Linking] Blocked: Accessing protected route "${path}" while logged out. Redirecting to Login.`);
      // Redirect to the login screen, optionally pass original path
      return {
        routes: [{ name: AUTH_STACK, state: { routes: [{ name: LOGIN_SCREEN, params: { returnTo: path }}]} }],
      };
    }
 
    // 2. Trying to access auth route (Login/Register) while already logged in?
    if (isAuthTarget && isLoggedIn) {
        console.log(`[Linking] Blocked: Accessing auth route "${path}" while logged in. Redirecting to default authenticated screen.`);
        // Redirect to the main authenticated stack (e.g., Dashboard)
        // Adapt the target screen as needed for your app's default logged-in view
        return {
            routes: [{ name: PROTECTED_STACK, state: { routes: [{ name: 'Dashboard' }]}}],
        };
    }
 
    // 3. Otherwise, proceed with the default determined state
    console.log(`[Linking] Proceeding with default state for path: "${path}"`);
    return defaultState;
  },
};
 
export default linking;

Key Idea: Access the Zustand store directly (outside React hooks) within getStateFromPath using useAuthStore.getState() to check isLoggedIn. Modify the initial navigation state returned by getStateFromPath based on the user's auth status and the target route's protection level.

2. Handling Specific API Errors in Login/Register

The useAuth hook exposes loginError and registerError from the mutations. Map backend error codes to user-friendly messages in the UI.

Challenge: Displaying meaningful feedback (e.g., "Invalid Credentials", "Email Taken").

Solution: Inspect the error object in the component using the mutation.

// Inside LoginScreen.tsx or similar component using useAuth
import { Alert } from 'react-native';
import { useAuth } from '@/features/auth/hooks/useAuth';
import { useEffect } from 'react';
 
// ... component setup ...
const { login, isLoggingIn, loginError } = useAuth();
 
useEffect(() => {
  if (loginError) {
    let message = 'An unknown error occurred. Please try again.';
    // AxiosError structure: error.response.data contains backend payload
    const backendError = (loginError as any)?.response?.data;
    const errorCode = backendError?.code; // Adjust based on your API's error structure
    const errorMessage = backendError?.message;
 
    console.error('[LoginScreen] Login Error Details:', backendError);
 
    switch (errorCode) {
      case 'INVALID_CREDENTIALS':
        message = 'Incorrect email or password. Please check your details and try again.';
        break;
      case 'ACCOUNT_LOCKED':
        message = 'Your account is currently locked due to too many failed attempts. Please contact support.';
        break;
      case 'EMAIL_NOT_VERIFIED':
          message = 'Please verify your email address before logging in.';
          break;
      // Add other specific error codes from your backend API
      default:
          // Use backend message if available and code is unknown
          if (errorMessage) {
              message = errorMessage;
          }
          break;
    }
    Alert.alert('Login Failed', message);
    // Reset error state in mutation if needed, though TanStack Query usually handles this
    // loginMutation.reset();
  }
}, [loginError]); // Run effect when loginError changes
 
const handleLoginSubmit = (values: LoginCredentials) => {
    login(values); // Trigger the mutation
};
 
// ... rest of component ...

(Do ✅) Define constants for backend error codes. (Consider 🤔) Centralizing error code-to-message mapping in a utility function if used in multiple places.

3. Understanding Token Refresh Robustness

The Axios interceptor handles automatic token refresh.

Key Robustness Features:

  • (Concurrency Handling ✅): If multiple API calls fail with 401 while a refresh is ongoing (isRefreshing === true), subsequent requests are added to failedQueue. They only proceed after the initial refresh attempt concludes (success or failure).
  • (Infinite Loop Prevention ✅): The interceptor checks !originalRequest._retry and the request URL (originalRequest.url !== API_ENDPOINTS.REFRESH_TOKEN) to prevent the refresh logic from triggering itself or retrying indefinitely.
  • (Refresh Failure Handling ✅): If the refreshToken API call fails, the interceptor logs the user out (dispatches clearAuthState, removes header/storage) and rejects all queued requests, ensuring a clean state.

(Be Aware ❗): Ensure the backend's /auth/refresh-token endpoint does not require the (expired) access token for authorization. It should only rely on the provided refreshToken in the request body.

  • DOC-01: Core Architecture Reference
  • DOC-03: API & State Management Reference
  • DOC-04: Security & Offline Framework Reference
  • GUIDE-01: Server State Management Guide
  • GUIDE-02: State Management Implementation Guide
  • GUIDE-06: Offline Support Implementation Guide
  • GUIDE-07: Social Login Implementation Guide
  • GUIDE-08: Biometric Authentication Implementation Guide