Ver Fonte

fixed rating landmark issue

cdmoss há 2 anos atrás
pai
commit
6c5ddb6009

+ 8 - 1
App.tsx

@@ -13,6 +13,8 @@ import { colors } from './src/utils/GlobalUtils';
 import { AuthContextProvider } from './src/data/Auth/AuthContext';
 import { QueryClient, QueryClientProvider, useQueryClient } from 'react-query';
 import { navigationRef } from './src/navigation/RootNavigator';
+import { PermissionsContextProvider } from './src/data/PermissionsContext';
+import { LOGGING } from './src/utils/logging';
 
 const App: React.FC = () => {
   const updateDismissed = useRef<boolean>(false)
@@ -20,10 +22,13 @@ const App: React.FC = () => {
   const queryClient = new QueryClient()
 
   useEffect(() => {
+    LOGGING.log("SYSTEM", 'info', "Launching app...")
     if (!__DEV__) {
+      LOGGING.log("SYSTEM", 'info', "App is release version, checking for updates...")
       const timer = setInterval(async () => {
         const update = await Updates.checkForUpdateAsync()
         if (update.isAvailable && !updateDismissed.current) {
+          LOGGING.log("SYSTEM", 'info', "Update available, prompting user...")
           updateDismissed.current = true
           setTimeout(() => {
             Alert.alert('Update Available', 'An update is available. Would you like to update now?', [
@@ -84,7 +89,9 @@ const App: React.FC = () => {
             <StatusBar barStyle='light-content' backgroundColor={colors.red}/>
             <NavigationContainer ref={navigationRef}>
               <AuthContextProvider>
-                <Atlas/>
+                <PermissionsContextProvider>
+                  <Atlas/>
+                </PermissionsContextProvider>
               </AuthContextProvider>
             </NavigationContainer>  
           </SafeAreaView>

+ 4 - 11
src/components/Atlas.tsx

@@ -14,6 +14,7 @@ import BaseStackNavigator from '../navigation/BaseStackNavigator';
 import { Error } from './Error';
 
 import { navigationRef } from '../navigation/RootNavigator';
+import { usePermissions } from '../data/PermissionsContext';
 
 export enum TokenState {
   CheckingToken,
@@ -28,12 +29,8 @@ const queryClient = new QueryClient();
  * @component
  */
 const Atlas : React.FC = () => {
-  const {loading, error, setError, setLoading} = useAuth()
-
-  const dismissError = () => {
-    setError('')
-    setLoading(false)
-  }
+  const {authStateLoading} = useAuth()
+  const {permissionsLoading} = usePermissions()
 
   // useEffect(() => {
   //   let isMounted = true
@@ -55,11 +52,7 @@ const Atlas : React.FC = () => {
 
   return (
     <>
-      {error ? 
-      <Error error={error} dismissErrorMethod={() => dismissError()}/> :
-        <>
-          {loading ? <Loading/> : <BaseStackNavigator />}
-        </> }
+      {authStateLoading || permissionsLoading ? <Loading/> : <BaseStackNavigator />}
     </>
   );
 }

+ 0 - 1
src/components/Map/MainMapComponent/IndoorMap.tsx

@@ -210,7 +210,6 @@ const IndoorMap: React.FC<IndoorMapProps> = ({ navigation, landmarks, promptAddL
               >
 
               <Svg onLayout={event => {
-                console.log("OFFICIAL: " + event.nativeEvent.layout.width + " , " + event.nativeEvent.layout.height)
                 setSVGdim([event.nativeEvent.layout.width, event.nativeEvent.layout.height])
                 const transformedLandmarks = localLandmarks.map(item => {
                   return { ...item, coordx: item.longitude * event.nativeEvent.layout.width, coordy: item.latitude * event.nativeEvent.layout.height }

+ 35 - 100
src/components/Map/MainMapComponent/OutdoorMap.tsx

@@ -5,21 +5,24 @@
  * <dev@clicknpush.ca>, January 2022
  */
 
-import { FontAwesome } from "@expo/vector-icons";
+import { createIconSetFromIcoMoon, FontAwesome } from "@expo/vector-icons";
 import { RouteProp, useNavigationState } from "@react-navigation/native";
 import { booleanPointInPolygon, circle } from '@turf/turf';
 import * as Notifications from 'expo-notifications';
 import { observer } from "mobx-react";
-import React, { useEffect, useRef, useState } from "react";
-import { AppState, Image, Keyboard, Modal, Platform, TouchableOpacity, TouchableWithoutFeedback, Text, View, ActivityIndicator } from "react-native";
-import MapView, { LatLng, Marker, Polygon, Region, Polyline } from "react-native-maps";
-import { PERMISSIONS } from "react-native-permissions";
-import Spokestack from 'react-native-spokestack';
+import React, { useEffect, useState } from "react";
+import { ActivityIndicator, Image, Keyboard, Modal, Text, TouchableOpacity, TouchableWithoutFeedback, View } from "react-native";
+import MapView, { LatLng, Marker, Polygon } from "react-native-maps";
+import { ToggleButton } from "react-native-paper";
+import { openSettings } from "react-native-permissions";
+import Spokestack, { activate } from 'react-native-spokestack';
+import { useAuth } from "../../../data/Auth/AuthContext";
 import { Landmark } from '../../../data/landmarks';
 import { NotifType } from "../../../data/notifications";
+import { usePermissions } from "../../../data/PermissionsContext";
 import { MainTabsNavigationProp, MainTabsParamList } from "../../../navigation/MainTabsNavigator";
 import { MapStackNavigationProp, MapStackParamList } from "../../../navigation/MapNavigator";
-import { checkVoicePermissions, colors, getMapPermissions, lmTypes } from "../../../utils/GlobalUtils";
+import { colors, lmTypes } from "../../../utils/GlobalUtils";
 import Badge from "../../Badge";
 import { IconButton } from "../../Buttons";
 import NearbyLandmarksPanel from "../Panels/NearbyLandmarksPanel";
@@ -65,6 +68,8 @@ interface OutdoorMapProps {
 }
 
 const OutdoorMap: React.FC<OutdoorMapProps> = (props) => {
+    const {locationPermissionsGranted, checkLocationPermissions, voicePermissionsGranted, checkVoicePermissions} = usePermissions();
+    const {setAlert} = useAuth()
     const mapState = useOutdoorMapState()
 
     const mapNavIndex = useNavigationState(state => state)
@@ -119,97 +124,11 @@ const OutdoorMap: React.FC<OutdoorMapProps> = (props) => {
             mapState.mapRef.current.animateToRegion({ latitude: props.newLandmark?.latitude, longitude: props.newLandmark?.longitude, latitudeDelta: 0.01, longitudeDelta: 0.01 })
         }
     }, [props.newLandmark])
-
-    /**
-     * Gets speech permissions from user, runs every time app is brought to foreground
-     */
-    useEffect(() => {
-        const getSpeechPermissions = async () => {
-            if (AppState.currentState == 'active') {
-                await getMapPermissions()
-                console.log('[Permissions]: Checking voice permissions...')
-                if (Platform.OS == 'android') {
-                    const permitted = await checkVoicePermissions([PERMISSIONS.ANDROID.RECORD_AUDIO])
-                    mapState.toggleVoicePermission(permitted)
-                    if (permitted) console.log('[Permissions]: Voice permission granted')
-                    else console.log('[Permissions]: Voice permission denied')
-                }
-                else if (Platform.OS == 'ios') {
-                    const permitted = await checkVoicePermissions([PERMISSIONS.IOS.SPEECH_RECOGNITION, PERMISSIONS.IOS.MICROPHONE])
-                    mapState.toggleVoicePermission(permitted)
-                    if (permitted) console.log('[Permissions]: Voice permission granted')
-                    else console.log('[Permissions]: Voice permission denied')
-                }
-            }
-        }
-        getSpeechPermissions()
-    }, [AppState.currentState])
-
-    /**
-     * Gets foreground location permissions from user, runs every time app is brought to foreground
-     */
-    useEffect(() => {
-        const checkForegroundLocationPermissions = async () => {
-            if (AppState.currentState == 'active') {
-                console.log('[Permissions]: Checking location permissions...')
-                if (Platform.OS == 'android') {
-                    const permitted = await checkVoicePermissions([PERMISSIONS.ANDROID.ACCESS_COARSE_LOCATION, PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION])
-                    mapState.toggleFgroundLocationPermission(permitted)
-                    if (permitted) console.log('[Permissions]: Location permission granted')
-                    else console.log('[Permissions]: Location permission denied')
-                }
-                else if (Platform.OS == 'ios') {
-                    const permitted = await checkVoicePermissions([PERMISSIONS.IOS.LOCATION_WHEN_IN_USE])
-                    mapState.toggleFgroundLocationPermission(permitted)
-                    if (permitted) console.log('[Permissions]: Location permission granted')
-                    else console.log('[Permissions]: Location permission denied')
-                }
-            }
-        }
-        checkForegroundLocationPermissions();
-    }, [AppState.currentState])
-
-    /**
-     * Gets background location permissions from user, runs every time app is brought to foreground
-     */
-    useEffect(() => {
-        const checkBackgroundLocationPermissions = async () => {
-            if (AppState.currentState == 'active') {
-                if (Platform.OS == 'android') {
-                    const permitted = await checkVoicePermissions([PERMISSIONS.ANDROID.ACCESS_BACKGROUND_LOCATION])
-                    mapState.toggleBgroundLocationPermission(permitted)
-                    if (permitted) console.log('[Permissions]: Background location permission granted')
-                    else console.log('[Permissions]: Background location permission denied')
-                }
-                else if (Platform.OS == 'ios') {
-                    const permitted = await checkVoicePermissions([PERMISSIONS.IOS.LOCATION_ALWAYS])
-                    mapState.toggleBgroundLocationPermission(permitted)
-                    if (permitted) console.log('[Permissions]: Background location permission granted')
-                    else console.log('[Permissions]: Background location permission denied')
-                }
-            }
-        }
-        checkBackgroundLocationPermissions();
-    }, [AppState.currentState])
-
-    /**
-     * Gets net location permission the existing location permission states. It will check foreground and background permissions and AppState, 
-     * then from that it will decide if location-enabled features should be activated (through the mapState state values).
-     */
-    useEffect(() => {
-        const updateLocationPermissionOnAppStateChange = async () => {
-            const netLocationPermissions = mapState.bgroundLocationPermission || (mapState.fgroundLocationPermission && AppState.currentState == 'active')
-            console.log('[Permissions]: Appstate, or location permissions changed, net location permissions found to be: ' + netLocationPermissions)
-            mapState.toggleLocationPermitted(netLocationPermissions)
-        }
-        updateLocationPermissionOnAppStateChange()
-    }, [AppState.currentState, mapState.bgroundLocationPermission, mapState.fgroundLocationPermission])
-
-
     /**
      * Animates the map to fly over to and focus on the user's location.
      */
     const flyToUser = () => {
+
         console.log('[Map]: Centering on user')
         if (mapState.userLocation) {
             mapState.mapRef.current?.animateToRegion({ latitude: mapState.userLocation.latitude, longitude: mapState.userLocation.longitude, latitudeDelta: 0.01, longitudeDelta: 0.01 })
@@ -219,10 +138,27 @@ const OutdoorMap: React.FC<OutdoorMapProps> = (props) => {
     /**
      * Activates speech recognition and opens the voice panel
      */
-    const startSpeech = () => {
+    const startSpeech = async () => {
+        if (!(voicePermissionsGranted && locationPermissionsGranted)) {
+            setAlert({
+                title: "Permissions",
+                message: "Please enable location and voice permissions in the settings menu",
+                type: "warning",
+                callback: () => openSettings(),
+                callbackButtonText: 'Open settings'
+            })
+            return
+        }
         props.toggleLmDetails(false);
         props.toggleLmAdd(false);
-        Spokestack.activate()
+        const spokestackInitialized = await Spokestack.isInitialized()
+        const spokestackStarted = await Spokestack.isStarted()
+
+        if (spokestackInitialized) 
+            if (spokestackStarted) {
+                mapState.toggleVoiceVisible(true)
+                Spokestack.activate()
+            }
     }
 
     /**
@@ -302,7 +238,7 @@ const OutdoorMap: React.FC<OutdoorMapProps> = (props) => {
                     style={{ width: '100%', height: '100%' }}
                     initialRegion={getInitialRegion()}
                     onLongPress={async (e) => await props.promptAddLandmark(e.nativeEvent.coordinate.longitude, e.nativeEvent.coordinate.latitude)}
-                    showsUserLocation={mapState.locationPermitted}
+                    showsUserLocation={locationPermissionsGranted}
                     onUserLocationChange={e => updateLocation(e.nativeEvent.coordinate)}
                     followsUserLocation={mapState.followUser}
                     showsMyLocationButton={false}>
@@ -358,8 +294,7 @@ const OutdoorMap: React.FC<OutdoorMapProps> = (props) => {
                         <FontAwesome name='exclamation-triangle' size={20} color='white' />
                         <Badge positioning={{ bottom: 7, right: 4 }} value={mapState.landmarksNearUser.length} />
                     </TouchableOpacity> : null}
-                {mapState.locationPermitted && mapState.voicePermission ?
-                    <IconButton size={20} color='white' style={[mapStyles.lowerMapButton, mapStyles.voiceButton]} icon="microphone" onPress={startSpeech} /> : null}
+                {locationPermissionsGranted && voicePermissionsGranted ?<IconButton size={20} color='white' style={[mapStyles.lowerMapButton, mapStyles.voiceButton]} icon="microphone" onPress={async () => await startSpeech()} /> : null}
                 <IconButton size={20} color='white' style={[mapStyles.lowerMapButton, mapStyles.addLandmarkButton]} icon="plus" onPress={async () => await props.promptAddLandmark(mapState.userLocation.longitude, mapState.userLocation.latitude)} />
                 <IconButton size={20} color='white' style={[mapStyles.lowerMapButton, mapStyles.userLocationButton]} icon="location-arrow" onPress={flyToUser} />
                 <NearbyLandmarksPanel
@@ -368,7 +303,7 @@ const OutdoorMap: React.FC<OutdoorMapProps> = (props) => {
                     toggleAlertedLmPanel={mapState.toggleNearbyLmPanel}
                     nearbyLandmarks={mapState.landmarksNearUser} />
                 {/*Map Panels*/}
-                {mapState.voicePermission && mapState.locationPermitted ?
+                {locationPermissionsGranted && voicePermissionsGranted ?
                     <VoicePanel
                         landmarksNearby={mapState.landmarksNearUser?.length > 0}
                         toggleAlertedLandmarksVisible={mapState.toggleNearbyLmPanel}

+ 0 - 6
src/components/Map/Panels/AddLandmarkPanel.tsx

@@ -150,7 +150,6 @@ const AddLandmarkPanel: React.FC<AddLandmarkProps> = ({ newLandmark, setNewLandm
                     result: 'base64'
                 })
 
-                console.log("Image is", uri.substring(0, 100))
                 await addLandmarkMutation.mutateAsync({ landmarkValue: newLandmark, photos: photos, indoorLmLocImg: uri }); // pass it in here
 
 
@@ -209,10 +208,6 @@ const AddLandmarkPanel: React.FC<AddLandmarkProps> = ({ newLandmark, setNewLandm
                 behavior={Platform.OS === "ios" ? "padding" : "height"}
                 enabled={photos?.length > 0}
             >
-                {console.log("state of keyboard is " + keyboardOpened)}
-                {/* {console.log("*THIS IS IN PANEL* navigationState is " + navigationState.index)}
-                {console.log("*THIS IS IN PANEL* currentRoute is " + currentRoute)} */}
-
                 <SafeAreaView style={{ backgroundColor: colors.red, height: determineModalHeight(), }}>
                     {addLandmarkMutation.isIdle ?
                         <>
@@ -314,7 +309,6 @@ const AddLandmarkPanel: React.FC<AddLandmarkProps> = ({ newLandmark, setNewLandm
                     {newLandmark == null || newLandmark.floor == null || newLandmark.landmark_type == null ? <></> :
                         <View style={styles.container}>
                             <Svg>
-                                {console.log("x coord is " + newLandmark.longitude + " and y coord is " + newLandmark.latitude)}
                                 <IndoorFloor floorNum={newLandmark.floor} />
                                 <ImageSVG x={newLandmark.longitude * imgWidth - 3} y={newLandmark.latitude * imgHeight - 3} width={imageDim} height={imageDim} href={lmTypes[newLandmark.landmark_type]['image'] as ImageSourcePropType} />
                             </Svg>

+ 0 - 1
src/components/Map/Panels/LandmarkDetailsPanel/DetailsBody.tsx

@@ -135,7 +135,6 @@ export const DetailsBody: React.FC<DetailsBodyProps> = (props) => {
                 <LandmarkTypePickerContainer />
                 <Separator style={{marginBottom: 20, opacity: .5}} color="lightgray" />
                 <Text style={{color: 'white', marginBottom: 10}}>Description</Text>
-                {/* {console.log("*DETAILS BODY: currentRotue is " + currentRoute)} */}
                 <ScrollView nestedScrollEnabled={true} style={{backgroundColor: 'white', marginBottom: 20}}>
                     <TextInput 
                         multiline={true} 

+ 16 - 3
src/components/Map/Panels/LandmarkDetailsPanel/DetailsHeader.tsx

@@ -12,6 +12,7 @@ import { QueryStatus } from "react-query";
 import { useAuth } from "../../../../data/Auth/AuthContext";
 import { Landmark } from "../../../../data/landmarks";
 import { UserProfile } from "../../../../data/profiles";
+import { MainTabsNavigationProp } from "../../../../navigation/MainTabsNavigator";
 import TouchOpaq from './TouchOpaq';
 
 interface DetailsHeaderProps {
@@ -28,6 +29,8 @@ interface DetailsHeaderProps {
     addPhotoStatus: QueryStatus
     deletePhotoStatus: QueryStatus
     ratedByUser: boolean
+    authNavigation: MainTabsNavigationProp
+    toggleLmDetails: (state: boolean) => void
 }
 
 /**
@@ -35,7 +38,10 @@ interface DetailsHeaderProps {
  * @param 
  */
 export const DetailsHeader: React.FC<DetailsHeaderProps> = (props) => {
-    const {landmarkOwnedByUser, anonUserId} = useAuth()
+
+    console.log(props.ratedByUser)
+
+    const {landmarkOwnedByUser, anonUserId, setAlert} = useAuth()
 
     const photosAreBusy = () => {
         return props.processingPhoto ||
@@ -44,6 +50,7 @@ export const DetailsHeader: React.FC<DetailsHeaderProps> = (props) => {
     }
 
     const HeaderContent: React.FC = () => {
+        console.log(landmarkOwnedByUser(props.landmark))
         // landmark is owned by user
         if (landmarkOwnedByUser(props.landmark)) {
             // editing is enabled
@@ -127,9 +134,15 @@ export const DetailsHeader: React.FC<DetailsHeaderProps> = (props) => {
                         </View> : // landmark has not been liked by user
                         <TouchableOpacity onPress={async () => { //this touchable will add a like to this landmark
                             if (anonUserId) {
-                                Alert.alert("You must be logged in to rate landmarks.");
+                                setAlert({
+                                    title: "Authentication required",
+                                    type: "warning",
+                                    message: "Oops! In order to rate landmarks, you have to log in.",
+                                    callback: () => {props.toggleLmDetails(false); props.authNavigation.navigate("Account")},
+                                    callbackButtonText: 'Go to login'
+                                });
                             }
-                            if (landmarkOwnedByUser(props.landmark)) {
+                            else if (!landmarkOwnedByUser(props.landmark)) {
                                 await props.rateLandmark(1);
                             }
                         }}>

+ 2 - 0
src/components/Map/Panels/LandmarkDetailsPanel/LandmarkDetails.tsx

@@ -374,6 +374,8 @@ const LandmarkDetails: React.FC<LandmarkDetailsProps> = ({authNavigation, landma
                 deleteLandmarkMutation.isIdle || deleteLandmarkMutation.isSuccess ?
                 <>
                 <DetailsHeader
+                    authNavigation={authNavigation}
+                    toggleLmDetails={toggleLmDetails}
                     ratedByUser={landmarkQuery?.data?.ratedByUser}
                     processingPhoto={processingPhoto}
                     addPhotoStatus={addLandmarkPhotoMutation.status}

+ 39 - 50
src/components/Map/Panels/VoicePanel.tsx

@@ -6,10 +6,9 @@
  */
 
 import { FontAwesome } from '@expo/vector-icons';
-import * as Linking from "expo-linking";
 import FastImage, {} from 'react-native-fast-image'
 import React, { useEffect, useMemo, useRef, useState } from 'react';
-import { ActivityIndicator, AppState, FlatList, Image, ImageRequireSource, ImageURISource, KeyboardAvoidingView, Text, TouchableOpacity, View, ViewStyle } from 'react-native';
+import { Linking, ActivityIndicator, AppState, FlatList, Image, ImageRequireSource, ImageURISource, KeyboardAvoidingView, Text, TouchableOpacity, View, ViewStyle } from 'react-native';
 import { Pulse, Wave } from 'react-native-animated-spinkit';
 import Config from 'react-native-config';
 import Modal from "react-native-modal";
@@ -20,6 +19,11 @@ import { colors, GlobalStyles, lmTypes } from '../../../utils/GlobalUtils';
 import { Separator } from '../../Separator';
 import * as Speech from 'expo-speech';
 import { Landmark, useAddLandmark } from '../../../data/landmarks';
+import { usePermissions } from '../../../data/PermissionsContext';
+import { useAuth } from '../../../data/Auth/AuthContext';
+import { openSettings } from 'react-native-permissions';
+import { CommonActions } from '@react-navigation/native';
+import { addNotificationResponseReceivedListener } from 'expo-notifications';
 
 export interface VoicePanelProps {
     voiceVisible: boolean,
@@ -66,6 +70,9 @@ export const VoicePanel: React.FC<VoicePanelProps> = ({
         setSelectedLandmarkId}
     ) => {
 
+    const {locationPermissionsGranted, checkLocationPermissions, voicePermissionsGranted, checkVoicePermissions, } = usePermissions()
+    const {setAlert} = useAuth()
+
     // An array of words that will cause the app to stop listening once heard
     const stopResponses = ["cancel", "stop", "goodbye", "bye"]
 
@@ -115,7 +122,7 @@ export const VoicePanel: React.FC<VoicePanelProps> = ({
     }
 
     useEffect(() => {
-        if (voiceVisible) console.log('[Map]: Voice panel openeing')
+        if (voiceVisible) console.log('[Map]: Voice panel opening')
         else console.log('[Map]: Voice panel closing')
     }, [voiceVisible])
 
@@ -125,11 +132,12 @@ export const VoicePanel: React.FC<VoicePanelProps> = ({
         }
     }, [nearbyLandmarksTrigger])
 
+    
     // Sets up and starts the Spokestack process the first time this component is rendered
     useEffect(() => {
         // Adds handler that will open the app, navigate to the Map screen and open the panel if necessary when Spokestack.activate() is called.
         // Also, listening will be set to true, and speechResult will be reset
-        Spokestack.addEventListener('activate', () => {
+        Spokestack.addEventListener('activate', async () => {
             console.log('[Voice]: Spokestack activated')
             if (!navigation.isFocused()) {
                 navigation.navigate('Map')
@@ -140,6 +148,7 @@ export const VoicePanel: React.FC<VoicePanelProps> = ({
             if (!voiceVisible) {
                 toggleVoiceVisible(true)
             }
+
             setSpeechResult('')
             setListening(true)
         });
@@ -163,6 +172,10 @@ export const VoicePanel: React.FC<VoicePanelProps> = ({
             setPartialSpeechResult(transcript)
         })  
 
+        Spokestack.addEventListener('error', ({error}) => {
+            console.log('[Voice]: Spokestack error: ' + error)
+        })
+
         Spokestack.initialize(
             'c361ff3a-70c3-42e6-b0ee-0207edd03b18', // account id
             '1A8196594C401EB93035CC6D7D6328CF1855C2B359744E720953AC34B6F658CA', // api token
@@ -176,7 +189,7 @@ export const VoicePanel: React.FC<VoicePanelProps> = ({
                     encode: 'https://s.spokestack.io/u/kHNUa/encode.tflite',
                 },
             }
-        ).then(Spokestack.start).catch(error => console.log("[Voice]: Something went wrong when starting spokestack: " +error))
+        ).then(Spokestack.start).catch(error => console.log("[Voice]: Something went wrong when starting spokestack: " + error))
 
         return () => {
             Spokestack.removeAllListeners();
@@ -195,17 +208,14 @@ export const VoicePanel: React.FC<VoicePanelProps> = ({
                 if (speechResult && !listening) {
                     await processSpeechTranscript();
                 }
-                // if there is no speech result has been detected, and Spokestack is not currently listening
-                if (!speechResult && !listening) {
-                    // if there is no current action, treat it as a timeout and close the voice panel
-                    if (!action) {
-                        await closeVoice(false);   
-                    }
-                    else {
-                        // otherwise give the user another chance to speak
-                        // setListening(true)
-                        // await Spokestack.activate()
-                    }
+                else if (!speechResult && !listening && action) {
+                    console.log('[Voice]: No speech result detected, but an action is in progress')
+                    setListening(true)
+                    await Spokestack.activate()
+                }
+                else if (!speechResult && !listening) {
+                    await closeVoice()
+                    return
                 }
             }
           }
@@ -219,7 +229,7 @@ export const VoicePanel: React.FC<VoicePanelProps> = ({
     const processSpeechTranscript = async () => {
         // stop voice if speech result is one of the stop cases - highest priority
         if (stopResponses.some(response => speechResult.includes(response)) || (speechResult.includes('no') && !action) || (speechResult.includes('back') && !action)) {
-            await closeVoice(true);
+            await closeVoice();
         }
         // when an action hasn't been initialized yet - discern which action is being requested
         else if (!action) {
@@ -231,7 +241,8 @@ export const VoicePanel: React.FC<VoicePanelProps> = ({
         }
         // handle unrecognized result
         else {
-            //await Spokestack.activate();
+            setResponse("Did not recognize command, please try again")
+            await Spokestack.activate();
         }
     }
 
@@ -256,7 +267,7 @@ export const VoicePanel: React.FC<VoicePanelProps> = ({
 
         // handle unrecognized result
         else {
-            await respond("Sorry, I don't recognize that command. Try again.")
+            setResponse("Did not recognize command, please try again")
         }
         await Spokestack.activate()
     }
@@ -266,7 +277,6 @@ export const VoicePanel: React.FC<VoicePanelProps> = ({
     const initiateAddLandmarkAction = async () => {
         setNewLandmark({longitude: userCoords.longitude, latitude: userCoords.latitude})
         setAction({actionType: "Add landmark", actionStep: 1})
-        await respond("Could you describe the landmark?")
         await Spokestack.activate();
     }   
 
@@ -274,10 +284,12 @@ export const VoicePanel: React.FC<VoicePanelProps> = ({
         if (landmarksNearby) {
             setAction(undefined)
             toggleNearbyLandmarksTrigger(true)
-            await closeVoice(false)   
+            await closeVoice()   
         }
+        // tell user there is no landmarks
         else {
-            await respond("No landmarks nearby")
+            setResponse("There are no landmarks nearby")
+            await Spokestack.activate();
         }
     }
 
@@ -294,7 +306,6 @@ export const VoicePanel: React.FC<VoicePanelProps> = ({
         }    
         // unrecognized speech
         else {
-            await respond("Sorry, I don't recognize that command. Try again.")
         }
 
         await Spokestack.activate();
@@ -325,7 +336,6 @@ export const VoicePanel: React.FC<VoicePanelProps> = ({
         else {
             setNewLandmark({...newLandmark, description: speechResult})
             setAction({...action, actionStep: action.actionStep + 1});
-            await respond("What type of landmark is it?")
         }
     }
 
@@ -336,38 +346,31 @@ export const VoicePanel: React.FC<VoicePanelProps> = ({
         const chosenType = typeLabels.filter(label => label.includes(speechResult.toLowerCase()))[0];
         if (speechResult == 'back') {
             setAction({...action, actionStep: action.actionStep - 1})
-            await respond("Could you describe the landmark?")
         }
-        else if (chosenType.length > 0) {
+        else if (chosenType?.length > 0) {
             const chosenTypeKey = parseInt(Object.keys(lmTypes).find(key => lmTypes[key].label == chosenType));
             // get corrected icon id by getting index of chosen icon type in labels array created above
             setNewLandmark({...newLandmark, landmark_type: chosenTypeKey, title: chosenType[0]})
             setAction({...action, actionStep: action.actionStep + 1});
-            await respond("Are you sure you want to add this landmark?")
         }
         else {
             setAction({...action, actionStep: action.actionStep});
-            await respond("Sorry. I don't recognize that type of landmark. Please try again.")
         }
     }
 
     const addLandmarkStep3 = async () => {
         if (speechResult == 'back') {
             setAction({...action, actionStep: action.actionStep - 1})
-            await respond("What type of landmark is it?")
         }
         else if (speechResult.includes("yes")) {
             setAction(undefined);
             await addLandmarkMutation.mutateAsync({landmarkValue: newLandmark});
-            await respond("Great, I've added the landmark. Anything else?")
         }
         else if (speechResult.includes("no")) {
             setAction(undefined);
-            await respond("Okay, I didn't add the landmark. Anything else?")
         }
         else {
             setAction({...action, actionStep: 3});
-            await respond("Okay, I didn't add the landmark. Anything else?")
         }
     }
     useEffect(() => {
@@ -393,28 +396,14 @@ export const VoicePanel: React.FC<VoicePanelProps> = ({
 
 // end action flows
     
-    const closeVoice = async (sayGoodbye: boolean) => {
+    const closeVoice = async () => {
+        await Spokestack.deactivate();
         setSpeechResult('')
         setPreviousResult('')
         setResponse('')
         setListening(false)
         setAction(undefined);
         toggleVoiceVisible(false)
-        if (sayGoodbye) {
-            await respond('Goodbye!')   
-        }
-        await Spokestack.deactivate();
-    }
-
-    const respond = async (responseSpeech: string) => {
-        await Spokestack.deactivate()
-        Speech.speak(responseSpeech)
-        while (true) {
-            if (!(await Speech.isSpeakingAsync())) {
-                break;
-            }
-        }
-        await Spokestack.activate()
     }
 
     const LmTypeDisplay: React.FC<{lmType: {image: ImageRequireSource, label:string}, style?: ViewStyle}> = ({lmType, style}) => {
@@ -511,7 +500,7 @@ export const VoicePanel: React.FC<VoicePanelProps> = ({
         return (
             <View style={[GlobalStyles.itemRowContainer, {width: '100%'}]} >
                 <Text style={{fontSize: 20, margin: 20, color: 'white', flexBasis: "70%"}}>{action ? action.actionType : "Waiting for command..."}</Text>
-                <TouchableOpacity style={{margin: 20}} onPress={async () => await closeVoice(true)}><FontAwesome size={20} color='white' name='times'/></TouchableOpacity>
+                <TouchableOpacity style={{margin: 20}} onPress={async () => await closeVoice()}><FontAwesome size={20} color='white' name='times'/></TouchableOpacity>
             </View>
         )
     }
@@ -523,7 +512,7 @@ export const VoicePanel: React.FC<VoicePanelProps> = ({
         useNativeDriver={true}
         useNativeDriverForBackdrop={true}
         avoidKeyboard={true}
-        onBackdropPress={async () => await closeVoice(true)}
+        onBackdropPress={async () => await closeVoice()}
         style={{justifyContent: "flex-end", height: '100%', margin: 0}}
         isVisible={voiceVisible} >
         <KeyboardAvoidingView>

+ 7 - 5
src/components/PhotoPicker.tsx

@@ -10,10 +10,12 @@ import { ImageInfo } from "expo-image-picker/build/ImagePicker.types"
 import React, { useEffect, useState } from "react"
 import {Alert, AlertButton, Linking, Platform, Text} from 'react-native'
 import { Menu, MenuOption, MenuOptions, MenuTrigger, renderers } from "react-native-popup-menu"
+import { usePermissions } from "../data/PermissionsContext"
 import { colors, getMediaPermissions } from "../utils/GlobalUtils"
 const {SlideInMenu} = renderers
 
 export const PhotoPicker: React.FC<{photoSourceMenuOpened: boolean, cancel: () => void, onBeforeLaunchPicker?: () => void, onReceivedPhotoResult?: (result: ImageInfo) => void, menuType: 'slideup' | 'alert', multiple: boolean}> = ({photoSourceMenuOpened, cancel, onBeforeLaunchPicker, onReceivedPhotoResult, menuType, multiple}) => {
+    const {mediaPermissionsGranted, checkMediaPermissions} = usePermissions()
 
     useEffect(() => {
         if (photoSourceMenuOpened && menuType ==  'alert') {
@@ -52,12 +54,12 @@ export const PhotoPicker: React.FC<{photoSourceMenuOpened: boolean, cancel: () =
         if (onBeforeLaunchPicker) {
             onBeforeLaunchPicker()
         }
-        
-        console.log("[Permissions]: Checking photo permissions...")
-        const granted = await getMediaPermissions();
-
+        let granted = mediaPermissionsGranted
+        if (!granted) {
+            granted = await checkMediaPermissions()
+        }
+    
         if (granted) {
-            console.log("[Permissions]: Photo permissions are granted")
             const imageOptions: ImagePicker.ImagePickerOptions = {
                 mediaTypes: ImagePicker.MediaTypeOptions.Images,
                 base64: true,

+ 1 - 0
src/components/Profile/Registration/RegistrationSteps/RegisterImage.tsx

@@ -95,6 +95,7 @@ const RegisterImage: React.FC<RegisterStepProps> = ({changeStep, formValues}) =>
         },
         authorized: false,
         errorMessage: 'There was an error registering your account. Please try again.',
+        loggingCategory: 'PROFILE'
       });
       if (response.status == 200) {
         resultMessage = 'Success! Congratulations on registering for your new account, you can now login!';  

+ 194 - 104
src/data/Auth/AuthContext.tsx

@@ -1,16 +1,17 @@
-import axios, { AxiosRequestConfig } from "axios"
+import axios, { AxiosRequestConfig, AxiosError } from "axios"
 import { loadAsync, makeRedirectUri, ResponseType } from "expo-auth-session"
 import { deleteItemAsync, getItemAsync, setItemAsync } from "expo-secure-store"
 import jwt_decode from 'jwt-decode'
-import React, { createContext, useContext, useEffect, useMemo, useState } from "react"
+import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from "react"
 import { useQueryClient } from "react-query"
-import { API_URL, reportAxiosError } from "../../utils/RequestUtils"
+import { API_URL } from "../../utils/RequestUtils"
 import { Landmark, } from "../landmarks"
 import { queryKeys } from "../query-keys"
 import {v4} from 'uuid'
 import { ErrorMessage } from "formik"
-import { Alert } from "react-native"
+import { Alert, AppState } from "react-native"
 import { navigate } from "../../navigation/RootNavigator"
+import { LogCategory, LOGGING } from "../../utils/logging"
 
 export const SECURESTORE_ACCESSTOKEN = "access"
 export const SECURESTORE_REFRESHTOKEN = "refresh"
@@ -26,18 +27,15 @@ interface AuthState {
     setRefreshTokenAsync: (token: string) => Promise<void>,
     setUserIdAsync: (id: string) => Promise<void>,
     clearAuthStorage: () => Promise<void>,
+    setAlert: (alert: GlobalAlert) => void,
     refreshToken: string,
     userId: string,
     anonUserId: string,
-    loading: boolean,
-    error: string,
-    setLoading: (state: boolean) => void,
-    setError: (error: string) => void,
+    authStateLoading: boolean,
+    setAuthStateLoading: (state: boolean) => void,
     sendApiRequestAsync: (config: RequestConfig) => Promise<any>,
-    login: () => Promise<void>,
+    login: () => Promise<AuthenticationResult>,
     logout: () => Promise<void>,
-    refreshAccessToken: () => Promise<void>,
-    getNotificationTokenFromServer: () => Promise<string>,
     landmarkOwnedByUser: (landmark: Landmark) => boolean,
 }
 
@@ -45,6 +43,7 @@ interface RequestConfig {
     axiosConfig: AxiosRequestConfig,
     authorized: boolean
     errorMessage: string
+    loggingCategory: LogCategory
 }
 
 export interface IdToken {
@@ -60,14 +59,17 @@ interface GlobalAlert {
     title: string
     message: string
     type: 'success' | 'error' | 'warning'
-    callback: () => void
+    callback?: () => void,
+    callbackButtonText?: string
 }
 
 const setStorageItem = async (key: string, value: string) => {
     if (value) {
+        LOGGING.log('SYSTEM', 'info', "Setting storage item for: " + key)
         await setItemAsync(key, value)
     }
     else {
+        LOGGING.log('SYSTEM', 'info', "Deleting storage item: " + key)
         await deleteItemAsync(key)
     }
 }
@@ -93,21 +95,31 @@ const AuthContext = createContext(null)
 });
 
 export const AuthContextProvider: React.FC = ({children}) => {
-    const [accessToken, setAccessToken] = useState<string>()
+    const [accessToken, _setAccessToken] = useState<string>()
     const [refreshToken, setRefreshToken] = useState<string>()
     const [notificationToken, setNotificationToken] = useState<string>()
     const [userId, setUserId] = useState<string>()
     const [anonUserId, setAnonUserId] = useState<string>()
-    const [loading, setLoading] = useState<boolean>(false)
+    const [authStateLoading, setAuthStateLoading] = useState<boolean>(false)
     const [alert, setAlert] = useState<GlobalAlert>()
 
+    const refreshingToken = useRef<boolean>(false)
+    const accessTokenRef = useRef<string>(accessToken)
+
+    const setAccessToken = (token: string) => {
+        accessTokenRef.current = token
+        _setAccessToken(token)
+    }
+
     const queryClient = useQueryClient()
 
     useEffect(() => {
         const loadAuthStateFromStorageOnAppLoad = async () => {
+            LOGGING.log("AUTH", 'info', "App started, loading existing auth state from storage...")
             const accessTokenFromStorage = await getItemAsync(SECURESTORE_ACCESSTOKEN)
 
             if (accessTokenFromStorage) {
+                LOGGING.log("AUTH", 'info', "Access token found in storage, testing if it is valid...")
                 try {
                     const response = await sendApiRequestAsync({
                         axiosConfig: {
@@ -116,23 +128,28 @@ export const AuthContextProvider: React.FC = ({children}) => {
                             headers: {Authorization: 'Bearer ' + accessTokenFromStorage}
                         }, 
                         authorized: false,
-                        errorMessage: 'Failed to retrieve user data from server'})
+                        errorMessage: 'Failed to retrieve user data from server',
+                        loggingCategory: 'AUTH'
+                    })
 
                     if (response.status == 200) {
                         setAccessToken(accessTokenFromStorage)
                         setRefreshToken(await getItemAsync(SECURESTORE_REFRESHTOKEN))
                         setNotificationToken(await getItemAsync(SECURESTORE_NOTIFTOKEN))
                         setUserId(await getItemAsync(SECURESTORE_ID))
+                        LOGGING.log("AUTH", 'info', "Access token is valid, auth state has been loaded...")
                         return
                     }
                 }
-                catch {}
+                catch {
+                }
             }
 
             await setAccessTokenAsync("")
             await setRefreshTokenAsync("")
             await setNotificationTokenAsync('')
             await setUserIdAsync('')
+            LOGGING.log("AUTH", 'info', "No auth state found in storage, starting with an empty auth state...")
             
             let anonUserId = await getItemAsync(SECURESTORE_ANONID)
             if (anonUserId) {
@@ -143,14 +160,53 @@ export const AuthContextProvider: React.FC = ({children}) => {
                 await setItemAsync(SECURESTORE_ANONID, anonUserId)
                 setAnonUserId(anonUserId)
             }
+            LOGGING.log("AUTH", 'info', "Created anonymous id for non account user: " + anonUserId)
         }
         loadAuthStateFromStorageOnAppLoad()
     }, [])
 
+    useEffect(() => {
+        const checkAuthStateOnAppForeground = async (state) => {
+            let currentAccessToken = accessTokenRef.current
+            if (currentAccessToken === undefined) {
+                currentAccessToken = await getItemAsync(SECURESTORE_ACCESSTOKEN)
+                console.log("access token found in storage:" + currentAccessToken) 
+            }
+
+            if (currentAccessToken && state == 'active') {
+                LOGGING.log("AUTH", 'info', "App was foregrounded and access token found, checking if it is still valid...")
+                try {   
+                    const response = await sendApiRequestAsync({
+                        axiosConfig: {
+                            method: 'GET',
+                            url: '/api/me/',
+                            headers: {Authorization: 'Bearer ' + accessTokenRef.current}
+                        }, 
+                        authorized: false,
+                        errorMessage: 'Failed to retrieve user data from server while checking access token',
+                        loggingCategory: 'AUTH'
+                    })
+                    if (response.status == 200) {
+                        LOGGING.log("AUTH", 'info', "Access token is valid, no action required")
+                        return
+                    }
+                } catch (error) {}
+            }
+        }
+
+        AppState.addEventListener('change', checkAuthStateOnAppForeground)
+
+        return () => {
+            AppState.removeEventListener('change', checkAuthStateOnAppForeground)
+        }
+    }, [])
+
     useEffect(() => {
         if (alert) {
+            LOGGING.log("SYSTEM", 'info', "Showing alert")
+            let buttons = [{text: alert.callbackButtonText ? alert.callbackButtonText : 'OK', onPress: alert.callback}, alert.callbackButtonText ? {text: 'Cancel'} : null]
             const alertTitle = alert.title
-            Alert.alert(alertTitle, alert.message, [{text: 'OK', onPress: alert.callback}])
+            Alert.alert(alertTitle, alert.message, buttons)
             setAlert(undefined)
         }
     }, [alert])
@@ -166,22 +222,22 @@ export const AuthContextProvider: React.FC = ({children}) => {
 
     const setAccessTokenAsync = async (token: string) => {
         setAccessToken(token)
-        setStorageItem(SECURESTORE_ACCESSTOKEN, token)
+        await setStorageItem(SECURESTORE_ACCESSTOKEN, token)
     }
 
     const setRefreshTokenAsync = async (token: string) => {
         setRefreshToken(token)
-        setStorageItem(SECURESTORE_REFRESHTOKEN, token)
+        await setStorageItem(SECURESTORE_REFRESHTOKEN, token)
     }
 
     const setUserIdAsync = async (id: string) => {
         setUserId(id)
-        setStorageItem(SECURESTORE_ID, id)
+        await setStorageItem(SECURESTORE_ID, id)
     }
 
     const setNotificationTokenAsync = async (token: string) => {
         setNotificationToken(token)
-        setStorageItem(SECURESTORE_NOTIFTOKEN, token)
+        await setStorageItem(SECURESTORE_NOTIFTOKEN, token)
     }
 
     const clearAuthStorage = async () => {
@@ -193,7 +249,7 @@ export const AuthContextProvider: React.FC = ({children}) => {
         ])
     }
 
-    const sendApiRequestAsync = async ({axiosConfig, authorized = false, errorMessage = 'An error occured'}: RequestConfig) => {
+    const sendApiRequestAsync = async ({axiosConfig, authorized = false, errorMessage = 'An error occured', loggingCategory = "SYSTEM"}: RequestConfig) => {
         if (authorized && !axiosConfig?.headers?.Authorization) {
             axiosConfig.headers = {
                 ...axiosConfig.headers,
@@ -206,78 +262,92 @@ export const AuthContextProvider: React.FC = ({children}) => {
         try {
             return await axios(axiosConfig)
         } catch (error) {
-            console.log(error.response.request._headers)
-            reportAxiosError(errorMessage, error)
+            const axiosError = error as AxiosError
+            if (axiosError.response.status == 401 || axiosError.response.status == 403) {
+                if (!refreshingToken.current) {
+                    await refreshAccessToken()
+                }
+            }
+            LOGGING.log("AUTH", 'error', errorMessage)
         }
     }
 
     const login = async (): Promise<AuthenticationResult> => {
-        setLoading(true)
-        console.log('[Authentication]: User is attempting to login, opening web portal login...')
-    
-        // initiate authentication request to the server 
-        const request = await loadAsync({
-            clientId: "atlas.mobile",
-            responseType: ResponseType.Code,
-            redirectUri,
-            usePKCE: true,
-            scopes: ['openid'],
-            
-        }, discovery)  
-    
-        // handle authentication response from the server
-        const response = await request.promptAsync(discovery);
-    
-        // if succesful, prepare a request for an access/id token
-        if (response.type == "success" && request.codeVerifier) {
-            console.log('[Authentication]: User authentication was successful.')
-            const tokenData = new URLSearchParams();
-            tokenData.append('grant_type', 'authorization_code');
-            tokenData.append('client_id', 'atlas.mobile');
-            tokenData.append('code', response.params.code);
-            tokenData.append('redirect_uri', request.redirectUri);
-            tokenData.append('code_verifier', request.codeVerifier);  
-            console.log('[Authentication]: Attempting to retrieve access token...')
-    
-            // send the token request
-            try {
-                const response = await axios.post(API_URL + `/o/token/`, tokenData, {
-                    headers: {
-                        'Content-Type': "application/x-www-form-urlencoded"
-                    },
-                });  
-    
-                const tokenResponse = response.data;
-    
-                // if its a successful response, decode the jwt id token, and store the tokens in the corresponding stores
-                const idToken = jwt_decode(tokenResponse.id_token) as IdToken;
-
-                setLoading(false)                
-    
-                await setAccessTokenAsync(tokenResponse.access_token);
-                await setRefreshTokenAsync(tokenResponse.refresh_token);
-                await setUserIdAsync(idToken.sub)
-
-                console.log('[Authentication]: Tokens successfully retrieved.')
+        try {
+            setAuthStateLoading(true)
+            LOGGING.log('AUTH', 'info', "Starting login process...")
+        
+            // initiate authentication request to the server 
+            const request = await loadAsync({
+                clientId: "atlas.mobile",
+                responseType: ResponseType.Code,
+                redirectUri,
+                usePKCE: true,
+                scopes: ['openid'],
                 
-    
-                return {success: true}
-            } catch (error) {
-                reportAxiosError("Something went wrong when retrieving access token", error);
-                setLoading(false)
-                setAlert({title:"Error", message: "Something went wrong when retrieving access token", callback: () => {}, type: 'error'})
-            } 
-        }
-        else if (response.type == "cancel") {
-            setLoading(false)
+            }, discovery)  
+        
+            // handle authentication response from the server
+            LOGGING.log('AUTH', 'info', "Prompting user with web browser...")
+            const response = await request.promptAsync(discovery);
+        
+            // if succesful, prepare a request for an access/id token
+            if (response.type == "success" && request.codeVerifier) {
+                const tokenData = new URLSearchParams();
+                tokenData.append('grant_type', 'authorization_code');
+                tokenData.append('client_id', 'atlas.mobile');
+                tokenData.append('code', response.params.code);
+                tokenData.append('redirect_uri', request.redirectUri);
+                tokenData.append('code_verifier', request.codeVerifier);  
+                LOGGING.log('AUTH', 'info', "User successfully authenticated, attempting to get access token...")
+        
+                // send the token request
+                try {
+                    const response = await axios.post(API_URL + `/o/token/`, tokenData, {
+                        headers: {
+                            'Content-Type': "application/x-www-form-urlencoded"
+                        },
+                    });  
+        
+                    const tokenResponse = response.data;
+        
+                    // if its a successful response, decode the jwt id token, and store the tokens in the corresponding stores
+                    const idToken = jwt_decode(tokenResponse.id_token) as IdToken;
+
+                    setAuthStateLoading(false)                
+        
+                    await setAccessTokenAsync(tokenResponse.access_token);
+                    await setRefreshTokenAsync(tokenResponse.refresh_token);
+                    await setUserIdAsync(idToken.sub)
+                    
+                    LOGGING.log('AUTH', 'info', "Successfully retrieved access token, login process completed")
+        
+                    return {success: true}
+                } catch (error) {
+                    LOGGING.log('AUTH', 'error', "An error occured when trying to retrieve access token: " + error)
+                    setAuthStateLoading(false)
+                } 
+            }
+            else if (response.type == "cancel") {
+                LOGGING.log('AUTH', 'info', "User canceled login")
+                setAuthStateLoading(false)
+            }
+            else {
+                LOGGING.log('AUTH', 'error', "An error occured when trying to authenticate user: " + response.type)
+                setAlert({title: 'Login error', message: "Something went wrong while logging in. Please try again.", callback: () => {}, type: 'error'})
+            }
         }
-        else {
-            setAlert({title: 'Error', message: "Something went wrong while logging in. Please try again.", callback: () => {}, type: 'error'})
+        catch (error) {
+            LOGGING.log('AUTH', 'error', "An error occured when trying to authenticate user: " + error)
+            setAlert({title: 'Login error', message: "Something went wrong while logging in. Please try again.", callback: () => {}, type: 'error'})
         }
+
+        return {success: false}
     } 
 
     const logout = async () => {
-        setLoading(true)
+        LOGGING.log('AUTH', 'info', "Starting logout process...")
+        setAuthStateLoading(true)
         try {
             const tokenParams = new URLSearchParams();
             tokenParams.append('client_id', 'atlas.mobile');
@@ -292,16 +362,18 @@ export const AuthContextProvider: React.FC = ({children}) => {
             queryClient.setQueryData(queryKeys.getOwnedProfile, null)
             await setAnonUserId(await getItemAsync(SECURESTORE_ANONID))
             await clearAuthStorage()
+            LOGGING.log('AUTH', 'info', "Successfully logged out. Local data has been cleared, and the device's anonymous id has been reloaded.")
 
         } catch (error) {
-            reportAxiosError("Something went wrong when logging out", error);
+            LOGGING.log('AUTH', 'info', "Something when wrong while logging out: " + error)
             setAlert({title: 'Error', message: "Something went wrong while logging out. Please try again.", callback: () => {}, type: 'error'})
         } 
-        setLoading(false)
+        setAuthStateLoading(false)
     }
 
     const refreshAccessToken = async () => {
-        setLoading(true)
+        let success = true;
+        refreshingToken.current = true
         let currentRefreshToken = refreshToken
         if (!currentRefreshToken) {
             currentRefreshToken = await getItemAsync(SECURESTORE_REFRESHTOKEN);
@@ -323,41 +395,58 @@ export const AuthContextProvider: React.FC = ({children}) => {
 
                 console.info('Successfully refreshed access token.')
 
+                setAlert({title: 'Login timeout', message: "It looks like you've been away for awhile! We were able to log you back in.", callback: () => {}, type: 'success'})
             }
             catch (error) {
-                reportAxiosError("[Authentication]: Error when trying to refresh access token", error);            
+                setAlert({
+                    title: 'Login timeout', 
+                    message: "It looks like you've been away for awhile! Unfortunately we weren't able to log you back in, you'll have to do that manually.", 
+                    callback: () => {navigate('Account')},
+                    type: 'error',
+                    callbackButtonText: "Go to login"
+                })
+                success = false
             }
         }
 
-        setLoading(false)
+        refreshingToken.current = false
+        return success
     }
 
     const landmarkOwnedByUser = (landmark: Landmark) => {
-        const owned = landmark?.user == userId || landmark?.anonymous == anonUserId
+        console.log(`owned by logged in user: ${landmark?.user == userId}`)
+        console.log(`owned by anon user: ${landmark?.anonymous == anonUserId}`)
+        const owned = landmark?.user && landmark?.user == userId || landmark?.anonymous && landmark?.anonymous == anonUserId
         return owned
     }
 
     const convertExistingAnonymousLandmarks = async () => {
         try {
+            LOGGING.log("AUTH", 'info', "Checking server to see if new logged in user has any anonymous landmarks...")
             const response = await sendApiRequestAsync({
                 axiosConfig: {
                     method: 'GET',
                     url: `/api/landmarks/anon/${anonUserId}/`
                 },
                 authorized: true,
-                errorMessage: 'An error occured while checking for anonymous landmarks'
+                errorMessage: 'An error occured while checking for anonymous landmarks',
+                loggingCategory: "AUTH",
             })
     
             if (response?.data?.has_landmark) {
                 // send request to convert landarks
+                LOGGING.log("AUTH", 'info', "Anonymous landmarks found belonging to user. Converting to owned landmarks...")
                 await sendApiRequestAsync({
                     axiosConfig: {
                         method: 'POST',
                         url: `/api/landmarks/convert/${anonUserId}/`
                     },
                     authorized: true,
-                    errorMessage: 'Something went wrong when converting anonymous landmarks'
+                    errorMessage: 'Something went wrong when converting anonymous landmarks',
+                    loggingCategory: "AUTH",
                 })
+
+                LOGGING.log("AUTH", 'info', "Successfully converted anonymous landmarks to owned landmarks.")
                 
                 setAlert({
                     title: 'Heads up',
@@ -369,18 +458,20 @@ export const AuthContextProvider: React.FC = ({children}) => {
 
             setAnonUserId('')
         } catch (error) {
-            reportAxiosError("[Authentication]: Error when checking for anonymous landmarks", error);
-            setAlert({
-                title: 'Error',
-                message: "Failed to convert your old anonymous landmarks to your account. Please try again manually by going to Account -> Information -> Transfer anonymous landmarks.",
-                callback: () => navigate("Account"),
-                type: 'error'
-            })
+            LOGGING.log("AUTH", 'error', "An error occured while converting anonymous landmarks: " + error)
+
+            // TODO: implement transfer anonymus landmarks in accounts
+            // setAlert({
+            //     title: 'Error',
+            //     message: "Failed to convert your old anonymous landmarks to your account. Please try again manually by going to Account -> Information -> Transfer anonymous landmarks.",
+            //     callback: () => navigate("Account"),
+            //     type: 'error'
+            // })
             return false
         }
     }
 
-    const authState = useMemo(() => ({
+    const authState = useMemo<AuthState>(() => ({
         accessToken,
         notificationToken,
         setNotificationTokenAsync,
@@ -391,15 +482,14 @@ export const AuthContextProvider: React.FC = ({children}) => {
         landmarkOwnedByUser,
         refreshToken,
         userId,
-        loading,
+        authStateLoading,
         anonUserId,
-        setLoading,
+        setAuthStateLoading,
         setAlert,
         sendApiRequestAsync,
         login,
         logout,
-        refreshAccessToken,
-    }), [accessToken, refreshToken, userId, loading, anonUserId])
+    }), [accessToken, refreshToken, userId, authStateLoading, anonUserId])
 
     return (
         <AuthContext.Provider value={authState}>

+ 215 - 0
src/data/PermissionsContext.tsx

@@ -0,0 +1,215 @@
+import Constants from "expo-constants"
+import React, { createContext, useContext, useState, useMemo, useEffect } from "react"
+import { Platform } from "react-native"
+import { checkMultiple, openSettings, Permission, PERMISSIONS, request, requestMultiple, RESULTS } from "react-native-permissions"
+import * as Notifications from 'expo-notifications'
+import { useAuth } from "./Auth/AuthContext"
+
+interface PermissionsState {
+    voicePermissionsGranted: boolean,
+    checkVoicePermissions: () => Promise<boolean>,
+    locationPermissionsGranted: boolean,
+    checkLocationPermissions: () => Promise<boolean>,
+    mediaPermissionsGranted: boolean,
+    checkMediaPermissions: () => Promise<boolean>,
+    notificationPermissionsGranted: boolean,
+    permissionsLoading: boolean,
+    
+}
+
+export type PermissionType = 'voice' | 'location' | 'media' | 'notification'
+
+const PermissionsContext = createContext(null)
+
+const permissionsMap = {
+    android: {
+        voice: [PERMISSIONS.ANDROID.RECORD_AUDIO],
+        location: [PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION],
+        media: [PERMISSIONS.ANDROID.CAMERA, PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE,PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE],
+        friendly: {
+            voice: ['Microphone'],
+            location: ['Location'],
+            media: ['Camera', "Storage"],
+        }
+    },
+    ios: {
+        voice: [PERMISSIONS.IOS.MICROPHONE, PERMISSIONS.IOS.SPEECH_RECOGNITION],
+        location: [PERMISSIONS.IOS.LOCATION_ALWAYS, PERMISSIONS.IOS.LOCATION_WHEN_IN_USE],
+        media: [PERMISSIONS.IOS.CAMERA, PERMISSIONS.IOS.PHOTO_LIBRARY],
+        friendly: {
+            voice: ['Microphone', 'Speech Recognition'],
+            location: ['Location'],
+            media: ['Camera', 'Storage']
+        }
+    }
+}
+
+
+export const PermissionsContextProvider: React.FC = ({children}) => {
+    const {setAlert} = useAuth()
+
+    const [voicePermissionsGranted, setVoicePermissions] = useState(false)
+    const [locationPermissionsGranted, setLocationPermissions] = useState(false)
+    const [mediaPermissionsGranted, setMediaPermissions] = useState(false)
+    const [notificationPermissionsGranted, setNotificiationPermissions] = useState(false)
+    const [permissionsLoading, setPermissionsLoading] = useState(false)
+    const [permissionsFinishedLoading, setPermissionsFinisedLoading] = useState(false)
+
+    const getPermissions = async (permission: PermissionType, setPermission: (state: boolean) => void) => {
+        try {
+            const permissions = permissionsMap[Platform.OS][permission]
+            const checkResult = await checkMultiple(permissions)
+            const requestResult = await requestMultiple(permissions.filter(p => checkResult[p] !== RESULTS.GRANTED))
+            let granted = true;
+
+            granted = Object.keys(requestResult).reduce((acc, permission) => acc && requestResult[permission] == RESULTS.GRANTED, granted)
+
+            setPermission(granted)
+            console.log(`${permission} permissions granted: ${granted}`)
+
+            return granted
+        } catch (error) {
+            console.log('[Permissions]: An error occured when requesting permissions: ' + error)
+            return false
+        }
+    }
+
+    const checkLocationPermissions = async () => {
+        try {
+            return await getPermissions('location', setLocationPermissions);
+        } catch (error) {
+            console.log('[Permissions]: An error occured when getting voice permissions: ' + error)
+            return false
+        }
+    }
+
+    const checkMediaPermissions = async () => {
+        try {
+            return await getPermissions('media', setMediaPermissions);
+        } catch (error) {
+            console.log('[Permissions]: An error occured when getting media permissions: ' + error)
+            return false
+        }
+    }
+
+    const checkVoicePermissions = async () => {
+        try {
+            return await getPermissions('voice', setVoicePermissions);
+        } catch (error) {
+            console.log('[Permissions]: An error occured when getting voice permissions: ' + error)
+            return false
+        }
+    }
+
+    const checkNotificationPermissions = async () => {
+        try {
+            if (Constants.isDevice) {
+                console.log('checking notification permissions')
+                const { status: existingStatus } = await Notifications.getPermissionsAsync();
+                let finalStatus = existingStatus;
+                if (existingStatus !== 'granted') {
+                    const { status } = await Notifications.requestPermissionsAsync();
+                    console.log("notification status: " +  status)
+                    finalStatus = status;
+                }
+                
+                return finalStatus === 'granted';
+            } 
+            else {
+                console.warn('[Notifcations]: A physical device must be used for push notifications');
+            }
+        }
+        catch {
+
+        }
+    }
+
+    const createAlertForPermission = (permission: PermissionType) => {
+        let permissionsMessage = ''
+        permissionsMap[Platform.OS]['friendly'][permission].forEach((permissionLabel: string) => {
+            permissionsMessage += `\n - ${permissionLabel}`
+        })
+
+        return permissionsMessage
+    }
+
+    const maybeShowPermissionsAlert = ({
+        voicePermissionsGranted, 
+        locationPermissionsGranted, 
+        notificationPermissionsGranted}) => {
+        let permissionsMessage = ''
+        if (!voicePermissionsGranted) {
+            permissionsMessage += createAlertForPermission('voice')
+        }
+        if (!locationPermissionsGranted) {
+            permissionsMessage += createAlertForPermission('location')
+        }
+        if (!notificationPermissionsGranted) {
+            permissionsMessage += `\n - Notifications`
+        }
+        if (permissionsMessage) {
+            setAlert({
+                title: 'Missing permissions',
+                message: `You are missing some permissions which will limit features of the app: ${permissionsMessage} \n\nGo to settings and grant the permissions to gain access to these features`,                        
+                type: 'warning',
+                callback: () => openSettings(),
+                callbackButtonText: 'Go to settings'
+            })
+        }
+    };
+
+    useEffect(() => {
+        let isMounted = true
+        const checkPermissionsOnLaunch = async () => {
+            console.log('[Permissions]: Checking permissions on launch')
+            const locationPermissionsGranted = await checkLocationPermissions()
+            const voicePermissionsGranted = await checkVoicePermissions()
+            const notificationPermissionsGranted = await checkNotificationPermissions()
+            console.log('[Permissions]: Permissions checked on launch')
+            return {
+                voicePermissionsGranted: voicePermissionsGranted,
+                locationPermissionsGranted: locationPermissionsGranted,
+                notificationPermissionsGranted: notificationPermissionsGranted
+            }
+        }
+        if (isMounted) {
+            setPermissionsLoading(true)
+            checkPermissionsOnLaunch().then(result => {
+                setPermissionsLoading(false)
+                setTimeout(() => {
+                    maybeShowPermissionsAlert(result);
+                }, 2000);
+            })
+    
+        }
+
+        return () => {
+            isMounted = false
+        }
+    }, [])
+    
+    const permissionsState = useMemo<PermissionsState>(() => ({
+        voicePermissionsGranted,
+        checkVoicePermissions,
+        locationPermissionsGranted,
+        checkLocationPermissions,
+        mediaPermissionsGranted,
+        checkMediaPermissions,
+        notificationPermissionsGranted,
+        permissionsLoading,
+    }), [voicePermissionsGranted, locationPermissionsGranted, mediaPermissionsGranted, notificationPermissionsGranted, permissionsLoading])
+
+    return (
+        <PermissionsContext.Provider value={permissionsState}>
+            {children}
+        </PermissionsContext.Provider>
+    )
+}
+
+export const usePermissions = () => {
+    const context = useContext<PermissionsState>(PermissionsContext)
+    if (context === null) {
+        throw new Error("usePermissions must be used within a PermissionsContextProvider")
+    }
+    return context
+}

+ 0 - 1
src/data/axios.ts

@@ -1 +0,0 @@
-}

+ 10 - 7
src/data/comments.ts

@@ -5,10 +5,9 @@
  * <dev@clicknpush.ca>, January 2022
  */
 
-import axios, { AxiosRequestConfig } from "axios";
-import { QueryClient, useMutation, useQuery, useQueryClient } from "react-query";
+import { useMutation, useQuery, useQueryClient } from "react-query";
+import { API_URL } from "../utils/RequestUtils";
 import { useAuth } from "./Auth/AuthContext";
-import { API_URL, reportAxiosError } from "../utils/RequestUtils";
 import { queryKeys } from "./query-keys";
 
 /**
@@ -65,7 +64,8 @@ export const useLandmarkComments = (landmarkId: string) => {
                         url: `${API_URL}/api/comments/${landmarkId}`
                     },
                     authorized: false,
-                    errorMessage: 'Something went wrong when retrieving comments'
+                    errorMessage: 'Something went wrong when retrieving comments',
+                    loggingCategory: 'COMMENTS',
                 });
                 return response?.data?.reverse();
         }
@@ -93,7 +93,8 @@ export const useAddComment = () => {
                     url: `/api/comments/`,
                 },
                 authorized: true,
-                errorMessage: 'Something went wrong when creating a comment'
+                errorMessage: 'Something went wrong when creating a comment',
+                loggingCategory: 'COMMENTS',
             })
             response?.data
         }
@@ -119,7 +120,8 @@ export const useEditComment = () => {
                     url: `/api/comments/`,
                 },
                 authorized: true,
-                errorMessage: 'Something went wrong when editing a comment'
+                errorMessage: 'Something went wrong when editing a comment',
+                loggingCategory: 'COMMENTS',
             })
             return response?.data;
         }
@@ -143,7 +145,8 @@ export const useDeleteComment = () => {
                     url: API_URL + `/api/comments/${id}`,
                 },
                 authorized: true,
-                errorMessage: 'Something went wrong when deleting a comment'
+                errorMessage: 'Something went wrong when deleting a comment',
+                loggingCategory: 'COMMENTS',
             });   
             return response?.data;
         }

+ 20 - 11
src/data/landmarks.ts

@@ -75,7 +75,8 @@ export const useLandmarks = () => {
                 url: `/api/landmarks/`,
             }, 
             authorized: false,
-            errorMessage: 'Something went wrong when retrieving landmarks'
+            errorMessage: 'Something went wrong when retrieving landmarks',
+            loggingCategory: 'LANDMARKS'
         });   
         return response?.data
     }
@@ -90,18 +91,20 @@ export const useLandmarks = () => {
 }
 
 export const useLandmark = (landmarkId: string) => {
-    const {sendApiRequestAsync} = useAuth()
+    const {sendApiRequestAsync, userId} = useAuth()
     const queryClient = useQueryClient();
 
      const getLandmark = async (landmarkId?: string) => {
+        console.log(userId)
         if (landmarkId) {
             const response = await sendApiRequestAsync({
                 axiosConfig: {
                     method: 'GET',
-                    url: `/api/landmark/${landmarkId}`,
+                    url: `/api/landmark/${landmarkId}/`,
                 },
-                authorized: false,
-                errorMessage: 'Something went wrong when retrieving the landmark'
+                authorized: !!userId,
+                errorMessage: 'Something went wrong when retrieving the landmark',
+                loggingCategory: 'LANDMARKS'
             });   
             return response?.data 
         }
@@ -142,7 +145,8 @@ export const useAddLandmark = () => {
                     },
                 },
                 authorized: true,
-                errorMessage: "Something went wrong when creating a landmark"
+                errorMessage: "Something went wrong when creating a landmark",
+                loggingCategory: "LANDMARKS"
             });   
             return response?.data;
         }
@@ -179,7 +183,8 @@ export const useEditLandmark = () => {
             const response = await sendApiRequestAsync({
                 axiosConfig: config, 
                 authorized: true,
-                errorMessage: 'Something went wrong when updating a landmark'
+                errorMessage: 'Something went wrong when updating a landmark',
+                loggingCategory: 'LANDMARKS'
             });   
             return response?.data;
         }
@@ -207,7 +212,8 @@ export const useRateLandmark = () => {
                     url: `/api/landmark/rate/`,
                 },
                 authorized: true,
-                errorMessage: 'Something went wrong when rating a landmark'
+                errorMessage: 'Something went wrong when rating a landmark',
+                loggingCategory: 'LANDMARKS'
             });   
             return response?.data?.rating;
         }
@@ -239,7 +245,8 @@ export const useDeleteLandmark = () => {
             const response = await sendApiRequestAsync({
                 axiosConfig: config,
                 authorized: true,
-                errorMessage: 'Something went wrong when deleting a landmark'
+                errorMessage: 'Something went wrong when deleting a landmark',
+                loggingCategory: 'LANDMARKS'
             });   
             return response?.data;
         }
@@ -270,7 +277,8 @@ export const useAddLandmarkPhoto = () => {
             const response = await sendApiRequestAsync({
                 axiosConfig: config, 
                 authorized: true,
-                errorMessage: 'Something went wrong when adding landmark photo'
+                errorMessage: 'Something went wrong when adding landmark photo',
+                loggingCategory: 'LANDMARKS'
             });   
             return response?.data
         }
@@ -307,7 +315,8 @@ export const useDeleteLandmarkPhoto = () => {
             const response = await sendApiRequestAsync({
                 axiosConfig: config,
                 authorized: true,
-                errorMessage: 'Something went wrong when deleting landmark photos'
+                errorMessage: 'Something went wrong when deleting landmark photos',
+                loggingCategory: 'LANDMARKS'
             });   
             return response?.data
         }

+ 31 - 34
src/data/notifications.ts

@@ -7,6 +7,8 @@ import { getItemAsync } from "expo-secure-store";
 import { AxiosRequestConfig } from "axios";
 import { Platform } from "react-native";
 import { navigate } from "../navigation/RootNavigator";
+import { usePermissions } from "./PermissionsContext";
+import { LOGGING } from "../utils/logging";
 
 export interface UserNotification {
     id: string
@@ -22,6 +24,7 @@ export const useNotificationToken = () => {
     const { sendApiRequestAsync, userId, accessToken } = useAuth();
 
     const ensureNotificationTokenExistsOnServer = async (notificationToken: string, retries: number) => {
+        LOGGING.log('NOTIFICATIONS', 'info', 'Checking if notification token exists on server, attempt: ' + retries);
         if (retries > 0) {
             try {
                 const response = await sendApiRequestAsync({
@@ -31,15 +34,19 @@ export const useNotificationToken = () => {
                         url: `/api/notif-token/${userId}/`,
                     },
                     authorized: true,
-                    errorMessage: 'Something went wrong when checking notification on server'
+                    errorMessage: 'Something went wrong when checking notification on server',
+                    loggingCategory: 'NOTIFICATIONS',
                 });   
-                return response.data;
+                if (response.status === 200) {
+                    LOGGING.log('NOTIFICATIONS', 'info', 'Found notification token on server');
+                    return response.data;
+                }
             } catch (error) {}
     
             await ensureNotificationTokenExistsOnServer(notificationToken, retries - 1)   
         }
         else {
-            throw new Error("Could not validate notification token on server");
+            LOGGING.log('NOTIFICATIONS', 'error', 'Exhausted all error retries when checking notification token on server');
         }
     }
     
@@ -61,7 +68,8 @@ export const useNotificationToken = () => {
                     url: `/api/notif-token/${userId}/`,
                 },
                 authorized: true,
-                errorMessage: 'Something went wrong when retrieving notification token'
+                errorMessage: 'Something went wrong when retrieving notification token',
+                loggingCategory: 'NOTIFICATIONS',
             });   
             
             if (response?.data) {
@@ -91,7 +99,8 @@ export const useNotifications = () => {
                 url: `/api/user-profile/notifications/${userId}/`,
             }, 
             authorized: true,
-            errorMessage: 'Something went wrong when retrieving notifications'});   
+            errorMessage: 'Something went wrong when retrieving notifications',
+            loggingCategory: 'NOTIFICATIONS',});   
         return response?.data   
     }
 
@@ -111,7 +120,8 @@ export const useMarkNotificationRead = () => {
                 url: `/api/user-profile/notifications/mark-read/${notificationId}/`,
             }, 
             authorized: true,
-            errorMessage: 'Something went wrong when marking notification as read'});   
+            errorMessage: 'Something went wrong when marking notification as read',
+            loggingCategory: 'NOTIFICATIONS',});   
         return response.data;
     }
 
@@ -133,7 +143,8 @@ export const useDeleteNotification = () => {
                     url: `/api/user-profile/notifications/delete/${notificationId}/`
                 },
                 authorized: true,
-                errorMessage: 'Something went wrong when removing notification'
+                errorMessage: 'Something went wrong when removing notification',
+                loggingCategory: 'NOTIFICATIONS'
             });
             return response.data;
         }
@@ -147,41 +158,27 @@ export const useDeleteNotification = () => {
 
 export const useRegisterNotifications = () => {
     const {setNotificationTokenAsync} = useAuth();
+    const {notificationPermissionsGranted} = usePermissions();
     const token = useNotificationToken()
     const {refetch: refetchNotifications} = useNotifications()
     const {mutateAsync: markNotificationRead} = useMarkNotificationRead()
     
-
-
     const registerNotificationsAsync = async () => {
-        if (Constants.isDevice) {
-            const { status: existingStatus } = await Notifications.getPermissionsAsync();
-            let finalStatus = existingStatus;
-            if (existingStatus !== 'granted') {
-                const { status } = await Notifications.requestPermissionsAsync();
-                finalStatus = status;
-            }
-            if (finalStatus !== 'granted') {
-                alert('Failed to get push token for push notification!');
-                return null;
-            }
-
+        if (notificationPermissionsGranted) {
+            if (Platform.OS === 'android') {
+                Notifications.setNotificationChannelAsync('default', {
+                    name: 'default',
+                    importance: Notifications.AndroidImportance.MAX,
+                    vibrationPattern: [0, 250, 250, 250],
+                    lightColor: '#FF231F7C',
+                });
+            }   
             return token
-        } 
+        }
         else {
-            console.warn('[Notifcations]: A physical device must be used for push notifications');
+            console.log('Notification permissions not granted')
+            return null
         }
-        
-        if (Platform.OS === 'android') {
-            Notifications.setNotificationChannelAsync('default', {
-                name: 'default',
-                importance: Notifications.AndroidImportance.MAX,
-                vibrationPattern: [0, 250, 250, 250],
-                lightColor: '#FF231F7C',
-            });
-        }   
-
-        return token
     };
 
     const handleNotificationInteraction = async (notifData: any) => {

+ 13 - 9
src/data/profiles.ts

@@ -5,11 +5,10 @@
  * <dev@clicknpush.ca>, January 2022
  */
 
-import axios, { AxiosRequestConfig } from "axios";
-import { QueryClient, useMutation, useQuery, useQueryClient } from "react-query";
-import { useAuth } from "./Auth/AuthContext";
+import { useMutation, useQuery, useQueryClient } from "react-query";
 import { RegisterCredsValues } from "../utils/RegistrationUtils";
-import { API_URL, reportAxiosError } from "../utils/RequestUtils";
+import { API_URL } from "../utils/RequestUtils";
+import { useAuth } from "./Auth/AuthContext";
 import { queryKeys } from "./query-keys";
 
 /**
@@ -54,7 +53,8 @@ export const useOwnedProfile = () => {
                 url: `/api/user-profile/${userId}/`,
             }, 
             authorized: true,
-            errorMessage: 'Something went wrong when retrieving user profile'
+            errorMessage: 'Something went wrong when retrieving user profile',
+            loggingCategory: 'PROFILE',
         });   
         return response.data;
     }
@@ -87,7 +87,8 @@ export const useEditProfile = () => {
                 data: values,
             },
             authorized: true,
-            errorMessage: 'Something went wrong when editing profile'
+            errorMessage: 'Something went wrong when editing profile',
+            loggingCategory: 'PROFILE',
         });   
         return response.data;
     }
@@ -109,7 +110,8 @@ export const useChangePassword = () => {
                     data: {password: password},
                 }, 
                 authorized: true,
-                errorMessage: 'Something went wrong when changing password'
+                errorMessage: 'Something went wrong when changing password',
+                loggingCategory: 'PROFILE',
             });   
             return response.data;
         }
@@ -135,7 +137,8 @@ export const useDeleteProfile = () => {
                     url: API_URL + `/api/user-profile/${userId}/`,
                 },
                 authorized: true,
-                errorMessage: 'Something went wrong when deleting profile'
+                errorMessage: 'Something went wrong when deleting profile',
+                loggingCategory: 'PROFILE',
             });   
             
             if (response.status == 200) {
@@ -188,7 +191,8 @@ export const useToggleTips = () => {
                     url: `/api/user-profile/toggle-tips/${userId}/`,
                 },
                 authorized: true,
-                errorMessage: 'Something went wrong when toggling tips'
+                errorMessage: 'Something went wrong when toggling tips',
+                loggingCategory: 'PROFILE',
             });   
             return response.data;
         }

+ 3 - 1
src/navigation/MainTabsNavigator.tsx

@@ -16,6 +16,7 @@ import { Feed } from '../components/Feed/Feed';
 import ProfileTemplate from '../components/Profile/ProfileTemplate';
 import { useAuth } from '../data/Auth/AuthContext';
 import { useMarkNotificationRead, useNotifications, useRegisterNotifications } from '../data/notifications';
+import { usePermissions } from '../data/PermissionsContext';
 import { useOwnedProfile, useToggleTips } from '../data/profiles';
 import { colors } from "../utils/GlobalUtils";
 import MapNavigator from './MapNavigator';
@@ -59,6 +60,7 @@ export type MainTabsNavigationProp = BottomTabNavigationProp<MainTabsParamList>
 const MainTabsNavigator: React.FC<{navigation}> = ({navigation}) => {
     const {profile} = useOwnedProfile()
     const {userId} = useAuth()
+    const {notificationPermissionsGranted} = usePermissions()
     const toggleTips = useToggleTips()
     const notificationsQuery = useNotifications()
     const {subscribeToNotifications, handleNotificationInteraction} = useRegisterNotifications()
@@ -89,7 +91,7 @@ const MainTabsNavigator: React.FC<{navigation}> = ({navigation}) => {
          * @memberOf Atlas
          */
         subscribeToNotifications()
-    }, [userId]);
+    }, [userId, notificationPermissionsGranted]);
 
     const getIconSize = (focused: boolean): number => {
         if (focused) {

+ 1 - 50
src/utils/GlobalUtils.ts

@@ -127,53 +127,4 @@ export const getMediaPermissions = async () => {
     catch (error) { 
         console.log('[Permissions]: An error occured when getting media permissions: ' + error)
     }
-}
-
-export const getMapPermissions = async () => {
-    try {
-        console.log('[Permissions]: Getting permissions for map')
-        let permissions = []
-        if (Platform.OS == "android") {  
-        permissions = [
-            PERMISSIONS.ANDROID.ACCESS_COARSE_LOCATION,
-            PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION,
-            PERMISSIONS.ANDROID.RECORD_AUDIO,
-        ]
-        }
-    
-        else if (Platform.OS == 'ios') {
-        permissions = [
-            PERMISSIONS.IOS.LOCATION_ALWAYS,
-            PERMISSIONS.IOS.LOCATION_WHEN_IN_USE,
-            PERMISSIONS.IOS.SPEECH_RECOGNITION,
-            PERMISSIONS.IOS.MICROPHONE,
-        ]
-        }
-        
-        try {
-        const permissionsResult = await checkMultiple(permissions)
-        const ungrantedPermissions = permissions.filter(permission => permissionsResult[permission] != RESULTS.GRANTED);
-        requestMultiple(ungrantedPermissions)
-        }
-        catch (error) { 
-        console.log('[Permissions]: An error occured when getting map permissions: ' + error)
-        }
-    }
-    catch (error) {
-        console.log('[Permissions]:An error occured when getting map permissions: ' + error)
-    }
-}
-
-export const md5ToUUID = (md5Hash: string) => {
-    return (
-        md5Hash.substring(0, 8) +
-        '-' +
-        md5Hash.substring(8, 12) +
-        '-' +
-        md5Hash.substring(12, 16) +
-        '-' +
-        md5Hash.substring(16, 20) +
-        '-' +
-        md5Hash.substring(20)
-      ).toLowerCase();
-}
+}

+ 1 - 0
src/utils/RegistrationUtils.ts

@@ -34,6 +34,7 @@ export const useValidation = () => {
         },
         authorized: false,
         errorMessage: 'Error validating input',
+        loggingCategory: 'PROFILE',
       });
       if (response.data.valid) {
         return true;

+ 2 - 16
src/utils/RequestUtils.ts

@@ -5,28 +5,14 @@
  * <dev@clicknpush.ca>, January 2022
  */
 
-import { AxiosError } from "axios";
-import Config from 'react-native-config'
-
-/**
- * Array that maps {@linkcode landmark_type} from {@link Landmark} to a string representing that landmark type.
- */
- export const reportAxiosError = (desc: string,  error: AxiosError, printResponse?: boolean) => {
-    let errorString = `XHR error: ${desc}\nError code: ${error.response?.status}\nError message: ${error.message}`;
-
-    if (printResponse) {
-        errorString + "\nError response: " + error.response;
-    }
-}
-
 /**
  * Change the api url here
  */
 //export const API_URL = 'http://192.168.3.81:8000'
 // export const API_URL = 'https://staging.clicknpush.ca'
 
-//export const API_URL = 'http://192.168.3.102:8000'   // Chase
+export const API_URL = 'http://192.168.1.68:8000'   // Chase
 //export const API_URL = 'http://192.168.0.22:8000'       // Eric
-export const API_URL = 'https://app.clicknpush.ca'
+//export const API_URL = 'https://app.clicknpush.ca'
 
 // export const API_URL = Config.API_URL

+ 13 - 0
src/utils/logging.ts

@@ -0,0 +1,13 @@
+export interface Logging {
+    log: (category: LogCategory, message: string, type?: Logtype) => void;
+    CATEGORIES: { [key: string]: LogCategory };
+}
+export type LogCategory = "SYSTEM" | "AUTH" | 'NOTIFICATIONS' | 'LANDMARKS' | 'COMMENTS' | 'PROFILE';
+export type Logtype = "error" | "warning" | "info"
+
+export const LOGGING = {
+    log: (category: LogCategory, type: Logtype, message: string) => {
+        const logMethod = type === "error" ? console.error : type === "warning" ? console.warn : console.log;
+        logMethod(`[${category}]: ${message}`)
+    },
+}