Source

src/components/Atlas.tsx

import {
  RacingSansOne_400Regular
} from '@expo-google-fonts/racing-sans-one';
import { NavigationContainer } from '@react-navigation/native';
import axios, { AxiosRequestConfig } from 'axios';
import { useFonts } from 'expo-font';
import { getItemAsync } from 'expo-secure-store';
import { observer } from 'mobx-react';
import React, { useEffect, useRef, useState } from 'react';
import { AppState } from 'react-native';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { QueryClient, QueryClientProvider } from 'react-query';
import { Splash } from '../components/Splash';
import { API_URL, reportAxiosError } from '../globals';
import { useAuth } from '../hooks/useAuth';
import UnauthorizedNavigator from '../navigation/UnauthorizedNavigator';
import AuthorizedNavigator from '../navigation/AuthorizedNavigator';
import { authStore } from '../stores/AuthStore';

export enum TokenState {
  CheckingToken,
  ValidToken,
  InvalidToken
}

const queryClient = new QueryClient();

/**
 * Sub-root component of the app. Contains all global providers (NavigationContainer and SafeAreaProvider for React Navigation, QueryClientProvider for react-query) and is responsible for restricting unauthenticated users to the Intro screen by listening to {@link AuthStore}'s accessToken value.
 * @component
 */
const Atlas : React.FC = () => {
  /**
   * Ref that keeps track of the app's state (opened or closed)
   */
  const appState = useRef(AppState.currentState);
  /**
   * Flag that is switched on when the app is checking for tokens in the keystore and in memory. When true, "Logging you in.." and a spinner will be displayed to the user.
   */
  const [checkingToken, setCheckingToken] = useState<boolean>(true);
  const { refreshAccessToken } = useAuth();
  const [fontsLoaded, error] = useFonts({
    RacingSansOne_400Regular
  });

  /**
   * Checks if there is an access token available in {@link AuthStore}, then checks if that access token is valid by calling the API. 
   * If the response is valid, the access token will be stored in memory, otherwise the user will be directed to intro screen.
   */
  const checkToken = async () => {
    // check both the mobx store and secure storage for the token
    let currentAccessToken = authStore.accessToken;
    if (!currentAccessToken) {
      currentAccessToken = await getItemAsync('accessToken');
    }

    if (currentAccessToken) {
      // check to see if the token is valid by making test call
      const requestConfig: AxiosRequestConfig = {
        method: 'GET',
        url: API_URL + "/api/me/",
        headers: { "Authorization": "Bearer " + currentAccessToken }
      };

      try {
        await axios(requestConfig);
        await authStore.setAccessTokenAsync(currentAccessToken);
      } catch (error) {
        // check if access token can be refreshed
        if (error.response.status == 401) {
          try {
            await refreshAccessToken();

            // update authorization header w/ new token
            await axios({...requestConfig, headers: { "Authorization": "Bearer " + authStore.accessToken }}); 
          } catch (error) {
            await authStore.setAccessTokenAsync(null);    
          }
        }
        // something went wrong with the api call, log error and delete access token
        reportAxiosError('Something went wrong when retrieving an access token', error)
        await authStore.setAccessTokenAsync(null);
      }
    }
    else {
      // no access token was found, user will be taken to login
      await authStore.setAccessTokenAsync(null);
    }

    setCheckingToken(false);
  }
  useEffect(() => {
    /**
     * useEffect hook that is responsible for registering an appState "change" handler that will call {@linkcode checkToken} each time the app is opened or closed on the device.
     * @memberOf Atlas
     */
    function registerAppStateChangeHandler() {
      AppState.addEventListener("change", (appState: string) => {
        if (appState == 'active') {
          checkToken(); 
        }
      });
      return () => {
        AppState.removeEventListener("change", (appState: string) => {
          if (appState == 'active') {
            checkToken(); 
          }
        });
      };
    }
    registerAppStateChangeHandler();
  }, []);

  useEffect(() => {
    /**
   * Calls {@linkcode checkToken} when a change to the access token stored in {@link AuthStore} is detected. 
   * @memberOf Atlas
   */
    const checkTokenOnAccessTokenChange = () => {
      checkToken()
    }
    checkTokenOnAccessTokenChange()
  }, [authStore.accessToken]);

  return (
    <SafeAreaProvider>
      <NavigationContainer>  
        {checkingToken ? <Splash/> :
        <QueryClientProvider client={queryClient}>
          {authStore.accessToken ? <AuthorizedNavigator  /> : <UnauthorizedNavigator /> } 
        </QueryClientProvider> }
      </NavigationContainer>  
    </SafeAreaProvider>
  );
}

export default observer(Atlas);