# Code coverage report for atlas-mobile-ts/src/components/Atlas.tsx

# All files / atlas-mobile-ts/src/components Atlas.tsx

0% Statements 0/84
0% Branches 0/16
0% Functions 0/10
0% Lines 0/84

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
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176

     ﻿/* Copyright (C) Click & Push Accessibility, Inc - All Rights Reserved
     * Unauthorized copying of this file, via any medium is strictly prohibited
     * Proprietary and confidential
     * Written and maintained by the Click & Push Development team
     * <dev@clicknpush.ca>, January 2022
     */

    import {
      RacingSansOne_400Regular
    } from '@expo-google-fonts/racing-sans-one';
    import { NavigationContainer } from '@react-navigation/native';
    import { MenuProvider } from 'react-native-popup-menu'
    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, SafeAreaView, StatusBar, Platform, Alert } from 'react-native';
    import { QueryClient, QueryClientProvider } from 'react-query';
    import { Splash } from '../components/Splash';
    import { colors, SECURESTORE_ACCESSTOKEN, SECURESTORE_REFRESHTOKEN } from '../utils/GlobalUtils';
    import { useAuth } from '../hooks/useAuth';
    import AuthorizedNavigator, { navigationRef } from '../navigation/AuthorizedNavigator';
    import UnauthorizedNavigator from '../navigation/UnauthorizedNavigator';
    import { authStore } from '../libs/auth/AuthStore';
    import { API_URL } from '../utils/RequestUtils';
    import {reportAxiosError} from '../libs/auth/core'

    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 = () => {
      /**
       * 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
        console.log('[Authentication]: Checking for access token in memory...')
        let currentAccessToken = authStore.accessToken;
        Iif (!currentAccessToken) {
          console.log('[Authentication]: No access token in memory, checking in secure store...')
          currentAccessToken = await getItemAsync(SECURESTORE_ACCESSTOKEN);
        }

        Iif (!currentAccessToken) {
          console.log('[Authentication]: No access token in secure store, attempting to use a refresh token...')
          let refreshToken  = authStore.refreshToken;
          Iif (!refreshToken) {
            refreshToken = await getItemAsync(SECURESTORE_REFRESHTOKEN)
          }

          Iif (refreshToken) {
            await refreshAccessToken()
            currentAccessToken = authStore.accessToken
          }
        }

        if (currentAccessToken) {
          console.log('[Authentication]: Found access token, testing its validity...')
          // 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 {
            const response = await axios(requestConfig);
            Iif (response.status == 200) {
              await authStore.setAccessTokenAsync(currentAccessToken);
              await authStore.setRefreshTokenAsync(await getItemAsync(SECURESTORE_REFRESHTOKEN))
              await authStore.setIdAsync(response.data.id)
              console.log('[Authentication]: Access token valid.')
            }
          } catch (error) {
            // check if access token can be refreshed
            console.log(error)
            Iif (error.response.status == 401) {
              try {
                await refreshAccessToken();
                // update authorization header w/ new token
                await axios({...requestConfig, headers: { "Authorization": "Bearer " + authStore.accessToken }});
              } catch (error) {

              }
            }
            // something went wrong with the api call, log error and clear auth state
            reportAxiosError('[Authentication]: Something went wrong when retrieving an access token', error)
            await authStore.setAccessTokenAsync(null);
            await authStore.setRefreshTokenAsync(null);
            await authStore.setNotificationTokenAsync(null);
            await authStore.setIdAsync(null);
          }
        }
        else {
          // no access token was found, user will be taken to login
          console.log('[Authentication]: No access token was found, prompting user to login.')
          await authStore.setAccessTokenAsync(null);
          await authStore.setRefreshTokenAsync(null);
          await authStore.setNotificationTokenAsync(null);
          await authStore.setIdAsync(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) => {
            Iif (appState == 'active') {
              console.log('[Authentication]: App opened, checking auth tokens...')
              checkToken();
            }
          });
          return () => {
            AppState.removeEventListener("change", (appState: string) => {
              Iif (appState == 'active') {
                checkToken();
              }
            });
          };
        }
        registerAppStateChangeHandler();
      }, [AppState.currentState]);

      useEffect(() => {
        /**
       * Calls {@linkcode checkToken} when a change to the access token stored in {@link AuthStore} is detected.
       * @memberOf Atlas
       */
        const checkTokenOnAccessTokenChange = async () => {
          console.log('[Authentication]: Change to accessToken detected, checking token state...')
          await checkToken()
        }
        checkTokenOnAccessTokenChange()
      }, [authStore.accessToken]);

      return (
        <MenuProvider>
            <SafeAreaView accessible={true} accessibilityLabel="rootView" style={{height: '100%', backgroundColor: colors.red}}>
            <StatusBar barStyle='light-content' backgroundColor={colors.red}/>
            <NavigationContainer ref={navigationRef}>
              {checkingToken ? <Splash/> :
              <QueryClientProvider client={queryClient}>
                {authStore.accessToken ? <AuthorizedNavigator  /> : <UnauthorizedNavigator /> }
              </QueryClientProvider> }
            </NavigationContainer>
          </SafeAreaView>
        </MenuProvider>
      );
    }

    export default observer(Atlas);