All files / atlas/src/components Atlas.tsx

26.32% Statements 10/38
7.14% Branches 1/14
11.11% Functions 1/9
26.32% Lines 10/38

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137                                                  2x           2x       2x       2x 2x 2x               2x                                                                                   2x                                           2x                     2x                        
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);