Bladeren bron

Merge branch 'global-alerts' of apps/atlas-mobile-ts into dev

chase 2 jaren geleden
bovenliggende
commit
f5f7eebb0d
98 gewijzigde bestanden met toevoegingen van 3642 en 3697 verwijderingen
  1. 1 1
      .env
  2. 0 50
      App.js
  3. 84 7
      App.tsx
  4. 1 0
      android/app/src/main/java/com/clicknpush/mobile/MainApplication.java
  5. 2 0
      android/settings.gradle
  6. BIN
      assets/childfriendly.png
  7. BIN
      assets/desk.png
  8. BIN
      assets/elevator.png
  9. BIN
      assets/garbage.png
  10. BIN
      assets/kiosk.png
  11. BIN
      assets/loudnoise.png
  12. 23 231
      assets/mapicon.svg
  13. BIN
      assets/monitor.png
  14. BIN
      assets/ramp.png
  15. BIN
      assets/rampinner.png
  16. BIN
      assets/tripping.png
  17. BIN
      assets/water.png
  18. 2 0
      ios/Podfile
  19. 55 2
      ios/Podfile.lock
  20. 16 0
      ios/cnpmobile.xcodeproj/project.pbxproj
  21. 367 64
      package-lock.json
  22. 11 0
      package.json
  23. 34 170
      src/components/Atlas.tsx
  24. 11 1
      src/components/Buttons.tsx
  25. 27 0
      src/components/Error.tsx
  26. 7 4
      src/components/Feed/Feed.tsx
  27. 32 0
      src/components/LandmarkTypePicker.tsx
  28. 26 0
      src/components/Loading.tsx
  29. 26 0
      src/components/Map/IndoorFloor.tsx
  30. 1 1
      src/components/Map/LandmarkPin.tsx
  31. 39 0
      src/components/Map/MainMapComponent/ArrowButton.tsx
  32. 19 0
      src/components/Map/MainMapComponent/BottomButtons.tsx
  33. 136 320
      src/components/Map/MainMapComponent/IndoorMap.tsx
  34. 16 3
      src/components/Map/MainMapComponent/Map.styles.tsx
  35. 204 183
      src/components/Map/MainMapComponent/OutdoorMap.tsx
  36. BIN
      src/components/Map/MainMapComponent/landmark_images/information.png
  37. BIN
      src/components/Map/MainMapComponent/landmark_images/power.png
  38. BIN
      src/components/Map/MainMapComponent/landmark_images/stairs.png
  39. 6 1
      src/components/Map/MainMapComponent/useMapState.ts
  40. 242 116
      src/components/Map/Panels/AddLandmarkPanel.tsx
  41. 52 26
      src/components/Map/Panels/FilterPanel/FilterLmTypes.tsx
  42. 4 9
      src/components/Map/Panels/FilterPanel/FilterPanel.tsx
  43. 5 3
      src/components/Map/Panels/LandmarkDetailsPanel/CommentView.tsx
  44. 16 34
      src/components/Map/Panels/LandmarkDetailsPanel/CommentsContainer.tsx
  45. 36 22
      src/components/Map/Panels/LandmarkDetailsPanel/DetailsBody.tsx
  46. 14 10
      src/components/Map/Panels/LandmarkDetailsPanel/DetailsHeader.tsx
  47. 88 100
      src/components/Map/Panels/LandmarkDetailsPanel/LandmarkDetails.tsx
  48. 10 7
      src/components/Map/Panels/LandmarkDetailsPanel/LandmarkPhotos.tsx
  49. 2 2
      src/components/Map/Panels/LandmarkDetailsPanel/TouchOpaq.tsx
  50. 6 13
      src/components/Map/Panels/NearbyLandmarksPanel.tsx
  51. 27 29
      src/components/Map/Panels/VoicePanel.tsx
  52. 20 0
      src/components/PrivacyLink.tsx
  53. 0 0
      src/components/Profile/AuthLayout.tsx
  54. 47 65
      src/components/Profile/LoginView.tsx
  55. 50 72
      src/components/Profile/Profile.tsx
  56. 5 5
      src/components/Profile/ProfileHeader.tsx
  57. 7 10
      src/components/Profile/ProfileSections.tsx
  58. 9 10
      src/components/Profile/ProfileSections/ProfileInformation.tsx
  59. 13 15
      src/components/Profile/ProfileSections/ProfileLegal.tsx
  60. 1 5
      src/components/Profile/ProfileSections/ProfilePrefs.tsx
  61. 1 1
      src/components/Profile/ProfileSections/ProfileSection.tsx
  62. 1 1
      src/components/Profile/ProfileSections/ProfileSectionHeader.tsx
  63. 1 5
      src/components/Profile/ProfileSections/ProfileSkills.tsx
  64. 2 5
      src/components/Profile/ProfileSections/ProfileSubscription.tsx
  65. 37 0
      src/components/Profile/ProfileTemplate.tsx
  66. 4 4
      src/components/Profile/Registration/RegisterMain.tsx
  67. 4 2
      src/components/Profile/Registration/RegistrationSteps/RegisterCredential.tsx
  68. 18 17
      src/components/Profile/Registration/RegistrationSteps/RegisterImage.tsx
  69. 1 1
      src/components/Profile/Registration/RegistrationSteps/RegisterMeasurements.tsx
  70. 3 2
      src/components/Profile/Registration/RegistrationSteps/RegisterPassword.tsx
  71. 4 4
      src/components/Profile/Styles/Profile.styles.tsx
  72. 0 23
      src/components/Splash.tsx
  73. 417 0
      src/data/Auth/AuthContext.tsx
  74. 1 0
      src/data/axios.ts
  75. 156 0
      src/data/comments.ts
  76. 325 0
      src/data/landmarks.ts
  77. 216 0
      src/data/notifications.ts
  78. 201 0
      src/data/profiles.ts
  79. 8 0
      src/data/query-keys.ts
  80. 0 136
      src/hooks/useAuth.ts
  81. 0 61
      src/hooks/useAuthorizedRequests.ts
  82. 0 234
      src/hooks/useComments.ts
  83. 0 473
      src/hooks/useLandmarks.ts
  84. 0 375
      src/hooks/useProfile.ts
  85. 0 197
      src/libs/auth/AuthStore.ts
  86. 0 142
      src/libs/auth/core.ts
  87. 0 58
      src/libs/notfications/helpers.ts
  88. 0 239
      src/navigation/AuthorizedNavigator.tsx
  89. 9 8
      src/navigation/BaseStackNavigator.tsx
  90. 134 0
      src/navigation/MainTabsNavigator.tsx
  91. 87 35
      src/navigation/MapNavigator.tsx
  92. 13 0
      src/navigation/RootNavigator.tsx
  93. 0 0
      src/navigation/contexts.tsx
  94. 0 9
      src/types.ts
  95. 52 7
      src/utils/GlobalUtils.ts
  96. 57 49
      src/utils/RegistrationUtils.ts
  97. 4 6
      src/utils/RequestUtils.ts
  98. 86 12
      yarn.lock

+ 1 - 1
.env

@@ -1,7 +1,7 @@
 SPOKESTACK_ID=c361ff3a-70c3-42e6-b0ee-0207edd03b18
 SPOKESTACK_TOKEN=1A8196594C401EB93035CC6D7D6328CF1855C2B359744E720953AC34B6F658CA
 
-API_URL=http://192.168.0.22
+API_URL=http://192.168.0.22:8000
 
 #API_URL=https://app.clicknpush.ca
 

+ 0 - 50
App.js

@@ -1,50 +0,0 @@
-import 'react-native-gesture-handler';
-import React, { useState } from 'react';
-import { LogBox } from 'react-native';
-import Atlas from './src/components/Atlas';
-import { SafeAreaProvider } from 'react-native-safe-area-context';
-import { Asset } from 'expo-asset';
-import AppLoading from 'expo-app-loading';
-
-const App = () => {
-  LogBox.ignoreAllLogs();
-  const [loading, setLoading] = useState(false);
-  const _cacheResourcesAsync = async () => {
-    const images = [
-      require('./assets/logo-white.png'),
-      require('./assets/cover-dark.png'),
-      require('./assets/cover.jpg'),
-      require('./assets/default-pfp.png'),
-      require('./assets/pothole.png'),
-      require('./assets/roadblock.png'),
-      require('./assets/barrier.png'),
-      require('./assets/bump.png'),
-      require('./assets/information.png'),
-      require('./assets/washroom.png'),
-      require('./assets/park.png'),
-    ];
-
-    const cacheImages = images.map(image => {
-      return Asset.fromModule(image).downloadAsync();
-    }); 
-
-    return Promise.all(cacheImages);
-  }
-
-  if (loading) {
-    return (
-      <AppLoading
-        // startAsync={_cacheResourcesAsync}
-        onFinish={() => setLoading(false)}
-        onError={console.warn}
-      />
-    ); 
-  }
-  return (
-    <SafeAreaProvider>
-      <Atlas/>
-    </SafeAreaProvider>
-  );
-}
-
-export default App

+ 84 - 7
App.tsx

@@ -1,20 +1,97 @@
-import React from 'react';
-import { LogBox } from 'react-native';
+import React, { useEffect, useRef, useState } from 'react';
+import { Alert, LogBox, SafeAreaView, StatusBar } from 'react-native';
 import 'react-native-gesture-handler';
 import { SafeAreaProvider } from 'react-native-safe-area-context';
 import Atlas from './src/components/Atlas';
 import 'expo-asset';
 import AppLoading from 'expo-app-loading';
 import * as Updates from "expo-updates";
+import { Asset } from 'expo-asset';
+import { MenuProvider } from 'react-native-popup-menu';
+import { createNavigationContainerRef, NavigationContainer } from '@react-navigation/native';
+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';
 
-/**
- * The root app component.
- * @component
- */
 const App: React.FC = () => {
+  const updateDismissed = useRef<boolean>(false)
+
+  const queryClient = new QueryClient()
+
+  useEffect(() => {
+    if (!__DEV__) {
+      const timer = setInterval(async () => {
+        const update = await Updates.checkForUpdateAsync()
+        if (update.isAvailable && !updateDismissed.current) {
+          updateDismissed.current = true
+          setTimeout(() => {
+            Alert.alert('Update Available', 'An update is available. Would you like to update now?', [
+              {"text": "Yes", "onPress": async () => {
+                await Updates.fetchUpdateAsync()
+                await Updates.reloadAsync()
+              }},
+              {"text": "No", "onPress": () => {
+                Alert.alert('Update Available', 'Update dismissed, you can always revisit it in settings', [
+                  {"text": "OK"}
+                ])
+              }}
+            ])
+          }, 1000);
+        }
+    }, 5000)
+    return () => clearInterval(timer)
+  }}, [])
+
   LogBox.ignoreAllLogs();
+  const [loading, setLoading] = useState(false);
+  const _cacheResourcesAsync = async () => {
+    const images = [
+      require('./assets/logo-white.png'),
+      require('./assets/cover-dark.png'),
+      require('./assets/cover.jpg'),
+      require('./assets/default-pfp.png'),
+      require('./assets/pothole.png'),
+      require('./assets/roadblock.png'),
+      require('./assets/barrier.png'),
+      require('./assets/bump.png'),
+      require('./assets/information.png'),
+      require('./assets/washroom.png'),
+      require('./assets/park.png'),
+    ];
+
+    const cacheImages = images.map(image => {
+      return Asset.fromModule(image).downloadAsync();
+    }); 
+
+    Promise.all(cacheImages);
+  }
 
-  return <SafeAreaProvider><Atlas/></SafeAreaProvider>
+  if (loading) {
+    return (
+      <AppLoading
+        startAsync={_cacheResourcesAsync}
+        onFinish={() => setLoading(false)}
+        onError={console.warn}
+      />
+    ); 
+  }
+  return (
+    <SafeAreaProvider>
+      <QueryClientProvider client={queryClient}>
+        <MenuProvider>
+          <SafeAreaView style={{height: '100%', backgroundColor: colors.red}}>
+            <StatusBar barStyle='light-content' backgroundColor={colors.red}/>
+            <NavigationContainer ref={navigationRef}>
+              <AuthContextProvider>
+                <Atlas/>
+              </AuthContextProvider>
+            </NavigationContainer>  
+          </SafeAreaView>
+        </MenuProvider>
+      </QueryClientProvider>
+    </SafeAreaProvider>
+  );
 }
 
 export default App;

+ 1 - 0
android/app/src/main/java/com/clicknpush/mobile/MainApplication.java

@@ -7,6 +7,7 @@ import androidx.annotation.NonNull;
 
 import com.facebook.react.PackageList;
 import com.facebook.react.ReactApplication;
+import fr.greweb.reactnativeviewshot.RNViewShotPackage;
 import com.lugg.ReactNativeConfig.ReactNativeConfigPackage;
 import com.facebook.react.ReactInstanceManager;
 import com.facebook.react.ReactNativeHost;

+ 2 - 0
android/settings.gradle

@@ -1,4 +1,6 @@
 rootProject.name = 'cnp-mobile'
+include ':react-native-view-shot'
+project(':react-native-view-shot').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-view-shot/android')
 include ':react-native-config'
 project(':react-native-config').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-config/android')
 

BIN
assets/childfriendly.png


BIN
assets/desk.png


BIN
assets/elevator.png


BIN
assets/garbage.png


BIN
assets/kiosk.png


BIN
assets/loudnoise.png


File diff suppressed because it is too large
+ 23 - 231
assets/mapicon.svg


BIN
assets/monitor.png


BIN
assets/ramp.png


BIN
assets/rampinner.png


BIN
assets/tripping.png


BIN
assets/water.png


+ 2 - 0
ios/Podfile

@@ -15,6 +15,8 @@ target 'cnpmobile' do
   # Convert all permission pods into static libraries
 pod 'react-native-config', :path => '../node_modules/react-native-config'
 
+pod 'react-native-view-shot', :path => '../node_modules/react-native-view-shot'
+
 pre_install do |installer|
   Pod::Installer::Xcode::TargetValidator.send(:define_method, :verify_no_static_framework_transitive_dependencies) {}
 

+ 55 - 2
ios/Podfile.lock

@@ -55,6 +55,15 @@ PODS:
     - ReactCommon/turbomodule/core (= 0.64.3)
   - filter_audio (0.5.0)
   - glog (0.3.5)
+  - libwebp (1.2.1):
+    - libwebp/demux (= 1.2.1)
+    - libwebp/mux (= 1.2.1)
+    - libwebp/webp (= 1.2.1)
+  - libwebp/demux (1.2.1):
+    - libwebp/webp
+  - libwebp/mux (1.2.1):
+    - libwebp/demux
+  - libwebp/webp (1.2.1)
   - Permission-Camera (3.1.0):
     - RNPermissions
   - Permission-LocationAccuracy (3.1.0):
@@ -281,6 +290,8 @@ PODS:
   - react-native-spokestack (6.1.4):
     - React
     - Spokestack-iOS (= 14.1.0)
+  - react-native-view-shot (3.1.2):
+    - React
   - React-perflogger (0.64.3)
   - React-RCTActionSheet (0.64.3):
     - React-Core/RCTActionSheetHeaders (= 0.64.3)
@@ -349,6 +360,14 @@ PODS:
     - React-Core
   - RNCPicker (2.3.0):
     - React-Core
+  - RNDeviceInfo (8.6.0):
+    - React-Core
+  - RNFastImage (8.5.11):
+    - React-Core
+    - SDWebImage (~> 5.11.1)
+    - SDWebImageWebPCoder (~> 0.8.4)
+  - RNFS (2.19.0):
+    - React-Core
   - RNGestureHandler (1.10.3):
     - React-Core
   - RNPermissions (3.1.0):
@@ -385,8 +404,16 @@ PODS:
   - RNScreens (3.8.0):
     - React-Core
     - React-RCTImage
+  - RNSha256 (1.4.7):
+    - React
   - RNSVG (12.1.1):
     - React
+  - SDWebImage (5.11.1):
+    - SDWebImage/Core (= 5.11.1)
+  - SDWebImage/Core (5.11.1)
+  - SDWebImageWebPCoder (0.8.4):
+    - libwebp (~> 1.0)
+    - SDWebImage/Core (~> 5.10)
   - Spokestack-iOS (14.1.0):
     - filter_audio (~> 0.5.0)
     - TensorFlowLiteSwift (~> 2.3.0)
@@ -455,6 +482,7 @@ DEPENDENCIES:
   - "react-native-slider (from `../node_modules/@react-native-community/slider`)"
   - react-native-spinkit (from `../node_modules/react-native-spinkit`)
   - react-native-spokestack (from `../node_modules/react-native-spokestack`)
+  - react-native-view-shot (from `../node_modules/react-native-view-shot`)
   - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
   - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
   - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`)
@@ -469,10 +497,14 @@ DEPENDENCIES:
   - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
   - "RNCCheckbox (from `../node_modules/@react-native-community/checkbox`)"
   - "RNCPicker (from `../node_modules/@react-native-picker/picker`)"
+  - RNDeviceInfo (from `../node_modules/react-native-device-info`)
+  - RNFastImage (from `../node_modules/react-native-fast-image`)
+  - RNFS (from `../node_modules/react-native-fs`)
   - RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
   - RNPermissions (from `../node_modules/react-native-permissions`)
   - RNReanimated (from `../node_modules/react-native-reanimated`)
   - RNScreens (from `../node_modules/react-native-screens`)
+  - RNSha256 (from `../node_modules/react-native-sha256`)
   - RNSVG (from `../node_modules/react-native-svg`)
   - UMTaskManagerInterface (from `../node_modules/unimodules-task-manager-interface/ios`)
   - Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
@@ -481,6 +513,9 @@ SPEC REPOS:
   trunk:
     - boost-for-react-native
     - filter_audio
+    - libwebp
+    - SDWebImage
+    - SDWebImageWebPCoder
     - Spokestack-iOS
     - TensorFlowLiteC
     - TensorFlowLiteSwift
@@ -588,6 +623,8 @@ EXTERNAL SOURCES:
     :path: "../node_modules/react-native-spinkit"
   react-native-spokestack:
     :path: "../node_modules/react-native-spokestack"
+  react-native-view-shot:
+    :path: "../node_modules/react-native-view-shot"
   React-perflogger:
     :path: "../node_modules/react-native/ReactCommon/reactperflogger"
   React-RCTActionSheet:
@@ -616,6 +653,12 @@ EXTERNAL SOURCES:
     :path: "../node_modules/@react-native-community/checkbox"
   RNCPicker:
     :path: "../node_modules/@react-native-picker/picker"
+  RNDeviceInfo:
+    :path: "../node_modules/react-native-device-info"
+  RNFastImage:
+    :path: "../node_modules/react-native-fast-image"
+  RNFS:
+    :path: "../node_modules/react-native-fs"
   RNGestureHandler:
     :path: "../node_modules/react-native-gesture-handler"
   RNPermissions:
@@ -624,6 +667,8 @@ EXTERNAL SOURCES:
     :path: "../node_modules/react-native-reanimated"
   RNScreens:
     :path: "../node_modules/react-native-screens"
+  RNSha256:
+    :path: "../node_modules/react-native-sha256"
   RNSVG:
     :path: "../node_modules/react-native-svg"
   UMTaskManagerInterface:
@@ -659,6 +704,7 @@ SPEC CHECKSUMS:
   FBReactNativeSpec: 1a1323e59b9777f66cbec06bb2f568ce5f72abf6
   filter_audio: 1f071989c5a9ad0e0c9c45d08084513deb30d065
   glog: 73c2498ac6884b13ede40eda8228cb1eee9d9d62
+  libwebp: 98a37e597e40bfdb4c911fc98f2c53d0b12d05fc
   Permission-Camera: 0db4fd6e1c556c1cf47f38b989a8084cea3ec3dd
   Permission-LocationAccuracy: f2fa6140c0362473a061b771dd1b702db581138c
   Permission-LocationAlways: f1e021c3b348946cd7c5760172925b749e5d07a6
@@ -685,6 +731,7 @@ SPEC CHECKSUMS:
   react-native-slider: c98f8413776ec1218ec9cd4f8ed32bfd91d428fc
   react-native-spinkit: da294fd828216ad211fe36a5c14c1e09f09e62db
   react-native-spokestack: dc94170589d1b505f1d46824051888909a8eb039
+  react-native-view-shot: 4475fde003fe8a210053d1f98fb9e06c1d834e1c
   React-perflogger: cc76a4254d19640f1d8ad1c66fdee800414b805c
   React-RCTActionSheet: 7448f049318d8d7e8a9a1ebb742ada721757eea8
   React-RCTAnimation: fb9b3fa1a4a9f5e6ab01b3368693ce69860ba76a
@@ -699,17 +746,23 @@ SPEC CHECKSUMS:
   ReactCommon: 8fea6422328e2fc093e25c9fac67adbcf0f04fb4
   RNCCheckbox: 6bd119c26c6eb8264a29d59ff66cb70a14de3349
   RNCPicker: c796ddf16cd93e980a5a5642eebcee9eb9da1e76
+  RNDeviceInfo: 7257d02f4ec94882c309c987cf42f3588d85103e
+  RNFastImage: 1f2cab428712a4baaf78d6169eaec7f622556dd7
+  RNFS: fc610f78fdf8bfc89a9e5cc2f898519f4dba1002
   RNGestureHandler: a479ebd5ed4221a810967000735517df0d2db211
   RNPermissions: 4b54095940aea8c03fa3e6c92d4ac3647b31ed4e
   RNReanimated: 241c586663f44f19a53883c63375fdd041253960
   RNScreens: 6e1ea5787989f92b0671049b808aef64fa1ef98c
+  RNSha256: bf2c90a9e0cec6dcbcc4100e4e19715ae7feaa34
   RNSVG: 551acb6562324b1d52a4e0758f7ca0ec234e278f
+  SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
+  SDWebImageWebPCoder: f93010f3f6c031e2f8fb3081ca4ee6966c539815
   Spokestack-iOS: 090ed0e757276f2407beec82fa851ec3e408b5b9
   TensorFlowLiteC: 51f50caf5777f740a70e2c1a5dbdc149e7aeb50b
   TensorFlowLiteSwift: fb152cc1eec36b25b03a23c07f5d58113170af58
   UMTaskManagerInterface: 5654c50e68af11b19b9d05452bacf23d19b3f30f
   Yoga: e6ecf3fa25af9d4c87e94ad7d5d292eedef49749
 
-PODFILE CHECKSUM: 606a2c00ea818ac7e33d41f324d5f6f09027970a
+PODFILE CHECKSUM: 36a2eb9184cd7de694ef4e992701e848c207fffb
 
-COCOAPODS: 1.11.2
+COCOAPODS: 1.11.3

+ 16 - 0
ios/cnpmobile.xcodeproj/project.pbxproj

@@ -299,10 +299,14 @@
 				"${BUILT_PRODUCTS_DIR}/RCTTypeSafety/RCTTypeSafety.framework",
 				"${BUILT_PRODUCTS_DIR}/RNCCheckbox/RNCCheckbox.framework",
 				"${BUILT_PRODUCTS_DIR}/RNCPicker/RNCPicker.framework",
+				"${BUILT_PRODUCTS_DIR}/RNDeviceInfo/RNDeviceInfo.framework",
+				"${BUILT_PRODUCTS_DIR}/RNFS/RNFS.framework",
+				"${BUILT_PRODUCTS_DIR}/RNFastImage/RNFastImage.framework",
 				"${BUILT_PRODUCTS_DIR}/RNGestureHandler/RNGestureHandler.framework",
 				"${BUILT_PRODUCTS_DIR}/RNReanimated/RNReanimated.framework",
 				"${BUILT_PRODUCTS_DIR}/RNSVG/RNSVG.framework",
 				"${BUILT_PRODUCTS_DIR}/RNScreens/RNScreens.framework",
+				"${BUILT_PRODUCTS_DIR}/RNSha256/RNSha256.framework",
 				"${BUILT_PRODUCTS_DIR}/React-Core/React.framework",
 				"${BUILT_PRODUCTS_DIR}/React-CoreModules/CoreModules.framework",
 				"${BUILT_PRODUCTS_DIR}/React-RCTAnimation/RCTAnimation.framework",
@@ -319,14 +323,18 @@
 				"${BUILT_PRODUCTS_DIR}/React-jsinspector/jsinspector.framework",
 				"${BUILT_PRODUCTS_DIR}/React-perflogger/reactperflogger.framework",
 				"${BUILT_PRODUCTS_DIR}/ReactCommon/ReactCommon.framework",
+				"${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework",
+				"${BUILT_PRODUCTS_DIR}/SDWebImageWebPCoder/SDWebImageWebPCoder.framework",
 				"${BUILT_PRODUCTS_DIR}/Yoga/yoga.framework",
 				"${BUILT_PRODUCTS_DIR}/glog/glog.framework",
+				"${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework",
 				"${BUILT_PRODUCTS_DIR}/react-native-config/react_native_config.framework",
 				"${BUILT_PRODUCTS_DIR}/react-native-get-random-values/react_native_get_random_values.framework",
 				"${BUILT_PRODUCTS_DIR}/react-native-maps/react_native_maps.framework",
 				"${BUILT_PRODUCTS_DIR}/react-native-safe-area-context/react_native_safe_area_context.framework",
 				"${BUILT_PRODUCTS_DIR}/react-native-slider/react_native_slider.framework",
 				"${BUILT_PRODUCTS_DIR}/react-native-spinkit/react_native_spinkit.framework",
+				"${BUILT_PRODUCTS_DIR}/react-native-view-shot/react_native_view_shot.framework",
 			);
 			name = "[CP] Embed Pods Frameworks";
 			outputPaths = (
@@ -336,10 +344,14 @@
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RCTTypeSafety.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNCCheckbox.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNCPicker.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNDeviceInfo.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNFS.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNFastImage.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNGestureHandler.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNReanimated.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNSVG.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNScreens.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNSha256.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CoreModules.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RCTAnimation.framework",
@@ -356,14 +368,18 @@
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/jsinspector.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/reactperflogger.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReactCommon.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImageWebPCoder.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/yoga.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/glog.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_config.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_get_random_values.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_maps.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_safe_area_context.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_slider.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_spinkit.framework",
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_view_shot.framework",
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 			shellPath = /bin/sh;

+ 367 - 64
package-lock.json

@@ -45,6 +45,7 @@
         "expo-web-browser": "~10.0.3",
         "formik": "^2.2.9",
         "jwt-decode": "^3.1.2",
+        "md5": "^2.3.0",
         "mobx": "^6.3.2",
         "mobx-react": "^7.2.0",
         "parcel": "^2.0.0-rc.0",
@@ -57,11 +58,16 @@
         "react-native-animated-spinkit": "^1.5.2",
         "react-native-collapsible": "^1.6.0",
         "react-native-config": "^1.4.5",
+        "react-native-device-info": "^8.6.0",
         "react-native-dialog": "^9.2.0",
         "react-native-dotenv": "^3.3.0",
+        "react-native-fast-image": "^8.5.11",
+        "react-native-fs": "^2.19.0",
         "react-native-gesture-handler": "~1.10.2",
         "react-native-get-random-values": "~1.7.0",
         "react-native-maps": "0.29.3",
+        "react-native-maps-directions": "^1.8.0",
+        "react-native-material-menu": "^2.0.0",
         "react-native-modal": "^12.0.3",
         "react-native-multi-selectbox": "^1.5.0",
         "react-native-multiple-select": "^0.5.7",
@@ -70,17 +76,22 @@
         "react-native-picker-select": "^7.0.0",
         "react-native-popup-menu": "^0.15.12",
         "react-native-reanimated": "~2.2.0",
+        "react-native-root-toast": "^3.3.0",
         "react-native-safe-area-context": "3.3.2",
         "react-native-screens": "~3.8.0",
         "react-native-sectioned-multi-select": "^0.8.1",
+        "react-native-sha256": "^1.4.7",
         "react-native-side-drawer": "^1.2.9",
         "react-native-spinkit": "^1.5.1",
         "react-native-spokestack": "^6.1.4",
         "react-native-svg": "^12.1.1",
         "react-native-svg-transformer": "^1.0.0",
+        "react-native-toast-message": "^2.1.3",
+        "react-native-view-shot": "3.1.2",
         "react-native-web": "^0.13",
         "react-native-windows": "0.64.2",
         "react-query": "^3.19.0",
+        "uuid": "^8.3.2",
         "yup": "^0.32.9"
       },
       "devDependencies": {
@@ -1964,14 +1975,6 @@
         "safe-json-stringify": "~1"
       }
     },
-    "node_modules/@expo/bunyan/node_modules/uuid": {
-      "version": "8.3.2",
-      "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
-      "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
-      "bin": {
-        "uuid": "dist/bin/uuid"
-      }
-    },
     "node_modules/@expo/config": {
       "version": "5.0.9",
       "resolved": "https://registry.npmjs.org/@expo/config/-/config-5.0.9.tgz",
@@ -3015,14 +3018,6 @@
         "node": ">=12"
       }
     },
-    "node_modules/@expo/rudder-sdk-node/node_modules/uuid": {
-      "version": "8.3.2",
-      "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
-      "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
-      "bin": {
-        "uuid": "dist/bin/uuid"
-      }
-    },
     "node_modules/@expo/schemer": {
       "version": "1.3.31",
       "resolved": "https://registry.npmjs.org/@expo/schemer/-/schemer-1.3.31.tgz",
@@ -5499,6 +5494,15 @@
         "node": ">=8"
       }
     },
+    "node_modules/@react-native-community/cli-platform-ios/node_modules/uuid": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
+      "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
+      "deprecated": "Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.",
+      "bin": {
+        "uuid": "bin/uuid"
+      }
+    },
     "node_modules/@react-native-community/cli-platform-ios/node_modules/xcode": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/xcode/-/xcode-2.1.0.tgz",
@@ -5917,6 +5921,15 @@
         "node": ">=10"
       }
     },
+    "node_modules/@react-native-windows/cli/node_modules/uuid": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
+      "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
+      "deprecated": "Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.",
+      "bin": {
+        "uuid": "bin/uuid"
+      }
+    },
     "node_modules/@react-native-windows/find-repo-root": {
       "version": "0.64.0",
       "resolved": "https://registry.npmjs.org/@react-native-windows/find-repo-root/-/find-repo-root-0.64.0.tgz",
@@ -9402,6 +9415,11 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/base-64": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz",
+      "integrity": "sha1-eAqZyE59YAJgNhURxId2E78k9rs="
+    },
     "node_modules/base-x": {
       "version": "3.0.8",
       "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.8.tgz",
@@ -13360,14 +13378,6 @@
         "node": ">=8"
       }
     },
-    "node_modules/expo-cli/node_modules/uuid": {
-      "version": "8.3.2",
-      "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
-      "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
-      "bin": {
-        "uuid": "dist/bin/uuid"
-      }
-    },
     "node_modules/expo-constants": {
       "version": "12.1.3",
       "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-12.1.3.tgz",
@@ -13378,6 +13388,15 @@
         "uuid": "^3.3.2"
       }
     },
+    "node_modules/expo-constants/node_modules/uuid": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
+      "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
+      "deprecated": "Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.",
+      "bin": {
+        "uuid": "bin/uuid"
+      }
+    },
     "node_modules/expo-crypto": {
       "version": "10.0.3",
       "resolved": "https://registry.npmjs.org/expo-crypto/-/expo-crypto-10.0.3.tgz",
@@ -13558,6 +13577,15 @@
         "node": ">=10"
       }
     },
+    "node_modules/expo-file-system/node_modules/uuid": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
+      "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
+      "deprecated": "Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.",
+      "bin": {
+        "uuid": "bin/uuid"
+      }
+    },
     "node_modules/expo-font": {
       "version": "10.0.3",
       "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-10.0.3.tgz",
@@ -14093,6 +14121,15 @@
         "node": ">= 10.0.0"
       }
     },
+    "node_modules/expo-notifications/node_modules/uuid": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
+      "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
+      "deprecated": "Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.",
+      "bin": {
+        "uuid": "bin/uuid"
+      }
+    },
     "node_modules/expo-pwa": {
       "version": "0.0.102",
       "resolved": "https://registry.npmjs.org/expo-pwa/-/expo-pwa-0.0.102.tgz",
@@ -14487,6 +14524,15 @@
         "node": ">=10"
       }
     },
+    "node_modules/expo-updates/node_modules/uuid": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
+      "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
+      "deprecated": "Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.",
+      "bin": {
+        "uuid": "bin/uuid"
+      }
+    },
     "node_modules/expo-web-browser": {
       "version": "10.0.3",
       "resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-10.0.3.tgz",
@@ -14496,6 +14542,15 @@
         "expo-modules-core": "~0.4.4"
       }
     },
+    "node_modules/expo/node_modules/uuid": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
+      "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
+      "deprecated": "Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.",
+      "bin": {
+        "uuid": "bin/uuid"
+      }
+    },
     "node_modules/express": {
       "version": "4.16.4",
       "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz",
@@ -16077,6 +16132,15 @@
         "graphql": "^0.12.0 || ^0.13.0"
       }
     },
+    "node_modules/graphql-tools/node_modules/uuid": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
+      "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
+      "deprecated": "Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.",
+      "bin": {
+        "uuid": "bin/uuid"
+      }
+    },
     "node_modules/gzip-size": {
       "version": "5.1.1",
       "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz",
@@ -24894,6 +24958,14 @@
         }
       }
     },
+    "node_modules/react-native-device-info": {
+      "version": "8.6.0",
+      "resolved": "https://registry.npmjs.org/react-native-device-info/-/react-native-device-info-8.6.0.tgz",
+      "integrity": "sha512-LxxaHkx2XQDuRFhk6545uBvjbjL/Xq7YNa8ufOCKg/BSPuZZPWmOW8SUwxDn/bteAGOx6Djv1YqGOMolX+bzSw==",
+      "peerDependencies": {
+        "react-native": "*"
+      }
+    },
     "node_modules/react-native-dialog": {
       "version": "9.2.0",
       "resolved": "https://registry.npmjs.org/react-native-dialog/-/react-native-dialog-9.2.0.tgz",
@@ -24918,6 +24990,33 @@
         "node": ">=10"
       }
     },
+    "node_modules/react-native-fast-image": {
+      "version": "8.5.11",
+      "resolved": "https://registry.npmjs.org/react-native-fast-image/-/react-native-fast-image-8.5.11.tgz",
+      "integrity": "sha512-cNW4bIJg3nvKaheG8vGMfqCt5LMWX9MS5+wMudgKIHbGO51spRr4sgnlhVgwHLcZ5aeNOVJ8CPRxDIWKRq/0QA==",
+      "peerDependencies": {
+        "react": "^16.8.6 || ^17.0.0",
+        "react-native": ">=0.60.0"
+      }
+    },
+    "node_modules/react-native-fs": {
+      "version": "2.19.0",
+      "resolved": "https://registry.npmjs.org/react-native-fs/-/react-native-fs-2.19.0.tgz",
+      "integrity": "sha512-Yl09IbETkV5UJcBtVtBLttyTmiAhJIHpGA/LvredI5dYiw3MXMMVu42bzELiuH2Bwj7F+qd0fMNvgfBDiDxd2A==",
+      "dependencies": {
+        "base-64": "^0.1.0",
+        "utf8": "^3.0.0"
+      },
+      "peerDependencies": {
+        "react-native": "*",
+        "react-native-windows": "*"
+      },
+      "peerDependenciesMeta": {
+        "react-native-windows": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/react-native-gesture-handler": {
       "version": "1.10.3",
       "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-1.10.3.tgz",
@@ -24976,6 +25075,29 @@
         "react-native-web": "^0.13"
       }
     },
+    "node_modules/react-native-maps-directions": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/react-native-maps-directions/-/react-native-maps-directions-1.8.0.tgz",
+      "integrity": "sha512-7KWfPrvPLU8VP2nEqsnrWlOuylidlRWWDdk3lqT5Nb1q87FeyzNgA7Ib7n6cZlQwH4usTRlJSnzNo/yQ3u4AZw==",
+      "dependencies": {
+        "lodash.isequal": "^4.5.0",
+        "prop-types": "^15.6.0"
+      },
+      "peerDependencies": {
+        "react": "*",
+        "react-native": "*",
+        "react-native-maps": ">=0.12.1"
+      }
+    },
+    "node_modules/react-native-material-menu": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/react-native-material-menu/-/react-native-material-menu-2.0.0.tgz",
+      "integrity": "sha512-SmO9PLE3E469EPbVWZqvdu6JGPPZIm7YjqDcWs2PPoY0k7w2V9tFo3BmmLXNzNZDCVCAi+PPSsL7h/5WkfHcSg==",
+      "peerDependencies": {
+        "react": "*",
+        "react-native": "*"
+      }
+    },
     "node_modules/react-native-modal": {
       "version": "12.1.0",
       "resolved": "https://registry.npmjs.org/react-native-modal/-/react-native-modal-12.1.0.tgz",
@@ -25085,6 +25207,23 @@
         "ua-parser-js": "^0.7.18"
       }
     },
+    "node_modules/react-native-root-siblings": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/react-native-root-siblings/-/react-native-root-siblings-4.1.1.tgz",
+      "integrity": "sha512-sdmLElNs5PDWqmZmj4/aNH4anyxreaPm61c4ZkRiR8SO/GzLg6KjAbb0e17RmMdnBdD0AIQbS38h/l55YKN4ZA=="
+    },
+    "node_modules/react-native-root-toast": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/react-native-root-toast/-/react-native-root-toast-3.3.0.tgz",
+      "integrity": "sha512-C4Pqu+Ae7kXsYJwTvz8NshyJ9SL5YJd+/vCkvgDAxxR8AYlPFggEcTCMNARIWXuRwthLbuwcakh4z9k6qg95dg==",
+      "dependencies": {
+        "prop-types": "^15.5.10",
+        "react-native-root-siblings": "^4.0.0"
+      },
+      "peerDependencies": {
+        "react-native": ">=0.47.0"
+      }
+    },
     "node_modules/react-native-safe-area-context": {
       "version": "3.3.2",
       "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-3.3.2.tgz",
@@ -25114,6 +25253,11 @@
         "prop-types": "^15.6.0"
       }
     },
+    "node_modules/react-native-sha256": {
+      "version": "1.4.7",
+      "resolved": "https://registry.npmjs.org/react-native-sha256/-/react-native-sha256-1.4.7.tgz",
+      "integrity": "sha512-VcIjOBGvHG6V2OCgbGnEKOymcatYC7byf1aM6mmCoUDqOUFQrGIjnU9fUMWGoMERAljVqisvsG/M1GdfhilkFg=="
+    },
     "node_modules/react-native-side-drawer": {
       "version": "1.2.9",
       "resolved": "https://registry.npmjs.org/react-native-side-drawer/-/react-native-side-drawer-1.2.9.tgz",
@@ -25224,6 +25368,15 @@
         "boolbase": "~1.0.0"
       }
     },
+    "node_modules/react-native-toast-message": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/react-native-toast-message/-/react-native-toast-message-2.1.3.tgz",
+      "integrity": "sha512-K3hHSWezWixxOZUDbxPSarEG+tPv2WcaJG4L7dvRUC1TOKNVCEKzXjsx+6ZMxllDrJ0sFczuYGqZibGpFe/ubA==",
+      "peerDependencies": {
+        "react": "*",
+        "react-native": "*"
+      }
+    },
     "node_modules/react-native-vector-icons": {
       "version": "8.1.0",
       "resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-8.1.0.tgz",
@@ -25282,6 +25435,15 @@
         "node": ">=10"
       }
     },
+    "node_modules/react-native-view-shot": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/react-native-view-shot/-/react-native-view-shot-3.1.2.tgz",
+      "integrity": "sha512-9u9fPtp6a52UMoZ/UCPrCjKZk8tnkI9To0Eh6yYnLKFEGkRZ7Chm6DqwDJbYJHeZrheCCopaD5oEOnRqhF4L2Q==",
+      "peerDependencies": {
+        "react": "*",
+        "react-native": "*"
+      }
+    },
     "node_modules/react-native-web": {
       "version": "0.13.18",
       "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.13.18.tgz",
@@ -26045,6 +26207,15 @@
         "node": ">=0.6"
       }
     },
+    "node_modules/request/node_modules/uuid": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
+      "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
+      "deprecated": "Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.",
+      "bin": {
+        "uuid": "bin/uuid"
+      }
+    },
     "node_modules/require-directory": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -27152,6 +27323,15 @@
         "node": ">=0.8.0"
       }
     },
+    "node_modules/sockjs/node_modules/uuid": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
+      "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
+      "deprecated": "Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.",
+      "bin": {
+        "uuid": "bin/uuid"
+      }
+    },
     "node_modules/socks": {
       "version": "2.6.1",
       "resolved": "https://registry.npmjs.org/socks/-/socks-2.6.1.tgz",
@@ -29212,6 +29392,11 @@
         "node": ">=8"
       }
     },
+    "node_modules/utf8": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz",
+      "integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ=="
+    },
     "node_modules/util": {
       "version": "0.12.4",
       "resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz",
@@ -29261,12 +29446,11 @@
       }
     },
     "node_modules/uuid": {
-      "version": "3.4.0",
-      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
-      "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
-      "deprecated": "Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.",
+      "version": "8.3.2",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+      "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
       "bin": {
-        "uuid": "bin/uuid"
+        "uuid": "dist/bin/uuid"
       }
     },
     "node_modules/v8-compile-cache": {
@@ -30384,6 +30568,15 @@
         "node": ">= 6"
       }
     },
+    "node_modules/webpack-log/node_modules/uuid": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
+      "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
+      "deprecated": "Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.",
+      "bin": {
+        "uuid": "bin/uuid"
+      }
+    },
     "node_modules/webpack-manifest-plugin": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-2.2.0.tgz",
@@ -31248,14 +31441,6 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/xdl/node_modules/uuid": {
-      "version": "8.3.2",
-      "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
-      "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
-      "bin": {
-        "uuid": "dist/bin/uuid"
-      }
-    },
     "node_modules/xdl/node_modules/yallist": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
@@ -32775,13 +32960,6 @@
         "mv": "~2",
         "safe-json-stringify": "~1",
         "uuid": "^8.0.0"
-      },
-      "dependencies": {
-        "uuid": {
-          "version": "8.3.2",
-          "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
-          "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
-        }
       }
     },
     "@expo/config": {
@@ -33607,13 +33785,6 @@
         "node-fetch": "^2.6.1",
         "remove-trailing-slash": "^0.1.0",
         "uuid": "^8.3.2"
-      },
-      "dependencies": {
-        "uuid": {
-          "version": "8.3.2",
-          "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
-          "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
-        }
       }
     },
     "@expo/schemer": {
@@ -35589,6 +35760,11 @@
             "supports-color": "^7.1.0"
           }
         },
+        "uuid": {
+          "version": "3.4.0",
+          "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
+          "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
+        },
         "xcode": {
           "version": "2.1.0",
           "resolved": "https://registry.npmjs.org/xcode/-/xcode-2.1.0.tgz",
@@ -35758,6 +35934,11 @@
           "requires": {
             "lru-cache": "^6.0.0"
           }
+        },
+        "uuid": {
+          "version": "3.4.0",
+          "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
+          "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
         }
       }
     },
@@ -38563,6 +38744,11 @@
         }
       }
     },
+    "base-64": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz",
+      "integrity": "sha1-eAqZyE59YAJgNhURxId2E78k9rs="
+    },
     "base-x": {
       "version": "3.0.8",
       "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.8.tgz",
@@ -41381,6 +41567,13 @@
         "md5-file": "^3.2.3",
         "pretty-format": "^26.4.0",
         "uuid": "^3.4.0"
+      },
+      "dependencies": {
+        "uuid": {
+          "version": "3.4.0",
+          "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
+          "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
+        }
       }
     },
     "expo-app-loading": {
@@ -41701,11 +41894,6 @@
           "requires": {
             "crypto-random-string": "^2.0.0"
           }
-        },
-        "uuid": {
-          "version": "8.3.2",
-          "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
-          "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
         }
       }
     },
@@ -41717,6 +41905,13 @@
         "@expo/config": "^5.0.9",
         "expo-modules-core": "~0.4.4",
         "uuid": "^3.3.2"
+      },
+      "dependencies": {
+        "uuid": {
+          "version": "3.4.0",
+          "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
+          "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
+        }
       }
     },
     "expo-crypto": {
@@ -41859,6 +42054,11 @@
           "requires": {
             "lru-cache": "^6.0.0"
           }
+        },
+        "uuid": {
+          "version": "3.4.0",
+          "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
+          "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
         }
       }
     },
@@ -42266,6 +42466,11 @@
           "version": "2.0.0",
           "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
           "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ=="
+        },
+        "uuid": {
+          "version": "3.4.0",
+          "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
+          "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
         }
       }
     },
@@ -42574,6 +42779,11 @@
           "requires": {
             "lru-cache": "^6.0.0"
           }
+        },
+        "uuid": {
+          "version": "3.4.0",
+          "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
+          "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
         }
       }
     },
@@ -43874,6 +44084,13 @@
         "deprecated-decorator": "^0.1.6",
         "iterall": "^1.1.3",
         "uuid": "^3.1.0"
+      },
+      "dependencies": {
+        "uuid": {
+          "version": "3.4.0",
+          "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
+          "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
+        }
       }
     },
     "gzip-size": {
@@ -50973,6 +51190,12 @@
       "integrity": "sha512-5oiAsoW88SOYDg/0cleJ2vJDqv98FJUbFQYEnH4sdMtEn3AAT3lb7BkTGW8HO/t3Vk9VOruwxUUnO4tzuxzCsw==",
       "requires": {}
     },
+    "react-native-device-info": {
+      "version": "8.6.0",
+      "resolved": "https://registry.npmjs.org/react-native-device-info/-/react-native-device-info-8.6.0.tgz",
+      "integrity": "sha512-LxxaHkx2XQDuRFhk6545uBvjbjL/Xq7YNa8ufOCKg/BSPuZZPWmOW8SUwxDn/bteAGOx6Djv1YqGOMolX+bzSw==",
+      "requires": {}
+    },
     "react-native-dialog": {
       "version": "9.2.0",
       "resolved": "https://registry.npmjs.org/react-native-dialog/-/react-native-dialog-9.2.0.tgz",
@@ -50994,6 +51217,21 @@
         }
       }
     },
+    "react-native-fast-image": {
+      "version": "8.5.11",
+      "resolved": "https://registry.npmjs.org/react-native-fast-image/-/react-native-fast-image-8.5.11.tgz",
+      "integrity": "sha512-cNW4bIJg3nvKaheG8vGMfqCt5LMWX9MS5+wMudgKIHbGO51spRr4sgnlhVgwHLcZ5aeNOVJ8CPRxDIWKRq/0QA==",
+      "requires": {}
+    },
+    "react-native-fs": {
+      "version": "2.19.0",
+      "resolved": "https://registry.npmjs.org/react-native-fs/-/react-native-fs-2.19.0.tgz",
+      "integrity": "sha512-Yl09IbETkV5UJcBtVtBLttyTmiAhJIHpGA/LvredI5dYiw3MXMMVu42bzELiuH2Bwj7F+qd0fMNvgfBDiDxd2A==",
+      "requires": {
+        "base-64": "^0.1.0",
+        "utf8": "^3.0.0"
+      }
+    },
     "react-native-gesture-handler": {
       "version": "1.10.3",
       "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-1.10.3.tgz",
@@ -51044,6 +51282,21 @@
         "@types/geojson": "^7946.0.7"
       }
     },
+    "react-native-maps-directions": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/react-native-maps-directions/-/react-native-maps-directions-1.8.0.tgz",
+      "integrity": "sha512-7KWfPrvPLU8VP2nEqsnrWlOuylidlRWWDdk3lqT5Nb1q87FeyzNgA7Ib7n6cZlQwH4usTRlJSnzNo/yQ3u4AZw==",
+      "requires": {
+        "lodash.isequal": "^4.5.0",
+        "prop-types": "^15.6.0"
+      }
+    },
+    "react-native-material-menu": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/react-native-material-menu/-/react-native-material-menu-2.0.0.tgz",
+      "integrity": "sha512-SmO9PLE3E469EPbVWZqvdu6JGPPZIm7YjqDcWs2PPoY0k7w2V9tFo3BmmLXNzNZDCVCAi+PPSsL7h/5WkfHcSg==",
+      "requires": {}
+    },
     "react-native-modal": {
       "version": "12.1.0",
       "resolved": "https://registry.npmjs.org/react-native-modal/-/react-native-modal-12.1.0.tgz",
@@ -51123,6 +51376,20 @@
         }
       }
     },
+    "react-native-root-siblings": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/react-native-root-siblings/-/react-native-root-siblings-4.1.1.tgz",
+      "integrity": "sha512-sdmLElNs5PDWqmZmj4/aNH4anyxreaPm61c4ZkRiR8SO/GzLg6KjAbb0e17RmMdnBdD0AIQbS38h/l55YKN4ZA=="
+    },
+    "react-native-root-toast": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/react-native-root-toast/-/react-native-root-toast-3.3.0.tgz",
+      "integrity": "sha512-C4Pqu+Ae7kXsYJwTvz8NshyJ9SL5YJd+/vCkvgDAxxR8AYlPFggEcTCMNARIWXuRwthLbuwcakh4z9k6qg95dg==",
+      "requires": {
+        "prop-types": "^15.5.10",
+        "react-native-root-siblings": "^4.0.0"
+      }
+    },
     "react-native-safe-area-context": {
       "version": "3.3.2",
       "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-3.3.2.tgz",
@@ -51143,6 +51410,11 @@
       "integrity": "sha512-iH/5PHBo64yFhKzjd+6sCG0aEvgDnUwpn3I7R0ohcGo/+gJdw47mYmJMTwJTmR9JqBFXHx+2bkQi4ij2ghfqRQ==",
       "requires": {}
     },
+    "react-native-sha256": {
+      "version": "1.4.7",
+      "resolved": "https://registry.npmjs.org/react-native-sha256/-/react-native-sha256-1.4.7.tgz",
+      "integrity": "sha512-VcIjOBGvHG6V2OCgbGnEKOymcatYC7byf1aM6mmCoUDqOUFQrGIjnU9fUMWGoMERAljVqisvsG/M1GdfhilkFg=="
+    },
     "react-native-side-drawer": {
       "version": "1.2.9",
       "resolved": "https://registry.npmjs.org/react-native-side-drawer/-/react-native-side-drawer-1.2.9.tgz",
@@ -51236,6 +51508,12 @@
         "path-dirname": "^1.0.2"
       }
     },
+    "react-native-toast-message": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/react-native-toast-message/-/react-native-toast-message-2.1.3.tgz",
+      "integrity": "sha512-K3hHSWezWixxOZUDbxPSarEG+tPv2WcaJG4L7dvRUC1TOKNVCEKzXjsx+6ZMxllDrJ0sFczuYGqZibGpFe/ubA==",
+      "requires": {}
+    },
     "react-native-vector-icons": {
       "version": "8.1.0",
       "resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-8.1.0.tgz",
@@ -51286,6 +51564,12 @@
         }
       }
     },
+    "react-native-view-shot": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/react-native-view-shot/-/react-native-view-shot-3.1.2.tgz",
+      "integrity": "sha512-9u9fPtp6a52UMoZ/UCPrCjKZk8tnkI9To0Eh6yYnLKFEGkRZ7Chm6DqwDJbYJHeZrheCCopaD5oEOnRqhF4L2Q==",
+      "requires": {}
+    },
     "react-native-web": {
       "version": "0.13.18",
       "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.13.18.tgz",
@@ -51775,6 +52059,11 @@
           "version": "6.5.2",
           "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
           "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
+        },
+        "uuid": {
+          "version": "3.4.0",
+          "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
+          "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
         }
       }
     },
@@ -52650,6 +52939,13 @@
         "faye-websocket": "^0.10.0",
         "uuid": "^3.4.0",
         "websocket-driver": "0.6.5"
+      },
+      "dependencies": {
+        "uuid": {
+          "version": "3.4.0",
+          "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
+          "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
+        }
       }
     },
     "sockjs-client": {
@@ -54270,6 +54566,11 @@
         "mem": "^4.3.0"
       }
     },
+    "utf8": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz",
+      "integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ=="
+    },
     "util": {
       "version": "0.12.4",
       "resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz",
@@ -54313,9 +54614,9 @@
       "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
     },
     "uuid": {
-      "version": "3.4.0",
-      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
-      "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
+      "version": "8.3.2",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+      "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
     },
     "v8-compile-cache": {
       "version": "2.3.0",
@@ -55425,6 +55726,13 @@
       "requires": {
         "ansi-colors": "^3.0.0",
         "uuid": "^3.3.2"
+      },
+      "dependencies": {
+        "uuid": {
+          "version": "3.4.0",
+          "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
+          "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
+        }
       }
     },
     "webpack-manifest-plugin": {
@@ -55948,11 +56256,6 @@
           "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.12.0.tgz",
           "integrity": "sha512-53RyidyjvkGpnWPMF9bQgFtWp+Sl8O2Rp13VavmJgfAP9WWG6q6TkrKU8iyJdnwnfgHI6k2hTlgqH4aSdjoTbg=="
         },
-        "uuid": {
-          "version": "8.3.2",
-          "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
-          "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
-        },
         "yallist": {
           "version": "3.1.1",
           "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",

+ 11 - 0
package.json

@@ -63,6 +63,7 @@
     "expo-web-browser": "~10.0.3",
     "formik": "^2.2.9",
     "jwt-decode": "^3.1.2",
+    "md5": "^2.3.0",
     "mobx": "^6.3.2",
     "mobx-react": "^7.2.0",
     "parcel": "^2.0.0-rc.0",
@@ -75,11 +76,16 @@
     "react-native-animated-spinkit": "^1.5.2",
     "react-native-collapsible": "^1.6.0",
     "react-native-config": "^1.4.5",
+    "react-native-device-info": "^8.6.0",
     "react-native-dialog": "^9.2.0",
     "react-native-dotenv": "^3.3.0",
+    "react-native-fast-image": "^8.5.11",
+    "react-native-fs": "^2.19.0",
     "react-native-gesture-handler": "~1.10.2",
     "react-native-get-random-values": "~1.7.0",
     "react-native-maps": "0.29.3",
+    "react-native-maps-directions": "^1.8.0",
+    "react-native-material-menu": "^2.0.0",
     "react-native-modal": "^12.0.3",
     "react-native-multi-selectbox": "^1.5.0",
     "react-native-multiple-select": "^0.5.7",
@@ -88,17 +94,22 @@
     "react-native-picker-select": "^7.0.0",
     "react-native-popup-menu": "^0.15.12",
     "react-native-reanimated": "~2.2.0",
+    "react-native-root-toast": "^3.3.0",
     "react-native-safe-area-context": "3.3.2",
     "react-native-screens": "~3.8.0",
     "react-native-sectioned-multi-select": "^0.8.1",
+    "react-native-sha256": "^1.4.7",
     "react-native-side-drawer": "^1.2.9",
     "react-native-spinkit": "^1.5.1",
     "react-native-spokestack": "^6.1.4",
     "react-native-svg": "^12.1.1",
     "react-native-svg-transformer": "^1.0.0",
+    "react-native-toast-message": "^2.1.3",
+    "react-native-view-shot": "3.1.2",
     "react-native-web": "^0.13",
     "react-native-windows": "0.64.2",
     "react-query": "^3.19.0",
+    "uuid": "^8.3.2",
     "yup": "^0.32.9"
   },
   "devDependencies": {

+ 34 - 170
src/components/Atlas.tsx

@@ -5,28 +5,15 @@
  * <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 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'
-import * as Updates from 'expo-updates'
-import { Manifest, UpdateCheckResult } from 'expo-updates';
+import React from 'react';
+import { QueryClient } from 'react-query';
+import { Loading } from './Loading';
+import { useAuth } from '../data/Auth/AuthContext';
+import BaseStackNavigator from '../navigation/BaseStackNavigator';
+import { Error } from './Error';
+
+import { navigationRef } from '../navigation/RootNavigator';
 
 export enum TokenState {
   CheckingToken,
@@ -41,162 +28,39 @@ const queryClient = new QueryClient();
  * @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 updateDismissed = useRef<boolean>(false)
-  const [fontsLoaded, error] = useFonts({
-    RacingSansOne_400Regular
-  });
-
-    useEffect(() => {
-      if (!__DEV__) {
-        const timer = setInterval(async () => {
-          const update = await Updates.checkForUpdateAsync()
-          if (update.isAvailable && !updateDismissed.current) {
-            updateDismissed.current = true
-            setTimeout(() => {
-              Alert.alert('Update Available', 'An update is available. Would you like to update now?', [
-                {"text": "Yes", "onPress": async () => {
-                  await Updates.fetchUpdateAsync()
-                  await Updates.reloadAsync()
-                }},
-                {"text": "No", "onPress": () => {
-                  Alert.alert('Update Available', 'Update dismissed, you can always revisit it in settings', [
-                    {"text": "OK"}
-                  ])
-                }}
-              ])
-            }, 1000);
-          }
-      }, 5000)
-      return () => clearInterval(timer)
-    }}, [])
+  const {loading, error, setError, setLoading} = useAuth()
 
-  /**
-   * 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;
-    if (!currentAccessToken) {
-      console.log('[Authentication]: No access token in memory, checking in secure store...')
-      currentAccessToken = await getItemAsync(SECURESTORE_ACCESSTOKEN);
-    }
+  const dismissError = () => {
+    setError('')
+    setLoading(false)
+  }
 
-    if (!currentAccessToken) {
-      console.log('[Authentication]: No access token in secure store, attempting to use a refresh token...')
-      let refreshToken  = authStore.refreshToken;
-      if (!refreshToken) {
-        refreshToken = await getItemAsync(SECURESTORE_REFRESHTOKEN)
-      }
+  // useEffect(() => {
+  //   let isMounted = true
+  //   const checkAuthStateOnAccessTokenUpdate = async () => {
+  //     let accessToken = authStore.accessToken
+  //     if (!accessToken) {
+  //       accessToken = await getItemAsync(SECURESTORE_ACCESSTOKEN)
+  //     }
 
-      if (refreshToken) {
-        await refreshAccessToken()
-        currentAccessToken = authStore.accessToken
-      }
-    }  
+  //     if (accessToken)
+  //       await checkAuthenticationState({})
+  //   }
     
-    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);
-        if (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)
-        if (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) => {
-        if (appState == 'active') {
-          console.log('[Authentication]: App opened, checking auth tokens...')
-          checkToken(); 
-        }
-      });
-      return () => {
-        AppState.removeEventListener("change", (appState: string) => {
-          if (appState == 'active') {
-            checkToken(); 
-          }
-        });
-      };
-    }
-    registerAppStateChangeHandler();
-  }, [AppState.currentState]);
+  //   if (isMounted)
+  //     checkAuthStateOnAccessTokenUpdate()
 
-  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 () => { isMounted = false }
+  // }, [])
 
   return (
-    <MenuProvider>
-        <SafeAreaView style={{height: '100%', backgroundColor: colors.red}}>
-        <StatusBar barStyle='light-content' backgroundColor={colors.red}/>
-        <NavigationContainer>  
-          {checkingToken ? <Splash/> :
-          <QueryClientProvider client={queryClient}>
-            {authStore.accessToken ? <AuthorizedNavigator  /> : <UnauthorizedNavigator /> } 
-          </QueryClientProvider> }
-        </NavigationContainer>  
-      </SafeAreaView>
-    </MenuProvider>
+    <>
+      {error ? 
+      <Error error={error} dismissErrorMethod={() => dismissError()}/> :
+        <>
+          {loading ? <Loading/> : <BaseStackNavigator />}
+        </> }
+    </>
   );
 }
 

+ 11 - 1
src/components/Buttons.tsx

@@ -7,7 +7,7 @@
 
 import { FontAwesome } from "@expo/vector-icons";
 import React from "react";
-import { StyleProp, StyleSheet, Text, TouchableOpacity, ViewStyle } from "react-native";
+import { Pressable, StyleProp, StyleSheet, Text, TouchableOpacity, ViewStyle } from "react-native";
 import { colors } from "../utils/GlobalUtils";
 
 interface ButtonProps {
@@ -55,6 +55,16 @@ export const IconButton: React.FC<IconButtonProps> = ({onPress, style, size, col
     )
 }
 
+export const GenericButton: React.FC<TextButtonProps> = ({onPress, style, text}) => {
+    const [isPressed, setIsPressed] = React.useState(false);
+
+    return (
+        <Pressable style={[style, { backgroundColor: isPressed ? '#D3D3D3' : 'white'}]} onPress={onPress} onPressIn={() => setIsPressed(true)} onPressOut={() => setIsPressed(false)}>
+            <Text style={{color: 'black', }}>{text}</Text>
+        </Pressable>
+    )
+}
+
 const styles = StyleSheet.create({
     btn: {
         borderRadius: 50,

+ 27 - 0
src/components/Error.tsx

@@ -0,0 +1,27 @@
+/* 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 React from "react"
+import { Image, StyleSheet, Text, View } from "react-native"
+import { ActivityIndicator } from "react-native-paper"
+import UnauthorizedLayout from "./Profile/AuthLayout"
+import { PrimaryButton } from "./Buttons"
+
+export const Error: React.FC<{error: string, dismissErrorMethod: () => void}> = ({error, dismissErrorMethod}) => {
+    return (
+        <UnauthorizedLayout>
+            <View style={{flex: 1, alignItems: 'center', justifyContent: 'center'}}>
+                <Text style={{color: 'white', fontSize: 20, marginVertical: 25}}>{error}</Text>
+                <PrimaryButton text="Okay" onPress={dismissErrorMethod} />
+            </View>
+        </UnauthorizedLayout>
+    )
+}
+
+const styles = StyleSheet.create({
+
+})

+ 7 - 4
src/components/Feed/Feed.tsx

@@ -9,8 +9,9 @@ import { FontAwesome } from "@expo/vector-icons"
 import React from "react"
 import { ListRenderItem, Text, View } from "react-native"
 import { FlatList, TouchableOpacity } from "react-native-gesture-handler"
-import { useProfile, UserNotification } from "../../hooks/useProfile"
-import { authStore } from "../../libs/auth/AuthStore"
+import { useAuth } from "../../data/Auth/AuthContext"
+import { useDeleteNotification, UserNotification } from "../../data/notifications"
+import { useOwnedProfile } from "../../data/profiles"
 
 /**
  * This component displays all the user's notifications sorted by most recent
@@ -18,8 +19,10 @@ import { authStore } from "../../libs/auth/AuthStore"
  * @returns 
  */
 export const Feed: React.FC<{notifications: UserNotification[], handleNotifInteraction}> = ({notifications, handleNotifInteraction}) => {
+    const {userId} = useAuth()
+
     // bring in the deleteNotification function from the useProfile hook
-    const {deleteNotification} = useProfile(authStore.userId)
+    const deleteNotificationMutation = useDeleteNotification()
 
     /**
      * Flatlist item render function for notifcations. Displays the notification's title and a delete button. 
@@ -37,7 +40,7 @@ export const Feed: React.FC<{notifications: UserNotification[], handleNotifInter
                         {item.title}
                     </Text>
                 </TouchableOpacity>
-                <TouchableOpacity onPress={async () => deleteNotification(item.id)}>
+                <TouchableOpacity onPress={async () => deleteNotificationMutation.mutateAsync(item.id)}>
                     <FontAwesome style={{textAlign: 'center', padding: 15}} name="trash" color='red' size={20} />
                 </TouchableOpacity>
             </View>

+ 32 - 0
src/components/LandmarkTypePicker.tsx

@@ -0,0 +1,32 @@
+import React from 'react'
+import Picker, { Item } from 'react-native-picker-select'
+import { FontAwesome } from "@expo/vector-icons";
+
+interface LandmarkTypePickerProps {
+    onValueChange: (value: any, index: number) => void,
+    value?: any,
+    items: Item[],
+    placeholder?: {} | Item 
+}
+
+const LandmarkTypePicker: React.FC<LandmarkTypePickerProps> = ({items, value, onValueChange, placeholder}) => {
+    return (
+    <Picker
+        style={{
+            inputIOS: { color: 'white' },
+            inputAndroid: { color: 'white', alignItems: 'center', justifyContent: 'center' },
+            viewContainer: { marginVertical: 5, flex: 1}, 
+            iconContainer: { height: '100%', justifyContent: 'center' },
+            placeholder: { color: 'white' }
+        }}
+        textInputProps={{ placeholderTextColor: 'white', selectionColor: 'white' }}
+        Icon={() => <FontAwesome name="chevron-down" color='white' style={{alignSelf: 'center'}} size={20} />}
+        placeholder={placeholder}
+        value={value}
+        onValueChange={onValueChange}
+        useNativeAndroidPickerStyle={true}
+        items={items}
+    />)
+}
+
+export default React.memo(LandmarkTypePicker)

+ 26 - 0
src/components/Loading.tsx

@@ -0,0 +1,26 @@
+/* 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 React from "react"
+import { Image, StyleSheet, Text, View } from "react-native"
+import { ActivityIndicator } from "react-native-paper"
+import UnauthorizedLayout from "./Profile/AuthLayout"
+
+export const Loading: React.FC = () => {
+    return (
+        <UnauthorizedLayout>
+            <View style={{flex: 1, alignItems: 'center', justifyContent: 'center'}}>
+                <ActivityIndicator size='large' color="white" />
+                <Text style={{color: 'white', fontSize: 20, marginTop: 25}}>Loading</Text>
+            </View>
+        </UnauthorizedLayout>
+    )
+}
+
+const styles = StyleSheet.create({
+
+})

+ 26 - 0
src/components/Map/IndoorFloor.tsx

@@ -0,0 +1,26 @@
+import { View, Text } from 'react-native'
+import React from 'react'
+import { Image } from 'react-native-svg'
+import BasementC from './MainMapComponent/images/BasementC.svg'
+import FirstFloorC from './MainMapComponent/images/FirstFloorC.svg'
+import SecondFloorC from './MainMapComponent/images/SecondFloorC.svg'
+import ThirdFloorC from './MainMapComponent/images/ThirdFloorC.svg'
+import FourthFloorC from './MainMapComponent/images/FourthFloorC.svg'
+import FifthFloorC from './MainMapComponent/images/FifthFloorC.svg'
+
+// export const IndoorFloor: React.FC<{floorNum: number}> = (props) => {
+function IndoorFloor(props) {
+  const compArray = [
+    <Image height={"100%"} width={"100%"} href={require('./MainMapComponent/images/basement.png')} />,
+    <Image height={'100%'} width="100%" href={require('./MainMapComponent/images/firstfloor.png')} />,
+    <Image height={"100%"} width={"100%"} href={require('./MainMapComponent/images/secondfloor.png')} />,
+    <Image height={"100%"} width={"100%"} href={require('./MainMapComponent/images/thirdfloor.png')} />,
+    <Image height={"100%"} width={"100%"} href={require('./MainMapComponent/images/fourthfloor.png')} />,
+    <Image height={"100%"} width={"100%"} href={require('./MainMapComponent/images/fifthfloor.png')} />,
+    // <FifthFloorC viewBox='257 153 270 290' style={{ opacity: 0.7 }} height={"100%"} width={"100%"} />,
+  ]
+
+  return compArray[props.floorNum]
+}
+
+export default IndoorFloor

+ 1 - 1
src/components/Map/LandmarkPin.tsx

@@ -8,8 +8,8 @@
 import React from "react";
 import { Image } from "react-native";
 import { Marker } from "react-native-maps";
+import { Landmark } from "../../data/landmarks";
 import { lmTypes } from "../../utils/GlobalUtils";
-import { Landmark } from "../../hooks/useLandmarks";
 
 /**
  * Props for the {@link LandmarkPin} component

+ 39 - 0
src/components/Map/MainMapComponent/ArrowButton.tsx

@@ -0,0 +1,39 @@
+import { View, Text, TouchableOpacity, StyleSheet, ToastAndroid} from 'react-native'
+import React from 'react'
+import { FontAwesome } from "@expo/vector-icons";
+import { colors} from "../../../utils/GlobalUtils";
+
+
+function ArrowButton(props) {
+    if (props.num != 0) {
+        return (
+            <TouchableOpacity style={styles.arrowButton} onPress={props.propEvent} >
+              <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
+                {/* <FontAwesomeIcon icon="fa-solid fa-left" /> */}
+                <FontAwesome style={{ marginLeft: (props.num == 1 ? 5 : -5) }} color={"white"} size={20} name={props.fontAweIcon} />
+              </View>
+            </TouchableOpacity>
+        )
+      }
+      else {
+        return (
+          <View style={{ flex: 1.2, marginHorizontal: 7, height: 53.5, maxWidth: 60, borderRadius: 10 }}>
+          </View>
+        )
+      }
+  }
+
+
+const styles = StyleSheet.create({
+    arrowButton: {
+        flex: 1,
+        backgroundColor: colors.red,
+        // backgroundColor: "blue",
+        height: 53.5,
+        borderRadius: 8,
+      },
+})
+
+
+
+export default ArrowButton

+ 19 - 0
src/components/Map/MainMapComponent/BottomButtons.tsx

@@ -0,0 +1,19 @@
+import { View, Text, TouchableOpacity, StyleSheet, Linking } from 'react-native'
+import React from 'react'
+import { colors } from "../../../utils/GlobalUtils";
+
+function BottomButtons(props) {
+  return (
+    <View style={{ flexDirection: "row", justifyContent: "space-around", alignItems: "flex-end", borderColor: "green", borderWidth: 0 }}>
+      <TouchableOpacity style={{ backgroundColor: colors.red, height: 30, paddingHorizontal: 7, paddingTop: 3, borderRadius: 5 }} onPress={() => props.navigation.goBack()}>
+        <Text style={{ fontSize: 16, color: "white", textAlign: "right", textAlignVertical: "bottom" }}>{"Go back outdoors"}</Text>
+      </TouchableOpacity>
+      <TouchableOpacity style={{ backgroundColor: colors.red, height: 30, paddingHorizontal: 7, paddingTop: 3, borderRadius: 5 }} onPress={() => Linking.openURL('https://www.ualberta.ca/facilities-operations/portfolio/emergency-management-office/emergency-procedures/alarms-evacuation.html')}>
+        <Text style={{ fontSize: 16, color: "white", textAlign: "center", }}>{"Resources"}</Text>
+      </TouchableOpacity>
+    </View>
+  )
+}
+
+
+export default BottomButtons

+ 136 - 320
src/components/Map/MainMapComponent/IndoorMap.tsx

@@ -1,24 +1,24 @@
 import React, { useState, useEffect, Children } from 'react';
-import { View, Text, StatusBar, StyleSheet, Dimensions, Button, ActivityIndicator, Alert, Modal, PanResponderCallbacks, PanResponderGestureState, GestureResponderEvent, ImageSourcePropType, TouchableOpacity } from 'react-native';
+import { View, Text, StatusBar, StyleSheet, Dimensions, Button, ActivityIndicator, Alert, Modal, PanResponderCallbacks, PanResponderGestureState, GestureResponderEvent, ImageSourcePropType, TouchableOpacity, Platform, Linking, Pressable, } from 'react-native';
 import { Svg, Defs, Rect, Mask, Circle, Marker, Path, Polyline, Image } from 'react-native-svg';
 import { RadioButton } from 'react-native-paper';
-import { Picker } from '@react-native-picker/picker';
+import { Picker as EricPicker } from '@react-native-picker/picker';
 import ReactNativeZoomableView from '@openspacelabs/react-native-zoomable-view/src/ReactNativeZoomableView';
 import Spinner from 'react-native-spinkit'
-import { colors, lmTypes } from "../../../utils/GlobalUtils";
+import { colors, lmTypesIndoor } from "../../../utils/GlobalUtils";
 import { MapStackNavigationProp } from "../../../navigation/MapNavigator"
 import CustomModal from './modal';
 import { FontAwesome } from "@expo/vector-icons";
-
-
-import BasementC from './images/BasementC.svg';
-import FirstFloorC from './images/FirstFloorC.svg'
-import SecondFloorC from './images/SecondFloorC.svg'
-import ThirdFloorC from './images/ThirdFloorC.svg'
-import FourthFloorC from './images/FourthFloorC.svg'
-import FifthFloorC from './images/FifthFloorC.svg'
-import { Landmark } from '../../../hooks/useLandmarks';
+import ReactDOMServer from 'react-dom/server'; //npm i --save-dev @types/react-dom
 import { ZoomableViewEvent } from '@openspacelabs/react-native-zoomable-view/src/typings';
+import IndoorFloor from '../IndoorFloor'
+// import Toast from 'react-native-toast-message';
+import Toast from 'react-native-root-toast';
+import ArrowButton from './ArrowButton'
+import BottomButtons from './BottomButtons'
+import Picker from 'react-native-picker-select';
+import { Landmark } from '../../../data/landmarks';
+
 
 
 interface IndoorMapProps {
@@ -33,91 +33,19 @@ interface IndoorMapProps {
 
 
 const IndoorMap: React.FC<IndoorMapProps> = ({ navigation, landmarks, promptAddLandmark, focusLandmark, applyFilter }) => {
-  const [floor, setFloor] = useState(0);
+  const [floor, setFloor] = useState(1);
   const [showME, setShowME] = useState(false);
   const [showDots, setShowDots] = useState(false);
   const [showAddedDot, setShowAddedDot] = useState(false)
-  const [showModal2, setShowModal] = useState(false)
-  const [checked, setChecked] = React.useState('information');
-  const [coords, setCoords] = useState([0, 0])
   const [SVGdim, setSVGdim] = useState([1, 1])
-  const [firstTime, setfirstTime] = useState(true)
 
   const [localLandmarks, setLocalLandmarks] = useState<Landmark[]>([])
 
   const imageDim = 0.05 * Dimensions.get("window").width;
 
-  // Main issue is that I need to first load the page to retrieve dimensions of SVG, THEN I can actually use the 
-  // real proper coordinate values. Before, I used raw numbers, which is why it could load immediately. However here,
-  // my state starts with ratio values (0.5 , 0.25, etc), retrieves SVG coordinates, then finally gets the real positioning.
-
-  // *** IN-MEMORY IMPLEMENTATION ***
-  // let indoorCircles: IndoorMarker[][] = [ //first to fifth floor, last element is basement
-  //   [{ key: 1, coordx: 0.5 * SVGdim[0], coordy: 0.5 * SVGdim[1], description: "nothing yet1", landmark: "stairs" },
-  //   { key: 2, coordx: 0.25 * SVGdim[0], coordy: 0.25 * SVGdim[1], description: "nothing yet2", landmark: "stairs" }],
-
-  //   [{ key: 3, coordx: 0.35 * SVGdim[0], coordy: 0.35 * SVGdim[1], description: "nothing yet1", landmark: "stairs" },
-  //   { key: 4, coordx: 0.15 * SVGdim[0], coordy: 0.15 * SVGdim[1], description: "nothing yet2", landmark: "power" }],
-
-  //   [{ key: 5, coordx: 0.5 * SVGdim[0], coordy: 0.5 * SVGdim[1], description: "nothing yet1", landmark: "stairs" },
-  //   { key: 6, coordx: 0.25 * SVGdim[0], coordy: 0.25 * SVGdim[1], description: "nothing yet2", landmark: "stairs" }],
-
-  //   [{ key: 7, coordx: 0.35 * SVGdim[0], coordy: 0.35 * SVGdim[1], description: "nothing yet1", landmark: "power" },
-  //   { key: 8, coordx: 0.15 * SVGdim[0], coordy: 0.15 * SVGdim[1], description: "nothing yet2", landmark: "power" }],
-
-  //   [{ key: 9, coordx: 0.5 * SVGdim[0], coordy: 0.5 * SVGdim[1], description: "nothing yet1", landmark: "stairs" },
-  //   { key: 10, coordx: 0.25 * SVGdim[0], coordy: 0.25 * SVGdim[1], description: "nothing yet2", landmark: "stairs" }],
-
-  //   [{ key: 11, coordx: 0.35 * SVGdim[0], coordy: 0.35 * SVGdim[1], description: "nothing yet1", landmark: "power" },
-  //   { key: 12, coordx: 0.15 * SVGdim[0], coordy: 0.15 * SVGdim[1], description: "nothing yet2", landmark: "stairs" }],
-  // ]
-
-  // const [indoorMarkers, setIndoorMarkers] = useState([ //first to fifth floor, last element is basement
-  //   [{ key: 1, coordx: 0.5 * SVGdim[0], coordy: 0.5 * SVGdim[1], description: "nothing yet1", landmark: "stairs" },
-  //   { key: 2, coordx: 0.25 * SVGdim[0], coordy: 0.25 * SVGdim[1], description: "nothing yet2", landmark: "power" }],
-
-  //   [{ key: 3, coordx: 0.35 * SVGdim[0], coordy: 0.35 * SVGdim[1], description: "nothing yet1", landmark: "stairs" },
-  //   { key: 4, coordx: 0.15 * SVGdim[0], coordy: 0.15 * SVGdim[1], description: "nothing yet2", landmark: "power" }],
-
-  //   [{ key: 5, coordx: 0.5 * SVGdim[0], coordy: 0.5 * SVGdim[1], description: "nothing yet1", landmark: "stairs" },
-  //   { key: 6, coordx: 0.25 * SVGdim[0], coordy: 0.25 * SVGdim[1], description: "nothing yet2", landmark: "stairs" }],
-
-  //   [{ key: 7, coordx: 0.35 * SVGdim[0], coordy: 0.35 * SVGdim[1], description: "nothing yet1", landmark: "power" },
-  //   { key: 8, coordx: 0.15 * SVGdim[0], coordy: 0.15 * SVGdim[1], description: "nothing yet2", landmark: "power" }],
-
-  //   [{ key: 9, coordx: 0.5 * SVGdim[0], coordy: 0.5 * SVGdim[1], description: "nothing yet1", landmark: "stairs" },
-  //   { key: 10, coordx: 0.25 * SVGdim[0], coordy: 0.25 * SVGdim[1], description: "nothing yet2", landmark: "stairs" }],
-
-  //   [{ key: 11, coordx: 0.35 * SVGdim[0], coordy: 0.35 * SVGdim[1], description: "nothing yet1", landmark: "power" },
-  //   { key: 12, coordx: 0.15 * SVGdim[0], coordy: 0.15 * SVGdim[1], description: "nothing yet2", landmark: "stairs" }],
-  // ])
-
-  function arrowBut(num, fontAweIcon) {
-    if (num != 0) {
-      return (
-        <View style={{ flex: 1.2, marginHorizontal: 7, backgroundColor: colors.red, height: 53.5, maxWidth: 60 }}>
-          <TouchableOpacity style={styles.arrowButton} onPress={() => { setFloor(prevState => prevState + num) }}>
-            <View style={{ flex: 1, justifyContent: "center" }}>
-              <FontAwesome style={{ alignSelf: "center" }} color={"black"} size={50} name={fontAweIcon} />
-            </View>
-          </TouchableOpacity>
-        </View>
-      )
-    }
-    else {
-      return (
-        <View style={{ flex: 1.2, marginHorizontal: 7, height: 53.5, maxWidth: 60, borderRadius: 10 }}>
-        </View>
-      )
-    }
-  }
-
-  let num = 0
-
-  const loadCircles = applyFilter(landmarks)?.map((item) => {
-    if (num < 2) {
-      // console.log("*this is inside loadCircles* SVGdim values are " + SVGdim[0] + " and " + SVGdim[1])
-      num += 1;
+  const loadLandmarks = applyFilter(landmarks)?.map((item) => {
+    if (!lmTypesIndoor[item.landmark_type]) {
+      return null
     }
     if (item.floor == floor && SVGdim[0] != 1 && SVGdim[1] != 1) {
       return (
@@ -128,116 +56,35 @@ const IndoorMap: React.FC<IndoorMapProps> = ({ navigation, landmarks, promptAddL
           y={item.latitude * SVGdim[1]}
           width={imageDim}
           height={imageDim}
-          href={lmTypes[item.landmark_type]['image'] as ImageSourcePropType} />
+          href={lmTypesIndoor[item.landmark_type].image as ImageSourcePropType} />
       )
     }
   }
   )
 
-  // const loadCircles = landmarks.filter(lm => lm.floor == floor).map(item => {
-  //   if (SVGdim[0] != 1 && SVGdim[1] != 1) {
-  //     console.log("svg dimensions are " + SVGdim[0] + " wide and " + SVGdim[1] + " tall")
-  //     return (
-  //       <Image
-  //         // onPress={() => handleDelete(item.longitude, item.latitude , item.id)}
-  //         onPress={() => focusLandmark(item)}
-  //         // onPress={() => Alert.alert("long is " + item.longitude  + " and lat is " + item.latitude)}
-  //         key={item.id}
-  //         x={item.longitude * SVGdim[0]}
-  //         y={item.latitude * SVGdim[1]}
-  //         // x={item.longitude}
-  //         // y={item.latitude}
-  //         width={0.05 * Dimensions.get("window").width}
-  //         height={0.05 * Dimensions.get("window").width}
-  //         href={lmTypes[item.landmark_type]['image'] as ImageSourcePropType} />)
-
-  //     /// *** IN-MEMORY IMPLEMENTATION ***
-  //     // if (item.landmark_type == "stairs") {
-  //     //   return (
-  //     //     <Image onPress={() => handleDelete(item.coordx, item.coordy)} key={item.key} x={item.coordx} y={item.coordy} width={0.05 * Dimensions.get("window").width} height={0.05 * Dimensions.get("window").width} href={require('./landmark_images/stairs.png')} />
-  //     //     // <Circle onPress={() => handleDelete(item.coordx, item.coordy)} key={item.key} cx={item.coordx} cy={item.coordy} r="4" fill="black" />)
-  //     //   )
-  //     // }
-  //     // else if (item.landmark == "power") {
-  //     //   return (
-  //     //     <Image onPress={() => handleDelete(item.coordx, item.coordy)} key={item.key} x={item.coordx} y={item.coordy} width={0.05 * Dimensions.get("window").width} height={0.05 * Dimensions.get("window").width} href={require('./landmark_images/power.png')} />
-  //     //   )
-  //     // }
-  //     // else if (item.landmark == "information") {
-  //     //   return (
-  //     //     <Image onPress={() => handleDelete(item.coordx, item.coordy)} key={item.key} x={item.coordx} y={item.coordy} width={0.05 * Dimensions.get("window").width} height={0.05 * Dimensions.get("window").width} href={require('./landmark_images/information.png')} />
-  //     //   )
-  //     // }
-  //   }
-  // }
-  // )
-
-  // function addCircle(evt: GestureResponderEvent) {
-  //   Alert.alert("Are you sure you want to add a landmark here?", undefined,
-  //     [{ text: "Cancel", onPress: () => console.log("Cancelled") }
-  //       ,
-  //     {
-  //       text: "Confirm", onPress: () => {
-  //         setShowModal(true)
-  //         setCoords([evt.nativeEvent.locationX, evt.nativeEvent.locationY])
-  //       }
-  //     }])
-  //   }
-
-  //   setCoords([evt.nativeEvent.locationX, evt.nativeEvent.locationY])
-  // }
 
   function addLandmark(evt: GestureResponderEvent) {
-    if (evt) {
+    if (evt != null) {
       Alert.alert("Are you sure you want to add a landmark here?", undefined,
-        [{ text: "Cancel", onPress: () => console.log("Cancelled") }
+        [{ text: "Cancel" }
           ,
         {
-          text: "Confirm", onPress: () => {
-            promptAddLandmark((evt.nativeEvent.locationX - imageDim / 2) / SVGdim[0], (evt.nativeEvent.locationY - imageDim / 2) / SVGdim[1], floor)
+          text: "Confirm", onPress: async () => {
+            try {
+              await promptAddLandmark((evt.nativeEvent.locationX - imageDim / 2) / SVGdim[0], (evt.nativeEvent.locationY - imageDim / 2) / SVGdim[1], floor)
+            }
+            catch (err) {
+              Toast.show("Please ensure finger is not moving when holding down on screen.", { duration: Toast.durations.LONG, })
+
+              // Alert.alert("An error has occured." , "Please ensure thumb is not moving when holding down on screen.")
+              // consider toast
+            }
           }
-        }])
+        }]
+      )
     }
   }
 
-  // *** IN-MEMORY IMPLEMENTATION ***
-  // function addCircleConfirmed() {
-  //   let newKey: number
-  //   if (indoorMarkers[floor].length == 0) {
-  //     newKey = floor * 100
-  //   }
-  //   else {
-  //     newKey = indoorMarkers[floor][indoorMarkers[floor].length - 1].key + 1
-  //   }
-  //   const newDot = { key: newKey, coordx: coords[0], coordy: coords[1], description: "filler", landmark: checked }
-  //   console.log(checked)
-  //   console.log(newDot)
-  //   indoorMarkers[floor].push(newDot)
-  //   setIndoorMarkers(indoorMarkers)
-  //   setShowAddedDot(true)
-  //   Alert.alert("Added Circle: coordinates are " + coords[0].toFixed(3) + " and " + coords[1].toFixed(3))
-  // }
-
-  function handleDelete(coordx: number, coordy: number, id: string) {
-    Alert.alert("Are you sure you want to delete this landmark?", undefined,
-      [{ text: "Cancel", onPress: () => console.log("Cancelled") }
-        ,
-      { text: "Confirm", onPress: () => console.log('delete') }])
-
-
-
-    //*** IN-MEMORY IMPLEMENTATION ***
-    // function handleDeleteConfirmed() {
-    //   const result = indoorMarkers[floor].filter(indoorMarker => indoorMarker['coordx'] != coordx && indoorMarker['coordy'] != coordy)
-    //   indoorMarkers[floor] = result
-    //   setIndoorMarkers(indoorMarkers);
-    //   setShowDots(true)
-    //   Alert.alert("Delete: Coordinates of deleted circle were " + coordx.toFixed(3) + " and " + coordy.toFixed(3))
-    //   console.log(result)
-    //   console.log(indoorMarkers[floor])
-    // }
-  }
-
   useEffect(() => {
     // Alert.alert("useEffect has been triggered")
     setShowAddedDot(false)
@@ -245,71 +92,100 @@ const IndoorMap: React.FC<IndoorMapProps> = ({ navigation, landmarks, promptAddL
     setTimeout(() => setShowME(true), 100);
   })
 
-  // useEffect(() => {
-  //   if (firstTime) {
-  //     setfirstTime(false) 
-  //     setIndoorMarkers(indoorCircles)  
-  //     console.log("SVGDIM set to: " + SVGdim)
-  //   }
-
-  // }, [SVGdim])
-
-
-  const compArray = [
-    <BasementC style={{ opacity: 0.7 }} height={"100%"} width={"100%"} />,
-    <FirstFloorC style={{ opacity: 0.7 }} height={"100%"} width={"100%"} />,
-    <SecondFloorC style={{ opacity: 0.7 }} height={"100%"} width={"100%"} />,
-    <ThirdFloorC viewBox='250 136 310 310' style={{ opacity: 0.7 }} height={"100%"} width={"100%"} />,
-    <FourthFloorC viewBox='260 151 270 260' style={{ opacity: 0.7 }} height={"100%"} width={"100%"} />,
-    <FifthFloorC viewBox='257 153 270 290' style={{ opacity: 0.7 }} height={"100%"} width={"100%"} />,
-  ]
-
-  // for (let i = 0; i < landmarks.length; i++) {
-  //   if (landmarks[i].floor == 1) {
-  //     console.log("The index that is on floor 1 is " + i)
-  //     console.log("The longitude is " + landmarks[i].longitude)
-  //     console.log("The latitude is " + landmarks[i].latitude)
-  //     console.log("The landmark_type is " + landmarks[i].landmark_type)
-  //   }
-  // }
+  const childToWeb = (child: any) => {
+    const { type, props } = child;
+    const name = type && type.displayName;
+    const webName = name && name[0].toLowerCase() + name.slice(1);
+    const Tag = webName ? webName : type;
+    return <Tag {...props}>{toWeb(props.children)}</Tag>;
+  };
+
+  const toWeb = (children: any) => React.Children.map(children, childToWeb);
+
+  function renderSvg() {
+    return (
+      <Svg height="100%" width="100%" style={{ backgroundColor: '#33AAFF' }}>
+        <Rect
+          x="50"
+          y="50"
+          width="50"
+          height="50"
+          fill="#3399ff"
+          strokeWidth="3"
+          stroke="rgb(0,0,0)"
+        />
+      </Svg>
+    )
+  }
+
+  function changer(num) {
+    setFloor(prevState => prevState + num)
+  }
 
   // TODO: wire up promptaddlandmark, applyfilters, and focuslandmark methods passed from MapNavigator
   return (
-    <View style={{ height: '100%', width: '100%', padding: 5 }}>
-      {/* {console.log("THE STATE IS NOW " + floor)} */}
+    <View style={{ height: '100%', width: Dimensions.get("screen").width, backgroundColor:colors.red }}>
 
-      <StatusBar backgroundColor="#121212" />
+      <StatusBar backgroundColor={colors.red} />
       <CustomModal />
-      <Text style={{ fontSize: 16, marginBottom: 5 }}>Please select a floor you would like to go to.</Text>
+      {/* <Text style={{ fontSize: 16, marginBottom: 5 }}>Please select a floor you would like to go to.</Text> */}
 
       <View style={{ borderColor: "blue", borderWidth: 0, maxHeight: 50, flex: 1, flexDirection: "row", justifyContent: "center", }}>
 
-      {floor == 0 ? arrowBut(0, "") : arrowBut(-1, "arrow-circle-o-left")}
+        {floor == 0 ? <ArrowButton num={0} fontAweIcon={""} /> : <ArrowButton num={-1} fontAweIcon={"chevron-left"} propEvent={() => changer(-1)} />}
+
+        <View style={{flex: 5, height: 53.5, width: 200 }}>
+          <Picker
+            placeholder={{}}
+            value={floor}
+            style={{ 
+              inputIOSContainer: {width:'70%', justifyContent: 'center', alignSelf: 'center'},
+              inputAndroid: {textAlign: 'center', color:"white", },
+              inputIOS: { color: 'white', textAlign: 'center', height: '100%', alignSelf: 'center'} ,
+              iconContainer: {height: '100%', justifyContent: 'center',}
+            }}
+            Icon={() => <FontAwesome name="chevron-down"  color='white' size={15} />}
+            onValueChange={(value) => {
+              setFloor(value)
+              setShowME(false)
+            }
+            }
+            items={[
+              { label: 'Basement', value: 0 },
+              { label: 'First Floor', value: 1 },
+              { label: 'Second Floor', value: 2 },
+              { label: 'Third Floor', value: 3 },
+              { label: 'Fourth Floor', value: 4 },
+              { label: 'Fifth Floor', value: 5 },
+            ]}
+          />
+        </View>
 
 
-        <Picker
-          style={{ backgroundColor: '#d9d9d9', width: 200, height: 50, flex: 5 }}
+        {/* <EricPicker
+          style={{ backgroundColor: colors.red, width: 200, height: 50, flex: 5, color: 'white' }}
           selectedValue={floor} // the text of what gets displayed on the dropdown header
           onValueChange={(itemValue, itemIndex: number) => {
             setFloor(itemIndex)
             setShowME(false)
           }}>
-          {/* The value in Picker.Item refers to selectedValue in Picker, which refers to the state "floor" */}
-          <Picker.Item label="Basement" value={0} />
-          <Picker.Item label="First Floor" value={1} />
-          <Picker.Item label="Second Floor" value={2} />
-          <Picker.Item label="Third Floor" value={3} />
-          <Picker.Item label="Fourth Floor" value={4} />
-          <Picker.Item label="Fifth Floor" value={5} />
-        </Picker>
+          The value in EricPicker.Item refers to selectedValue in EricPicker, which refers to the state "floor"
+          <EricPicker.Item label="Basement" value={0} />
+          <EricPicker.Item label="First Floor" value={1} />
+          <EricPicker.Item label="Second Floor" value={2} />
+          <EricPicker.Item label="Third Floor" value={3} />
+          <EricPicker.Item label="Fourth Floor" value={4} />
+          <EricPicker.Item label="Fifth Floor" value={5} />
+        </EricPicker> */}
 
-        {floor == 5 ? arrowBut(0, "") : arrowBut(1, "arrow-circle-o-right")}
 
-      </View>
+        {/* {floor == 5 ? arrowBut(0, "") : arrowBut(1, "chevron-right")} */}
+        {floor == 5 ? <ArrowButton num={0} fontAweIcon={""} /> : <ArrowButton num={1} fontAweIcon={"chevron-right"} propEvent={() => changer(1)} />}
 
 
+      </View>
 
-      <View style={{ flex: 1, alignItems: "center", height: '100%', width: '100%' }}>
+      <View style={{ flex: 1, alignItems: "center", height: '100%', width: '100%', borderColor: 'purple', borderWidth: 0 }}>
         <View style={styles.container}>
           {showME === false ?
             <View style={{ display: 'flex', flexDirection: 'row', justifyContent: "center", }}>
@@ -317,104 +193,37 @@ const IndoorMap: React.FC<IndoorMapProps> = ({ navigation, landmarks, promptAddL
             </View> :
 
             <ReactNativeZoomableView
-              zoomStep={2.8}
+              panBoundaryPadding={100}
+              // bindToBorders={false}
               bindToBorders={true}
+              zoomStep={2.8}
               // initialZoom={2.2}
               maxZoom={2.8}
               minZoom={1}
               initialOffsetY={5}
               onLongPress={(event) => {
-                console.log(event.nativeEvent)
+                // serialize()
                 addLandmark(event)
-              }}>
-              {/** IN-MEMORY IMPLEMENTATION */}
-              {/* <Modal transparent={true} visible={showModal2}>
-                <View style={{ backgroundColor: "#000000aa", flex: 1 }}>
-                <View style={{ backgroundColor: "#ffffff", margin: 50, padding: 20, borderRadius: 10, }}>
-                <Text style={{ fontSize: 18, marginBottom: 10 }}>Type of Landmark to Add:</Text>
-                
-                <View style={{ flexDirection: "row", alignItems: "center" }}>
-                <RadioButton
-                value="information"
-                status={checked === 'information' ? 'checked' : 'unchecked'}
-                onPress={() => setChecked('information')}
-                />
-                <Text style={{ fontSize: 16 }}>Information</Text>
-                </View>
-                
-                <View style={{ flexDirection: "row", alignItems: "center" }}>
-                <RadioButton
-                value="power"
-                status={checked === 'power' ? 'checked' : 'unchecked'}
-                onPress={() => setChecked('power')}
-                />
-                <Text style={{ fontSize: 16 }}>Power</Text>
-                </View>
-                
-                <View style={{ flexDirection: "row", alignItems: "center", marginBottom: 11 }}>
-                <RadioButton
-                value="stairs"
-                status={checked === 'stairs' ? 'checked' : 'unchecked'}
-                onPress={() => setChecked('stairs')}
-                />
-                <Text style={{ fontSize: 16 }}>Stairs</Text>
-                </View>
-                
-                <Button color={"red"} title='OK' onPress={() => {
-                  //addCircleConfirmed()
-                      setShowModal(false)
-                    }}></Button>
-                    </View>
-                    </View>
-                  </Modal> */}
+              }}
+              movementSensibility={3}
+              longPressDuration={200}
+              >
+
               <Svg onLayout={event => {
-                // console.log("OFFICIAL: " + event.nativeEvent.layout.width + " , " + event.nativeEvent.layout.height)
+                console.log("OFFICIAL: " + event.nativeEvent.layout.width + " , " + event.nativeEvent.layout.height)
                 setSVGdim([event.nativeEvent.layout.width, event.nativeEvent.layout.height])
-
-                // *** IN-MEMORY IMPLEMENTATION *** 
-                // TODO: change this mapping to apply to Landmark type
-                // DONE!!!
                 const transformedLandmarks = localLandmarks.map(item => {
                   return { ...item, coordx: item.longitude * event.nativeEvent.layout.width, coordy: item.latitude * event.nativeEvent.layout.height }
                 })
-                console.log("*this is within onLayout* SVGdim values are " + SVGdim[0] + " AND " + SVGdim[1])
+                //console.log("*this is within onLayout* SVGdim values are " + SVGdim[0] + " AND " + SVGdim[1])
                 setLocalLandmarks(transformedLandmarks)
-              }}>
-
+              }}
+              >
                 {/* {firstTime == true ? undefined : loadCircles} */}
-                {loadCircles}
-
-
-
-                {/* {console.log("Landmark[0]'s landmark type is now " + landmarks[0].landmark_type)}
-{console.log("Landmark[0]'s longitude (x) is now " + landmarks[0].longitude)}
-{console.log("Landmark[0]'s latitude (y) is now " + landmarks[0].latitude)}
-{console.log("Landmark[0]'s floor is now " + landmarks[0].floor)}
-{console.log(lmTypes[2]['image'] as ImageSourcePropType)} */}
-
-
-                {/* {console.log(landmarks.filter(lm => lm.floor == floor))} */}
-
-
-                {/* <Image
-                  // onPress={() => handleDelete(item.longitude, item.landmark_type)}
-                  onPress={() => { Alert.alert("A landmark has been tapped") }}
-                  key={landmarks[344].id}
-                  x={landmarks[344].longitude}
-                  y={landmarks[344].latitude}
-                  // y={item.latitude * SVGdim[1]}
-                  width={0.06 * Dimensions.get("window").width}
-                  height={0.06 * Dimensions.get("window").width}
-                  href={landmarks[344].landmark_type as ImageSourcePropType}
-                // href={require('./landmark_images/stairs.png')}
-                /> */}
-
-
-                {compArray[floor]}
+                <IndoorFloor floorNum={floor} />
+                {loadLandmarks}
               </Svg>
-
             </ReactNativeZoomableView>
-
           }
 
         </View>
@@ -424,12 +233,18 @@ const IndoorMap: React.FC<IndoorMapProps> = ({ navigation, landmarks, promptAddL
         setLocalLandmarks(landmarks)
       }} /> */}
 
-      <Button title="Go back to map" onPress={() => navigation.goBack()} />
+      {/* <Button title='Press me to svgString' color={colors.red} onPress={serialize}></Button> */}
+      {/* <View style={{ flex: 0.1, flexDirection: 'row', alignItems:'flex-end', justifyContent: 'space-around' , borderColor:'green' , borderWidth:0 ,}}>
+      <Button title="Go back to map" color={colors.red} onPress={() => navigation.goBack()} />
+      <Button title="Resources" color={colors.red} onPress={() => Linking.openURL('https://www.library.ualberta.ca/')} />
+      </View> */}
       {/* <TouchableOpacity style={styles.arrowButton} onPress={() => setFloor(prevState => prevState+1)} ><Text>Increase floor by 1</Text></TouchableOpacity> */}
 
 
+      {/* <BottomButtons navigation={navigation}/> */}
+      {/* <Text>{floor}</Text> */}
 
-    </View>
+    </View >
   );
 }
 
@@ -439,9 +254,9 @@ const styles = StyleSheet.create({
     // backgroundColor: "#fff",
     justifyContent: "center",
     borderColor: "black",
-    borderWidth: 2,
-    marginVertical: 7,
-    aspectRatio: 8 / 10,  // (caters to portrait mode) (width is 66% the value of height dimension)
+    borderWidth: 0,
+    marginVertical: 0,
+    aspectRatio: 8 / 10,  // (caters to portrait mode) (width is 80% the value of height dimension)
     // flex: 1,
     // // backgroundColor: "#fff",
     // justifyContent: "center",
@@ -451,7 +266,9 @@ const styles = StyleSheet.create({
     width: '100%',
     height: '100%',
     maxWidth: "100%",
-    maxHeight: "100%"
+    maxHeight: "100%",
+    backgroundColor:"white",
+    
   },
   image: {
     alignItems: 'center',
@@ -463,12 +280,11 @@ const styles = StyleSheet.create({
   },
   arrowButton: {
     flex: 1,
-    backgroundColor: "#d4d4d4",
+    backgroundColor: colors.red,
     // backgroundColor: "blue",
     height: 53.5,
     // borderRadius: 10,
-
-  }
+  },
 });
 
 

+ 16 - 3
src/components/Map/MainMapComponent/Map.styles.tsx

@@ -9,10 +9,23 @@ import { StyleSheet } from "react-native";
 import { colors } from "../../../utils/GlobalUtils";
 import { AppState, Platform, ScrollView, View, Image, Dimensions, } from "react-native"
 
+const mapButtonShadow = StyleSheet.create({
+    mapButtonShadow: {
+        shadowColor: "grey",
+        shadowOffset: {
+            width: 0,
+            height: 0,
+        },
+        shadowRadius: 2,
+        shadowOpacity: 1,
+        elevation: 10, 
+    },
+})
+
 const mapStyles = StyleSheet.create({
     lowerMapButton: {
+        ...mapButtonShadow.mapButtonShadow,
         backgroundColor: colors.red, 
-        elevation: 10, 
         right: 20, 
         position: 'absolute', 
         height: 40, width: 40, 
@@ -21,7 +34,7 @@ const mapStyles = StyleSheet.create({
         alignItems: 'center'
     },
     filterButtonOutdoor: {
-        elevation: 10, 
+        ...mapButtonShadow.mapButtonShadow,
         height: 40, 
         width: 40, 
         borderRadius: 20, 
@@ -30,7 +43,7 @@ const mapStyles = StyleSheet.create({
         backgroundColor: 'white', 
     },
     filterButtonIndoor: {
-        elevation: 5, 
+        ...mapButtonShadow.mapButtonShadow,
         height: 30, 
         width:  30, 
         borderRadius: 20, 

+ 204 - 183
src/components/Map/MainMapComponent/OutdoorMap.tsx

@@ -10,29 +10,22 @@ 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, { MutableRefObject, useEffect, useRef, useState } from "react";
-import { Alert, AppState, Image, Keyboard, Platform, TouchableOpacity, TouchableWithoutFeedback, View } from "react-native";
-import { ScrollView, State } from "react-native-gesture-handler";
-import MapView, { LatLng, Marker, Polygon, Region } from "react-native-maps";
-import { Chip } from "react-native-paper";
+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 { Landmark, useLandmarks } from "../../../hooks/useLandmarks";
-import { useProfile } from "../../../hooks/useProfile";
-import { AuthTabsParamList as AuthTabsParamList, AuthTabsNavigationProp } from "../../../navigation/AuthorizedNavigator";
-import { authStore } from "../../../libs/auth/AuthStore";
-import { NotifType } from "../../../types";
+import { Landmark } from '../../../data/landmarks';
+import { NotifType } from "../../../data/notifications";
+import { MainTabsNavigationProp, MainTabsParamList } from "../../../navigation/MainTabsNavigator";
+import { MapStackNavigationProp, MapStackParamList } from "../../../navigation/MapNavigator";
 import { checkVoicePermissions, colors, getMapPermissions, lmTypes } from "../../../utils/GlobalUtils";
 import Badge from "../../Badge";
 import { IconButton } from "../../Buttons";
-import AddLandmarkPanel from "../Panels/AddLandmarkPanel";
 import NearbyLandmarksPanel from "../Panels/NearbyLandmarksPanel";
-import { FilterPanel } from "../Panels/FilterPanel/FilterPanel";
-import LandmarkDetails from "../Panels/LandmarkDetailsPanel/LandmarkDetails";
 import { VoicePanel } from "../Panels/VoicePanel";
 import mapStyles from "./Map.styles";
-import { useMapState, useOutdoorMapState } from "./useMapState";
-import { MapStackNavigationProp, MapStackParamList } from "../../../navigation/MapNavigator";
+import { useOutdoorMapState } from "./useMapState";
 
 /**
  * An interface representing the user location retrieved from [expo-location]{@link https://docs.expo.dev/versions/latest/sdk/location/}.
@@ -46,7 +39,7 @@ export interface UserLocation {
 
 export type MapStackRouteProp = RouteProp<MapStackParamList, 'Outdoor'>;
 
-export type AuthTabsMapRouteProp = RouteProp<AuthTabsParamList, 'Map'>;
+export type AuthTabsMapRouteProp = RouteProp<MainTabsParamList, 'Map'>;
 
 /**
  * The screen component containing the Map and all related functionality. Uses [react-native-maps]{@link https://github.com/react-native-maps/react-native-maps}
@@ -55,8 +48,9 @@ export type AuthTabsMapRouteProp = RouteProp<AuthTabsParamList, 'Map'>;
  */
 
 interface OutdoorMapProps {
-    mapNavigation: MapStackNavigationProp, 
-    authNavigation: AuthTabsNavigationProp, 
+    mapNavigation: MapStackNavigationProp,
+    authNavigation: MainTabsNavigationProp,
+    authNavIndex: number,
     route: AuthTabsMapRouteProp,
     focusLandmark: (landmark: Landmark) => void,
     setSelectedLandmarkId: (id: string) => void,
@@ -71,17 +65,20 @@ interface OutdoorMapProps {
 }
 
 const OutdoorMap: React.FC<OutdoorMapProps> = (props) => {
-    const currentRegion = useRef<Region>()
-
     const mapState = useOutdoorMapState()
 
-    const navIndex = useNavigationState(state => state.index)
+    const mapNavIndex = useNavigationState(state => state)
+
+    const [coordinates] = useState([
+        { latitude: 53.527086340019856, longitude: -113.52358410971608, }, // Cameron library
+        { latitude: 53.52516024715472, longitude: -113.52154139033108, }, // University station
+    ]);
 
     /**
      * If the ReactNavigation route prop changes, check if it contains incoming selected landmarks, display them if there are. This will be triggered by incoming notifcations 
      * (See the AuthorizedNavigator page for the useEffect that will trigger this)
      */
-     useEffect(() => {
+    useEffect(() => {
         if (props.route?.params?.selectedLandmark) {
             props.setSelectedLandmarkId(props.route?.params?.selectedLandmark)
         }
@@ -91,41 +88,47 @@ const OutdoorMap: React.FC<OutdoorMapProps> = (props) => {
     }, [props.route])
 
     useEffect(() => {
-        mapState.setRefreshKey(Math.floor(Math.random() * 100))
-        console.log(currentRegion.current)
-        mapState.mapRef.current.animateToRegion(currentRegion.current)
-    }, [navIndex])
+        const toMap = mapNavIndex.index == 0 && props.authNavIndex == 0
+
+        if (toMap) mapState.setLoading(true)
+
+        setTimeout(() => {
+            mapState.setLoading(false)
+        }, 500);
+
+    }, [mapNavIndex, props.authNavIndex])
 
     /**
      * Toggle the lm details panel when a new selected landmark is detected (triggered by pressing on a map marker, or from the list of nearby landmarks)
      */
-     useEffect(() => {
+    useEffect(() => {
         console.log("[LandmarkDetails]: Landmark selected - " + props.selectedLandmarkId)
         if (props.selectedLandmarkId) {
             const landmark = props.landmarks.find(lm => lm.id == props.selectedLandmarkId)
-            mapState.mapRef.current.animateToRegion({latitude: landmark.latitude, longitude: landmark.longitude, latitudeDelta: 0.01, longitudeDelta: 0.01})
+            mapState.mapRef.current.animateToRegion({ latitude: landmark.latitude, longitude: landmark.longitude, latitudeDelta: 0.01, longitudeDelta: 0.01 })
             props.toggleLmDetails(true)
         }
+
     }, [props.selectedLandmarkId])
 
     /**
      * Move to pressed location when newlandmark changes
      */
-     useEffect(() => {
+    useEffect(() => {
         if (props.selectedLandmarkId) {
-            mapState.mapRef.current.animateToRegion({latitude: props.newLandmark.latitude, longitude: props.newLandmark.longitude, latitudeDelta: 0.01, longitudeDelta: 0.01})
+            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(() => {
+    useEffect(() => {
         const getSpeechPermissions = async () => {
             if (AppState.currentState == 'active') {
                 await getMapPermissions()
                 console.log('[Permissions]: Checking voice permissions...')
-                if (Platform.OS == 'android') {        
+                if (Platform.OS == 'android') {
                     const permitted = await checkVoicePermissions([PERMISSIONS.ANDROID.RECORD_AUDIO])
                     mapState.toggleVoicePermission(permitted)
                     if (permitted) console.log('[Permissions]: Voice permission granted')
@@ -137,86 +140,86 @@ const OutdoorMap: React.FC<OutdoorMapProps> = (props) => {
                     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('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')
-                    }
+    }, [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')
                 }
-            }
-            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')
-                    }
+                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')
                 }
             }
-            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)
+        }
+        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')
+                }
             }
-            updateLocationPermissionOnAppStateChange()
-        }, [AppState.currentState, mapState.bgroundLocationPermission, mapState.fgroundLocationPermission]) 
+        }
+        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 = () => {
+    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})
+            mapState.mapRef.current?.animateToRegion({ latitude: mapState.userLocation.latitude, longitude: mapState.userLocation.longitude, latitudeDelta: 0.01, longitudeDelta: 0.01 })
         }
     }
 
     /**
      * Activates speech recognition and opens the voice panel
      */
-     const startSpeech = () => {
+    const startSpeech = () => {
         props.toggleLmDetails(false);
         props.toggleLmAdd(false);
         Spokestack.activate()
@@ -226,11 +229,8 @@ const OutdoorMap: React.FC<OutdoorMapProps> = (props) => {
      * Gets initial region that map should zoom into from current user location
      */
     const getInitialRegion = () => {
-        if (mapState.userLocation && !currentRegion.current) {
-            return {latitude: mapState.userLocation.latitude, longitude: mapState.userLocation.longitude, latitudeDelta: 0.01, longitudeDelta: 0.01}
-        }
-        else {
-            return currentRegion.current
+        if (mapState.userLocation) {
+            return { latitude: mapState.userLocation.latitude, longitude: mapState.userLocation.longitude, latitudeDelta: 0.01, longitudeDelta: 0.01 }
         }
     }
 
@@ -238,9 +238,10 @@ const OutdoorMap: React.FC<OutdoorMapProps> = (props) => {
      * Method that runs every time user location changes, updates user location state in memory and checks if any landmarks are nearby
      */
     const updateLocation = async (coord: LatLng) => {
+
         mapState.setUserLocation(coord)
         // get 10m radius around user
-        const userAlertRadius = circle([coord.longitude, coord.latitude], 10, {units: 'meters'})
+        const userAlertRadius = circle([coord.longitude, coord.latitude], 10, { units: 'meters' })
 
         // check each landmark to see if its inside user radius. if it is, and it isn't already in the list of notified landmarks, add it
         const newLandmarksNearUser = props.landmarks?.filter(lm => {
@@ -250,7 +251,7 @@ const OutdoorMap: React.FC<OutdoorMapProps> = (props) => {
 
         // to prevent duplicate notifications make a list of landmarks that weren't previously near the user. 
         // these are the only ones that the user will be notified of
-        const newLandmarksNotPreviouslyNearUser = newLandmarksNearUser.filter(lm => mapState.landmarksNearUser.some(origLm => lm == origLm.id))
+        const newLandmarksNotPreviouslyNearUser = newLandmarksNearUser?.filter(lm => mapState.landmarksNearUser.some(origLm => lm == origLm.id))
 
         // update list
         mapState.setLandmarksNearUser(newLandmarksNearUser)
@@ -259,15 +260,15 @@ const OutdoorMap: React.FC<OutdoorMapProps> = (props) => {
         if (newLandmarksNotPreviouslyNearUser?.length > 0) {
             const body = newLandmarksNotPreviouslyNearUser.length > 1 ? "There are new landmarks near by. Tap here to view" : "There is a new landmark close by. Tap here to view"
             const notifType: NotifType = newLandmarksNotPreviouslyNearUser.length > 1 ? 'near-landmarks' : 'near-landmark'
-            const data = {notif_type: notifType, landmarks: newLandmarksNotPreviouslyNearUser.length == 1 ? newLandmarksNearUser : null}
+            const data = { notif_type: notifType, landmarks: newLandmarksNotPreviouslyNearUser.length == 1 ? newLandmarksNearUser : null }
             await Notifications.scheduleNotificationAsync({
                 content: {
-                  title: "⚠ Landmarks close by ⚠",
-                  body: body,
-                  data: data
+                    title: "⚠ Landmarks close by ⚠",
+                    body: body,
+                    data: data
                 },
                 trigger: { seconds: 2 },
-              });
+            });
         }
     }
 
@@ -275,7 +276,7 @@ const OutdoorMap: React.FC<OutdoorMapProps> = (props) => {
         if (mapState.landmarksNearUser?.length > 1) {
             mapState.toggleNearbyLmPanel(true)
         }
-        else if (mapState.landmarksNearUser?.length === 1) { 
+        else if (mapState.landmarksNearUser?.length === 1) {
             props.setSelectedLandmarkId(mapState.landmarksNearUser[0].id)
         }
     }
@@ -283,85 +284,105 @@ const OutdoorMap: React.FC<OutdoorMapProps> = (props) => {
     return (
         <TouchableWithoutFeedback>
             <>
-            {/*Main map component*/}
-            <MapView 
-                key={mapState.refreshKey}
-                toolbarEnabled={false}
-                onPress={() => Keyboard.dismiss()}
-                testID="mapView"
-                ref={mapState.mapRef} 
-                style={{width: '100%', height: '100%'}}
-                initialRegion={getInitialRegion()} 
-                onLongPress={(e) => props.promptAddLandmark(e.nativeEvent.coordinate.longitude, e.nativeEvent.coordinate.latitude)} 
-                showsUserLocation={mapState.locationPermitted} 
-                onUserLocationChange={e => updateLocation(e.nativeEvent.coordinate)}
-                onRegionChange={region => currentRegion.current = region}
-                followsUserLocation={mapState.followUser}
-                showsMyLocationButton={false}>
-            <Polygon // polygon for cameron library
-              coordinates={[
-                {latitude: 53.527190, longitude: -113.524205 },
-                {latitude: 53.526510, longitude: -113.524205 },
-                {latitude: 53.526510, longitude: -113.523452 },
-                {latitude: 53.527190, longitude: -113.523452 },
-                // { name: "5", latitude: 60, longitude: -105 },
-              ]}
-              fillColor={`rgba(100,100,200,0.3)`}
-              strokeWidth={2.5}
-              tappable={true}
-              onPress={() => props.mapNavigation.navigate("Indoor")}
-              />
-
-            {props.applyFilters(props.landmarks)?.map((landmark) => {
-                if(landmark.floor == null){
-                    let trackChanges = false;
-                    if (landmark?.id == props.selectedLandmarkId) {
-                        trackChanges = true;
+                {/*Main map component*/}
+                <Modal transparent={true} animationType="fade" visible={mapState.loading}>
+                    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: 'rgba(0,0,0,0.3)' }}>
+                        <View style={{ width: '60%', height: '30%', backgroundColor: colors.red, justifyContent: 'center', alignItems: 'center', borderRadius: 20 }}>
+                            <ActivityIndicator size="large" color="white" style={{ marginBottom: 20 }} />
+                            <Text style={{ fontSize: 15, color: 'white' }}>Refreshing</Text>
+                        </View>
+                    </View>
+                </Modal>
+                <MapView
+                    key={mapState.refreshKey}
+                    toolbarEnabled={false}
+                    onPress={() => Keyboard.dismiss()}
+                    testID="mapView"
+                    ref={mapState.mapRef}
+                    style={{ width: '100%', height: '100%' }}
+                    initialRegion={getInitialRegion()}
+                    onLongPress={async (e) => await props.promptAddLandmark(e.nativeEvent.coordinate.longitude, e.nativeEvent.coordinate.latitude)}
+                    showsUserLocation={mapState.locationPermitted}
+                    onUserLocationChange={e => updateLocation(e.nativeEvent.coordinate)}
+                    followsUserLocation={mapState.followUser}
+                    showsMyLocationButton={false}>
+                    <Polygon // polygon for cameron library
+                        coordinates={[
+                            { latitude: 53.527190, longitude: -113.524205 },
+                            { latitude: 53.526510, longitude: -113.524205 },
+                            { latitude: 53.526510, longitude: -113.523452 },
+                            { latitude: 53.527190, longitude: -113.523452 },
+                            // { name: "5", latitude: 60, longitude: -105 },
+                        ]}
+                        fillColor={`rgba(100,100,200,0.3)`}
+                        strokeWidth={2.5}
+                        tappable={true}
+                        onPress={() => props.mapNavigation.navigate("Indoor")}
+                    />
+
+                    {props.applyFilters(props.landmarks)?.map((landmark) => {
+                        if (landmark.floor == null) {
+                            let trackChanges = false;
+                            if (landmark?.id == props.selectedLandmarkId) {
+                                trackChanges = true;
+                            }
+                            return (
+                                <Marker
+                                    tracksViewChanges={trackChanges}
+                                    onPress={() => props.focusLandmark(landmark)}
+                                    key={landmark.id}
+                                    coordinate={{ latitude: landmark.latitude as number, longitude: landmark.longitude as number }} >
+                                    {landmark.landmark_type ? <Image style={{ height: 35, width: 25 }} source={lmTypes[landmark.landmark_type].image} /> : null}
+                                </Marker>)
+                        }
                     }
-                    return (
-                        <Marker 
-                        tracksViewChanges={trackChanges}
-                        onPress={() => props.focusLandmark(landmark)}
-                        key={landmark.id} 
-                        coordinate={{latitude: landmark.latitude as number, longitude: landmark.longitude as number}} >
-                        { landmark.landmark_type ? <Image style={{height: 35, width: 25}} source={lmTypes[landmark.landmark_type].image} /> : null}
-                    </Marker>)}
-                }
-            )}
-
-            </MapView>
-            
-            {/*Map buttons*/}
-            {mapState.landmarksNearUser?.length > 0 ? 
-            <TouchableOpacity style={[mapStyles.lowerMapButton, mapStyles.alertButton]} onPress={focusNearbyLandmarks}>
-                <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}
-            <IconButton size={20} color='white' style={[mapStyles.lowerMapButton, mapStyles.addLandmarkButton]} icon="plus" onPress={() => props.promptAddLandmark(mapState.userLocation.longitude, mapState.userLocation.latitude)}/>
-            <IconButton size={20} color='white' style={[mapStyles.lowerMapButton, mapStyles.userLocationButton]} icon="location-arrow" onPress={flyToUser}/>
-            <NearbyLandmarksPanel 
-                focusLandmark={props.focusLandmark}
-                nearbyLmPanelVisible={mapState.nearbyLmPanelVisible}
-                toggleAlertedLmPanel={mapState.toggleNearbyLmPanel}
-                nearbyLandmarks={mapState.landmarksNearUser}/>
-            {/*Map Panels*/}
-            {mapState.voicePermission && mapState.locationPermitted ? 
-            <VoicePanel 
-                landmarksNearby={mapState.landmarksNearUser.length > 0} 
-                toggleAlertedLandmarksVisible={mapState.toggleNearbyLmPanel}
-                navigation={props.authNavigation}
-                userCoords={{longitude: mapState.userLocation?.longitude, latitude: mapState.userLocation?.latitude}}
-                toggleVoiceVisible={mapState.toggleVoiceVisible} 
-                toggleLmDetails={props.toggleLmDetails}
-                setSelectedLandmarkId={props.setSelectedLandmarkId}
-                voiceVisible={mapState.voiceVisible}
-                newLandmark={props.newLandmark} 
-                setNewLandmark={props.setNewLandmark}
-                /> : null }
+                    )}
+
+
+                    {/* <MapViewDirections
+                        origin={coordinates[0]}
+                        destination={coordinates[1]}
+                        apikey={"AIzaSyBpckHhiuieLglacinLqewC_HfWkLehwWI"} // insert your API Key here
+                        strokeWidth={4}
+                        strokeColor="#111111"
+                    />
+                    <Marker coordinate={coordinates[1]} pinColor={colors.red} /> */}
+
+
+
+
+                </MapView>
+                {/*Map buttons*/}
+                {mapState.landmarksNearUser?.length > 0 ?
+                    <TouchableOpacity style={[mapStyles.lowerMapButton, mapStyles.alertButton]} onPress={focusNearbyLandmarks}>
+                        <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}
+                <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
+                    focusLandmark={props.focusLandmark}
+                    nearbyLmPanelVisible={mapState.nearbyLmPanelVisible}
+                    toggleAlertedLmPanel={mapState.toggleNearbyLmPanel}
+                    nearbyLandmarks={mapState.landmarksNearUser} />
+                {/*Map Panels*/}
+                {mapState.voicePermission && mapState.locationPermitted ?
+                    <VoicePanel
+                        landmarksNearby={mapState.landmarksNearUser?.length > 0}
+                        toggleAlertedLandmarksVisible={mapState.toggleNearbyLmPanel}
+                        navigation={props.authNavigation}
+                        userCoords={{ longitude: mapState.userLocation?.longitude, latitude: mapState.userLocation?.latitude }}
+                        toggleVoiceVisible={mapState.toggleVoiceVisible}
+                        toggleLmDetails={props.toggleLmDetails}
+                        setSelectedLandmarkId={props.setSelectedLandmarkId}
+                        voiceVisible={mapState.voiceVisible}
+                        newLandmark={props.newLandmark}
+                        setNewLandmark={props.setNewLandmark}
+                    /> : null}
             </>
-        </TouchableWithoutFeedback> )
+        </TouchableWithoutFeedback>)
 }
 
 export default observer(OutdoorMap);

BIN
src/components/Map/MainMapComponent/landmark_images/information.png


BIN
src/components/Map/MainMapComponent/landmark_images/power.png


BIN
src/components/Map/MainMapComponent/landmark_images/stairs.png


+ 6 - 1
src/components/Map/MainMapComponent/useMapState.ts

@@ -7,8 +7,8 @@
 
 import { useRef, useState } from "react";
 import { UserLocation } from "./OutdoorMap";
-import { Landmark } from "../../../hooks/useLandmarks";
 import MapView from "react-native-maps";
+import { Landmark } from "../../../data/landmarks";
 
 export const useMapState = () => {
     /**
@@ -72,6 +72,10 @@ export const useOutdoorMapState = () => {
      * Used to force refreshes
      */
      const [refreshKey, setRefreshKey] = useState<number>(Math.floor(Math.random() * 100));
+    /**
+     * Used to force refreshes
+     */
+     const [loading, setLoading] = useState<boolean>(false);
     /**
      * Holds the visibility state of the {@link AddLandmark} modal.
      */
@@ -115,6 +119,7 @@ export const useOutdoorMapState = () => {
     const mapRef = useRef<MapView>();
 
     return {
+        loading, setLoading,
         refreshKey, setRefreshKey,
         nearbyLmPanelVisible, toggleNearbyLmPanel,
         followUser, toggleFollowUser,

+ 242 - 116
src/components/Map/Panels/AddLandmarkPanel.tsx

@@ -6,19 +6,27 @@
  */
 
 import { FontAwesome } from "@expo/vector-icons";
-import * as ImagePicker from 'expo-image-picker';
 import { ImageInfo } from "expo-image-picker/build/ImagePicker.types";
-import React, { memo, useEffect, useState } from "react";
-import { ActivityIndicator, Dimensions, Image, Platform, SafeAreaView, Text, TextInput, TouchableOpacity, View } from 'react-native';
+import React, { memo, useEffect, useState, useRef } from "react";
+import { ActivityIndicator, Dimensions, Image, Platform, SafeAreaView, Text, TextInput, TouchableOpacity, View, ImageSourcePropType, Share, KeyboardEventName, Keyboard, StyleSheet, KeyboardAvoidingView, Alert } from 'react-native';
 import { ScrollView } from "react-native-gesture-handler";
 import Modal from 'react-native-modal';
-import { checkMultiple, PERMISSIONS, RESULTS } from "react-native-permissions";
 import Picker from 'react-native-picker-select';
-import { Landmark, LMPhoto, useLandmarks } from "../../../hooks/useLandmarks";
-import { colors, getMediaPermissions, lmTypes } from "../../../utils/GlobalUtils";
+import { Landmark, LMPhoto, useAddLandmark } from "../../../data/landmarks";
+import { colors, lmTypes as allLmTypes, lmTypesIndoor } from "../../../utils/GlobalUtils";
 import { IconButton, SecondaryButton } from "../../Buttons";
 import { PhotoPicker } from "../../PhotoPicker";
 import TouchOpaq from "./LandmarkDetailsPanel/TouchOpaq";
+import { Svg, Rect, Image as ImageSVG, Circle } from 'react-native-svg'
+import ReactDOMServer from 'react-dom/server'; //npm i --save-dev @types/react-dom
+
+
+import IndoorFloor from "../IndoorFloor";
+import ViewShot, { captureRef, captureScreen } from "react-native-view-shot";
+
+import { useNavigationState } from "@react-navigation/native"
+import LandmarkTypePicker from "../../LandmarkTypePicker";
+
 
 /**
  * Props for the {@link AddLandmarkPanel} component.
@@ -27,7 +35,7 @@ export interface AddLandmarkProps {
     /**
      * Whether the landmark is being added at the current users location
      */
-     landmarkAtCurrentLocation?: boolean;
+    landmarkAtCurrentLocation?: boolean;
     /**
      * The {@link landmark} object to be added.
      */
@@ -48,15 +56,69 @@ export interface AddLandmarkProps {
  * @component
  * @category Map
  */
-const AddLandmarkPanel: React.FC<AddLandmarkProps> = ({newLandmark, setNewLandmark, setVisible, visible}) => {
+const AddLandmarkPanel: React.FC<AddLandmarkProps> = ({ newLandmark, setNewLandmark, setVisible, visible }) => {
     const [photos, setPhotos] = useState<LMPhoto[]>([])
     const [photoSourceMenuOpened, togglePhotoSourceMenu] = useState<boolean>(false)
+    const [keyboardOpened, setKeyboardOpened] = useState<boolean>(false);
+
+    const addLandmarkMutation = useAddLandmark()
+
+    const navigationState = useNavigationState(state => state)
+    const [currentRoute, setCurrentRoute] = useState<string>()
+    useEffect(() => {
+        const currentRouteIndex = navigationState?.routes[0]?.state?.index
+        const currentRouteName = navigationState?.routes[0]?.state?.routeNames[currentRouteIndex]
+        setCurrentRoute(currentRouteName)
+    }, [navigationState])
+
+    
+    useEffect(() => {
+        let eventString = Platform.OS == "android" ? 'keyboardDidShow' : Platform.OS == "ios" ? 'keyboardWillShow' : null;
+        if (eventString) {
+            const validEventString: KeyboardEventName = eventString as KeyboardEventName;
+
+            const keyboardDidShowListener = Keyboard.addListener(
+                validEventString,
+                () => {
+                    setKeyboardOpened(true); // or some other action
+                }
+            );
+            const keyboardDidHideListener = Keyboard.addListener(
+                validEventString,
+                () => {
+                    setKeyboardOpened(false); // or some other action
+                }
+            );
 
-    const { 
-        addLandmarkAsync, 
-        resetAddLm, 
-        addLandmarkStatus, 
-    } = useLandmarks();
+            return () => {
+                keyboardDidHideListener.remove();
+                keyboardDidShowListener.remove();
+            };
+        }
+    }, []);
+
+    /**
+     * Returns a height for the modal depending on if an image is maximzed, if the keyboard is opened, and if the current landmark has photos associated with it
+     */
+    const determineModalHeight = () => {
+        if (keyboardOpened) {
+            return Dimensions.get("window").height * .45
+        }
+        else if (photos?.length > 0)
+            return Dimensions.get("window").height * .9
+        else
+            return Dimensions.get("window").height * .6
+    }
+    const capture: any = useRef();
+
+    const imgWidth = 346
+    const imgHeight = 448
+    const imageDim = 25
+    
+    let lmTypes = allLmTypes
+    if(currentRoute=="Indoor") {
+        lmTypes = lmTypesIndoor
+    }
 
     useEffect(() => {
         /**
@@ -65,22 +127,51 @@ const AddLandmarkPanel: React.FC<AddLandmarkProps> = ({newLandmark, setNewLandma
          * @memberOf AddLandmark
          */
         const resetAddMutationOnSuccess = () => {
-            if (addLandmarkStatus == 'success') {
-                resetAddLm();
+            if (addLandmarkMutation.isSuccess) {
+                addLandmarkMutation.reset()
             }
         }
         resetAddMutationOnSuccess();
-    }, [addLandmarkStatus]);
+    }, [addLandmarkMutation.status]);
 
     useEffect(() => {
-        resetAddLm();
+        addLandmarkMutation.reset()
     }, [visible]);
 
     /**
      * Calls {@link addLandmarkAsync} from {@link useLandmarks} to initate the process of adding a landmark, then closes the modal.
      */
     const submit = async () => {
-        await addLandmarkAsync({landmarkValue: newLandmark, photos: photos})
+        if (typeof newLandmark.floor === 'number') {
+            try {
+                const uri = await captureRef(capture, {
+                    format: "jpg",
+                    quality: 1,
+                    result: 'base64'
+                })
+
+                console.log("Image is", uri.substring(0, 100))
+                await addLandmarkMutation.mutateAsync({ landmarkValue: newLandmark, photos: photos, indoorLmLocImg: uri }); // pass it in here
+
+
+            } catch (error) {
+                console.error("Oops, snapshot failed", error)
+            }
+        }
+        else {
+            await addLandmarkMutation.mutateAsync({ landmarkValue: newLandmark, photos: photos })
+        }
+
+
+        // create svg content here, then pass it to addLandmarkAsync as the value of indoorLmLocImg
+        // if (typeof newLandmark.floor === 'number') {
+        //     let rectangle = serialize()
+        //     await addLandmarkAsync({ landmarkValue: newLandmark, photos: photos, indoorLmLocImg: rectangle }); // pass it in here
+        // }
+        // else {
+        //     await addLandmarkAsync({ landmarkValue: newLandmark, photos: photos });
+        // }
+
         close()
     }
 
@@ -96,7 +187,7 @@ const AddLandmarkPanel: React.FC<AddLandmarkProps> = ({newLandmark, setNewLandma
 
     const addPhoto = (result: ImageInfo) => {
         togglePhotoSourceMenu(false)
-        const photo: LMPhoto = {id: '', image_b64: 'data:image/png;base64,' + result.base64, height: result.height, width: result.width, landmark: ''}
+        const photo: LMPhoto = { id: '', image_b64: 'data:image/png;base64,' + result.base64, height: result.height, width: result.width, landmark: '' }
         setPhotos([...photos, photo])
     }
 
@@ -108,111 +199,146 @@ const AddLandmarkPanel: React.FC<AddLandmarkProps> = ({newLandmark, setNewLandma
         <Modal
             useNativeDriver={true}
             useNativeDriverForBackdrop={true}
-            
             testID="addLMModal"
-            avoidKeyboard={photos.length == 0}
+            avoidKeyboard={photos?.length > 0}
             onBackdropPress={close}
-            style={{justifyContent: "flex-end", height: '100%', margin: 0}}
+            style={{ flex: 0, justifyContent: "flex-end", height: '100%', margin: 0 }}
             isVisible={visible} >
-            <SafeAreaView style={{backgroundColor: colors.red, height: photos.length>0 ? Dimensions.get('window').height * .8 : Dimensions.get('window').height * .6}}>
-                {addLandmarkStatus == 'idle' ?
-                <>
-                    <View style={{
-                        justifyContent: 'space-between', 
-                        alignItems: 'center', 
-                        flexDirection: "row", 
-                        marginBottom: 15, 
-                        borderBottomWidth: 1, 
-                        borderBottomColor: 'white', 
-                        paddingHorizontal: 20, 
-                        paddingVertical: 10}}>
-                        <Text style={{color: 'white', fontSize: 15}}>Add landmark here?</Text>
-                        <TouchOpaq
-                            func={close}
-                            name={"close"}
-                            size={25}
-                            col={"white"}
-                        />
-
-                    </View>
-                    <ScrollView>
-                        <View style={{paddingHorizontal: 20, paddingBottom: 20 }}>
-                            <TextInput
-                                returnKeyType="done"
-                                blurOnSubmit={true}
-                                multiline={true} 
-                                style={{backgroundColor: 'white', textAlignVertical: 'top', paddingHorizontal: 10, paddingTop: 10, paddingBottom: 10, marginBottom: 20, height: 150}} 
-                                placeholder="Description"
-                                onChangeText={value => setNewLandmark({...newLandmark, description: value})}>
-                                {newLandmark?.description}
-                            </TextInput>
-                            <View style={{flexDirection: 'row'}}>
-                                <Picker
-                                    style={{
-                                        inputIOS: {color: 'white'}, 
-                                        inputAndroid: {color: 'white'},
-                                        viewContainer: {marginVertical: 5, flex: 1}, placeholder: {color: 'white'}}}
-                                    textInputProps={{placeholderTextColor: 'white', selectionColor: 'white'}}
-                                    Icon={() => <FontAwesome name="chevron-down" color='white' size={20} />}
-                                    placeholder={{label: "Select a landmark type...", value: 0}}
-                                    value={newLandmark?.landmark_type}
-                                    onValueChange={(value) => {
-                                        if (value) {
-                                            setNewLandmark({...newLandmark, landmark_type: value, title: lmTypes[value].label})
-                                        }
-                                        else {
-                                            setNewLandmark({...newLandmark, landmark_type: undefined, title: 'no title'})
-                                        }
-                                    }}
-                                    useNativeAndroidPickerStyle={true}
-                                    items={Object.keys(lmTypes)?.map(icon  => {
-                                        return (
-                                            {label: lmTypes[parseInt(icon)]?.label.toUpperCase(), value: icon, key: icon}
-                                        )})}
+
+            <KeyboardAvoidingView
+                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 ?
+                        <>
+                            <View style={{
+                                justifyContent: 'space-between',
+                                alignItems: 'center',
+                                flexDirection: "row",
+                                marginBottom: 15,
+                                borderBottomWidth: 1,
+                                borderBottomColor: 'white',
+                                paddingHorizontal: 20,
+                                paddingVertical: 10,
+                            }}
+                            onTouchStart={() => Keyboard.dismiss()}
+                            >
+                                <Text style={{ color: 'white', fontSize: 15 }}>Add landmark here?</Text>
+                                <TouchOpaq
+                                    func={close}
+                                    name={"close"}
+                                    size={25}
+                                    col={"white"}
                                 />
-                                {newLandmark?.landmark_type ? <Image style={{marginLeft: 20}} source={lmTypes[newLandmark.landmark_type].image}/>
-                                : null}
+
                             </View>
-                        </View>
-                        {newLandmark?.landmark_type ?
-                        <View style={{justifyContent: 'flex-end', flexDirection: 'row', paddingHorizontal: 20, marginTop: 5}}>
-                            {newLandmark.description && newLandmark.title ?
-                            <View style={{flexDirection: 'row' }}>
-                                <TouchableOpacity onPress={async () => await submit()}><Text style={{color: 'white', marginRight: 25}}>Add</Text></TouchableOpacity>
-                                <TouchableOpacity onPress={close}><Text style={{color: 'white',  marginRight: 25}}>Cancel</Text></TouchableOpacity>
-                                {photos.length == 0 ? <TouchableOpacity onPress={() => togglePhotoSourceMenu(true)}><Text style={{color: 'white'}}>Include photos</Text></TouchableOpacity> : null }
-                            </View> : null}
-                        </View> : null}
-                        {photos?.length ? 
-                        <View>
-                            <ScrollView style={{borderTopWidth: 1, borderColor: 'lightgray', paddingTop: 20, marginHorizontal: 20, flexDirection: 'row', marginBottom: 5, marginTop: 30}} horizontal={true}>
-                                {photos.map((photo, i) => {
-                                    return (
-                                        <View key={i} style={{marginHorizontal: 1, padding: 15}}>
-                                            <IconButton style={{position: 'absolute', top: 0, right: 0, zIndex: 10, }} icon="times-circle" color="lightgray" size={20} onPress={() => deletePhoto(i)} />
-                                            <Image style={{borderWidth: 1, alignSelf: 'center', height: 200, width: 200 * photo.width / photo.height}} source={{uri: photo.image_b64}} /> 
-                                        </View>
-                                    )
-                                })}
-                                {photos.length < 5 ? <IconButton style={{alignSelf: 'center', padding: 10, opacity: .5, marginLeft: 10}} color='white' size={30} icon="plus" onPress={() => togglePhotoSourceMenu(true)} /> : null}
+                            <ScrollView>
+                                <View style={{ paddingHorizontal: 20, paddingBottom: 20 }}>
+                                    <TextInput
+                                        returnKeyType="done"
+                                        blurOnSubmit={true}
+                                        multiline={true}
+                                        style={{ backgroundColor: 'white', textAlignVertical: 'top', paddingHorizontal: 10, paddingTop: 10, paddingBottom: 10, marginBottom: 20, height: 150 }}
+                                        placeholder="Description"
+                                        onChangeText={value => setNewLandmark({ ...newLandmark, description: value })}>
+                                        {newLandmark?.description}
+                                    </TextInput>
+                                    <View style={{ flexDirection: 'row' }}>
+                                        <LandmarkTypePicker 
+                                            placeholder={{ label: "Select a landmark type...", value: 0 }}
+                                            value={newLandmark?.landmark_type}
+                                            onValueChange={(value) => {
+                                                if (value) {
+                                                    setNewLandmark({ ...newLandmark, landmark_type: value, title: lmTypes[value].label })
+                                                }
+                                                else {
+                                                    setNewLandmark({ ...newLandmark, landmark_type: undefined, title: 'no title' })
+                                                }
+                                            }}
+                                            items={Object.keys(lmTypes)?.map(icon => {
+                                                return (
+                                                    { label: lmTypes[parseInt(icon)]?.label.toUpperCase(), value: icon, key: icon }
+                                                )
+                                            })}/>
+
+                                        {newLandmark?.landmark_type ? <Image style={{ marginLeft: 20 }} source={lmTypes[newLandmark.landmark_type].image} />
+                                            : null}
+                                    </View>
+                                </View>
+                                {newLandmark?.landmark_type ?
+                                    <View style={{ justifyContent: 'flex-end', flexDirection: 'row', paddingHorizontal: 20, marginTop: 5 }}>
+                                        {newLandmark.description && newLandmark.title ?
+                                            <View style={{ flexDirection: 'row' }}>
+                                                <TouchableOpacity onPress={async () => await submit()}><Text style={{ color: 'white', marginRight: 25 }}>Add</Text></TouchableOpacity>
+                                                <TouchableOpacity onPress={close}><Text style={{ color: 'white', marginRight: 25 }}>Cancel</Text></TouchableOpacity>
+                                                {photos.length == 0 ? <TouchableOpacity onPress={() => togglePhotoSourceMenu(true)}><Text style={{ color: 'white' }}>Include photos</Text></TouchableOpacity> : null}
+                                            </View> : null}
+                                    </View> : null}
+                                {photos?.length > 0 && !keyboardOpened ?
+                                    <View>
+                                        <ScrollView style={{ borderTopWidth: 1, borderColor: 'lightgray', paddingTop: 20, marginHorizontal: 20, flexDirection: 'row', marginBottom: 5, marginTop: 30 }} horizontal={true}>
+                                            {photos.map((photo, i) => {
+                                                return (
+                                                    <View key={i} style={{ marginHorizontal: 1, padding: 15 }}>
+                                                        <IconButton style={{ position: 'absolute', top: 0, right: 0, zIndex: 10, }} icon="times-circle" color="lightgray" size={20} onPress={() => deletePhoto(i)} />
+                                                        <Image style={{ borderWidth: 1, alignSelf: 'center', height: 200, width: 200 * photo.width / photo.height }} source={{ uri: photo.image_b64 }} />
+                                                    </View>
+                                                )
+                                            })}
+                                            {photos.length < 5 ? <IconButton style={{ alignSelf: 'center', padding: 10, opacity: .5, marginLeft: 10 }} color='white' size={30} icon="plus" onPress={() => togglePhotoSourceMenu(true)} /> : null}
+                                        </ScrollView>
+                                    </View> : null}
                             </ScrollView>
-                        </View> : null}
-                    </ScrollView>
-                </> :
-                <View style={{height: '100%', justifyContent: "space-evenly", alignItems: "center"}}>
-                    <Text style={{color: 'white', fontSize: 20}}>{
-                        addLandmarkStatus == "loading" ? 'Uploading landmark...' :
-                        addLandmarkStatus == "error" ? 'Something went wrong when trying to upload the landmark.' : null }
-                    </Text>
-                    {
-                        addLandmarkStatus == "loading" ? <ActivityIndicator color='white' size="large"/> :
-                        addLandmarkStatus == "error" ? <SecondaryButton text="Okay" onPress={close}/> : null
+                        </> :
+                        <View style={{ height: '100%', justifyContent: "space-evenly", alignItems: "center" }}>
+                            <Text style={{ color: 'white', fontSize: 20 }}>{
+                                addLandmarkMutation.isLoading ? 'Uploading landmark...' :
+                                    addLandmarkMutation.isError ? 'Something went wrong when trying to upload the landmark.' : null}
+                            </Text>
+                            {
+                                addLandmarkMutation.isLoading ? <ActivityIndicator color='white' size="large" /> :
+                                    addLandmarkMutation.isError ? <SecondaryButton text="Okay" onPress={close} /> : null
+                            }
+                        </View>}
+                </SafeAreaView>
+                <PhotoPicker multiple={true} menuType='alert' photoSourceMenuOpened={photoSourceMenuOpened} onReceivedPhotoResult={result => addPhoto(result)} cancel={() => togglePhotoSourceMenu(false)} />
+
+
+                <ViewShot style={{ width: imgWidth + 20, height: imgHeight + 20, position: 'absolute', right: -2000 }} ref={capture} >
+                    {/* {console.log("newLandmark is " + newLandmark)} */}
+                    {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>
+                        </View>
                     }
-                </View> }
-            </SafeAreaView>
-            <PhotoPicker multiple={true} menuType='alert' photoSourceMenuOpened={photoSourceMenuOpened} onReceivedPhotoResult={result => addPhoto(result)} cancel={() => togglePhotoSourceMenu(false)} />
+                </ViewShot>
+            </KeyboardAvoidingView>
         </Modal>
+
     )
 }
 
-export default memo(AddLandmarkPanel);
+const styles = StyleSheet.create({
+    container: {
+        aspectRatio: 8 / 10,  // (caters to portrait mode) (width is 80% the value of height dimension)
+        width: '100%',
+        height: '100%',
+        maxWidth: "100%",
+        maxHeight: "100%",
+        backgroundColor: "white",
+        padding: 10
+    }
+})
+
+
+
+export default memo(AddLandmarkPanel);

+ 52 - 26
src/components/Map/Panels/FilterPanel/FilterLmTypes.tsx

@@ -5,43 +5,69 @@
  * <dev@clicknpush.ca>, January 2022
  */
 
-import React from "react"
-import { View, Text } from "react-native"
+import React, { useEffect, useState } from "react";
+import { View, Text, ScrollView } from "react-native"
 import Select from "react-native-multiple-select"
-import { lmTypes } from "../../../../utils/GlobalUtils"
+import { lmTypes as lmTypess, lmTypesIndoor } from "../../../../utils/GlobalUtils"
+import { useNavigation, useNavigationState, useRoute } from "@react-navigation/native"
+
 
 interface FilterLmTypesProps {
     localFilterTypes: number[],
     setLocalFilterTypes: (types: number[]) => void
 }
 
+
+
+
 /**
  * Component that offers a selector for landmark types to be filtered
  */
-export const FilterLmTypes: React.FC<FilterLmTypesProps> = ({setLocalFilterTypes, localFilterTypes}) => {
+export const FilterLmTypes: React.FC<FilterLmTypesProps> = ({ setLocalFilterTypes, localFilterTypes }) => {
+
+    const navigationState = useNavigationState(state => state)
+    const [currentRoute, setCurrentRoute] = useState<string>()
+    useEffect(() => {
+        const currentRouteIndex = navigationState?.routes[0]?.state?.index
+        const currentRouteName = navigationState?.routes[0]?.state?.routeNames[currentRouteIndex]
+        setCurrentRoute(currentRouteName)
+    }, [navigationState])
+
+    let lmTypes = lmTypess
+
+    if (currentRoute == "Indoor") {
+        lmTypes = lmTypesIndoor
+    }
+
     return (
-        <View style={{marginBottom: 10, justifyContent: 'flex-start'}}>
-            <Text style={{marginRight: 10, marginBottom: 5}}>Landmark type:</Text>
-            <View style={{borderColor:"blue" , borderWidth:0, width: '100%', justifyContent: 'center'}}>
-                <Select
-                    styleRowList={{borderColor:"red" , borderWidth:0}}
-                    textColor='black'  
-                    itemTextColor='black'
-                    displayKey="label"
-                    uniqueKey="value"
-                    submitButtonText="Confirm"
-                    submitButtonColor='black'
-                    onSelectedItemsChange={(types) => {
-                        setLocalFilterTypes(types)
-                    }}
-
-                    selectedItems={localFilterTypes}
-                    items={Object.keys(lmTypes)?.map(icon => {
-                        return (
-                            {label: lmTypes[parseInt(icon)].label.toUpperCase(), value: parseInt(icon), key: icon}
-                        )})}>
-                </Select>
+        <View style={{ justifyContent: 'flex-start' }}>
+            <Text style={{ marginRight: 10, marginBottom: 5 }}>Landmark type:</Text>
+            <View style={{ borderColor: "blue", borderWidth: 0, width: '100%', justifyContent: 'center' }}>
+
+                {/* <ScrollView style={{height:"60%"}}> */}
+                    <Select
+                        styleRowList={{ borderColor: "red", borderWidth: 0 }}
+                        textColor='black'
+                        itemTextColor='black'
+                        displayKey="label"
+                        uniqueKey="value"
+                        submitButtonText="Confirm"
+                        submitButtonColor='black'
+                        onSelectedItemsChange={(types) => {
+                            setLocalFilterTypes(types)
+                        }}
+
+                        selectedItems={localFilterTypes}
+                        items={Object.keys(lmTypes)?.map(icon => {
+                            return (
+                                    {label: lmTypes[parseInt(icon)].label.toUpperCase(), value: parseInt(icon), key: icon }
+                            )
+                        })}>
+                    </Select>
+                {/* </ScrollView> */}
+
+                {/* {console.log("*FILTER PANEL* currentRoute is " + currentRoute)} */}
             </View>
-        </View> 
+        </View>
     )
 }

+ 4 - 9
src/components/Map/Panels/FilterPanel/FilterPanel.tsx

@@ -5,16 +5,11 @@
  * <dev@clicknpush.ca>, January 2022
  */
 
-import Slider from '@react-native-community/slider'
+import Checkbox from "@react-native-community/checkbox"
 import React, { useEffect, useState } from "react"
-import { Keyboard, Text, TextInput, TouchableOpacity, View } from "react-native"
+import { Keyboard, Text, TouchableOpacity, View } from "react-native"
 import Modal from 'react-native-modal'
-import Select from "react-native-multiple-select"
-import Checkbox from "@react-native-community/checkbox"
 import { SafeAreaView } from 'react-native-safe-area-context'
-import { useLandmarks } from '../../../../hooks/useLandmarks'
-import { lmTypes } from "../../../../utils/GlobalUtils"
-import { IconButton } from "../../../Buttons"
 import { Separator } from "../../../Separator"
 import { FilterLmTypes } from './FilterLmTypes'
 import { FilterMinRating } from './FilterMinRating'
@@ -125,9 +120,9 @@ export const FilterPanel: React.FC<FilterPanelProps> = (props) => {
 
     const OwnedFilter: React.FC = () => {
         return (
-            <View style={{flexDirection: 'row', alignItems: 'center', marginBottom: 10}} >
+            <View style={{flexDirection: 'row', paddingTop: 10, paddingBottom: 15}} >
                 <Text>Only show my landmarks:</Text>
-                <Checkbox value={localOwned} onValueChange={() => toggleLocalOwned(!localOwned)} boxType="square" style={{marginLeft: 20, width: 25, height: 25, marginBottom:10}} />
+                <Checkbox value={localOwned} onValueChange={() => toggleLocalOwned(!localOwned)} boxType="square" style={{marginLeft: 10, width: 20, height: 20}} />
             </View>
         )
     }

+ 5 - 3
src/components/Map/Panels/LandmarkDetailsPanel/CommentView.tsx

@@ -9,8 +9,8 @@ import { FontAwesome } from "@expo/vector-icons"
 import { format, parseISO } from "date-fns"
 import React from "react"
 import { TouchableOpacity, View, Text } from "react-native"
-import { LMComment } from "../../../../hooks/useComments"
-import { authStore } from "../../../../libs/auth/AuthStore"
+import { LMComment } from "../../../../data/comments"
+import { useAuth } from "../../../../data/Auth/AuthContext"
 
 /**
  * Props for the {@link Comment} component.
@@ -34,6 +34,8 @@ import { authStore } from "../../../../libs/auth/AuthStore"
  * @component
  */
  export const CommentView: React.FC<CommentProps> = ({comment, selected, focusComment: selectComment, startEditingComment: startEditingComment, deleteComment}) => {
+    const {userId} = useAuth()
+
     return (
         <TouchableOpacity style={[{paddingHorizontal: 10}, selected ? {backgroundColor: '#E8E8E8'}: null]} onPress={() => selectComment(comment.id)}>
             <View style={{paddingTop: 10,  flexDirection: 'row', justifyContent: 'space-between'}}>
@@ -44,7 +46,7 @@ import { authStore } from "../../../../libs/auth/AuthStore"
                 <Text style={{paddingBottom: 10}} >{comment.content}</Text>
                 <View style={{flexDirection: 'row', alignSelf: 'flex-end'}}>
                     {comment.edited ? <Text style={{color: 'grey', alignSelf: 'flex-end'}}>Edited</Text> : null}
-                    {selected && comment.poster == authStore.userId ?
+                    {selected && comment.poster == userId ?
                     <View style={{marginTop: 10, flexDirection: 'row', alignSelf: 'flex-end'}}>
                         <FontAwesome size={25} name="edit" style={{paddingTop: 1, marginLeft: 20}} onPress={() => startEditingComment(comment)}/>
                         <FontAwesome color="red" size={25} style={{marginLeft: 15}} name="trash" onPress={() => deleteComment(comment.id)}/>

+ 16 - 34
src/components/Map/Panels/LandmarkDetailsPanel/CommentsContainer.tsx

@@ -8,7 +8,11 @@
 import { FontAwesome } from "@expo/vector-icons";
 import React, { MutableRefObject } from "react";
 import { FlatList, Keyboard, ListRenderItem, StyleSheet, Text, TextInput, View } from "react-native";
-import { LMComment } from "../../../../hooks/useComments";
+import { useAuth } from "../../../../data/Auth/AuthContext";
+import { LMComment } from "../../../../data/comments";
+import { MainTabsNavigationProp } from "../../../../navigation/MainTabsNavigator";
+import { navigate } from "../../../../navigation/RootNavigator";
+import { PrimaryButton, SecondaryButton } from "../../../Buttons";
 import { CommentView } from "./CommentView";
 
 interface CommentsContainerProps {
@@ -27,12 +31,16 @@ interface CommentsContainerProps {
     commentTextInputRef: MutableRefObject<TextInput>
     setKeyboardOpened: (state: boolean) => void
     keyboardOpened: boolean
+    authNavigation: MainTabsNavigationProp
+    toggleLmDetails: (state: boolean) => void
 }
 
 /**
  * Renders all [comments]{@link LMComment} associated with the {@linkcode selectedLandmark} as items for the [FlatList]{@link https://reactnative.dev/docs/flatlist} in this component.
 */
 export const CommentsContainer: React.FC<CommentsContainerProps> = (props) => {
+    
+    const {accessToken} = useAuth()
 
     /**
      * Flatlist render item method for each comment. 
@@ -52,38 +60,6 @@ export const CommentsContainer: React.FC<CommentsContainerProps> = (props) => {
         );
     };
 
-    /**
-     * || DEPRECATED ||
-     * Sets a flag that tracks whether keyboard is shown
-     */
-    // React.useEffect(() => {
-    //     if (Platform.OS == 'android') {
-    //         Keyboard.addListener('keyboardDidShow', () => props.setKeyboardOpened(true))
-    //         Keyboard.addListener('keyboardDidHide', clearStateOnKeybordDismiss);
-    //     }
-    
-    //     // cleanup function
-    //     return () => {
-    //       Keyboard.removeAllListeners('keyboardDidHide');
-    //       Keyboard.removeAllListeners('keyboardDidShow');
-    //     };
-    //   }, []);
-
-    /**
-     * || DEPRECATED ||
-     * Clears out the comment input if keyboard is dismissed without posting the comment **DEPRECATED**
-     */
-    // const clearStateOnKeybordDismiss = () => {
-    //     props.setKeyboardOpened(false)
-    //     props.setCommentBeingEdited(undefined);
-    //     if (props.commentBeingEdited) {
-    //         props.setCommentBeingEdited(undefined);
-    //     }
-    //     if (props.newCommentId) {
-    //         props.setNewComment('');
-    //     }
-    // }
-
     /**
      * Simple check to see if the landmark has any comments
      */
@@ -106,7 +82,9 @@ export const CommentsContainer: React.FC<CommentsContainerProps> = (props) => {
                 style={{backgroundColor: 'white'}}
                 getItemLayout={(data, index) => ({length: props.comments.length, offset: props.comments.length * index, index})}/>
         </> : 
-        <Text style={{marginVertical: 20, color: 'white'}}>Be the first to comment on this landmark!</Text> }
+        <Text style={{marginVertical: 20, color: 'white'}}>There are no comments on this landmark</Text> }
+        {accessToken ?
+        <>
         <View style={{height: 1, borderBottomWidth: 1, borderColor: 'lightgray'}}></View>
         <View style={{flexDirection: 'row', backgroundColor: 'white', paddingRight: 15}}>
             <TextInput 
@@ -125,6 +103,10 @@ export const CommentsContainer: React.FC<CommentsContainerProps> = (props) => {
                 <FontAwesome name="paper-plane" size={20} style={{ backgroundColor: 'white', padding: 'auto'}} onPress={async () => props.addComment()} /> }
             </View> : null}
         </View>
+        </> : 
+        <View>
+            <SecondaryButton text="Login to add comments" onPress={() => {props.toggleLmDetails(false); props.authNavigation.navigate("Account")}} style={{marginBottom: 20}}/>
+        </View>}
     </View>)
 }
 

+ 36 - 22
src/components/Map/Panels/LandmarkDetailsPanel/DetailsBody.tsx

@@ -6,20 +6,22 @@
  */
 
 import { FontAwesome } from "@expo/vector-icons";
-import { ImageInfo } from "expo-image-picker/build/ImagePicker.types";
+import { useNavigationState } from "@react-navigation/native";
 import React, { MutableRefObject, useEffect, useState } from "react";
-import { ActivityIndicator, FlatList, Image, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View } from "react-native";
+import { FlatList, Image, ScrollView, StyleSheet, Text, TextInput, View } from "react-native";
 import Picker from "react-native-picker-select";
 import { QueryStatus } from "react-query";
-import { LMComment } from "../../../../hooks/useComments";
-import { Landmark, LMPhoto, useLandmarks } from "../../../../hooks/useLandmarks";
-import { lmTypes } from "../../../../utils/GlobalUtils";
-import { IconButton, PrimaryButton } from "../../../Buttons";
-import { PhotoPicker } from "../../../PhotoPicker";
+import { LMComment } from "../../../../data/comments";
+import { Landmark, LMPhoto } from "../../../../data/landmarks";
+import { MainTabsNavigationProp } from "../../../../navigation/MainTabsNavigator";
+import { lmTypes as allLmTypes, lmTypesIndoor } from "../../../../utils/GlobalUtils";
+import LandmarkTypePicker from "../../../LandmarkTypePicker";
 import { Separator } from "../../../Separator";
 import { CommentsContainer } from "./CommentsContainer";
 import { LandmarkPhotos } from "./LandmarkPhotos";
 
+
+
 interface DetailsBodyProps {
     editingEnabled: boolean,
     updatedLandmark?: Landmark,
@@ -49,6 +51,7 @@ interface DetailsBodyProps {
     profileId: string
     processingPhoto: boolean
     setProcessingPhoto: (state: boolean) => void
+    authNavigation: MainTabsNavigationProp
 }
 
 /**
@@ -56,6 +59,21 @@ interface DetailsBodyProps {
 */
 export const DetailsBody: React.FC<DetailsBodyProps> = (props) => {
 
+    const navigationState = useNavigationState(state => state)
+    const [currentRoute, setCurrentRoute] = useState<string>()
+    useEffect(() => {
+        const currentRouteIndex = navigationState?.routes[0]?.state?.index
+        const currentRouteName = navigationState?.routes[0]?.state?.routeNames[currentRouteIndex]
+        setCurrentRoute(currentRouteName)
+    }, [navigationState])
+
+
+    let lmTypes = allLmTypes
+    if(currentRoute=="Indoor") {
+        lmTypes = lmTypesIndoor
+    }
+
+
     useEffect(() => {
         if (props.editingEnabled) {
             console.log("[LandmarkDetails]: Editing is enabled")
@@ -70,28 +88,21 @@ export const DetailsBody: React.FC<DetailsBodyProps> = (props) => {
      * Sub-component that renders picker for landmark types
      * @param 
      */
-    const LandmarkTypePicker: React.FC = () => {
+    const LandmarkTypePickerContainer: React.FC = () => {
         return (
         <View style={{flexDirection: 'row', marginBottom: 20, justifyContent: "space-between"}}>
             {props.updatedLandmark?.landmark_type ? 
             <>
-                <Picker
-                    style={{
-                        inputIOS: {color: 'white'}, 
-                        inputAndroid: {color: 'white'},
-                        iconContainer: {flex: 1, justifyContent: 'center', height: '100%'},
-                        viewContainer: {padding: 5, elevation: 1, flex: 1, justifyContent: 'center'}, placeholder: {color: 'white'}}}
+                <LandmarkTypePicker 
                     placeholder={{}}
-                    value={props.updatedLandmark?.landmark_type}
+                    value={props.updatedLandmark?.landmark_type} 
                     onValueChange={(value) => {
                         props.setUpdatedLandmark({...props.updatedLandmark, landmark_type: value, title: lmTypes[value].label})
-                    }}
-                    useNativeAndroidPickerStyle={true}
+                    }}  
                     items={Object.keys(lmTypes)?.filter(icon => parseInt(icon) != props.landmark?.landmark_type).map(icon => {
-                        console.log(icon)
                         return (
                             {label: lmTypes[parseInt(icon)].label.toUpperCase(), value: icon, key: icon}
-                        )})} />
+                        )})}/>
                 {props.updatedLandmark ? <Image style={{marginLeft: 20}} source={lmTypes[props.updatedLandmark?.landmark_type].image}/> : null}
             </>
             : null}
@@ -112,7 +123,7 @@ export const DetailsBody: React.FC<DetailsBodyProps> = (props) => {
                         <Text style={{color: 'white', fontSize: 13}}>{props.landmark?.description}</Text>
                     </ScrollView>
                 </View>
-                {props.landmark?.landmark_type ? <Image source={lmTypes[props.landmark?.landmark_type].image} /> : null}
+                {props.landmark?.landmark_type ? <Image source={lmTypes[props.landmark?.landmark_type]?.image} /> : null}
             </View>
         )
     }
@@ -121,9 +132,10 @@ export const DetailsBody: React.FC<DetailsBodyProps> = (props) => {
         <ScrollView nestedScrollEnabled={true} contentContainerStyle={{justifyContent: 'space-between'}} style={{flex: 1, marginHorizontal: 20}}>
             {props.editingEnabled ?
             <>
-                <LandmarkTypePicker />
+                <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} 
@@ -134,6 +146,8 @@ export const DetailsBody: React.FC<DetailsBodyProps> = (props) => {
             </>: <EditingDisabledUpperView />}
             {!props.editingEnabled ?
             <CommentsContainer
+                toggleLmDetails={props.toggleLmDetails}
+                authNavigation={props.authNavigation}
                 setKeyboardOpened={props.setKeyboardOpened}
                 keyboardOpened={props.keyboardOpened}
                 comments={props.comments}
@@ -149,7 +163,7 @@ export const DetailsBody: React.FC<DetailsBodyProps> = (props) => {
                 editComment={props.editComment}
                 startEditingComment={props.startEditingComment}
                 deleteComment={props.deleteComment} /> : null}
-            {!props.editingEnabled ?
+            {!props.editingEnabled && !props.keyboardOpened ?
             <LandmarkPhotos 
                 profileId={props.profileId}
                 deletePhotoStatus={props.deletePhotoStatus}

+ 14 - 10
src/components/Map/Panels/LandmarkDetailsPanel/DetailsHeader.tsx

@@ -7,13 +7,12 @@
 
 import { FontAwesome } from "@expo/vector-icons";
 import React from "react";
-import { View, Text, TouchableOpacity, StyleSheet, } from "react-native";
+import { Alert, StyleSheet, Text, TouchableOpacity, View } from "react-native";
 import { QueryStatus } from "react-query";
-import { Landmark } from "../../../../hooks/useLandmarks";
-import { UserProfile } from "../../../../hooks/useProfile";
-import { authStore } from "../../../../libs/auth/AuthStore";
-import { colors } from "../../../../utils/GlobalUtils";
-import TouchOpaq from './TouchOpaq'
+import { useAuth } from "../../../../data/Auth/AuthContext";
+import { Landmark } from "../../../../data/landmarks";
+import { UserProfile } from "../../../../data/profiles";
+import TouchOpaq from './TouchOpaq';
 
 interface DetailsHeaderProps {
     landmark?: Landmark,
@@ -23,12 +22,12 @@ interface DetailsHeaderProps {
     editLandmark: () => void,
     removeLandmark: () => void,
     toggleDetailsPanel: (state: boolean) => void,
-    landmarkRatedByUser: boolean,
     profile?: UserProfile,
     rateLandmark: (rating: 1 | -1) => void
     processingPhoto: boolean,
     addPhotoStatus: QueryStatus
     deletePhotoStatus: QueryStatus
+    ratedByUser: boolean
 }
 
 /**
@@ -36,6 +35,8 @@ interface DetailsHeaderProps {
  * @param 
  */
 export const DetailsHeader: React.FC<DetailsHeaderProps> = (props) => {
+    const {landmarkOwnedByUser, anonUserId} = useAuth()
+
     const photosAreBusy = () => {
         return props.processingPhoto ||
             props.addPhotoStatus == "loading" ||
@@ -44,7 +45,7 @@ export const DetailsHeader: React.FC<DetailsHeaderProps> = (props) => {
 
     const HeaderContent: React.FC = () => {
         // landmark is owned by user
-        if (authStore.userId == props.landmark?.user) {
+        if (landmarkOwnedByUser(props.landmark)) {
             // editing is enabled
             if (props.editingEnabled) {
                 return (
@@ -113,7 +114,7 @@ export const DetailsHeader: React.FC<DetailsHeaderProps> = (props) => {
         else {
             return (
                 <View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: "center", width: '100%' }}>
-                    {props.landmarkRatedByUser ?  // landmark has already been liked by the current user
+                    {props.ratedByUser ?  // landmark has already been liked by the current user
                         <View style={{ flexDirection: 'row' }} >
                             <Text style={{ color: 'white', fontSize: 20, marginTop: 2 }} >{props.landmark.rating}</Text>
                             <FontAwesome style={{ marginLeft: 5, marginTop: 2, marginRight: 30 }} color="white" size={25} name="thumbs-up" />
@@ -125,7 +126,10 @@ export const DetailsHeader: React.FC<DetailsHeaderProps> = (props) => {
                             </TouchableOpacity>
                         </View> : // landmark has not been liked by user
                         <TouchableOpacity onPress={async () => { //this touchable will add a like to this landmark
-                            if (props.profile?.id !== props.landmark?.id) {
+                            if (anonUserId) {
+                                Alert.alert("You must be logged in to rate landmarks.");
+                            }
+                            if (landmarkOwnedByUser(props.landmark)) {
                                 await props.rateLandmark(1);
                             }
                         }}>

+ 88 - 100
src/components/Map/Panels/LandmarkDetailsPanel/LandmarkDetails.tsx

@@ -10,14 +10,15 @@ import React, { memo, useEffect, useRef, useState } from "react";
 import { ActivityIndicator, Alert, Dimensions, FlatList, Image, Keyboard, Platform, SafeAreaView, StyleSheet, Text, TextInput, TouchableOpacity, View } from "react-native";
 import { ScrollView } from "react-native-gesture-handler";
 import Modal from 'react-native-modal';
-import { LMComment, useComments } from "../../../../hooks/useComments";
-import { Landmark, LMPhoto, useLandmarks } from "../../../../hooks/useLandmarks";
-import { useProfile } from "../../../../hooks/useProfile";
-import { authStore } from "../../../../libs/auth/AuthStore";
+import { LMComment, useAddComment, useDeleteComment, useEditComment, useLandmarkComments } from "../../../../data/comments";
+import { Landmark, useAddLandmarkPhoto, useDeleteLandmark, useDeleteLandmarkPhoto, useEditLandmark, useLandmark, useRateLandmark } from "../../../../data/landmarks";
+import { useAuth } from "../../../../data/Auth/AuthContext";
 import { colors } from "../../../../utils/GlobalUtils";
 import { IconButton, PrimaryButton } from "../../../Buttons";
 import { DetailsBody } from "./DetailsBody";
 import { DetailsHeader } from "./DetailsHeader";
+import { useOwnedProfile } from "../../../../data/profiles";
+import { MainTabsNavigationProp } from "../../../../navigation/MainTabsNavigator";
 
 /**
  * Props for the {@link LandmarkDetails} component.
@@ -45,6 +46,7 @@ export interface LandmarkDetailsProps {
     editingEnabled: boolean,
     visible: boolean,
     toggleLmDetails: (state: boolean) => void
+    authNavigation: MainTabsNavigationProp
 }
 
 /**
@@ -52,7 +54,9 @@ export interface LandmarkDetailsProps {
  * @component
  * @category Map
  */
-const LandmarkDetails: React.FC<LandmarkDetailsProps> = ({landmarkId, setLandmark, toggleDetailsPanel, setEditing, editingEnabled, visible, toggleLmDetails}) => {
+const LandmarkDetails: React.FC<LandmarkDetailsProps> = ({authNavigation, landmarkId, setLandmark, toggleDetailsPanel, setEditing, editingEnabled, visible, toggleLmDetails}) => {
+    const {userId, landmarkOwnedByUser} = useAuth()
+
     // /**
     //  * Holds the state of the {@link Landmark} being displayed.
     //  */
@@ -88,26 +92,18 @@ const LandmarkDetails: React.FC<LandmarkDetailsProps> = ({landmarkId, setLandmar
     const [photosBusy, setPhotosBusy] = useState<boolean>(false)
     const [processingPhoto, setProcessingPhoto] = useState<boolean>(false)
 
-    const { 
-        updateLandmark, updateLandmarkStatus, resetUpdateLm,
-        rateLandmarkAsync, rateLandmarkStatus, resetRateLandmark, rating,
-        deleteLandmark, deleteLandmarkStatus, resetDeleteLm,
-        landmarkRatedByUser, refetchCheckIfRatedByUser,
-        landmark, getLandmarkStatus, refetchLandmark,
-        addPhoto, addPhotoStatus, resetAddPhoto,
-        deletePhoto, deletePhotoStatus, resetDeletePhoto 
-    } = useLandmarks({
-        landmarkId: landmarkId,
-        userLMPairing: {userId: authStore.userId, landmarkId: landmarkId}
-    });
-
-    const {
-        comments, 
-        addCommentAsync, addCommentStatus, resetAddComment,
-        updateCommentAsync, updateCommentStatus, resetUpdateComment,
-        deleteCommentAsync, deleteCommentStatus, resetDeleteComment,
-    } = useComments(landmarkId)
-    const { profile } = useProfile(authStore.userId)
+    const landmarkQuery = useLandmark(landmarkId)
+    const editLandmarkMutation = useEditLandmark()
+    const rateLandmarkMutation = useRateLandmark()
+    const deleteLandmarkMutation = useDeleteLandmark()
+    const addLandmarkPhotoMutation = useAddLandmarkPhoto()
+    const deleteLandmarkPhotoMutation = useDeleteLandmarkPhoto()
+    
+    const commentsQuery = useLandmarkComments(landmarkId)
+    const addCommentMutation = useAddComment()
+    const editCommentMutation = useEditComment()
+    const deleteCommentMutation = useDeleteComment()
+    const { profile } = useOwnedProfile()
 
     /**
      * Holds a reference to the Flatlist containing the comments.
@@ -120,13 +116,13 @@ const LandmarkDetails: React.FC<LandmarkDetailsProps> = ({landmarkId, setLandmar
 
     useEffect(() => {
     const keyboardDidShowListener = Keyboard.addListener(
-      'keyboardDidShow',
+      'keyboardWillShow',
       () => {
         setKeyboardOpened(true); // or some other action
       }
     );
     const keyboardDidHideListener = Keyboard.addListener(
-      'keyboardDidHide',
+      'keyboardWillHide',
       () => {
         setKeyboardOpened(false); // or some other action
       }
@@ -145,18 +141,18 @@ const LandmarkDetails: React.FC<LandmarkDetailsProps> = ({landmarkId, setLandmar
          * @memberOf LandmarkDetails
          */
          const resetUpdateLandmark = async () => {
-            if (rateLandmarkStatus == 'success') {
-                resetRateLandmark();
-                await refetchLandmark()
+            if (rateLandmarkMutation.isSuccess) {
+                rateLandmarkMutation.reset()
+                await landmarkQuery.refetch()
             }
 
-            if (updateLandmarkStatus == 'success') {
-                resetUpdateLm();
-                await refetchLandmark()
+            if (editLandmarkMutation.isSuccess) {
+                editLandmarkMutation.reset()
+                await landmarkQuery.refetch()
             }
         }
         resetUpdateLandmark();
-    }, [rateLandmarkStatus, updateLandmarkStatus]);
+    }, [rateLandmarkMutation.status, editLandmarkMutation.status]);
 
     useEffect(() => {
         /**
@@ -165,14 +161,14 @@ const LandmarkDetails: React.FC<LandmarkDetailsProps> = ({landmarkId, setLandmar
          * @memberOf LandmarkDetails
          */
          const resetUpdateComment = () => {
-            if (updateCommentStatus == 'success') {
+            if (editCommentMutation.isSuccess) {
                 commentListRef.current?.scrollToItem({animated: true, item: commentBeingEdited});
                 setCommentBeingEdited(undefined)
                 Keyboard.dismiss();
             }
         }
         resetUpdateComment();
-    }, [updateCommentStatus]);
+    }, [editCommentMutation.status]);
 
     useEffect(() => {
         /**
@@ -181,12 +177,12 @@ const LandmarkDetails: React.FC<LandmarkDetailsProps> = ({landmarkId, setLandmar
          * @memberOf LandmarkDetails
          */
         const resetDeleteLMOnSuccess = () => {
-            if (deleteLandmarkStatus == 'success') {
-                resetDeleteLm();
+            if (deleteLandmarkMutation.isSuccess) {
+                deleteLandmarkMutation.reset()
             }
         }
         resetDeleteLMOnSuccess();
-    }, [deleteLandmarkStatus]);
+    }, [deleteLandmarkMutation.status]);
 
     useEffect(() => {
         /**
@@ -195,15 +191,15 @@ const LandmarkDetails: React.FC<LandmarkDetailsProps> = ({landmarkId, setLandmar
          * @memberOf LandmarkDetailsLandmark
          */
         const resetAddCommentOnSuccess = () => {
-            if (addCommentStatus == 'success') {
-                resetAddComment();
+            if (addCommentMutation.isSuccess) {
+                addCommentMutation.reset()
             }
             setNewComment('')
             commentListRef.current?.scrollToIndex({animated: true, index: 0});
             Keyboard.dismiss();
         }
         resetAddCommentOnSuccess();
-    }, [addCommentStatus]);
+    }, [addCommentMutation.status]);
 
     useEffect(() => {
         /**
@@ -216,23 +212,12 @@ const LandmarkDetails: React.FC<LandmarkDetailsProps> = ({landmarkId, setLandmar
         clearSelectedOnNewCommentChange();
     }, [newCommentId]);
 
-    useEffect(() => {
-        /**
-         * Refetch the user's rate pairing for the current landmark when {@linkcode landmark} state changes.
-         * @memberOf LandmarkDetails
-         */
-        const refetechRateStatus = async () => {
-            await refetchCheckIfRatedByUser();
-        }
-        refetechRateStatus();
-    }, [landmark]);
-
     /**
      * Calls the {@linkcode updateLandmark} mutation from the {@link useLandmarks} hook and closes the modal once finished.
      */
     const editLandmark = async () => {
         if (updatedLandmark) {
-            await updateLandmark(updatedLandmark);       
+            await editLandmarkMutation.mutateAsync(updatedLandmark) 
         }
         
         setEditing(false);
@@ -242,8 +227,8 @@ const LandmarkDetails: React.FC<LandmarkDetailsProps> = ({landmarkId, setLandmar
      * Calls the {@linkcode rateLandmarkAsunc} mutation from the {@link useLandmarks} hook. If 1, the landmark will be upvoted. If -1, it will be downvoted
      */
      const rateLandmark = async (rating: 1 | -1) => {
-        if (landmark) {
-            await rateLandmarkAsync({id: landmark.id, rating: rating});
+        if (landmarkQuery?.data?.landmark) {
+            await rateLandmarkMutation.mutateAsync({id: landmarkId, rating: rating});
         }
     }
 
@@ -252,11 +237,11 @@ const LandmarkDetails: React.FC<LandmarkDetailsProps> = ({landmarkId, setLandmar
      */
     const removeLandmark = async () => {
         Alert.alert("Are you sure you want to delete landmark here?", undefined,
-      [{ text: "Cancel", onPress: () => console.log("Cancelled") }
+      [{ text: "Cancel", }
         ,
       {
-        text: "Confirm", onPress: () => {
-            deleteLandmark(landmark?.id);   
+        text: "Confirm", onPress: async () => {
+            await deleteLandmarkMutation.mutateAsync(landmarkId);   
             toggleDetailsPanel(false);
             Alert.alert("LANDMARK HAS BEEN DELETED")
         }
@@ -268,11 +253,11 @@ const LandmarkDetails: React.FC<LandmarkDetailsProps> = ({landmarkId, setLandmar
      */
     const addComment = async () => {
         if (newCommentId) {
-            await addCommentAsync({
+            await addCommentMutation.mutateAsync({
                 edited: false,
                 content: newCommentId,
-                landmark: landmark?.id,
-                poster: authStore.userId,
+                landmark: landmarkId,
+                poster: userId,
                 poster_name: profile.username,
                 id: ''
             });
@@ -304,14 +289,14 @@ const LandmarkDetails: React.FC<LandmarkDetailsProps> = ({landmarkId, setLandmar
      * Calls the {@linkcode editComment} mutation from the {@link useComments} hook.
      */
     const editComment = async (comment: LMComment) => {
-        await updateCommentAsync(comment);
+        await editCommentMutation.mutateAsync(comment);
     }
 
     /**
      * Calls the {@linkcode deleteComment} mutation from the {@link useComments} hook.
      */
     const deleteComment = async (id: string) => {
-        deleteCommentAsync(id);
+        await deleteCommentMutation.mutateAsync(id);
     }
 
     const getWindowWidth = () => {
@@ -329,9 +314,9 @@ const LandmarkDetails: React.FC<LandmarkDetailsProps> = ({landmarkId, setLandmar
                 {text: 'Yes', 
                 onPress: async () => {
                     setPhotosBusy(true)
-                    await deletePhoto(photoId)
+                    await deleteLandmarkPhotoMutation.mutateAsync(photoId)
                     setSelectedImage(-1)
-                    await refetchLandmark()
+                    await landmarkQuery.refetch()
                 }},
                 {text: 'No'} 
             ])
@@ -343,19 +328,20 @@ const LandmarkDetails: React.FC<LandmarkDetailsProps> = ({landmarkId, setLandmar
     const determineModalHeight = () => {
         if (selectedImage > -1) 
             return Dimensions.get("window").height 
-        else if (keyboardOpened && Platform.OS == 'android' || editingEnabled || (profile?.id != landmark?.user && landmark?.photos?.length == 0))
-            return Dimensions.get("window").height * .5
-        else if (landmark?.photos?.length > 0) 
+        else if (keyboardOpened || editingEnabled || (!landmarkOwnedByUser(landmarkQuery?.data?.landmark) && landmarkQuery?.data?.landmark?.photos?.length == 0)) {
+            return Dimensions.get("window").height * .4
+        }
+        else if (landmarkQuery?.data?.landmark?.photos?.length > 0) 
             return Dimensions.get("window").height * .9 
         else
-            return Dimensions.get("window").height * .7
+            return Dimensions.get("window").height * .6
     }
 
     return (
         <Modal 
             useNativeDriver={true}
             useNativeDriverForBackdrop={true}
-            avoidKeyboard={false}
+            avoidKeyboard={true}
             onBackdropPress={() => {
                 if (editingEnabled) {
                     Keyboard.dismiss();
@@ -367,6 +353,7 @@ const LandmarkDetails: React.FC<LandmarkDetailsProps> = ({landmarkId, setLandmar
             isVisible={visible}>
             <SafeAreaView 
                 style={[styles.container, {height: determineModalHeight()}]}>
+
                 {selectedImage > -1 ?
                 <View style={{ padding: 14}}>
                     <View style={{justifyContent: 'space-between', flexDirection: 'row'}}>
@@ -375,24 +362,24 @@ const LandmarkDetails: React.FC<LandmarkDetailsProps> = ({landmarkId, setLandmar
                                 <Text style={{color: 'white', fontSize:20}}><FontAwesome name="arrow-left" color="white" size={20}/> Back</Text>
                             </>
                         </TouchableOpacity>
-                        <IconButton style={{alignSelf: 'flex-end', marginBottom: 20}} icon="trash" color="white" size={25} onPress={() => tryDeletePhoto(landmark?.photos[selectedImage].id)}/>
+                        <IconButton style={{alignSelf: 'flex-end', marginBottom: 20}} icon="trash" color="white" size={25} onPress={() => tryDeletePhoto(landmarkQuery?.data?.landmark?.photos[selectedImage].id)}/>
                     </View>
                     <ScrollView style={{width: '100%', }}>
-                        <Image style={{resizeMode: 'contain', alignSelf: 'center', height: Dimensions.get('window').height * .9, width: getWindowWidth()}} source={{uri: 'data:image/png;base64,' + landmark?.photos[selectedImage].image_b64}}/> 
+                        <Image style={{resizeMode: 'contain', alignSelf: 'center', height: Dimensions.get('window').height * .9, width: getWindowWidth()}} source={{uri: 'data:image/png;base64,' + landmarkQuery?.data?.landmark?.photos[selectedImage].image_b64}}/> 
                     </ScrollView>
                 </View>
                 :
-                (getLandmarkStatus == 'success' || getLandmarkStatus == 'idle') && 
-                updateLandmarkStatus == "idle" || updateLandmarkStatus == "success" && 
-                deleteLandmarkStatus == "idle" || deleteLandmarkStatus == "success" ?
+                (landmarkQuery.isSuccess || landmarkQuery.isIdle) && 
+                editLandmarkMutation.isIdle || editLandmarkMutation.isSuccess && 
+                deleteLandmarkMutation.isIdle || deleteLandmarkMutation.isSuccess ?
                 <>
                 <DetailsHeader
+                    ratedByUser={landmarkQuery?.data?.ratedByUser}
                     processingPhoto={processingPhoto}
-                    addPhotoStatus={addPhotoStatus}
-                    deletePhotoStatus={deletePhotoStatus}
+                    addPhotoStatus={addLandmarkPhotoMutation.status}
+                    deletePhotoStatus={deleteLandmarkPhotoMutation.status}
                     toggleDetailsPanel={toggleDetailsPanel}
-                    landmark={landmark}
-                    landmarkRatedByUser={landmarkRatedByUser}
+                    landmark={landmarkQuery?.data?.landmark}
                     editLandmark={editLandmark}
                     editingEnabled={editingEnabled}
                     toggleEditing={setEditing}
@@ -400,19 +387,20 @@ const LandmarkDetails: React.FC<LandmarkDetailsProps> = ({landmarkId, setLandmar
                     removeLandmark={removeLandmark}
                     updatedLandmark={updatedLandmark}
                     profile={profile} />
-                <DetailsBody 
+                <DetailsBody
+                    authNavigation={authNavigation} 
                     setProcessingPhoto={setProcessingPhoto}
                     processingPhoto={processingPhoto}
                     profileId={profile?.id}
                     setKeyboardOpened={setKeyboardOpened}
                     keyboardOpened={keyboardOpened}
                     setSelectedImage={setSelectedImage}
-                    landmark={landmark}
+                    landmark={landmarkQuery?.data?.landmark}
                     updatedLandmark={updatedLandmark}
                     commentListRef={commentListRef}
                     commentTextInputRef={commentTextInputRef}
                     commentBeingEdited={commentBeingEdited}
-                    comments={comments}
+                    comments={commentsQuery?.data}
                     addComment={addComment}
                     setCommentBeingEdited={setCommentBeingEdited}
                     newCommentId={newCommentId}
@@ -425,29 +413,29 @@ const LandmarkDetails: React.FC<LandmarkDetailsProps> = ({landmarkId, setLandmar
                     editingEnabled={editingEnabled}
                     setUpdatedLandmark={setUpdatedLandmark}
                     tryDeletePhoto={tryDeletePhoto}
-                    deletePhotoStatus={deletePhotoStatus}
+                    deletePhotoStatus={deleteLandmarkPhotoMutation.status}
                     toggleLmDetails={toggleLmDetails}
-                    addPhoto={addPhoto}
-                    addPhotoStatus={addPhotoStatus}/>
+                    addPhoto={addLandmarkPhotoMutation.mutateAsync}
+                    addPhotoStatus={addLandmarkPhotoMutation.status}/>
                 </> :
                 <View style={{height: '100%', justifyContent: "space-evenly", alignItems: "center", marginHorizontal: 20}}>
                     <Text style={{color: 'white', fontSize: 20}}>{
-                        getLandmarkStatus == 'loading' ? "Loading landmark..." : 
-                        getLandmarkStatus == 'error' ? "Something went wrong trying to load the landmark" :
-                        deleteLandmarkStatus == 'loading' ? "Deleting landmark..." : 
-                        deleteLandmarkStatus == 'error' ? "Something went wrong trying to delete the landmark" :
-                        updateLandmarkStatus == 'loading' ? "Updating landmark..." : 
-                        updateLandmarkStatus == 'error' ? "Something went wrong trying to update the landmark" :
-                        addCommentStatus == 'loading' ? "Adding comment..." : 
-                        addCommentStatus == 'error' ? "Something went wrong trying to add the comment" : 
-                        updateCommentStatus == 'loading' ? "Updating comment..." : 
-                        updateCommentStatus == 'error' ? "Something went wrong trying to update the comment" :  
-                        deleteCommentStatus == 'loading' ? "Deleting comment..." : 
-                        deleteCommentStatus == 'error' ? "Something went wrong trying to delete the comment" : null} 
+                        landmarkQuery.isLoading ? "Loading landmark..." : 
+                        landmarkQuery.isError ? "Something went wrong trying to load the landmark" :
+                        deleteLandmarkMutation.isLoading ? "Deleting landmark..." : 
+                        deleteLandmarkMutation.isError ? "Something went wrong trying to delete the landmark" :
+                        editLandmarkMutation.isLoading ? "Updating landmark..." : 
+                        editLandmarkMutation.isError ? "Something went wrong trying to update the landmark" :
+                        addCommentMutation.isLoading ? "Adding comment..." : 
+                        addCommentMutation.isError ? "Something went wrong trying to add the comment" : 
+                        editCommentMutation.isLoading ? "Updating comment..." : 
+                        editCommentMutation.isError ? "Something went wrong trying to update the comment" :  
+                        deleteCommentMutation.isLoading ? "Deleting comment..." : 
+                        deleteCommentMutation.error ? "Something went wrong trying to delete the comment" : null} 
                     </Text>
                     {
-                        deleteLandmarkStatus == 'loading' || updateLandmarkStatus == 'loading' || getLandmarkStatus == "loading" ? <ActivityIndicator color='white' size="large"/> :
-                        deleteLandmarkStatus == 'error' || updateLandmarkStatus == 'error' || getLandmarkStatus == "error" ? <PrimaryButton text="Okay" style={{borderColor: 'white', borderWidth: 1}} onPress={() => refetchLandmark()} /> : null
+                        deleteLandmarkMutation.isLoading || editLandmarkMutation.isLoading || landmarkQuery.isLoading ? <ActivityIndicator color='white' size="large"/> :
+                        deleteLandmarkMutation.isError || editCommentMutation.isLoading || landmarkQuery.isError ? <PrimaryButton text="Okay" style={{borderColor: 'white', borderWidth: 1}} onPress={async () => await landmarkQuery.refetch()} /> : null
                     }
                 </View>}
             </SafeAreaView>

+ 10 - 7
src/components/Map/Panels/LandmarkDetailsPanel/LandmarkPhotos.tsx

@@ -1,9 +1,10 @@
 import { FontAwesome } from "@expo/vector-icons"
 import { ImageInfo } from "expo-image-picker/build/ImagePicker.types"
-import React, { useEffect, useState } from "react"
-import { View, Text, ActivityIndicator, ScrollView, TouchableOpacity, Image } from "react-native"
+import React, { useState } from "react"
+import { ActivityIndicator, Image, ScrollView, Text, TouchableOpacity, View } from "react-native"
 import { QueryStatus } from "react-query"
-import { Landmark, LMPhoto, useLandmarks } from "../../../../hooks/useLandmarks"
+import { useAuth } from "../../../../data/Auth/AuthContext"
+import { Landmark, LMPhoto } from "../../../../data/landmarks"
 import { IconButton, PrimaryButton } from "../../../Buttons"
 import { PhotoPicker } from "../../../PhotoPicker"
 
@@ -24,6 +25,8 @@ interface LandmarkPhotosProps {
 }
 
 export const LandmarkPhotos: React.FC<LandmarkPhotosProps> = (props) => {
+    const {landmarkOwnedByUser} = useAuth()
+
     /**
      * Flag that toggles the photo source menu being displayed
     */
@@ -83,13 +86,13 @@ export const LandmarkPhotos: React.FC<LandmarkPhotosProps> = (props) => {
                 {props.landmark?.photos?.length > 0 ? 
                 <>
                     <ScrollView nestedScrollEnabled={true} contentContainerStyle={{alignItems: 'center'}} style={{flexDirection: 'row', marginBottom: 5, alignSelf: 'center'}} horizontal={true}>
-                        {props.landmark?.photos?.length < 5 && props.profileId == props.landmark?.user ? <IconButton style={{alignSelf: 'center', padding: 10, opacity: .5, marginLeft: 10}} color='white' size={30} icon="plus" onPress={() => togglePhotoSourceMenu(true)} />: null}
+                        {props.landmark?.photos?.length < 5 && landmarkOwnedByUser(props.landmark) ? <IconButton style={{alignSelf: 'center', padding: 10, opacity: .5, marginLeft: 10}} color='white' size={30} icon="plus" onPress={() => togglePhotoSourceMenu(true)} />: null}
                         {props.landmark?.photos?.map((photo, i) => {    
                             
                             return (
                                 <TouchableOpacity activeOpacity={1} key={i} style={{marginHorizontal: 1, padding: 14, zIndex: 11}} onPress={() => maximizePhoto(i)}>
                                     <Image style={{alignSelf: 'center', height: 300, width: 200}} source={{uri: 'data:image/png;base64,' + photo.image_b64}}/> 
-                                    {props.landmark?.user == props.profileId ? <IconButton icon="times-circle" color="lightgray" style={{position: 'absolute', top: -2, right: 0}} size={25} onPress={() => props.tryDeletePhoto(photo.id)} /> : null}
+                                    {landmarkOwnedByUser(props.landmark) ? <IconButton icon="times-circle" color="lightgray" style={{position: 'absolute', top: -2, right: 0}} size={25} onPress={() => props.tryDeletePhoto(photo.id)} /> : null}
                                 </TouchableOpacity>
                             )
                         })}
@@ -97,9 +100,9 @@ export const LandmarkPhotos: React.FC<LandmarkPhotosProps> = (props) => {
                     
                 </> : 
                 <>
-                {props.landmark?.user == props.profileId ?
+                {landmarkOwnedByUser(props.landmark) ?
                 <TouchableOpacity style={{marginTop: 30, justifyContent: 'center', alignItems: 'center', opacity: .7}} onPress={() => {togglePhotoSourceMenu(true)}}>
-                    <Text style={{fontSize: 20, marginBottom: 10, color: 'white'}}>Add photo</Text>
+                    <Text style={{fontSize: 20, marginBottom: 10, color: 'white'}}>Add photo of landmark</Text>
                     <FontAwesome name="plus" size={30} color='white' />
                 </TouchableOpacity> : null}
                 </>}

+ 2 - 2
src/components/Map/Panels/LandmarkDetailsPanel/TouchOpaq.tsx

@@ -1,6 +1,6 @@
-import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'
-import React from 'react'
 import { FontAwesome } from "@expo/vector-icons";
+import React from 'react';
+import { StyleSheet, TouchableOpacity, View } from 'react-native';
 import { colors } from "../../../../utils/GlobalUtils";
 
 

+ 6 - 13
src/components/Map/Panels/NearbyLandmarksPanel.tsx

@@ -6,19 +6,12 @@
  */
 
 import { FontAwesome } from "@expo/vector-icons";
-import * as ImagePicker from 'expo-image-picker';
-import { ImageInfo } from "expo-image-picker/build/ImagePicker.types";
-import React, { memo, useEffect, useState } from "react";
-import { ActivityIndicator, Dimensions, Image, Platform, SafeAreaView, Text, TextInput, TouchableOpacity, View } from 'react-native';
-import { FlatList, ScrollView } from "react-native-gesture-handler";
+import React, { memo, useState } from "react";
+import { Dimensions, Image, SafeAreaView, Text, TouchableOpacity, View } from 'react-native';
+import { ScrollView } from "react-native-gesture-handler";
 import Modal from 'react-native-modal';
-import { checkMultiple, PERMISSIONS, RESULTS } from "react-native-permissions";
-import Picker from 'react-native-picker-select';
-import { Landmark, LMPhoto, useLandmarks } from "../../../hooks/useLandmarks";
-import { colors, getMediaPermissions, lmTypes } from "../../../utils/GlobalUtils";
-import Badge from "../../Badge";
-import { IconButton, SecondaryButton } from "../../Buttons";
-import { PhotoPicker } from "../../PhotoPicker";
+import { Landmark } from "../../../data/landmarks";
+import { colors, lmTypes } from "../../../utils/GlobalUtils";
 
 /**
  * Props for the {@link AddLandmarkPanel} component.
@@ -59,7 +52,7 @@ const NearbyLandmarksPanel: React.FC<NearbyLandmarksPanelProps> = ({nearbyLandma
             isVisible={alertedLmPanelVisible} >
             <SafeAreaView style={{backgroundColor: colors.red, height: Dimensions.get('window').height * .6}}>
                 <ScrollView>
-                    {alertedLandmarks.map((lm, i) => {
+                    {alertedLandmarks?.map((lm, i) => {
                         return (
                             <TouchableOpacity onPress={() => selectLm(lm)} key={i} style={{flexDirection: 'row', alignItems: 'center', paddingVertical: 10, marginHorizontal: 15, justifyContent: 'space-between', borderBottomWidth: 1, borderColor: 'lightgray'}}>
                                 <Image source={lmTypes[lm.landmark_type].image}/>

+ 27 - 29
src/components/Map/Panels/VoicePanel.tsx

@@ -7,18 +7,19 @@
 
 import { FontAwesome } from '@expo/vector-icons';
 import * as Linking from "expo-linking";
-import React, { useEffect, useState } from 'react';
-import { ActivityIndicator, AppState, FlatList, Image, ImageURISource, KeyboardAvoidingView, Text, TouchableOpacity, View, ViewStyle } from 'react-native';
+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 { Pulse, Wave } from 'react-native-animated-spinkit';
 import Config from 'react-native-config';
 import Modal from "react-native-modal";
 import { SafeAreaView } from 'react-native-safe-area-context';
 import Spokestack, { PipelineProfile } from 'react-native-spokestack';
-import { Landmark, useLandmarks } from '../../../hooks/useLandmarks';
-import { AuthTabsNavigationProp } from '../../../navigation/AuthorizedNavigator';
+import { MainTabsNavigationProp } from '../../../navigation/MainTabsNavigator';
 import { colors, GlobalStyles, lmTypes } from '../../../utils/GlobalUtils';
 import { Separator } from '../../Separator';
 import * as Speech from 'expo-speech';
+import { Landmark, useAddLandmark } from '../../../data/landmarks';
 
 export interface VoicePanelProps {
     voiceVisible: boolean,
@@ -30,7 +31,7 @@ export interface VoicePanelProps {
     setSelectedLandmarkId: (state: string) => void,
     toggleLmDetails: (state: boolean) => void,
     userCoords: {latitude: number, longitude: number}
-    navigation: AuthTabsNavigationProp
+    navigation: MainTabsNavigationProp
 }
 
 export interface VoiceAction {
@@ -86,7 +87,8 @@ export const VoicePanel: React.FC<VoicePanelProps> = ({
     // Holds the current action state of a ongoing voice recognition flow
     const [action, setAction] = useState<VoiceAction>();
     const [nearbyLandmarksTrigger, toggleNearbyLandmarksTrigger] = useState<boolean>(false)
-    const {addLandmarkAsync, addLandmarkStatus, resetAddLm} = useLandmarks();
+
+    const addLandmarkMutation = useAddLandmark()
 
     const containsAddVerb = () => {
         return (speechResult.includes("add") || speechResult.includes('create') || speechResult.includes('make'))
@@ -201,8 +203,8 @@ export const VoicePanel: React.FC<VoicePanelProps> = ({
                     }
                     else {
                         // otherwise give the user another chance to speak
-                        setListening(true)
-                        await Spokestack.activate()
+                        // setListening(true)
+                        // await Spokestack.activate()
                     }
                 }
             }
@@ -229,7 +231,7 @@ export const VoicePanel: React.FC<VoicePanelProps> = ({
         }
         // handle unrecognized result
         else {
-            await Spokestack.activate();
+            //await Spokestack.activate();
         }
     }
 
@@ -356,7 +358,7 @@ export const VoicePanel: React.FC<VoicePanelProps> = ({
         }
         else if (speechResult.includes("yes")) {
             setAction(undefined);
-            await addLandmarkAsync({landmarkValue: newLandmark});
+            await addLandmarkMutation.mutateAsync({landmarkValue: newLandmark});
             await respond("Great, I've added the landmark. Anything else?")
         }
         else if (speechResult.includes("no")) {
@@ -375,17 +377,17 @@ export const VoicePanel: React.FC<VoicePanelProps> = ({
          * @memberOf AddLandmark
          */
         const finishAddAttempt = async () => {
-            if (addLandmarkStatus == 'success') {
-                resetAddLm();
+            if (addLandmarkMutation.isSuccess) {
+                addLandmarkMutation.reset();
                 setSelectedLandmarkId(newLandmark.id)
                 await Spokestack.activate();
             }
-            else if (addLandmarkStatus == 'error') {
+            else if (addLandmarkMutation.isError) {
                 await Spokestack.activate();
             }
         }
         finishAddAttempt();
-    }, [addLandmarkStatus]);
+    }, [addLandmarkMutation.status]);
 
     // end add landmark flow
 
@@ -415,17 +417,13 @@ export const VoicePanel: React.FC<VoicePanelProps> = ({
         await Spokestack.activate()
     }
 
-    const back = async () => {
-        setAction({...action, actionStep: action.actionStep - 1})
-    }
-
-    const LmTypeDisplay: React.FC<{lmType: {image: ImageURISource, label:string}, style?: ViewStyle}> = ({lmType, style}) => {
-        return (
+    const LmTypeDisplay: React.FC<{lmType: {image: ImageRequireSource, label:string}, style?: ViewStyle}> = ({lmType, style}) => {
+        return useMemo(() => (
             <View style={[{marginVertical: 5, flexDirection: 'row'}, style]}>  
-                <Image style={{height: 25, width: 18}} source={lmType.image} />
+                <FastImage style={{height: 25, width: 18}} source={lmType.image} />
                 <Text style={{fontSize: 15, marginLeft: 10, textAlign: 'center', color: 'white'}}>{lmType.label}</Text>
             </View>
-        )
+        ), [lmType])
     }
 
     const ActionOptions: React.FC<{currentAction?: VoiceAction}> = ({currentAction}) => {
@@ -443,12 +441,12 @@ export const VoicePanel: React.FC<VoicePanelProps> = ({
                                     <Text style={{fontSize: 17, margin: 20, color: 'white'}}>Choose from the available types: </Text> 
                                     <FlatList 
                                         style={{marginBottom: 20, marginHorizontal:20}}
-                                        data={Object.keys(lmTypes).map(typeId => typeId)}
+                                        data={Object.values(lmTypes).map(value => {return {label: value.label, image: value.image}})}
                                         numColumns={2}
-                                        keyExtractor={item => item}
-                                        renderItem={(item => {
+                                        keyExtractor={(item) => item.label}
+                                        renderItem={((item) => {
                                             return (
-                                                <LmTypeDisplay key={item.item} lmType={lmTypes[item.item]} style={{flexBasis: '50%'}} />
+                                                <LmTypeDisplay key={item.item.label} lmType={item.item} style={{flexBasis: '50%'}} />
                                             )})} />
                                 </View>
                             )
@@ -477,10 +475,10 @@ export const VoicePanel: React.FC<VoicePanelProps> = ({
             }   
         }
         
-        if (addLandmarkStatus == 'loading') {
+        if (addLandmarkMutation.isLoading) {
             return <ActivityIndicator color='white' size="large"/>
         }
-        else if (addLandmarkStatus == "error") {
+        else if (addLandmarkMutation.isError) {
             return <Text style={{fontSize: 20, margin: 20, color: 'white'}}>Something went wrong when trying to upload the landmark.</Text>
         }
     }
@@ -530,7 +528,7 @@ export const VoicePanel: React.FC<VoicePanelProps> = ({
         isVisible={voiceVisible} >
         <KeyboardAvoidingView>
             <SafeAreaView style={{backgroundColor: colors.red}}>
-                {addLandmarkStatus == "loading" ?
+                {addLandmarkMutation.isLoading ?
                 <ActionProcessingIndicator /> : 
                 <View>
                     <VoicePanelHeader />

+ 20 - 0
src/components/PrivacyLink.tsx

@@ -0,0 +1,20 @@
+import React from "react"
+import { API_URL } from "../utils/RequestUtils"
+import * as WebBrowser from 'expo-web-browser'
+import { TouchableOpacity, Text } from "react-native"
+import { ProfileMainStyles } from "./Profile/Styles/Profile.styles"
+
+export const PrivacyLink: React.FC = () => {
+    /**
+     * Opens up the company privacy policy in the browser.
+     */
+    const openPrivacyPolicy = async () => {
+        await WebBrowser.openBrowserAsync(API_URL + "/privacy")
+    }
+
+    return (
+        <TouchableOpacity onPress={openPrivacyPolicy}>
+            <Text style={ProfileMainStyles.privacyButtonText}>Privacy policy</Text>
+        </TouchableOpacity>
+    )
+}

+ 0 - 0
src/components/Auth/AuthLayout.tsx → src/components/Profile/AuthLayout.tsx


+ 47 - 65
src/components/Auth/Intro.tsx → src/components/Profile/LoginView.tsx

@@ -1,22 +1,24 @@
-/* Copyright (C) Click & Push Accessibility, Inc - All Rights Reserved
+/* 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 axios from "axios";
-import { loadAsync, makeRedirectUri, ResponseType } from "expo-auth-session";
+import { makeRedirectUri } from "expo-auth-session";
 import * as WebBrowser from 'expo-web-browser';
 import { maybeCompleteAuthSession } from "expo-web-browser";
-import jwt_decode from 'jwt-decode';
 import React, { useState } from "react";
-import { ActivityIndicator, Image, StyleSheet, Text, TouchableOpacity, View } from "react-native";
-import { UnAuthStackNavigationProp } from "../../navigation/UnauthorizedNavigator";
-import { authStore, IdToken } from "../../libs/auth/AuthStore";
-import {authenticate} from '../../libs/auth/core'
+import { ActivityIndicator, Image, Pressable, StyleSheet, Text, TouchableHighlight, TouchableOpacity, View } from "react-native";
+import { useAuth } from "../../data/Auth/AuthContext";
+import { BaseStackNavigationProp } from "../../navigation/BaseStackNavigator";
 import { API_URL } from "../../utils/RequestUtils";
-import { PrimaryButton, SecondaryButton } from "../Buttons";
+import { GenericButton, PrimaryButton } from "../Buttons";
+import { ProfileSectionHeader } from "./ProfileSections/ProfileSectionHeader";
+import * as Linking from 'expo-linking';
+import { BrowserLink, ProfileLegal } from "./ProfileSections/ProfileLegal";
+import { Separator } from "../Separator";
+import Collapsible from "react-native-collapsible";
 import UnauthorizedLayout from "./AuthLayout";
 
 /**
@@ -24,23 +26,9 @@ import UnauthorizedLayout from "./AuthLayout";
  */
 export interface IntroProps {
     /**The {@link AuthStackNavigationProp} navigation object used to interact with the {@link Auth} navigator.*/
-    navigation: UnAuthStackNavigationProp;
+    navigation: BaseStackNavigationProp;
 }
 
-/**
- * A base url for the api's authorization endpoints
- */
-const issuer = API_URL + "/o";
-
-/**
- * An object containing the discovery endpoints for the api, necessary for OIDC authentication {@link https://swagger.io/docs/specification/authentication/openid-connect-discovery/}
- */
-const discovery = {
-    authorizationEndpoint: issuer + "/authorize/",
-    tokenEndpoint: issuer + "/token/",
-    revocationEndpoint: issuer + "/revoke/",
-};
-
 /**
  * Creates a browser session through which the user will login
  */
@@ -51,51 +39,27 @@ maybeCompleteAuthSession();
  * @component
  * @category Unauthorized
  */
-const Intro : React.FC<IntroProps> = ({navigation}) => {
+const LoginView : React.FC<{navigation: BaseStackNavigationProp}> = ({navigation}) => {
+    const {login} = useAuth()
+
     /**
      * @type {string} 
      * React state holding the login result message to display to the user.
      * */
     const loginMessageState = "";
     const [loginMessage, setLoginMessage] = useState<string>(loginMessageState);
+    const [legalCollapsed, toggleLegal] = useState<boolean>(true)
     /**
      * @type {boolean} 
      * React state holding the error state of the component.
      * */
-     const errorState = false;
+    const errorState = false;
     const [error, setError] = useState<boolean>(errorState);
 
     const redirectUri = makeRedirectUri({
         path: 'callback'
     });
 
-    /**
-     * Function that initiates the login flow. It opens up with the in app browser and sends an authorization request to the API, which redirects the user to the backend login page. 
-     * If the credentials entered are valid, the OAuth Authorization Code flow will occur, which results in the user recieving an access token and refresh token which are then stored in {@link AuthStore}.
-     * */
-    const login = async () => {
-        setLoginMessage("Logging you in...");
-        
-        const result = await authenticate({
-            clientId: "atlas.mobile",
-            responseType: ResponseType.Code,
-            redirectUri,
-            usePKCE: true,
-            scopes: ['openid'],
-            
-        }, discovery)
-
-        if (!result.success) {
-            if (result.errorMessage == 'cancel' || result.errorMessage == 'dismiss') {
-                setLoginMessage("");    
-                return
-            }
-            setLoginMessage("Login failed. Please try again.");
-            setError(true);
-            return;
-        }
-    }   
-
     /**
      * Navigates to {@link RegisterMain}.
      */
@@ -112,17 +76,35 @@ const Intro : React.FC<IntroProps> = ({navigation}) => {
         await WebBrowser.openBrowserAsync(API_URL + "/privacy")
     }
 
+    const openFeedback = () => {
+        Linking.openURL('mailto:dev@clicknpush.ca')
+    }
+
+    const openLegal = () => {
+        toggleLegal(!legalCollapsed)
+    }
+
     return (
         <UnauthorizedLayout>
             {!loginMessage ?
             <View style={styles.introContainer}>
                 <View style={styles.brandContainer}>
-                    <Image style={{flex: 1}} resizeMode="contain" source={require('../../../assets/logo-white.png')}></Image>
-                    <Text style={styles.title} >Click & Push</Text>
+                    <Image resizeMode="contain" style={{height: 100}} source={require('../../../assets/logo-white.png')}></Image>
                 </View>
                 <View style={styles.btnContainer}>
                     <PrimaryButton text="Login" onPress={login}/>
-                    <SecondaryButton text="Create account" onPress={goToRegistration} />
+                    <PrimaryButton text="Create account" onPress={goToRegistration} />
+                    <View style={{width: '100%'}}>
+                        <Separator color="#E0E0E0" />
+                        <GenericButton text="Legal" style={{justifyContent: 'center', alignItems: 'center', padding: 20, width: '100%', }} onPress={openLegal}/>
+                        <Separator color="#E0E0E0" />
+                        <Collapsible collapsed={legalCollapsed} style={{borderColor: '#E0E0E0', borderBottomWidth: 1, borderRightWidth: 1, borderLeftWidth: 1, justifyContent: 'center', alignItems: 'center', padding: 20}} >
+                            <BrowserLink style={{marginBottom: 20}} text="Mobile app third-party licenses" route="mobile-tpl"/>
+                            <BrowserLink text="API third-party licenses" route="api-tpl"/>
+                        </Collapsible>
+                    </View>
+                    <Separator color="#D3D3D3" />
+                    <GenericButton text="Provide feedback" style={{justifyContent: 'center', alignItems: 'center', padding: 20, width: '100%', }} onPress={openFeedback}/>
                 </View>
             </View> :
             <View style={{height: '100%', justifyContent: "center", alignItems: "center"}}>
@@ -138,25 +120,25 @@ const Intro : React.FC<IntroProps> = ({navigation}) => {
 
 const styles = StyleSheet.create({
     introContainer: {
-        flex: 1,
-        marginVertical: 50,
         alignItems: "center",
-        justifyContent: 'space-between'
+        justifyContent: 'flex-start'
     },
     brandContainer: {
-        flex: 2,
-        marginVertical: 50,
         alignItems: "center",
     },
     title: {
-        marginTop: 30, 
+        marginTop: 10, 
         color: 'white',
-        fontSize: 30
+        fontSize: 20
     },
     btnContainer: {
-        flex: 1,
+        marginVertical: 20,
+        padding: 20,
+        backgroundColor: 'white',
+        borderRadius: 10,
         alignItems: 'center',
         width: '100%',
+        
     },
     registerBtn: {
         borderColor: 'white',
@@ -165,4 +147,4 @@ const styles = StyleSheet.create({
     }
 })
 
-export default Intro;
+export default LoginView;

+ 50 - 72
src/components/Profile/Profile.tsx

@@ -12,14 +12,15 @@ import { observer } from "mobx-react";
 import React, { useEffect, useState } from "react";
 import { ActivityIndicator, Alert, AppState, Button, Image, ImageBackground, ScrollView, Text, TouchableOpacity, View } from 'react-native';
 import { renderers } from 'react-native-popup-menu';
-import { useAuth } from "../../hooks/useAuth";
-import { useProfile } from "../../hooks/useProfile";
-import { authStore } from "../../libs/auth/AuthStore";
 import { API_URL, reportAxiosError } from '../../utils/RequestUtils';
 import { PhotoPicker } from '../PhotoPicker';
 import { ProfileHeader } from "./ProfileHeader";
 import { ProfileSections } from "./ProfileSections";
+import { PrivacyLink } from '../PrivacyLink';
 import { ProfileMainStyles } from "./Styles/Profile.styles";
+import { useAuth } from '../../data/Auth/AuthContext';
+import { BaseStackNavigationProp } from '../../navigation/BaseStackNavigator';
+import { useChangePassword, useDeleteProfile, useEditProfile, useOwnedProfile, useToggleTips } from '../../data/profiles';
 const {SlideInMenu} = renderers
 
 /**
@@ -27,25 +28,19 @@ const {SlideInMenu} = renderers
  * @component
  */
 const Profile: React.FC = () => {
+    const {userId, accessToken} = useAuth()
     const [changingPassword, toggleChangingPassword] = useState<boolean>(false) 
     const [photoSourceMenuOpened, togglePhotoSourceMenu] = useState<boolean>(false)
     const [newPhotoB64, setNewPhoto] = useState<string>()
     const [loadingImg, setLoadingImg] = useState<boolean>(false)
     const [uploadingImg, setUploadingImg] = useState<boolean>(false)
-    const {
-        profile, 
-        changePasswordAsync, 
-        changePasswordStatus, 
-        resetChangePassword, 
-        updateProfile, 
-        updateProfileStatus, 
-        resetUpdateProfile, 
-        toggleTipsAsync, 
-        refetchProfile,
-        deleteAccount,
-        deleteAccountStatus
-    } = useProfile(authStore.userId)
     
+    const {profile} = useOwnedProfile()
+    const editProfileMutation = useEditProfile()
+    const changePasswordMutation = useChangePassword()
+    const toggleTipsMutation = useToggleTips()
+    const deleteAccountMutation = useDeleteProfile()
+
     useEffect(() => {
         if (newPhotoB64) {
             Alert.alert("Confirm new picture", "Are you sure you want to change your profile picture?", 
@@ -74,14 +69,13 @@ const Profile: React.FC = () => {
         try {
             const response = await axios({
               method: 'post',
-              url: API_URL + '/api/user-profile/change-picture/' + authStore.userId + '/',
-              headers: { "Authorization": "Bearer " + authStore.accessToken, },
+              url: API_URL + '/api/user-profile/change-picture/' + userId + '/',
+              headers: { "Authorization": "Bearer " + accessToken, },
               timeout: 50000,
               data: photoData,
             });
     
             if (response.status == 200) {
-                await refetchProfile()
                 Alert.alert("You successfully changed your profile picture!")
                 setUploadingImg(false)
             }
@@ -97,22 +91,6 @@ const Profile: React.FC = () => {
         setLoadingImg(false)
         setNewPhoto('')
     }
-    
-
-    const PrivacyLink: React.FC = () => {
-        /**
-         * Opens up the company privacy policy in the browser.
-         */
-        const openPrivacyPolicy = async () => {
-            await WebBrowser.openBrowserAsync(API_URL + "/privacy")
-        }
-
-        return (
-            <TouchableOpacity onPress={openPrivacyPolicy}>
-                <Text style={ProfileMainStyles.privacyButtonText}>Privacy policy</Text>
-            </TouchableOpacity>
-        )
-    }
     const LogoutButton: React.FC = () => {
         const {logout} = useAuth()
 
@@ -124,44 +102,44 @@ const Profile: React.FC = () => {
     }
 
     return (
-        <ImageBackground source={require('../../../assets/cover.jpg')} style={ProfileMainStyles.profileMainContainer}>
+        <>
             <ScrollView contentContainerStyle={{justifyContent: "flex-end", alignItems: 'center', padding: 10}}>
-                {loadingImg || uploadingImg ?
-                <View style={{marginVertical: 30}}>
-                    <Text style={{color: 'white', fontSize: 20, marginBottom: 10}}>{
-                        loadingImg ? 'Loading image...' :
-                        uploadingImg ? 'Uploading image' : null }
-                    </Text>
-                    <ActivityIndicator color='white' size="large"/> 
-                </View> :
-                <ImageBackground style={ProfileMainStyles.profileImage} source={newPhotoB64 ? {uri: 'data:image/png;base64,' + newPhotoB64} : profile?.image_b64 ? {uri: 'data:image/png;base64,' + profile?.image_b64} : require('../../../assets/default-pfp.png')}>
-                    <TouchableOpacity style={{width: "100%", height: "100%", zIndex: 12}} onPress={() => togglePhotoSourceMenu(true)} >
-                        <View style={ProfileMainStyles.profileImageOverlay}>
-                            <Text style={{fontSize: 12, textAlign: 'center', color: 'white', opacity: 1}}>Change profile picture</Text>
-                        </View>
-                    </TouchableOpacity>
-                </ImageBackground> }
-                
-                <View style={ProfileMainStyles.profileSubContainer}>
-                    <Text style={ProfileMainStyles.headerUsername}>{profile?.username}</Text>
-                    <ProfileHeader profile={profile} />
-                    <ProfileSections 
-                        resetChangeInfo={resetUpdateProfile}
-                        changeInfo={updateProfile}
-                        changeInfoStatus={updateProfileStatus}
-                        resetChangePassword={resetChangePassword}
-                        changePasswordStatus={changePasswordStatus} 
-                        deleteAccount={deleteAccount}
-                        deleteAccountStatus={deleteAccountStatus}
-                        profile={profile} 
-                        toggleTipsAsync={toggleTipsAsync} 
-                        changePassword={changePasswordAsync} />
-                    <LogoutButton />
-                    <PrivacyLink />
-                </View>
-            </ScrollView>
-            <PhotoPicker multiple={false} menuType='alert' cancel={cancelNewPhoto} photoSourceMenuOpened={photoSourceMenuOpened} onBeforeLaunchPicker={initiateNewPhotoSelect} onReceivedPhotoResult={result => onPhotoSelected(result)} />
-        </ImageBackground>
+                    {loadingImg || uploadingImg ?
+                    <View style={{marginVertical: 30}}>
+                        <Text style={{color: 'white', fontSize: 20, marginBottom: 10}}>{
+                            loadingImg ? 'Loading image...' :
+                            uploadingImg ? 'Uploading image' : null }
+                        </Text>
+                        <ActivityIndicator color='white' size="large"/> 
+                    </View> :
+                    <ImageBackground style={ProfileMainStyles.profileImage} source={newPhotoB64 ? {uri: 'data:image/png;base64,' + newPhotoB64} : profile?.image_b64 ? {uri: 'data:image/png;base64,' + profile?.image_b64} : require('../../../assets/default-pfp.png')}>
+                        <TouchableOpacity style={{width: "100%", height: "100%", zIndex: 12}} onPress={() => togglePhotoSourceMenu(true)} >
+                            <View style={ProfileMainStyles.profileImageOverlay}>
+                                <Text style={{fontSize: 12, textAlign: 'center', color: 'white', opacity: 1}}>Change profile picture</Text>
+                            </View>
+                        </TouchableOpacity>
+                    </ImageBackground> }
+                    
+                    <View style={ProfileMainStyles.profileSubContainer}>
+                        <Text style={ProfileMainStyles.headerUsername}>{profile?.username}</Text>
+                        <ProfileHeader profile={profile} />
+                        <ProfileSections 
+                            resetChangeInfo={editProfileMutation.reset}
+                            changeInfo={editProfileMutation.mutateAsync}
+                            changeInfoStatus={editProfileMutation.status}
+                            resetChangePassword={changePasswordMutation.reset}
+                            changePasswordStatus={changePasswordMutation.status} 
+                            deleteAccount={deleteAccountMutation.mutateAsync}
+                            deleteAccountStatus={deleteAccountMutation.status}
+                            profile={profile} 
+                            toggleTipsAsync={toggleTipsMutation.mutateAsync} 
+                            changePassword={changePasswordMutation.mutateAsync} />
+                        <LogoutButton />
+                        <PrivacyLink />
+                    </View>
+                </ScrollView>
+                <PhotoPicker multiple={false} menuType='alert' cancel={cancelNewPhoto} photoSourceMenuOpened={photoSourceMenuOpened} onBeforeLaunchPicker={initiateNewPhotoSelect} onReceivedPhotoResult={result => onPhotoSelected(result)} />
+            </>            
     )
 }
 

+ 5 - 5
src/components/Profile/ProfileHeader.tsx

@@ -7,18 +7,18 @@
 
 import React, { memo } from "react"
 import { View, Text } from "react-native"
-import { useLandmarks } from "../../hooks/useLandmarks"
-import { UserProfile } from "../../hooks/useProfile"
+import { useLandmarks } from "../../data/landmarks"
+import { UserProfile } from "../../data/profiles"
 
 export const ProfileHeader: React.FC<{profile?: UserProfile}> = memo(({profile}) => {
-    const {landmarks} = useLandmarks()
+    const landmarkQuery = useLandmarks()
 
     const getLandmarkCount = () => {
-        return landmarks?.filter(lm => lm.user == profile?.id).length
+        return landmarkQuery?.data?.filter(lm => lm.user == profile?.id).length
     }
 
     const getTotalLandmarkRating = () => {
-        const userLandmarks = landmarks?.filter(lm => lm.user == profile?.id)
+        const userLandmarks = landmarkQuery?.data?.filter(lm => lm.user == profile?.id)
         if (userLandmarks?.length > 0)
             return userLandmarks?.filter(lm => lm.user == profile?.id)?.reduce((prev, current) => {return {rating: prev.rating + current.rating}}).rating
         else 

+ 7 - 10
src/components/Profile/ProfileSections.tsx

@@ -5,19 +5,16 @@
  * <dev@clicknpush.ca>, January 2022
  */
 
+import * as Linking from 'expo-linking'
 import React, { useState } from "react"
 import { View } from "react-native"
-import { Landmark } from "../../hooks/useLandmarks"
-import { UserProfile } from "../../hooks/useProfile"
+import { UserProfile } from "../../data/profiles"
 import { RegisterCredsValues } from "../../utils/RegistrationUtils"
 import { ProfileInformation } from "./ProfileSections/ProfileInformation"
 import { ProfileLegal } from "./ProfileSections/ProfileLegal"
 import { ProfilePrefs } from "./ProfileSections/ProfilePrefs"
-import { ProfileSkills } from "./ProfileSections/ProfileSkills"
-import { ProfileSubscription } from "./ProfileSections/ProfileSubscription"
-import * as Linking from 'expo-linking' 
-import { ProfileSection } from "./ProfileSections/ProfileSection"
 import { ProfileSectionHeader } from "./ProfileSections/ProfileSectionHeader"
+import { ProfileSubscription } from "./ProfileSections/ProfileSubscription"
 
 interface ProfileSectionsProps {
     profile: UserProfile, 
@@ -82,6 +79,10 @@ export const ProfileSections: React.FC<ProfileSectionsProps> = ({
         if (!legalCollapsed) toggleLegal(true)
     }
 
+    const openFeedback = () => {
+        Linking.openURL('mailto:dev@clicknpush.ca')
+    }
+
     const openLegal = () => {
         toggleLegal(!legalCollapsed)
         if (!infoCollapsed) toggleInfo(true)
@@ -90,10 +91,6 @@ export const ProfileSections: React.FC<ProfileSectionsProps> = ({
         if (!prefsCollapsed) togglePrefs(true)
     }
 
-    const openFeedback = () => {
-        Linking.openURL('mailto:dev@clicknpush.ca')
-    }
-
     return (
         <View>
             <ProfileInformation 

+ 9 - 10
src/components/Profile/ProfileSections/ProfileInformation.tsx

@@ -7,20 +7,15 @@
 
 import { Formik } from "formik"
 import React, { useEffect, useState } from "react"
-import { ActivityIndicator, Alert, Button, StyleSheet, Text, TextInput, View, ViewStyle } from "react-native"
+import { ActivityIndicator, Button, StyleSheet, Text, TextInput, View, ViewStyle } from "react-native"
 import Dialog from "react-native-dialog"
 import { TouchableOpacity } from "react-native-gesture-handler"
 import { colors, GlobalStyles } from "../../../utils/GlobalUtils"
-import { PasswordForm, PasswordFormValues } from "../../PasswordForm"
-import { ProfileSectionStyles } from "../Styles/ProfileSections.styles"
-import { ProfileSection } from "./ProfileSection"
-import * as Yup from 'yup';
-import { PasswordFormStyles, PasswordValues } from "../../Auth/RegistrationSteps/RegisterPassword"
-import { credsSchema, passwordSchema, profileCredsSchema, RegisterCredsValues } from "../../../utils/RegistrationUtils"
-import { useProfile } from "../../../hooks/useProfile"
-import { authStore } from "../../../libs/auth/AuthStore"
-import { Landmark } from "../../../hooks/useLandmarks"
+import { useValidation, RegisterCredsValues } from "../../../utils/RegistrationUtils"
+import { PasswordFormValues } from "../../PasswordForm"
 import { Separator } from "../../Separator"
+import { PasswordFormStyles, PasswordValues } from "../Registration/RegistrationSteps/RegisterPassword"
+import { ProfileSection } from "./ProfileSection"
 
 interface ProfileInformationProps {
     openInfo: () => void
@@ -135,6 +130,8 @@ export const ProfileInformation: React.FC<ProfileInformationProps> = (props) =>
     }
 
     const ChangePasswordForm: React.FC = React.memo(() => {
+        const {passwordSchema} = useValidation()
+
         return (
             <Formik
             initialValues={initialPasswordValues}
@@ -168,6 +165,8 @@ export const ProfileInformation: React.FC<ProfileInformationProps> = (props) =>
     })
 
     const EditInfoForm: React.FC = () => {
+        const {profileCredsSchema} = useValidation()
+
         return (
             <Formik
                     initialValues={initialInfoValues}

+ 13 - 15
src/components/Profile/ProfileSections/ProfileLegal.tsx

@@ -5,32 +5,30 @@
  * <dev@clicknpush.ca>, January 2022
  */
 
-import React, { useEffect, useState } from "react"
-import { Switch, Text, TouchableOpacity, View, ViewStyle } from "react-native"
-import { GlobalStyles } from "../../../utils/GlobalUtils"
-import { ProfileSection } from "./ProfileSection"
-import * as Linking from 'expo-linking'
 import * as WebBrowser from 'expo-web-browser'
+import React from "react"
+import { Text, TouchableOpacity, ViewStyle } from "react-native"
 import { API_URL } from "../../../utils/RequestUtils"
+import { ProfileSection } from "./ProfileSection"
 
 interface ProfileLegalProps {
     openLegal: () => void
     legalCollapsed: boolean
 }
 
-export const ProfileLegal: React.FC<ProfileLegalProps> = ({openLegal, legalCollapsed}) => {    
-    const BrowserLink: React.FC<{text:string, route:string, style?: ViewStyle}> = ({style, text, route}) => {
-        return (
-            <TouchableOpacity style={[{alignItems: 'center'}, style]} onPress={async () => await WebBrowser.openBrowserAsync(API_URL + "/" + route)}>
-                <Text style={{fontWeight: 'bold', textDecorationLine:'underline'}}>{text}</Text>
-            </TouchableOpacity>
-        )
-    }
+export const BrowserLink: React.FC<{text:string, route:string, style?: ViewStyle}> = ({style, text, route}) => {
+    return (
+        <TouchableOpacity style={[{alignItems: 'center'}, style]} onPress={async () => await WebBrowser.openBrowserAsync(API_URL + "/" + route)}>
+            <Text style={{fontWeight: 'bold', }}>{text}</Text>
+        </TouchableOpacity>
+    )
+}
 
+export const ProfileLegal: React.FC<ProfileLegalProps> = ({openLegal, legalCollapsed}) => {    
     return (
         <ProfileSection isCollapsed={legalCollapsed} collapseToggleMethod={openLegal} title="Terms">
-            <BrowserLink style={{marginBottom: 10}} text="MOBILE APP THIRD-PARTY LICENSES" route="mobile-tpl"/>
-            <BrowserLink text="API THIRD-PARTY LICENSES" route="api-tpl"/>
+            <BrowserLink style={{marginBottom: 10}} text="Mobile app third-party licenses" route="mobile-tpl"/>
+            <BrowserLink text="API third-party licenses" route="api-tpl"/>
         </ProfileSection>
     )
 }

+ 1 - 5
src/components/Profile/ProfileSections/ProfilePrefs.tsx

@@ -6,13 +6,9 @@
  */
 
 import React, { useEffect, useState } from "react"
-import { Button, Switch, Text, TouchableOpacity, View } from "react-native"
-import Collapsible from "react-native-collapsible"
+import { Switch, Text, View } from "react-native"
 import { GlobalStyles } from "../../../utils/GlobalUtils"
-import { Separator } from "../../Separator"
-import { ProfileSectionStyles } from "../Styles/ProfileSections.styles"
 import { ProfileSection } from "./ProfileSection"
-import { ProfileSectionHeader } from "./ProfileSectionHeader"
 
 interface PrefsInformationProps {
     openPrefs: () => void

+ 1 - 1
src/components/Profile/ProfileSections/ProfileSection.tsx

@@ -6,7 +6,7 @@
  */
 
 import React from "react"
-import { Button, Text, TouchableOpacity, View, ViewStyle } from "react-native"
+import { View, ViewStyle } from "react-native"
 import Collapsible from "react-native-collapsible"
 import { Separator } from "../../Separator"
 import { ProfileSectionStyles } from "../Styles/ProfileSections.styles"

+ 1 - 1
src/components/Profile/ProfileSections/ProfileSectionHeader.tsx

@@ -6,7 +6,7 @@
  */
 
 import React from "react"
-import { TouchableOpacity, Text } from "react-native"
+import { Text } from "react-native"
 import { TouchableHighlight } from "react-native-gesture-handler"
 import { ProfileSectionStyles } from "../Styles/ProfileSections.styles"
 

+ 1 - 5
src/components/Profile/ProfileSections/ProfileSkills.tsx

@@ -6,13 +6,9 @@
  */
 
 import React from "react"
-import { Button, Text, TouchableOpacity, View } from "react-native"
-import Collapsible from "react-native-collapsible"
+import { Button, Text, View } from "react-native"
 import { GlobalStyles } from "../../../utils/GlobalUtils"
-import { Separator } from "../../Separator"
-import { ProfileSectionStyles } from "../Styles/ProfileSections.styles"
 import { ProfileSection } from "./ProfileSection"
-import { ProfileSectionHeader } from "./ProfileSectionHeader"
 
 interface SkillsInformationProps {
     openSkills: () => void

+ 2 - 5
src/components/Profile/ProfileSections/ProfileSubscription.tsx

@@ -5,12 +5,9 @@
  * <dev@clicknpush.ca>, January 2022
  */
 
-import React, { useEffect, useState } from "react"
-import { Button, Switch, Text, TouchableOpacity, View } from "react-native"
-import Collapsible from "react-native-collapsible"
-import { Separator } from "../../Separator"
+import React from "react"
+import { Text } from "react-native"
 import { ProfileSection } from "./ProfileSection"
-import { ProfileSectionHeader } from "./ProfileSectionHeader"
 
 interface ProfileSubscriptionProps {
     openSubscription: () => void

+ 37 - 0
src/components/Profile/ProfileTemplate.tsx

@@ -0,0 +1,37 @@
+/* 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 { observer } from "mobx-react"
+import React from "react"
+import { ImageBackground, Text } from "react-native"
+import { useAuth } from "../../data/Auth/AuthContext"
+import { BaseStackNavigationProp } from "../../navigation/BaseStackNavigator"
+import LoginView from "./LoginView"
+import Profile from "./Profile"
+import { ProfileSectionHeader } from "./ProfileSections/ProfileSectionHeader"
+import { ProfileMainStyles } from "./Styles/Profile.styles"
+import * as Linking from 'expo-linking'
+
+/**
+ * The screen component that displays the user's profile. Gets user information from the {@link useProfile} hook.
+ * @component
+ */
+const ProfileTemplate: React.FC<{navigation: BaseStackNavigationProp}> = ({navigation}) => {
+    const {accessToken} = useAuth()
+
+    const openFeedback = () => {
+        Linking.openURL('mailto:dev@clicknpush.ca')
+    }
+
+    return (
+        <ImageBackground source={require('../../../assets/cover.jpg')} style={ProfileMainStyles.profileMainContainer}>
+            {accessToken ? <Profile /> : <LoginView navigation={navigation} />}
+        </ImageBackground>
+    )
+}
+
+export default observer(ProfileTemplate)

+ 4 - 4
src/components/Auth/RegisterMain.tsx → src/components/Profile/Registration/RegisterMain.tsx

@@ -9,9 +9,9 @@ import React, { useEffect, useState } from 'react';
 import { Alert, Dimensions, StyleSheet, View } from 'react-native';
 import 'react-native-get-random-values';
 import { Text } from 'react-native-paper';
-import { UnAuthStackNavigationProp } from '../../navigation/UnauthorizedNavigator';
-import { PrimaryButton } from '../Buttons';
-import UnauthorizedLayout from './AuthLayout';
+import { BaseStackNavigationProp } from '../../../navigation/BaseStackNavigator';
+import { PrimaryButton } from '../../Buttons';
+import UnauthorizedLayout from '../AuthLayout';
 import RegisterCredentials from './RegistrationSteps/RegisterCredential';
 import RegisterImage from './RegistrationSteps/RegisterImage';
 import RegisterMeasurements from './RegistrationSteps/RegisterMeasurements';
@@ -22,7 +22,7 @@ import RegisterPassword from './RegistrationSteps/RegisterPassword';
  */
 export interface RegisterProps {
   /**The navigation object used to interact with the {@link Auth} navigator.*/
-  navigation: UnAuthStackNavigationProp
+  navigation: BaseStackNavigationProp
 }
 
 export interface RegisterStepProps {

+ 4 - 2
src/components/Auth/RegistrationSteps/RegisterCredential.tsx → src/components/Profile/Registration/RegistrationSteps/RegisterCredential.tsx

@@ -10,14 +10,16 @@ import React from 'react';
 import { Dimensions, StyleSheet, TextInput, View } from 'react-native';
 import 'react-native-get-random-values';
 import { Text } from 'react-native-paper';
-import { credsSchema, RegisterCredsValues } from '../../../utils/RegistrationUtils';
-import { PrimaryButton } from '../../Buttons';
+import { RegisterCredsValues, useValidation } from '../../../../utils/RegistrationUtils';
+import { PrimaryButton } from '../../../Buttons';
 import { RegisterStepProps } from '../RegisterMain';
 
 /**
  * This component displays a form for the new user's email and password to-be.
  */
 const RegisterCredentials: React.FC<RegisterStepProps> = ({changeStep, setFormValues, formValues}) => {
+  const {credsSchema} = useValidation()
+
     /**
    * The initial measurement values are set to the current email and username values of the parent form data object
    */

+ 18 - 17
src/components/Auth/RegistrationSteps/RegisterImage.tsx → src/components/Profile/Registration/RegistrationSteps/RegisterImage.tsx

@@ -10,16 +10,18 @@ import React, { useState } from 'react';
 import { Image, TouchableOpacity, View } from 'react-native';
 import 'react-native-get-random-values';
 import { ActivityIndicator, Text } from 'react-native-paper';
-import { API_URL, reportAxiosError } from '../../../utils/RequestUtils';
-import { PrimaryButton, SecondaryButton } from '../../Buttons';
-import { PhotoPicker } from '../../PhotoPicker';
-import { Separator } from '../../Separator';
 import { RegisterStepProps } from '../RegisterMain';
+import {useAuth} from '../../../../data/Auth/AuthContext'
+import {Separator} from '../../../Separator'
+import {PrimaryButton, SecondaryButton} from '../../../Buttons'
+import {PhotoPicker} from '../../../PhotoPicker'
 
 /**
  * This component displays an interface to upload a profile picture
  */
 const RegisterImage: React.FC<RegisterStepProps> = ({changeStep, formValues}) => {
+  const {sendApiRequestAsync} = useAuth()
+
   /**
    * Stores the base64 of the user's selected profile pic
    */ 
@@ -84,23 +86,22 @@ const RegisterImage: React.FC<RegisterStepProps> = ({changeStep, formValues}) =>
     // send request, notify user of result
 
     try {
-      const response = await axios({
-        method: 'post',
-        url: API_URL + '/api/register/',
-        timeout: 50000,
-        data: formData,
+      const response = await sendApiRequestAsync({
+        axiosConfig: {
+          method: 'post',
+          url: '/api/register/',
+          timeout: 50000,
+          data: formData,
+        },
+        authorized: false,
+        errorMessage: 'There was an error registering your account. Please try again.',
       });
       if (response.status == 200) {
         resultMessage = 'Success! Congratulations on registering for your new account, you can now login!';  
       }
-      else {
-        resultMessage = 'Something went wrong when trying to set up your account. Please try again.'; // TODO: add contact details?  
-      }
       
-    } catch (error) {
-      resultMessage = 'Something went wrong when trying to set up your account. Please try again.'; // TODO: add contact details?
-      reportAxiosError('Something went wrong with user registration', error);
-    }
+    } catch (error) {}
+
     toggleLoading(false);
     changeStep(5, resultMessage); 
   }
@@ -112,7 +113,7 @@ const RegisterImage: React.FC<RegisterStepProps> = ({changeStep, formValues}) =>
         <TouchableOpacity onPress={() => togglePhotoSourceMenu(true)} style={{justifyContent: 'center', }}>
           { photoBase64 != null ?
           <Image style={{alignSelf: 'center', borderRadius: 100, width: 200, height: 200, marginBottom: 5, marginTop: 30,}} source={{uri: 'data:image/png;base64,' + photoBase64}} /> :
-          <Image style={{alignSelf: 'center', borderRadius: 100, width: 200, height: 200, marginBottom: 5, marginTop: 30,}} source={require('../../../../assets/default-pfp.png')} /> 
+          <Image style={{alignSelf: 'center', borderRadius: 100, width: 200, height: 200, marginBottom: 5, marginTop: 30,}} source={require('../../../../../assets/default-pfp.png')} /> 
           }   
           <Text style={{alignSelf: 'center', marginVertical: 20, color: 'white'}}>Choose photo</Text>
         </TouchableOpacity>

+ 1 - 1
src/components/Auth/RegistrationSteps/RegisterMeasurements.tsx → src/components/Profile/Registration/RegistrationSteps/RegisterMeasurements.tsx

@@ -12,7 +12,7 @@ import 'react-native-get-random-values';
 import { RadioButton, Text } from 'react-native-paper';
 import 'react-native-vector-icons';
 import * as Yup from 'yup';
-import { PrimaryButton, SecondaryButton } from '../../Buttons';
+import { PrimaryButton, SecondaryButton } from '../../../Buttons';
 import { RegisterStepProps } from '../RegisterMain';
 
 export interface RegisterMeasurementValues {

+ 3 - 2
src/components/Auth/RegistrationSteps/RegisterPassword.tsx → src/components/Profile/Registration/RegistrationSteps/RegisterPassword.tsx

@@ -10,8 +10,8 @@ import React from 'react';
 import { StyleSheet, TextInput, View } from 'react-native';
 import 'react-native-get-random-values';
 import { Text } from 'react-native-paper';
-import { passwordSchema } from '../../../utils/RegistrationUtils';
-import { PrimaryButton, SecondaryButton } from '../../Buttons';
+import { useValidation } from '../../../../utils/RegistrationUtils';
+import { PrimaryButton, SecondaryButton } from '../../../Buttons';
 import { RegisterStepProps } from '../RegisterMain';
 
 export interface PasswordValues {
@@ -20,6 +20,7 @@ export interface PasswordValues {
 }
 
 const RegisterPassword: React.FC<RegisterStepProps> = ({changeStep, setFormValues, formValues}) => {
+  const {passwordSchema} = useValidation();
   /**
  * The initial measurement values are set to the current password values of the parent form data object
  */

+ 4 - 4
src/components/Profile/Styles/Profile.styles.tsx

@@ -66,10 +66,10 @@ export const ProfileMainStyles = StyleSheet.create({
         paddingTop: 40, 
         paddingBottom: 25, 
         width: '100%', 
-        borderTopLeftRadius: 25, 
-        borderTopRightRadius: 25, 
-        borderBottomLeftRadius: 25, 
-        borderBottomRightRadius: 25
+        borderTopLeftRadius: 10, 
+        borderTopRightRadius: 10, 
+        borderBottomLeftRadius: 10, 
+        borderBottomRightRadius: 10
     },
     headerUsername: {
         textAlign: 'center', 

+ 0 - 23
src/components/Splash.tsx

@@ -1,23 +0,0 @@
-/* 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 React from "react"
-import { Image, StyleSheet, Text } from "react-native"
-import UnauthorizedLayout from "./Auth/AuthLayout"
-
-export const Splash: React.FC = () => {
-    return (
-        <UnauthorizedLayout>
-            <Image source={require('../../assets/logo-white.png')}></Image>
-            <Text >Click & Push</Text>
-        </UnauthorizedLayout>
-    )
-}
-
-const styles = StyleSheet.create({
-
-})

+ 417 - 0
src/data/Auth/AuthContext.tsx

@@ -0,0 +1,417 @@
+import axios, { AxiosRequestConfig } 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 { useQueryClient } from "react-query"
+import { API_URL, reportAxiosError } 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 { navigate } from "../../navigation/RootNavigator"
+
+export const SECURESTORE_ACCESSTOKEN = "access"
+export const SECURESTORE_REFRESHTOKEN = "refresh"
+export const SECURESTORE_NOTIFTOKEN = 'notif'
+export const SECURESTORE_ID = 'id'
+export const SECURESTORE_ANONID = 'anon'
+
+interface AuthState {
+    accessToken: string,
+    notificationToken: string,
+    setNotificationTokenAsync: (token: string) => Promise<void>,
+    setAccessTokenAsync: (token: string) => Promise<void>,
+    setRefreshTokenAsync: (token: string) => Promise<void>,
+    setUserIdAsync: (id: string) => Promise<void>,
+    clearAuthStorage: () => Promise<void>,
+    refreshToken: string,
+    userId: string,
+    anonUserId: string,
+    loading: boolean,
+    error: string,
+    setLoading: (state: boolean) => void,
+    setError: (error: string) => void,
+    sendApiRequestAsync: (config: RequestConfig) => Promise<any>,
+    login: () => Promise<void>,
+    logout: () => Promise<void>,
+    refreshAccessToken: () => Promise<void>,
+    getNotificationTokenFromServer: () => Promise<string>,
+    landmarkOwnedByUser: (landmark: Landmark) => boolean,
+}
+
+interface RequestConfig {
+    axiosConfig: AxiosRequestConfig,
+    authorized: boolean
+    errorMessage: string
+}
+
+export interface IdToken {
+    sub: string
+}
+
+interface AuthenticationResult {
+    success: boolean
+    errorMessage?: string
+}
+
+interface GlobalAlert {
+    title: string
+    message: string
+    type: 'success' | 'error' | 'warning'
+    callback: () => void
+}
+
+const setStorageItem = async (key: string, value: string) => {
+    if (value) {
+        await setItemAsync(key, value)
+    }
+    else {
+        await deleteItemAsync(key)
+    }
+}
+
+const AuthContext = createContext(null)
+
+/**
+ * A base url for the api's authorization endpoints
+ */
+ const issuer = API_URL + "/o";
+
+ /**
+  * An object containing the discovery endpoints for the api, necessary for OIDC authentication {@link https://swagger.io/docs/specification/authentication/openid-connect-discovery/}
+  */
+ const discovery = {
+     authorizationEndpoint: issuer + "/authorize/",
+     tokenEndpoint: issuer + "/token/",
+     revocationEndpoint: issuer + "/revoke/",
+ };
+
+ const redirectUri = makeRedirectUri({
+    path: 'callback'
+});
+
+export const AuthContextProvider: React.FC = ({children}) => {
+    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 [alert, setAlert] = useState<GlobalAlert>()
+
+    const queryClient = useQueryClient()
+
+    useEffect(() => {
+        const loadAuthStateFromStorageOnAppLoad = async () => {
+            const accessTokenFromStorage = await getItemAsync(SECURESTORE_ACCESSTOKEN)
+
+            if (accessTokenFromStorage) {
+                try {
+                    const response = await sendApiRequestAsync({
+                        axiosConfig: {
+                            method: 'GET',
+                            url: '/api/me/',
+                            headers: {Authorization: 'Bearer ' + accessTokenFromStorage}
+                        }, 
+                        authorized: false,
+                        errorMessage: 'Failed to retrieve user data from server'})
+
+                    if (response.status == 200) {
+                        setAccessToken(accessTokenFromStorage)
+                        setRefreshToken(await getItemAsync(SECURESTORE_REFRESHTOKEN))
+                        setNotificationToken(await getItemAsync(SECURESTORE_NOTIFTOKEN))
+                        setUserId(await getItemAsync(SECURESTORE_ID))
+                        return
+                    }
+                }
+                catch {}
+            }
+
+            await setAccessTokenAsync("")
+            await setRefreshTokenAsync("")
+            await setNotificationTokenAsync('')
+            await setUserIdAsync('')
+            
+            let anonUserId = await getItemAsync(SECURESTORE_ANONID)
+            if (anonUserId) {
+                setAnonUserId(anonUserId)
+            }
+            else {
+                anonUserId = v4()
+                await setItemAsync(SECURESTORE_ANONID, anonUserId)
+                setAnonUserId(anonUserId)
+            }
+        }
+        loadAuthStateFromStorageOnAppLoad()
+    }, [])
+
+    useEffect(() => {
+        if (alert) {
+            const alertTitle = alert.title
+            Alert.alert(alertTitle, alert.message, [{text: 'OK', onPress: alert.callback}])
+            setAlert(undefined)
+        }
+    }, [alert])
+
+    useEffect(() => {
+        const convertExistingAnonymousLandmarksOnAccessTokenChange = async () => {
+            if (accessToken && anonUserId) {
+                await convertExistingAnonymousLandmarks()
+            }
+        }
+        convertExistingAnonymousLandmarksOnAccessTokenChange()
+    }, [accessToken])
+
+    const setAccessTokenAsync = async (token: string) => {
+        setAccessToken(token)
+        setStorageItem(SECURESTORE_ACCESSTOKEN, token)
+    }
+
+    const setRefreshTokenAsync = async (token: string) => {
+        setRefreshToken(token)
+        setStorageItem(SECURESTORE_REFRESHTOKEN, token)
+    }
+
+    const setUserIdAsync = async (id: string) => {
+        setUserId(id)
+        setStorageItem(SECURESTORE_ID, id)
+    }
+
+    const setNotificationTokenAsync = async (token: string) => {
+        setNotificationToken(token)
+        setStorageItem(SECURESTORE_NOTIFTOKEN, token)
+    }
+
+    const clearAuthStorage = async () => {
+        await Promise.all([
+            setAccessTokenAsync(""),
+            setRefreshTokenAsync(""),
+            setNotificationTokenAsync(""),
+            setUserIdAsync("")
+        ])
+    }
+
+    const sendApiRequestAsync = async ({axiosConfig, authorized = false, errorMessage = 'An error occured'}: RequestConfig) => {
+        if (authorized && !axiosConfig?.headers?.Authorization) {
+            axiosConfig.headers = {
+                ...axiosConfig.headers,
+                Authorization: `Bearer ${accessToken}`,
+            }   
+        }
+
+        axiosConfig.baseURL = API_URL
+
+        try {
+            return await axios(axiosConfig)
+        } catch (error) {
+            console.log(error.response.request._headers)
+            reportAxiosError(errorMessage, error)
+        }
+    }
+
+    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.')
+                
+    
+                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)
+        }
+        else {
+            setAlert({title: 'Error', message: "Something went wrong while logging in. Please try again.", callback: () => {}, type: 'error'})
+        }
+    } 
+
+    const logout = async () => {
+        setLoading(true)
+        try {
+            const tokenParams = new URLSearchParams();
+            tokenParams.append('client_id', 'atlas.mobile');
+            tokenParams.append('token', accessToken as string);
+            
+            await axios.post(API_URL + `/o/revoke-token/`, tokenParams, {
+                headers: {
+                    'Content-Type': 'application/x-www-form-urlencoded'
+                },
+            });  
+
+            queryClient.setQueryData(queryKeys.getOwnedProfile, null)
+            await setAnonUserId(await getItemAsync(SECURESTORE_ANONID))
+            await clearAuthStorage()
+
+        } catch (error) {
+            reportAxiosError("Something went wrong when logging out", error);
+            setAlert({title: 'Error', message: "Something went wrong while logging out. Please try again.", callback: () => {}, type: 'error'})
+        } 
+        setLoading(false)
+    }
+
+    const refreshAccessToken = async () => {
+        setLoading(true)
+        let currentRefreshToken = refreshToken
+        if (!currentRefreshToken) {
+            currentRefreshToken = await getItemAsync(SECURESTORE_REFRESHTOKEN);
+        }
+
+        if (currentRefreshToken) {
+            try {
+                const tokenData = new URLSearchParams();
+                tokenData.append('grant_type', 'refresh_token');
+                tokenData.append('refresh_token', currentRefreshToken);
+                tokenData.append('client_id', 'atlas.mobile');
+                console.log('[Authentication]: Attempting to refresh token...')
+                const { data: refreshResponseData } = await axios.post(API_URL + "/o/token/", tokenData, {
+                    headers: { 'Content-Type': "application/x-www-form-urlencoded" }
+                });   
+                
+                await setRefreshTokenAsync(refreshResponseData.refresh_token);
+                await setAccessTokenAsync(refreshResponseData.access_token);
+
+                console.info('Successfully refreshed access token.')
+
+            }
+            catch (error) {
+                reportAxiosError("[Authentication]: Error when trying to refresh access token", error);            
+            }
+        }
+
+        setLoading(false)
+    }
+
+    const landmarkOwnedByUser = (landmark: Landmark) => {
+        const owned = landmark?.user == userId || landmark?.anonymous == anonUserId
+        return owned
+    }
+
+    const convertExistingAnonymousLandmarks = async () => {
+        try {
+            const response = await sendApiRequestAsync({
+                axiosConfig: {
+                    method: 'GET',
+                    url: `/api/landmarks/anon/${anonUserId}/`
+                },
+                authorized: true,
+                errorMessage: 'An error occured while checking for anonymous landmarks'
+            })
+    
+            if (response?.data?.has_landmark) {
+                // send request to convert landarks
+                await sendApiRequestAsync({
+                    axiosConfig: {
+                        method: 'POST',
+                        url: `/api/landmarks/convert/${anonUserId}/`
+                    },
+                    authorized: true,
+                    errorMessage: 'Something went wrong when converting anonymous landmarks'
+                })
+                
+                setAlert({
+                    title: 'Heads up',
+                    message: "It looks like you added some landmarks before creating an account, so those landmarks are now owned by your newly created account.", 
+                    callback: () => {},
+                    type: 'warning'
+                })
+            }
+
+            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'
+            })
+            return false
+        }
+    }
+
+    const authState = useMemo(() => ({
+        accessToken,
+        notificationToken,
+        setNotificationTokenAsync,
+        setAccessTokenAsync,
+        setRefreshTokenAsync,
+        setUserIdAsync,
+        clearAuthStorage,
+        landmarkOwnedByUser,
+        refreshToken,
+        userId,
+        loading,
+        anonUserId,
+        setLoading,
+        setAlert,
+        sendApiRequestAsync,
+        login,
+        logout,
+        refreshAccessToken,
+    }), [accessToken, refreshToken, userId, loading, anonUserId])
+
+    return (
+        <AuthContext.Provider value={authState}>
+            {children}
+        </AuthContext.Provider>
+    )
+}
+
+export const useAuth = () => {
+    const context = useContext<AuthState>(AuthContext)
+    if (context === undefined) {
+        throw new Error('useAuth must be used within a AuthProvider')
+    }
+    return context
+}

+ 1 - 0
src/data/axios.ts

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

+ 156 - 0
src/data/comments.ts

@@ -0,0 +1,156 @@
+/* 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 axios, { AxiosRequestConfig } from "axios";
+import { QueryClient, useMutation, useQuery, useQueryClient } from "react-query";
+import { useAuth } from "./Auth/AuthContext";
+import { API_URL, reportAxiosError } from "../utils/RequestUtils";
+import { queryKeys } from "./query-keys";
+
+/**
+ * Interface representing a landmark's comment.
+ */
+export interface LMComment {
+    /**
+     * The comment's id.
+     */
+    id: string,
+    /**
+     * The id of the user who posted the comment
+     */
+    poster: string,
+    /**
+     * The username of the user who posted the comment
+     */
+     poster_name: string,
+    /**
+     * The text content of the comment
+     */
+    content: string
+    /**
+     * The landmark id that this comment is associated with.
+     */
+    landmark: string
+    /**
+     * Timestamp of when the comment was posted.
+     */
+     timestamp?: Date
+    /**
+     * Whether or not the comment has been edited.
+     */
+     edited: boolean
+}
+
+/**
+ * A custom hook containing [react-query]{@link https://react-query.tanstack.com/} queries and mutations and other logic related to interacting with {@link comment} objects.
+ * @category Hooks
+ * @namespace useComments
+ * @param landmarkId - The id of the current landmark focused by the user. Used to fetch only the comments relevant to that landmark.
+ */
+
+export const useLandmarkComments = (landmarkId: string) => {
+    const {sendApiRequestAsync} = useAuth()
+    const queryClient = useQueryClient()
+
+    const getCommentsForLandmark = async (landmarkId: string) => {
+        if (landmarkId) { 
+    
+                const response = await sendApiRequestAsync({
+                    axiosConfig: {
+                        method: 'GET',
+                        url: `${API_URL}/api/comments/${landmarkId}`
+                    },
+                    authorized: false,
+                    errorMessage: 'Something went wrong when retrieving comments'
+                });
+                return response?.data?.reverse();
+        }
+    }
+
+    return useQuery<LMComment[], Error>([queryKeys.getComments, landmarkId], async () => getCommentsForLandmark(landmarkId), {
+        placeholderData: () => queryClient.getQueryData(queryKeys.getComments),
+        staleTime: 1000,
+        refetchInterval: 30000,
+        refetchOnReconnect: true,
+        refetchOnMount: false
+    })
+}
+
+export const useAddComment = () => {
+    const {sendApiRequestAsync} = useAuth()
+    const queryClient = useQueryClient()
+
+    const createComment = async (commentValue: LMComment) => {
+        if (commentValue) {
+            const response = await sendApiRequestAsync({
+                axiosConfig: {
+                    method: 'POST',
+                    data: commentValue,
+                    url: `/api/comments/`,
+                },
+                authorized: true,
+                errorMessage: 'Something went wrong when creating a comment'
+            })
+            response?.data
+        }
+    }
+
+    return useMutation(createComment, {
+        onSuccess: data => {
+            queryClient.invalidateQueries(queryKeys.getComments)
+        },  
+    })
+}
+
+export const useEditComment = () => {
+    const {sendApiRequestAsync} = useAuth()
+    const queryClient = useQueryClient()
+
+    const editComment =  async (commentValue: LMComment) => {
+        if (commentValue) { 
+            const response = await sendApiRequestAsync({
+                axiosConfig: {
+                    method: 'PUT',
+                    data: {...commentValue, edited: true},
+                    url: `/api/comments/`,
+                },
+                authorized: true,
+                errorMessage: 'Something went wrong when editing a comment'
+            })
+            return response?.data;
+        }
+    }
+
+    return useMutation(editComment, {
+        onSuccess: () => queryClient.invalidateQueries(queryKeys.getComments),  
+        onError: () => queryClient.invalidateQueries(queryKeys.getComments),  
+    })
+}
+
+export const useDeleteComment = () => {
+    const {sendApiRequestAsync} = useAuth()
+    const queryClient = useQueryClient()
+
+    const removeComment =  async (id?: string | null) => {
+        if (id) {
+            const response = await sendApiRequestAsync({
+                axiosConfig: {
+                    method: 'DELETE',
+                    url: API_URL + `/api/comments/${id}`,
+                },
+                authorized: true,
+                errorMessage: 'Something went wrong when deleting a comment'
+            });   
+            return response?.data;
+        }
+    }
+
+    return useMutation(removeComment, {
+        onSuccess: () => queryClient.invalidateQueries(queryKeys.getComments),  
+        onError: () => queryClient.invalidateQueries(queryKeys.getComments),  
+    })
+}

+ 325 - 0
src/data/landmarks.ts

@@ -0,0 +1,325 @@
+/* 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 { AxiosRequestConfig } from "axios";
+import { useMutation, useQuery, useQueryClient } from "react-query";
+import { useAuth } from "./Auth/AuthContext";
+import { LMComment } from "./comments";
+import { queryKeys } from "./query-keys";
+
+/**
+ * Interface representing a landmark object
+ */
+export interface Landmark {
+    /*** The id of the landmark.*/
+    id?: string | null,
+    /*** The rating of the landmark.*/
+    rating?: number | null,
+    /*** The id of the user who created the landmark.*/
+    user?: string | null,
+    /*** The x coordinate (or longitude) of the landmark's location.*/
+    longitude?: number | null,
+    /*** The y coordinate (or latitude) of the landmark's location.*/
+    latitude?: number | null, 
+    /*** The landmark's title.*/
+    title?: string | null,
+    /*** The landmark's description.*/
+    description?: string | null,
+    /*** User [comments]{@link LMComment} associated with this landmark.*/
+    comments?: LMComment[] | null,
+    /*** [Photos]{@link LMPhoto} associated with this landmark.*/
+     photos?: LMPhoto[] | null,
+    /*** An integer representing the type of this landmark.*/
+    landmark_type?: number | null, // for working with existing database schema, should be changed
+    /*** A Date object representing when this landmark was created.*/
+    time?: Date | null,
+    //TODO: add floor property
+    // DONE! 
+    floor?: number | null,
+    anonymous?: string
+}
+
+export interface LMPhoto {
+    id: string
+    landmark: string,
+    image_b64: string
+    height: number,
+    width: number
+}
+
+export interface UseLandmarkOptions {
+    userId?: string
+    landmarkId?: string
+    userLMPairing?: {userId?: string, landmarkId?: string}
+    changingPassword?: boolean
+}
+
+/**
+ * A custom hook containing [react-query]{@link https://react-query.tanstack.com/} queries and mutations and other logic related to interacting with {@link Landmark} objects.
+ * @category Hooks
+ * @namespace useLandmarks
+ */
+
+export const useLandmarks = () => {
+    const { sendApiRequestAsync } = useAuth();
+    const queryClient = useQueryClient();
+
+    const getLandmarks = async () => {
+        const response = await sendApiRequestAsync({
+           axiosConfig:  {
+                method: 'GET',
+                url: `/api/landmarks/`,
+            }, 
+            authorized: false,
+            errorMessage: 'Something went wrong when retrieving landmarks'
+        });   
+        return response?.data
+    }
+
+    return useQuery<Landmark[], Error>(queryKeys.getLandmarks, () => getLandmarks(), {
+        placeholderData: () => queryClient.getQueryData(queryKeys.getLandmarks),
+        staleTime: 1000,
+        refetchInterval: 30000,
+        refetchOnReconnect: true,
+        refetchOnMount: false
+    })
+}
+
+export const useLandmark = (landmarkId: string) => {
+    const {sendApiRequestAsync} = useAuth()
+    const queryClient = useQueryClient();
+
+     const getLandmark = async (landmarkId?: string) => {
+        if (landmarkId) {
+            const response = await sendApiRequestAsync({
+                axiosConfig: {
+                    method: 'GET',
+                    url: `/api/landmark/${landmarkId}`,
+                },
+                authorized: false,
+                errorMessage: 'Something went wrong when retrieving the landmark'
+            });   
+            return response?.data 
+        }
+    }
+
+    return useQuery<{landmark: Landmark, ratedByUser: boolean}, Error>([queryKeys.getLandmark, landmarkId], () => getLandmark(landmarkId), {
+        placeholderData: () => queryClient.getQueryData(queryKeys.getLandmark),
+        refetchOnReconnect: true,
+        refetchOnMount: false
+    })
+}
+
+export const useAddLandmark = () => {
+    const {sendApiRequestAsync, userId, anonUserId} = useAuth()
+    const queryClient = useQueryClient();
+
+    const createLandmark = async (data: {landmarkValue: Landmark | undefined, photos?: LMPhoto[], indoorLmLocImg?: string}): Promise<Landmark | undefined> => {
+        if (data.landmarkValue) {
+            console.log("user id is: " + userId)
+            if (userId) {
+                data.landmarkValue.user = userId
+            } else if (anonUserId) {
+                data.landmarkValue.anonymous = anonUserId
+            }
+            else {
+                console.warn("[LandmarkData]: Couldn't create landmark, user id or anon id wasn't given.")
+                return
+            }
+
+            const response = await sendApiRequestAsync({
+                axiosConfig: {
+                    method: 'POST',
+                    url: `/api/landmark/`,
+                    data: {
+                        landmark: data.landmarkValue,
+                        photos: data.photos,
+                        indoorLmLocImg: data.indoorLmLocImg
+                    },
+                },
+                authorized: true,
+                errorMessage: "Something went wrong when creating a landmark"
+            });   
+            return response?.data;
+        }
+        else {
+            console.warn("[LandmarkData]: Can't create landmark. Given landmark value is null.")
+        }
+    }
+
+    return useMutation(createLandmark, {
+        onSuccess: data => {
+            queryClient.invalidateQueries(queryKeys.getLandmarks)
+        },  
+    })
+}     
+
+export const useEditLandmark = () => {
+    const {sendApiRequestAsync, accessToken, anonUserId} = useAuth()
+    const queryClient = useQueryClient();
+
+    const editLandmark =  async (landmarkValue: Landmark) => {
+        if (landmarkValue) {
+            const config: AxiosRequestConfig = {
+                method: 'PUT',
+                url: `/api/landmark/`,
+                data: {
+                    landmark: landmarkValue
+                }
+            }
+
+            if (!accessToken) {
+                config.data = {...config.data, anonymous: anonUserId}
+            }
+
+            const response = await sendApiRequestAsync({
+                axiosConfig: config, 
+                authorized: true,
+                errorMessage: 'Something went wrong when updating a landmark'
+            });   
+            return response?.data;
+        }
+        else {
+            console.warn("[LandmarkData]: Can't update landmark. Given landmark value is null.")
+        }
+    }
+
+    return useMutation(editLandmark, {
+        onSuccess: () => { queryClient.invalidateQueries(queryKeys.getLandmarks)},  
+        onError: () => queryClient.invalidateQueries(queryKeys.getLandmarks),  
+    })
+}
+
+export const useRateLandmark = () => {
+    const {sendApiRequestAsync} = useAuth()
+    const queryClient = useQueryClient();
+
+    const rateLandmark =  async (data: {id: string, rating: 1 | -1}) => {
+        if (data) {
+            const response = await sendApiRequestAsync({
+                axiosConfig:{
+                    method: 'POST',
+                    data: data,
+                    url: `/api/landmark/rate/`,
+                },
+                authorized: true,
+                errorMessage: 'Something went wrong when rating a landmark'
+            });   
+            return response?.data?.rating;
+        }
+    }
+
+    return useMutation(rateLandmark, {
+        onSuccess: () => { 
+            queryClient.invalidateQueries(queryKeys.getLandmark)
+        },  
+        onError: () => queryClient.invalidateQueries(queryKeys.getLandmark),  
+    })    
+}
+
+export const useDeleteLandmark = () => {
+    const {sendApiRequestAsync, accessToken, anonUserId} = useAuth()
+    const queryClient = useQueryClient();
+
+    const deleteLandmark =  async (id?: string) => {
+        if (id) {
+            const config: AxiosRequestConfig = {
+                method: 'DELETE',
+                url: `/api/landmark/${id}/`,
+            }
+
+            if (!accessToken) config.data = {...config.data, anonymous: anonUserId}
+
+            console.log(config)
+
+            const response = await sendApiRequestAsync({
+                axiosConfig: config,
+                authorized: true,
+                errorMessage: 'Something went wrong when deleting a landmark'
+            });   
+            return response?.data;
+        }
+    }
+
+    return useMutation(deleteLandmark, {
+        onSuccess: () => queryClient.invalidateQueries(queryKeys.getLandmarks),  
+        onError: () => queryClient.invalidateQueries(queryKeys.getLandmarks),  
+    })
+}
+
+export const useAddLandmarkPhoto = () => {
+    const {sendApiRequestAsync, accessToken, anonUserId} = useAuth()
+    const queryClient = useQueryClient();
+
+    const addLandmarkPhoto = async (photo: LMPhoto) => {
+        if (photo) {
+            const config: AxiosRequestConfig = {
+                method: 'POST',
+                url: `/api/landmark/photos/`,
+                data: photo
+            }
+        
+            if (!accessToken) config.data = {...config.data, anonymous: anonUserId}
+
+            console.log(accessToken)
+
+            const response = await sendApiRequestAsync({
+                axiosConfig: config, 
+                authorized: true,
+                errorMessage: 'Something went wrong when adding landmark photo'
+            });   
+            return response?.data
+        }
+    }
+
+    return useMutation(addLandmarkPhoto, {
+        onSuccess: () => {
+            queryClient.invalidateQueries(queryKeys.getLandmark)
+        },
+        
+        onError: () => {
+            queryClient.invalidateQueries(queryKeys.getLandmark)
+        },
+    })
+} 
+
+export const useDeleteLandmarkPhoto = () => {
+    const {sendApiRequestAsync, accessToken, anonUserId} = useAuth()
+    const queryClient = useQueryClient();
+
+    const deleteLandmarkPhoto = async (photoId: string) => {
+        if (photoId) {
+            const config: AxiosRequestConfig = {
+                method: 'DELETE',
+                url: `/api/landmark/photos/${photoId}/`
+            }
+
+            if (!accessToken) {
+                config.data = {...config.data, anonymous: anonUserId}
+            }
+
+            console.log(accessToken)
+
+            const response = await sendApiRequestAsync({
+                axiosConfig: config,
+                authorized: true,
+                errorMessage: 'Something went wrong when deleting landmark photos'
+            });   
+            return response?.data
+        }
+    }
+
+    return useMutation(deleteLandmarkPhoto, {
+        onSuccess: () => {
+            queryClient.invalidateQueries(queryKeys.getLandmark)
+        },
+        
+        onError: () => {
+            queryClient.invalidateQueries(queryKeys.getLandmark)
+        },
+    })
+}   

+ 216 - 0
src/data/notifications.ts

@@ -0,0 +1,216 @@
+import Constants from "expo-constants";
+import { useMutation, useQuery, useQueryClient } from "react-query";
+import { SECURESTORE_NOTIFTOKEN, useAuth } from "./Auth/AuthContext";
+import { queryKeys } from "./query-keys";
+import * as Notifications from "expo-notifications";
+import { getItemAsync } from "expo-secure-store";
+import { AxiosRequestConfig } from "axios";
+import { Platform } from "react-native";
+import { navigate } from "../navigation/RootNavigator";
+
+export interface UserNotification {
+    id: string
+    user: string
+    title: string
+    data: string
+    read: boolean
+}
+
+export type NotifType = "near-landmark" | 'near-landmarks' | "landmark-like"
+
+export const useNotificationToken = () => {
+    const { sendApiRequestAsync, userId, accessToken } = useAuth();
+
+    const ensureNotificationTokenExistsOnServer = async (notificationToken: string, retries: number) => {
+        if (retries > 0) {
+            try {
+                const response = await sendApiRequestAsync({
+                    axiosConfig: {
+                        method: 'POST',
+                        data: {token: notificationToken},
+                        url: `/api/notif-token/${userId}/`,
+                    },
+                    authorized: true,
+                    errorMessage: 'Something went wrong when checking notification on server'
+                });   
+                return response.data;
+            } catch (error) {}
+    
+            await ensureNotificationTokenExistsOnServer(notificationToken, retries - 1)   
+        }
+        else {
+            throw new Error("Could not validate notification token on server");
+        }
+    }
+    
+    const getNotificationToken = async () => {
+        try {
+            // first check storage
+            let token = await getItemAsync(SECURESTORE_NOTIFTOKEN)
+            if (token) {
+                const tokenValidationResponse = await ensureNotificationTokenExistsOnServer(token, 3)
+                if (tokenValidationResponse) {
+                    return token
+                }
+            }
+
+            // then check server
+            const response = await sendApiRequestAsync({
+                axiosConfig: {
+                    method: 'GET',
+                    url: `/api/notif-token/${userId}/`,
+                },
+                authorized: true,
+                errorMessage: 'Something went wrong when retrieving notification token'
+            });   
+            
+            if (response?.data) {
+                return response?.data
+            }
+
+            token = (await Notifications.getExpoPushTokenAsync()).data;      
+            
+        } catch (error) {}
+    }
+
+    const {data: token} = useQuery<string, Error>(queryKeys.getNotificationToken, getNotificationToken, {
+        enabled: !!userId && !!accessToken
+    })
+
+    return token
+}
+
+export const useNotifications = () => {
+    const queryClient = useQueryClient();
+    const {sendApiRequestAsync, accessToken, userId} = useAuth()
+
+    const getNotifications = async () => {
+        const response = await sendApiRequestAsync({
+            axiosConfig: { 
+                method: 'GET',
+                url: `/api/user-profile/notifications/${userId}/`,
+            }, 
+            authorized: true,
+            errorMessage: 'Something went wrong when retrieving notifications'});   
+        return response?.data   
+    }
+
+    return useQuery<UserNotification[], Error>(queryKeys.getNotifications, getNotifications, {
+        enabled: !!userId&& !!accessToken
+    })
+}
+
+export const useMarkNotificationRead = () => {
+    const queryClient = useQueryClient();
+    const {sendApiRequestAsync} = useAuth()
+
+    const markNotificationRead = async (notificationId: string) => {
+        const response = await sendApiRequestAsync({
+            axiosConfig: {
+                method: 'POST',
+                url: `/api/user-profile/notifications/mark-read/${notificationId}/`,
+            }, 
+            authorized: true,
+            errorMessage: 'Something went wrong when marking notification as read'});   
+        return response.data;
+    }
+
+    return useMutation(markNotificationRead, {
+        onSuccess: () => { queryClient.invalidateQueries(queryKeys.getNotifications)},  
+        onError: () => queryClient.invalidateQueries(queryKeys.getNotifications),  
+    })
+}
+
+export const useDeleteNotification = () => {
+    const queryClient = useQueryClient();
+    const {sendApiRequestAsync} = useAuth()
+
+    const removeNotificaiton = async (notificationId: string) => {
+        if (notificationId) {        
+            const response = await sendApiRequestAsync({
+                axiosConfig: {
+                    method: 'DELETE',
+                    url: `/api/user-profile/notifications/delete/${notificationId}/`
+                },
+                authorized: true,
+                errorMessage: 'Something went wrong when removing notification'
+            });
+            return response.data;
+        }
+    }
+
+    return useMutation(removeNotificaiton, {
+        onSuccess: () => { queryClient.invalidateQueries(queryKeys.getNotifications)},  
+        onError: () => queryClient.invalidateQueries(queryKeys.getNotifications),  
+    })
+}
+
+export const useRegisterNotifications = () => {
+    const {setNotificationTokenAsync} = useAuth();
+    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;
+            }
+
+            return token
+        } 
+        else {
+            console.warn('[Notifcations]: A physical device must be used for push notifications');
+        }
+        
+        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) => {
+        await markNotificationRead(notifData.notif_id)
+        await refetchNotifications()
+        if (notifData?.notif_type as NotifType == 'landmark-like' || notifData?.notif_type as NotifType == 'near-landmark')
+            navigate('OutdoorMap', {selectedLandmark: notifData.landmark_id})
+        if (notifData?.notif_type as NotifType == 'near-landmarks')
+            navigate('OutdoorMap', {selectedLandmarks: notifData.landmarks})
+    }
+
+    const subscribeToNotifications = async () => {
+        const token = await registerNotificationsAsync()
+        if (token) {
+            const notifReceivedSubscription = Notifications.addNotificationReceivedListener(async notification => { 
+                await refetchNotifications()
+            })
+    
+            const notifResponseReceivedSubscription = Notifications.addNotificationResponseReceivedListener(async response => {
+                const notifData = response.notification.request.content.data
+                handleNotificationInteraction(notifData)
+            });
+    
+            return () => {
+                notifReceivedSubscription.remove()
+                notifResponseReceivedSubscription.remove()
+            }; 
+        }
+    }
+
+    return {subscribeToNotifications, handleNotificationInteraction}
+}

+ 201 - 0
src/data/profiles.ts

@@ -0,0 +1,201 @@
+/* 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 axios, { AxiosRequestConfig } from "axios";
+import { QueryClient, useMutation, useQuery, useQueryClient } from "react-query";
+import { useAuth } from "./Auth/AuthContext";
+import { RegisterCredsValues } from "../utils/RegistrationUtils";
+import { API_URL, reportAxiosError } from "../utils/RequestUtils";
+import { queryKeys } from "./query-keys";
+
+/**
+ * Interface that contains data in a user's profile.
+ */
+export interface UserProfile {
+    /**
+     * The user's id
+     */
+    id?: string
+    /**
+     * The user's username
+     */
+    username?: string
+    /**
+     * The user's email
+     */
+    email?: string
+    /**
+     * The user's base64 encoded profile picture.
+     */
+    image_b64?: string
+    /**
+     * The user's preference for seeing tips.
+     */
+    show_tips?: boolean
+}
+
+/**
+ * A custom hook containing [react-query]{@link https://react-query.tanstack.com/} queries and mutations and other logic related to interacting with {@link UserProfile} objects.
+ * @category Hooks
+ * @namespace useProfile
+ */
+export const useOwnedProfile = () => {
+    const queryClient = useQueryClient();
+    const {sendApiRequestAsync, accessToken, userId} = useAuth()
+
+    const getOwnedProfile = async () => {
+        const response = await sendApiRequestAsync({
+            axiosConfig: {
+                method: 'GET',
+                url: `/api/user-profile/${userId}/`,
+            }, 
+            authorized: true,
+            errorMessage: 'Something went wrong when retrieving user profile'
+        });   
+        return response.data;
+    }
+
+    const {data: profile} = useQuery<UserProfile, Error>(queryKeys.getOwnedProfile, getOwnedProfile, {
+        enabled: !!userId && !!accessToken
+    })
+
+    const changeProfile = async (profile: UserProfile) => {
+        queryClient.setQueriesData(queryKeys.getOwnedProfile, profile)
+    }
+
+    const clearProfile = async () => {
+        queryClient.setQueryData(queryKeys.getOwnedProfile, null);
+        queryClient.removeQueries();
+    }
+
+ return {profile, changeProfile, clearProfile}
+}
+
+export const useEditProfile = () => {
+    const queryClient = useQueryClient();
+    const {sendApiRequestAsync, userId} = useAuth()
+
+     const editProfile = async (values: RegisterCredsValues) => {
+        const response = await sendApiRequestAsync({
+            axiosConfig: {
+                method: 'PUT',
+                url: API_URL + `/api/user-profile/${userId}/`,
+                data: values,
+            },
+            authorized: true,
+            errorMessage: 'Something went wrong when editing profile'
+        });   
+        return response.data;
+    }
+    return useMutation(editProfile, {
+        onSuccess: () => { queryClient.invalidateQueries(queryKeys.getOwnedProfile)},  
+        onError: () => queryClient.invalidateQueries(queryKeys.getOwnedProfile),  
+    })
+}
+
+export const useChangePassword = () => {
+    const queryClient = useQueryClient();
+    const {sendApiRequestAsync, userId} = useAuth()
+    const changePassword = async (password: string) => {
+        if (password && userId) { 
+            const response = await sendApiRequestAsync({
+                axiosConfig: {
+                    method: 'POST',
+                    url: `/api/user-profile/change-password/${userId}/`,
+                    data: {password: password},
+                }, 
+                authorized: true,
+                errorMessage: 'Something went wrong when changing password'
+            });   
+            return response.data;
+        }
+    }
+
+    return useMutation(changePassword, {
+        onSuccess: () => { queryClient.invalidateQueries(queryKeys.getOwnedProfile)},  
+        onError: () => queryClient.invalidateQueries(queryKeys.getOwnedProfile),  
+    })
+}
+
+export const useDeleteProfile = () => {
+    const {clearProfile} = useOwnedProfile()
+    const {clearAuthStorage} = useAuth()
+    const queryClient = useQueryClient();
+    const {sendApiRequestAsync, userId} = useAuth()
+
+    const deleteProfile = async () => {
+        if (userId) { 
+            const response = await sendApiRequestAsync({
+                axiosConfig: {
+                    method: 'DELETE',
+                    url: API_URL + `/api/user-profile/${userId}/`,
+                },
+                authorized: true,
+                errorMessage: 'Something went wrong when deleting profile'
+            });   
+            
+            if (response.status == 200) {
+                await clearProfile()
+                await clearAuthStorage()
+            }
+        }
+    }
+
+    return useMutation(deleteProfile, {
+        onSuccess: () => { queryClient.invalidateQueries(queryKeys.getOwnedProfile)},  
+        onError: () => queryClient.invalidateQueries(queryKeys.getOwnedProfile),  
+    })
+}
+    //*NOT CURRENTLY IN USER*
+    // const changePicture = async () => {
+    //     if (userId) {
+    //         const config: AxiosRequestConfig = {
+    //             method: 'POST',
+    //             url: API_URL + `/api/user-profile/change-picture/${userId}/`,
+    //             headers: { "Authorization": "Bearer " + accessToken, }
+    //         }   
+    //         try {
+    //             const response = await axios(config);   
+    //             return response.data;
+    //         } catch (error) {
+    //             if (error.response.status == 401) {
+    //                 try {
+    //                     await refreshAccessToken()    
+    //                     const response = await axios({...config, headers: { "Authorization": "Bearer " + accessToken }});   
+    //                     return response.data;
+    //                 } catch (error) {
+    //                     // refreshAccessToken will report errors
+    //                 }
+    //             }
+    //         }
+    //     }
+    // }
+
+    
+export const useToggleTips = () => {
+    const {sendApiRequestAsync, userId} = useAuth()
+    const queryClient = useQueryClient();
+
+    const toggleTips = async () => {
+        if (userId) { 
+            const response = await sendApiRequestAsync({
+                axiosConfig: {
+                    method: 'POST',
+                    url: `/api/user-profile/toggle-tips/${userId}/`,
+                },
+                authorized: true,
+                errorMessage: 'Something went wrong when toggling tips'
+            });   
+            return response.data;
+        }
+    }
+
+    return useMutation(toggleTips, {
+        onSuccess: () => { queryClient.invalidateQueries(queryKeys.getOwnedProfile)},  
+        onError: () => queryClient.invalidateQueries(queryKeys.getOwnedProfile),  
+    })
+}

+ 8 - 0
src/data/query-keys.ts

@@ -0,0 +1,8 @@
+export const queryKeys = {
+    getLandmarks: 'getLandmarks',
+    getLandmark: 'getLandmark',
+    getComments: 'getCommentsForLandmark',
+    getOwnedProfile: 'getOwnedProfile',
+    getNotifications: 'getNotifications',
+    getNotificationToken: 'getNotificationToken',
+}

+ 0 - 136
src/hooks/useAuth.ts

@@ -1,136 +0,0 @@
-/* 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 axios, { AxiosRequestConfig, AxiosError } from "axios";
-import { API_URL, reportAxiosError } from "../utils/RequestUtils";
-import { authStore, IdToken } from "../libs/auth/AuthStore";
-import jwtDecode from "jwt-decode";
-import * as SecureStore from 'expo-secure-store'
-import { SECURESTORE_ACCESSTOKEN } from "../utils/GlobalUtils";
-
-/**
- * Hook that exposes common authentication logic.
- * @category Hooks
- * @namespace useAuth
- */
-export const useAuth = () => {
-    /**
-     * Logs out the user
-     * @memberOf useAuth
-     */
-    const logout = async () => {
-        try {
-            const tokenParams = new URLSearchParams();
-            tokenParams.append('client_id', 'atlas.mobile');
-            tokenParams.append('token', authStore.accessToken as string);
-            
-            const response = await axios.post(API_URL + `/o/revoke-token/`, tokenParams, {
-                headers: {
-                    'Content-Type': 'application/x-www-form-urlencoded'
-                },
-            });  
-
-            await authStore.setAccessTokenAsync(null);
-            await authStore.setRefreshTokenAsync(null);
-            await authStore.setNotificationTokenAsync(null);
-            await authStore.setIdAsync(null);
-        } catch (error) {
-            reportAxiosError("Something went wrong when retrieving access token", error);
-        } 
-    }
-
-    const getNotificationTokenFromServer = async () => {
-        const config: AxiosRequestConfig = {
-            method: 'GET',
-            url: API_URL + `/api/notif-token/${authStore.userId}/`,
-            headers: { "Authorization": "Bearer " + authStore.accessToken, }
-        }
-        
-        try {
-            const response = await axios(config);   
-            return response.data;
-        } catch (error) {
-            if (error.response.status == 401) {
-                try {
-                    await refreshAccessToken()    
-                    const response = await axios({...config, headers: { "Authorization": "Bearer " + authStore.accessToken }});   
-
-                    return response.data;
-                } catch (error) {
-                    // refreshAccessToken will report errors
-                }
-            }
-
-            reportAxiosError('Something went wrong when retrieving landmarks', error)
-            throw new Error;
-        }
-    }
-
-    const ensureNotificationTokenExistsOnServer = async (notificationToken: string, retries: number) => {
-        if (retries > 0) {
-            const config: AxiosRequestConfig = {
-                method: 'POST',
-                data: {token: notificationToken},
-                url: API_URL + `/api/notif-token/${authStore.userId}/`,
-                headers: { "Authorization": "Bearer " + authStore.accessToken, }
-            } 
-            try {
-                const response = await axios(config);   
-                return response.data;
-            } catch (error) {
-                if (error.response.status == 401) {
-                    try {
-                        await refreshAccessToken()    
-                        const response = await axios({...config, headers: { "Authorization": "Bearer " + authStore.accessToken }});   
-
-                        return response.data;
-                    } catch (error) {
-                        // refreshAccessToken will report errors
-                    }
-                }
-                reportAxiosError('Something went wrong when retrieving landmarks', error)
-            }
-    
-            ensureNotificationTokenExistsOnServer(notificationToken, retries - 1)   
-        }
-        else {
-            throw new Error;
-        }
-    }
-
-    /**
-     * If there is a refresh token available, attempts to use it to obtain a new, valid access token.
-     * Used in {@link Atlas}
-     * @memberOf useAuth
-     */
-    const refreshAccessToken = async () => {
-        if (authStore.refreshToken) {
-            try {
-                const tokenData = new URLSearchParams();
-                tokenData.append('grant_type', 'refresh_token');
-                tokenData.append('refresh_token', authStore.refreshToken);
-                tokenData.append('client_id', 'atlas.mobile');
-                console.log('[Authentication]: Attempting to refresh token...')
-                const { data: refreshResponseData } = await axios.post(API_URL + "/o/token/", tokenData, {
-                    headers: { 'Content-Type': "application/x-www-form-urlencoded" }
-                });   
-                
-                const idToken = jwtDecode(refreshResponseData.id_token) as IdToken;
-                await authStore.setIdAsync(idToken.sub);
-                await authStore.setRefreshTokenAsync(refreshResponseData.refresh_token);
-                await authStore.setAccessTokenAsync(refreshResponseData.access_token);
-
-                console.info('Successfully refreshed access token, re-attempting initial request...')
-            }
-            catch (error) {
-                reportAxiosError("[Authentication]: Error when trying to refresh access token", error);            
-            }
-        }
-    }
-
-    return { refreshAccessToken, logout, getNotificationTokenFromServer, ensureNotificationTokenExistsOnServer }
-}

+ 0 - 61
src/hooks/useAuthorizedRequests.ts

@@ -1,61 +0,0 @@
-/* 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 axios, { AxiosError, AxiosPromise, AxiosRequestConfig, AxiosResponse } from "axios"
-// import { API_URL, reportAxiosError } from "../globals";
-// import { authStore } from "../stores/AuthStore";
-
-// export const useAuthorizedRequests = () => {
-//     const sendAuthorizedRequestAsync = async (requestConfig: AxiosRequestConfig, errorMessage: string):  => {
-//         try {
-//             const response = fetch(requestConfig);
-//             return response;
-//         } catch (error) {
-//             if (error.response.status == 401) {
-//                 console.info("An authorized request failed, trying to refresh access token...")
-
-//                 if (authStore.refreshToken) {
-//                     const tokenData = new URLSearchParams();
-//                     tokenData.append('grant_type', 'refresh_token');
-//                     tokenData.append('refresh_token', authStore.refreshToken);
-//                     tokenData.append('client_id', 'atlas.mobile');
-
-//                     try {
-//                         const { data: refreshResponseData } = await axios.post(API_URL + "/o/token/", tokenData, {
-//                             headers: { 'Content-Type': "application/x-www-form-urlencoded" }
-//                         });   
-
-//                         await authStore.setRefreshTokenAsync(refreshResponseData.refresh_token);
-//                         await authStore.setAccessTokenAsync(refreshResponseData.access_token);
-
-//                         console.info('Successfully refreshed access token, re-attempting initial request...')
-
-//                         try {
-//                             const response = await axios(requestConfig);  
-//                         } catch (error) {
-//                             reportAxiosError("Something went wrong when re-attempting initial request", error);                
-//                         } 
-//                     } catch (error) {
-//                         reportAxiosError("Error when trying to refresh access token", error);            
-//                     }
-//                 }
-
-//                 // no refresh token, user will be directed back to login
-//                 else {
-//                     await authStore.setAccessTokenAsync(null);
-//                     await authStore.setRefreshTokenAsync(null);
-//                     return
-//                 }
-//             }
-//             else {
-//                 reportAxiosError(errorMessage, error);
-//             }
-//         }
-//     }
-
-//     return { sendAuthorizedRequestAsync }
-// }

+ 0 - 234
src/hooks/useComments.ts

@@ -1,234 +0,0 @@
-/* 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 axios, { AxiosRequestConfig } from "axios";
-import { QueryClient, useMutation, useQuery, useQueryClient } from "react-query";
-import { authStore } from "../libs/auth/AuthStore";
-import { API_URL, reportAxiosError } from "../utils/RequestUtils";
-import { useAuth } from "./useAuth";
-
-/**
- * Interface representing a landmark's comment.
- */
-export interface LMComment {
-    /**
-     * The comment's id.
-     */
-    id: string,
-    /**
-     * The id of the user who posted the comment
-     */
-    poster: string,
-    /**
-     * The username of the user who posted the comment
-     */
-     poster_name: string,
-    /**
-     * The text content of the comment
-     */
-    content: string
-    /**
-     * The landmark id that this comment is associated with.
-     */
-    landmark: string
-    /**
-     * Timestamp of when the comment was posted.
-     */
-     timestamp?: Date
-    /**
-     * Whether or not the comment has been edited.
-     */
-     edited: boolean
-}
-
-/**
- * A custom hook containing [react-query]{@link https://react-query.tanstack.com/} queries and mutations and other logic related to interacting with {@link comment} objects.
- * @category Hooks
- * @namespace useComments
- * @param landmarkId - The id of the current landmark focused by the user. Used to fetch only the comments relevant to that landmark.
- */
-export const useComments = (landmarkId: string) => {
-    const { refreshAccessToken } = useAuth();
-
-    /**
-     * The local instance of the [react-query QueryClient]{@link https://react-query.tanstack.com/reference/QueryClient#_top}.
-     * @memberOf useComments
-     */
-    let queryClient: QueryClient;
-
-    try {
-        queryClient = useQueryClient()
-    } catch (error) {
-        console.error("[CommentsData]: Something went wrong when retrieving query client: " + error)
-    }
-
-    /**
-     * The callback responsible for retrieving [comments]{@link LMComment} from the API, used by the [react-query useQuery]{@link https://react-query.tanstack.com/reference/useQuery#_top} hook.
-     * * @memberOf useComments
-     */
-    const getCommentsForLandmark = async (landmarkId: string) => {
-        if (landmarkId) {
-            const config: AxiosRequestConfig = {
-                method: 'GET',
-                url: `${API_URL}/api/comments/${landmarkId}`,
-                headers: { "Authorization": "Bearer " + authStore.accessToken, }
-            } 
-    
-            try {
-                const response = await axios(config);
-                return response.data.reverse();
-            } catch (error) {
-                if (error.response.status == 401) {
-                    try {
-                        await refreshAccessToken()    
-                        const response = await axios({...config, headers: { "Authorization": "Bearer " + authStore.accessToken }});   
-                        return response.data.reverse();
-                    } catch (error) {
-                        // refreshAccessToken will report errors
-                    }
-                }
-    
-                reportAxiosError('Something went wrong when retrieving landmarks', error)
-                throw new Error;
-            }      
-        }
-    }
-
-     /**
-     * The callback responsible for adding a new {@link Landmark} to the server, used by the [react-query useMutation]{@link https://react-query.tanstack.com/reference/useMutation#_top} hook.
-     * * @memberOf useLandmarks
-     */
-    const createComment = async (commentValue: LMComment): Promise<LMComment | undefined> => {
-        if (commentValue) {
-            const config: AxiosRequestConfig = {
-                method: 'POST',
-                data: commentValue,
-                url: API_URL + `/api/comments/`,
-                headers: { "Authorization": "Bearer " + authStore.accessToken, }
-            } 
-    
-            try {
-                const response = await axios(config);   
-                return response.data;
-            } catch (error) {
-                if (error.response.status == 401) {
-                    try {
-                        await refreshAccessToken()    
-                        const response = await axios({...config, headers: { "Authorization": "Bearer " + authStore.accessToken }});   
-    
-                        return response.data;
-                    } catch (error) {
-                        // refreshAccessToken will report errors
-                    }
-                }
-    
-                reportAxiosError('Something went wrong when retrieving landmarks', error)
-                throw new Error;
-            }   
-        }
-    }
-
-    /**
-     * The callback responsible for updating a {@link Landmark} on the server, used by the [react-query useMutation]{@link https://react-query.tanstack.com/reference/useMutation#_top} hook.
-     * * @memberOf useComments
-     */
-    const editComment =  async (commentValue: LMComment) => {
-        if (commentValue) {
-            const config: AxiosRequestConfig = {
-                method: 'PUT',
-                data: {...commentValue, edited: true},
-                url: API_URL + `/api/comments/`,
-                headers: { "Authorization": "Bearer " + authStore.accessToken, }
-            } 
-    
-            try {
-                const response = await axios(config);   
-                return response.data;
-            } catch (error) {
-                if (error.response.status == 401) {
-                    try {
-                        await refreshAccessToken()    
-                        const response = await axios({...config, headers: { "Authorization": "Bearer " + authStore.accessToken }});   
-    
-                        return response.data;
-                    } catch (error) {
-                        // refreshAccessToken will report errors
-                    }
-                }
-    
-                reportAxiosError('Something went wrong when retrieving landmarks', error)
-                throw new Error;
-            }   
-        }
-    }
-
-    /**
-     * The callback responsible for deleting a {@link Landmark} from the server, used by the [react-query useMutation]{@link https://react-query.tanstack.com/reference/useMutation#_top} hook.
-     * * @memberOf useLandmarks
-     */
-    const removeComment =  async (id?: string | null) => {
-        if (id) {
-            const config: AxiosRequestConfig = {
-                method: 'DELETE',
-                url: API_URL + `/api/comments/${id}`,
-                headers: { "Authorization": "Bearer " + authStore.accessToken, }
-            } 
-            try {
-                const response = await axios(config);   
-                return response.data;
-            } catch (error) {
-                if (error.response.status == 401) {
-                    try {
-                        await refreshAccessToken()    
-    
-                        // add new access token to header
-                        const response = await axios({...config, headers: {"Authorization": "Bearer " + authStore.accessToken}});  
-                        return response.data;
-                    } catch (error) {
-                        // refreshAccessToken will report errors
-                    }
-                }
-    
-                reportAxiosError('Something went wrong when retrieving landmarks', error)
-                throw new Error;
-            }   
-        }
-    }
-
-    // get-all query
-    const { data: comments, status: getCommentsForLandmarkStatus, refetch: refetechCommentsForLandmark } = useQuery<LMComment[], Error>(['getCommentsForLandmark', landmarkId], async () => getCommentsForLandmark(landmarkId), {
-        placeholderData: () => queryClient.getQueryData('getCommentsForLandmark'),
-        staleTime: 1000,
-        refetchInterval: 30000,
-        refetchOnReconnect: true,
-        refetchOnMount: false
-    })
-
-    // mutations
-    const { status: addCommentStatus, mutateAsync: addCommentAsync, reset: resetAddComment, data: newComment } = useMutation(createComment, {
-        onSuccess: data => {
-            queryClient.invalidateQueries('getCommentsForLandmark')
-        },  
-    })
-
-    const { status: updateCommentStatus, mutateAsync: updateCommentAsync, reset: resetUpdateComment } = useMutation(editComment, {
-        onSuccess: () => queryClient.invalidateQueries('getCommentsForLandmark'),  
-        onError: () => queryClient.invalidateQueries('getCommentsForLandmark'),  
-    })
-
-    const { status: deleteCommentStatus, mutateAsync: deleteCommentAsync, reset: resetDeleteComment } = useMutation(removeComment, {
-        onSuccess: () => queryClient.invalidateQueries('getCommentsForLandmark'),  
-        onError: () => queryClient.invalidateQueries('getCommentsForLandmark'),  
-    })
-
-    return { 
-        comments, getCommentsForLandmarkStatus, refetechCommentsForLandmark,  //reading
-        addCommentAsync, resetAddComment, addCommentStatus, newComment, // creating
-        updateCommentAsync, resetUpdateComment, updateCommentStatus, // updating
-        deleteCommentAsync, resetDeleteComment, deleteCommentStatus, // deleting
-    }
-}

+ 0 - 473
src/hooks/useLandmarks.ts

@@ -1,473 +0,0 @@
-/* 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 axios, { AxiosRequestConfig } from "axios";
-import { QueryClient, useMutation, useQuery, useQueryClient } from "react-query";
-import { authStore } from "../libs/auth/AuthStore";
-import { API_URL, reportAxiosError } from "../utils/RequestUtils";
-import { useAuth } from "./useAuth";
-import { LMComment } from "./useComments";
-
-/**
- * Interface representing a landmark object
- */
-export interface Landmark {
-    /*** The id of the landmark.*/
-    id?: string | null,
-    /*** The rating of the landmark.*/
-    rating?: number | null,
-    /*** The id of the user who created the landmark.*/
-    user?: string | null,
-    /*** The x coordinate (or longitude) of the landmark's location.*/
-    longitude?: number | null,
-    /*** The y coordinate (or latitude) of the landmark's location.*/
-    latitude?: number | null, 
-    /*** The landmark's title.*/
-    title?: string | null,
-    /*** The landmark's description.*/
-    description?: string | null,
-    /*** User [comments]{@link LMComment} associated with this landmark.*/
-    comments?: LMComment[] | null,
-    /*** [Photos]{@link LMPhoto} associated with this landmark.*/
-     photos?: LMPhoto[] | null,
-    /*** An integer representing the type of this landmark.*/
-    landmark_type?: number | null, // for working with existing database schema, should be changed
-    /*** A Date object representing when this landmark was created.*/
-    time?: Date | null,
-    //TODO: add floor property
-    // DONE! 
-    floor?: number | null,
-}
-
-export interface LMPhoto {
-    id: string
-    landmark: string,
-    image_b64: string
-    height: number,
-    width: number
-}
-
-export interface UseLandmarkOptions {
-    userId?: string
-    landmarkId?: string
-    userLMPairing?: {userId?: string, landmarkId?: string}
-    changingPassword?: boolean
-}
-
-/**
- * A custom hook containing [react-query]{@link https://react-query.tanstack.com/} queries and mutations and other logic related to interacting with {@link Landmark} objects.
- * @category Hooks
- * @namespace useLandmarks
- */
-export const useLandmarks = (options?: UseLandmarkOptions) => {
-    const { refreshAccessToken } = useAuth();
-
-    /**
-     * The local instance of the [react-query QueryClient]{@link https://react-query.tanstack.com/reference/QueryClient#_top}.
-     * @memberOf useLandmarks
-     */
-    let queryClient: QueryClient;
-
-    try {
-        queryClient = useQueryClient()
-    } catch (error) {
-        console.error("[LandmarkData]: Something went wrong when retrieving query client: " + error)
-    }
-
-    /**
-     * The callback responsible for retrieving {@link Landmark} from the API, used by the [react-query useQuery]{@link https://react-query.tanstack.com/reference/useQuery#_top} hook.
-     * * @memberOf useLandmarks
-     */
-    const getLandmarks = async () => {
-        let url = `${API_URL}/api/landmarks/`   
-        const config: AxiosRequestConfig = {
-            method: 'GET',
-            url: url,
-            headers: { "Authorization": "Bearer " + authStore.accessToken, }
-        } 
-
-        try {
-            const response = await axios(config);   
-            return response.data
-        } catch (error) {
-            if (error.response.status == 401) {
-                try {
-                    await refreshAccessToken()    
-                    const response = await axios({...config, headers: { "Authorization": "Bearer " + authStore.accessToken }});   
-                    return response.data
-                } catch (error) {
-                    // refreshAccessToken will report errors
-                }
-            }
-
-            reportAxiosError('Something went wrong when retrieving landmarks', error)
-            throw new Error;
-        }   
-        // }
-        // else {
-        //     return [];
-        // }
-    }
-
-    /**
-     * The callback responsible for retrieving photos for a {@link Landmark}, used by the [react-query useQuery]{@link https://react-query.tanstack.com/reference/useQuery#_top} hook.
-     * * @memberOf useLandmarks
-     */
-    const getLandmark = async (landmarkId?: string) => {
-        if (landmarkId) {
-            let url = `${API_URL}/api/landmark/${landmarkId}`   
-        
-            const config: AxiosRequestConfig = {
-                method: 'GET',
-                url: url,
-                headers: { "Authorization": "Bearer " + authStore.accessToken, } 
-            }
-    
-            try {
-                const response = await axios(config);   
-                return response.data
-            } catch (error) {
-                if (error.response.status == 401) {
-                    try {
-                        await refreshAccessToken()    
-                        const response = await axios({...config, headers: { "Authorization": "Bearer " + authStore.accessToken }});   
-                        return response.data
-                    } catch (error) {
-                        // refreshAccessToken will report errors
-                    }
-                }
-    
-                reportAxiosError('Something went wrong when retrieving landmarks', error)
-                throw new Error;
-            }   
-        }
-        
-        // }
-        // else {
-        //     return [];
-        // }
-    }
-
-    const addLandmarkPhoto = async (photo: LMPhoto) => {
-        if (photo) {
-            let url = `${API_URL}/api/landmark/photos/`   
-        
-            const config: AxiosRequestConfig = {
-                method: 'POST',
-                url: url,
-                data: photo,
-                headers: { "Authorization": "Bearer " + authStore.accessToken, } 
-            }
-    
-            try {
-                const response = await axios(config);   
-                return response.data
-            } catch (error) {
-                if (error.response.status == 401) {
-                    try {
-                        await refreshAccessToken()    
-                        const response = await axios({...config, headers: { "Authorization": "Bearer " + authStore.accessToken }});   
-                        return response.data
-                    } catch (error) {
-                        // refreshAccessToken will report errors
-                    }
-                }
-    
-                reportAxiosError('Something went wrong when retrieving landmarks', error)
-                throw new Error;
-            }   
-        }
-    }
-
-    const deleteLandmarkPhoto = async (photoId: string) => {
-        if (photoId) {
-            let url = `${API_URL}/api/landmark/photos/${photoId}`   
-        
-            const config: AxiosRequestConfig = {
-                method: 'DELETE',
-                url: url,
-                headers: { "Authorization": "Bearer " + authStore.accessToken, } 
-            }
-    
-            try {
-                const response = await axios(config);   
-                return response.data
-            } catch (error) {
-                if (error.response.status == 401) {
-                    try {
-                        await refreshAccessToken()    
-                        const response = await axios({...config, headers: { "Authorization": "Bearer " + authStore.accessToken }});   
-                        return response.data
-                    } catch (error) {
-                        // refreshAccessToken will report errors
-                    }
-                }
-    
-                reportAxiosError('Something went wrong when retrieving landmarks', error)
-                throw new Error;
-            }   
-        }
-        
-        // }
-        // else {
-        //     return [];
-        // }
-    }
-
-    const checkIfRatedByUser = async (userLMPairing: {userId?: string, landmarkId?: string}) => {
-        if (options?.userLMPairing) {
-            if (userLMPairing?.landmarkId && userLMPairing?.userId) {
-                const config: AxiosRequestConfig = {
-                    method: 'GET',
-                    url: `${API_URL}/api/landmark/check-rate-pairing/?landmark=${options?.userLMPairing?.landmarkId}&user=${options.userLMPairing.userId}`,
-                    headers: { "Authorization": "Bearer " + authStore.accessToken, }
-                } 
-        
-                try {
-                    const response = await axios(config);   
-                    return response.data;
-                } catch (error) {
-                    if (error.response.status == 401) {
-                        try {
-                            await refreshAccessToken()    
-                            const response = await axios({...config, headers: { "Authorization": "Bearer " + authStore.accessToken }});   
-                            return response.data;
-                        } catch (error) {
-                            // refreshAccessToken will report errors
-                        }
-                    }
-        
-                    reportAxiosError('Something went wrong when retrieving landmarks', error)
-                    throw new Error;
-                }      
-            }
-            else {
-                console.warn("[LandmarkData]: Can't check landmark rating pairing. Given id values must not be null.")
-            }
-        }
-    }
-
-     /**
-     * The callback responsible for adding a new {@link Landmark} to the server, used by the [react-query useMutation]{@link https://react-query.tanstack.com/reference/useMutation#_top} hook.
-     * * @memberOf useLandmarks
-     */
-    const createLandmark = async (data: {landmarkValue: Landmark | undefined, photos?: LMPhoto[]}): Promise<Landmark | undefined> => {
-        if (data.landmarkValue) {
-            const config: AxiosRequestConfig = {
-                method: 'POST',
-                data: {
-                    landmark: data.landmarkValue,
-                    photos: data.photos
-                },
-                url: API_URL + `/api/landmark/`,
-                headers: { "Authorization": "Bearer " + authStore.accessToken, }
-            } 
-    
-            try {
-                const response = await axios(config);   
-                return response.data;
-            } catch (error) {
-                if (error.response.status == 401) {
-                    try {
-                        await refreshAccessToken()    
-                        const response = await axios({...config, headers: { "Authorization": "Bearer " + authStore.accessToken }});   
-    
-                        return response.data;
-                    } catch (error) {
-                        // refreshAccessToken will report errors
-                    }
-                }
-    
-                reportAxiosError('[LandmarkData]: Something went wrong when retrieving landmarks', error)
-                throw new Error;
-            }   
-        }
-        else {
-            console.warn("[LandmarkData]: Can't create landmark. Given landmark value is null.")
-        }
-    }
-
-    /**
-     * The callback responsible for updating a {@link Landmark} on the server, used by the [react-query useMutation]{@link https://react-query.tanstack.com/reference/useMutation#_top} hook.
-     * * @memberOf useLandmarks
-     */
-    const editLandmark =  async (landmarkValue: Landmark) => {
-        if (landmarkValue) {
-            const config: AxiosRequestConfig = {
-                method: 'PUT',
-                data: landmarkValue,
-                url: API_URL + `/api/landmark/`,
-                headers: { "Authorization": "Bearer " + authStore.accessToken, }
-            } 
-
-            try {
-                const response = await axios(config);   
-                return response.data;
-            } catch (error) {
-                if (error.response.status == 401) {
-                    try {
-                        await refreshAccessToken()    
-                        const response = await axios({...config, headers: { "Authorization": "Bearer " + authStore.accessToken }});   
-
-                        return response.data;
-                    } catch (error) {
-                        // refreshAccessToken will report errors
-                    }
-                }
-
-                reportAxiosError('Something went wrong when retrieving landmarks', error)
-                throw new Error;
-            }
-        }
-        else {
-            console.warn("[LandmarkData]: Can't update landmark. Given landmark value is null.")
-        }
-    }
-
-    /**
-     * The callback responsible for updating a {@link Landmark} on the server, used by the [react-query useMutation]{@link https://react-query.tanstack.com/reference/useMutation#_top} hook.
-     * * @memberOf useLandmarks
-     */
-     const rateLandmark =  async (data: {id: string, rating: 1 | -1}) => {
-        const config: AxiosRequestConfig = {
-            method: 'POST',
-            data: data,
-            url: API_URL + `/api/landmark/rate/`,
-            headers: { "Authorization": "Bearer " + authStore.accessToken, }
-        } 
-
-        try {
-            const response = await axios(config);   
-            return response.data.rating;
-        } catch (error) {
-            if (error.response.status == 401) {
-                try {
-                    await refreshAccessToken()    
-                    const response = await axios({...config, headers: { "Authorization": "Bearer " + authStore.accessToken }});   
-
-                    return response.data;
-                } catch (error) {
-                    // refreshAccessToken will report errors
-                }
-            }
-
-            reportAxiosError('[LandmarkData]: Something went wrong when retrieving landmarks', error)
-            throw new Error;
-        }
-    }
-
-    /**
-     * The callback responsible for deleting a {@link Landmark} from the server, used by the [react-query useMutation]{@link https://react-query.tanstack.com/reference/useMutation#_top} hook.
-     * * @memberOf useLandmarks
-     */
-    const removeLandmark =  async (id?: string | null) => {
-        const config: AxiosRequestConfig = {
-            method: 'DELETE',
-            url: API_URL + `/api/landmark/${id}`,
-            headers: { "Authorization": "Bearer " + authStore.accessToken, }
-        } 
-        try {
-            const response = await axios(config);   
-            return response.data;
-        } catch (error) {
-            if (error.response.status == 401) {
-                try {
-                    await refreshAccessToken()    
-
-                    // add new access token to header
-                    const response = await axios({...config, headers: {"Authorization": "Bearer " + authStore.accessToken}});   
-
-                    return response.data;
-                } catch (error) {
-                    // refreshAccessToken will report errors
-                }
-            }
-
-            reportAxiosError('[LandmarkData]: Something went wrong when retrieving landmarks', error)
-            throw new Error;
-        }
-    }
-
-    // get-all query
-    const { data: landmarks, status: getLandmarksStatus, refetch: refetchLandmarks } = useQuery<Landmark[], Error>('getLandmarks', () => getLandmarks(), {
-        placeholderData: () => queryClient.getQueryData('getLandmarks'),
-        staleTime: 1000,
-        refetchInterval: 30000,
-        refetchOnReconnect: true,
-        refetchOnMount: false
-    })
-
-    // get-details query
-    const { data: landmark, status: getLandmarkStatus, refetch: refetchLandmark } = useQuery<Landmark, Error>(['getLandmark', options?.landmarkId], () => getLandmark(options?.landmarkId), {
-        placeholderData: () => queryClient.getQueryData('getLandmark'),
-        refetchOnReconnect: true,
-        refetchOnMount: false
-    })
-
-    const { data: landmarkRatedByUser, status: checkIfRatedByUserStatus, refetch: refetchCheckIfRatedByUser } = useQuery<boolean, Error>(['checkIfRatedByUser', options?.userLMPairing], () => checkIfRatedByUser(options?.userLMPairing), {
-        placeholderData: () => queryClient.getQueryData('checkIfRatedByUser'),
-        refetchOnReconnect: true,
-        refetchOnMount: false
-    })
-
-    // mutations
-    const { status: addLandmarkStatus, mutateAsync: addLandmarkAsync, reset: resetAddLm, data: newLandmark } = useMutation(createLandmark, {
-        onSuccess: data => {
-            queryClient.invalidateQueries('getLandmarks')
-        },  
-    })
-
-    const { status: updateLandmarkStatus, mutateAsync: updateLandmark, reset: resetUpdateLm } = useMutation(editLandmark, {
-        onSuccess: () => { queryClient.invalidateQueries('getLandmarks')},  
-        onError: () => queryClient.invalidateQueries('getLandmarks'),  
-    })
-
-    const { data: rating, status: rateLandmarkStatus, mutateAsync: rateLandmarkAsync, reset: resetRateLandmark } = useMutation(rateLandmark, {
-        onSuccess: () => { 
-            queryClient.invalidateQueries('getLandmarks')
-            queryClient.invalidateQueries('checkIfRatedByUser')
-        },  
-        onError: () => queryClient.invalidateQueries('getLandmarks'),  
-    })
-
-    const { status: deleteLandmarkStatus, mutateAsync: deleteLandmark, reset: resetDeleteLm } = useMutation(removeLandmark, {
-        onSuccess: () => queryClient.invalidateQueries('getLandmarks'),  
-        onError: () => queryClient.invalidateQueries('getLandmarks'),  
-    })
-
-    const { status: addPhotoStatus, mutateAsync: addPhoto, reset: resetAddPhoto } = useMutation(addLandmarkPhoto, {
-        onSuccess: () => {
-            queryClient.invalidateQueries('getLandmark')
-        },
-        
-        onError: () => {
-            queryClient.invalidateQueries('getLandmark')
-        },
-    })
-
-    const { status: deletePhotoStatus, mutateAsync: deletePhoto, reset: resetDeletePhoto } = useMutation(deleteLandmarkPhoto, {
-        onSuccess: () => {
-            queryClient.invalidateQueries('getLandmark')
-        },
-        
-        onError: () => {
-            queryClient.invalidateQueries('getLandmark')
-        },
-    })
-
-    return { 
-        landmarks, getLandmarksStatus, refetchLandmarks,  //reading
-        landmark, getLandmarkStatus, refetchLandmark, //photos
-        addPhoto, addPhotoStatus, resetAddPhoto, //add photos
-        deletePhoto, deletePhotoStatus, resetDeletePhoto, //delete photos
-        addLandmarkAsync, resetAddLm, addLandmarkStatus, newLandmark, // creating
-        updateLandmark, resetUpdateLm, updateLandmarkStatus, // updating
-        deleteLandmark, resetDeleteLm, deleteLandmarkStatus, // deleting
-        rateLandmarkAsync, resetRateLandmark, rateLandmarkStatus, rating, // rating
-        landmarkRatedByUser, checkIfRatedByUserStatus, refetchCheckIfRatedByUser // check if landmark is rated by user
-    }
-}

+ 0 - 375
src/hooks/useProfile.ts

@@ -1,375 +0,0 @@
-/* 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 axios, { AxiosRequestConfig } from "axios";
-import { QueryClient, useMutation, useQuery, useQueryClient } from "react-query";
-import { authStore } from "../libs/auth/AuthStore";
-import { RegisterCredsValues } from "../utils/RegistrationUtils";
-import { API_URL, reportAxiosError } from "../utils/RequestUtils";
-import { useAuth } from "./useAuth";
-
-/**
- * Interface that contains data in a user's profile.
- */
-export interface UserProfile {
-    /**
-     * The user's id
-     */
-    id?: string
-    /**
-     * The user's username
-     */
-    username?: string
-    /**
-     * The user's email
-     */
-    email?: string
-    /**
-     * The user's base64 encoded profile picture.
-     */
-    image_b64?: string
-    /**
-     * The user's preference for seeing tips.
-     */
-    show_tips?: boolean
-}
-
-export interface UserNotification {
-    id: string
-    user: string
-    title: string
-    data: string
-    read: boolean
-}
-
-/**
- * A custom hook containing [react-query]{@link https://react-query.tanstack.com/} queries and mutations and other logic related to interacting with {@link UserProfile} objects.
- * @category Hooks
- * @namespace useProfile
- */
-export const useProfile = (userId: string) => {
-    /**
-     * The local instance of the [react-query QueryClient]{@link https://react-query.tanstack.com/reference/QueryClient#_top}.
-     * @memberOf useLandmarks
-     */
-    let queryClient: QueryClient;
-
-    try {
-        queryClient = useQueryClient()
-    } catch (error) {
-        console.warn("[ProfileData]: Something went wrong when retrieving query client: " + error)
-    }
-
-    const { refreshAccessToken } = useAuth();
-
-     /**
-     * The callback responsible for retrieving the {@link Profile} matching the given ID from the API , used by the [react-query useQuery]{@link https://react-query.tanstack.com/reference/useQuery#_top} hook.
-     * * @memberOf useProfile
-     */
-    const getProfile = async (id: string) => {
-        if (userId) {
-        const config: AxiosRequestConfig = {
-            method: 'GET',
-            url: API_URL + `/api/user-profile/${id}/`,
-            headers: { "Authorization": "Bearer " + authStore.accessToken, }
-        } 
-        try {
-            const response = await axios(config);   
-            return response.data;
-        } catch (error) {
-            if (error.response.status == 401) {
-                try {
-                    await refreshAccessToken()    
-                    const response = await axios({...config, headers: { "Authorization": "Bearer " + authStore.accessToken }});   
-                    return response.data;
-                } catch (error) {
-                    // refreshAccessToken will report errors
-                }
-            }
-
-            reportAxiosError('Something went wrong when retrieving user profile', error)
-            throw new Error;
-        }
-        
-        }
-    }
-
-    /**
-     * The callback responsible for retrieving {@link Landmark} from the API, used by the [react-query useQuery]{@link https://react-query.tanstack.com/reference/useQuery#_top} hook.
-     * * @memberOf useLandmarks
-     */
-    const getNotifications = async () => {
-        if (authStore.userId) {
-            let url = `${API_URL}/api/user-profile/notifications/${authStore.userId}/`   
-            const config: AxiosRequestConfig = {
-                method: 'GET',
-                url: url,
-                headers: { "Authorization": "Bearer " + authStore.accessToken, }
-            } 
-    
-            try {
-                const response = await axios(config);   
-                return response.data
-            } catch (error) {
-                if (error.response.status == 401) {
-                    try {
-                        await refreshAccessToken()    
-                        const response = await axios({...config, headers: { "Authorization": "Bearer " + authStore.accessToken }});   
-                        return response.data
-                    } catch (error) {
-                        // refreshAccessToken will report errors
-                    }
-                }
-    
-                reportAxiosError('Something went wrong when retrieving notifications', error)
-                throw new Error;
-            }   
-        }
-    }
-
-    const markNotificationRead = async (notificationId: string) => {
-        const config: AxiosRequestConfig = {
-            method: 'POST',
-            url: API_URL + `/api/user-profile/notifications/mark-read/${notificationId}/`,
-            headers: { "Authorization": "Bearer " + authStore.accessToken, }
-        } 
-        try {
-            const response = await axios(config);   
-            return response.data;
-        } catch (error) {
-            if (error.response.status == 401) {
-                try {
-                    await refreshAccessToken()    
-                    const response = await axios({...config, headers: { "Authorization": "Bearer " + authStore.accessToken }});   
-                    return response.data;
-                } catch (error) {
-                    // refreshAccessToken will report errors
-                }
-            }
-
-            reportAxiosError('Something went wrong when editing user profile', error)
-            throw new Error;
-        }
-    }
-
-    const removeNotificaiton = async (notificationId: string) => {
-        const config: AxiosRequestConfig = {
-            method: 'DELETE',
-            url: API_URL + `/api/user-profile/notifications/delete/${notificationId}/`,
-            headers: { "Authorization": "Bearer " + authStore.accessToken, }
-        } 
-        try {
-            const response = await axios(config);   
-            return response.data;
-        } catch (error) {
-            if (error.response.status == 401) {
-                try {
-                    await refreshAccessToken()    
-                    const response = await axios({...config, headers: { "Authorization": "Bearer " + authStore.accessToken }});   
-                    return response.data;
-                } catch (error) {
-                    // refreshAccessToken will report errors
-                }
-            }
-
-            reportAxiosError('Something went wrong when editing user profile', error)
-            throw new Error;
-        }
-    }
-
-    const editProfile = async (values: RegisterCredsValues) => {
-        if (userId) {
-            const config: AxiosRequestConfig = {
-                method: 'PUT',
-                url: API_URL + `/api/user-profile/${authStore.userId}/`,
-                data: values,
-                headers: { "Authorization": "Bearer " + authStore.accessToken, }
-            } 
-        try {
-            const response = await axios(config);   
-            return response.data;
-        } catch (error) {
-            if (error.response.status == 401) {
-                try {
-                    await refreshAccessToken()    
-                    const response = await axios({...config, headers: { "Authorization": "Bearer " + authStore.accessToken }});   
-                    return response.data;
-                } catch (error) {
-                    // refreshAccessToken will report errors
-                }
-            }
-
-            reportAxiosError('Something went wrong when editing user profile', error)
-            throw new Error;
-        }
-        
-        }
-    }
-
-    const changePassword = async (password: string) => {
-        if (userId) {
-            const config: AxiosRequestConfig = {
-                method: 'POST',
-                url: API_URL + `/api/user-profile/change-password/${authStore.userId}/`,
-                data: {password: password},
-                headers: { "Authorization": "Bearer " + authStore.accessToken, }
-            } 
-            try {
-                const response = await axios(config);   
-                return response.data;
-            } catch (error) {
-                if (error.response.status == 401) {
-                    try {
-                        await refreshAccessToken()    
-                        const response = await axios({...config, headers: { "Authorization": "Bearer " + authStore.accessToken }});   
-                        return response.data;
-                    } catch (error) {
-                        // refreshAccessToken will report errors
-                    }
-                }
-
-                reportAxiosError('Something went wrong when editing user profile', error)
-                throw new Error;
-            }
-            
-        }
-    }
-    //*NOT CURRENTLY IN USER*
-    // const changePicture = async () => {
-    //     if (userId) {
-    //         const config: AxiosRequestConfig = {
-    //             method: 'POST',
-    //             url: API_URL + `/api/user-profile/change-picture/${authStore.userId}/`,
-    //             headers: { "Authorization": "Bearer " + authStore.accessToken, }
-    //         }   
-    //         try {
-    //             const response = await axios(config);   
-    //             return response.data;
-    //         } catch (error) {
-    //             if (error.response.status == 401) {
-    //                 try {
-    //                     await refreshAccessToken()    
-    //                     const response = await axios({...config, headers: { "Authorization": "Bearer " + authStore.accessToken }});   
-    //                     return response.data;
-    //                 } catch (error) {
-    //                     // refreshAccessToken will report errors
-    //                 }
-    //             }
-    //         }
-    //     }
-    // }
-
-    const deleteProfile = async () => {
-        if (userId) {
-            const config: AxiosRequestConfig = {
-                method: 'DELETE',
-                url: API_URL + `/api/user-profile/${authStore.userId}/`,
-                headers: { "Authorization": "Bearer " + authStore.accessToken, }
-            } 
-            try {
-                const response = await axios(config);   
-                
-                if (response.status == 200) {
-                    await authStore.setAccessTokenAsync('')   
-                    await authStore.setIdAsync('')   
-                    await authStore.setNotificationTokenAsync('')   
-                    await authStore.setRefreshTokenAsync('')   
-                }
-            } catch (error) {
-                if (error.response.status == 401) {
-                    try {
-                        await refreshAccessToken()    
-                        const response = await axios({...config, headers: { "Authorization": "Bearer " + authStore.accessToken }});   
-                        return response.data;
-                    } catch (error) {
-                        // refreshAccessToken will report errors
-                    }
-                }
-    
-                reportAxiosError('Something went deleting when editing user profile', error)
-                throw new Error;
-            }
-            
-        }
-    }
-
-    const toggleTips = async () => {
-        if (userId) {
-            const config: AxiosRequestConfig = {
-                method: 'POST',
-                url: API_URL + `/api/user-profile/toggle-tips/${authStore.userId}/`,
-                headers: { "Authorization": "Bearer " + authStore.accessToken, }
-            } 
-            try {
-                const response = await axios(config);   
-                return response.data;
-            } catch (error) {
-                if (error.response.status == 401) {
-                    try {
-                        await refreshAccessToken()    
-                        const response = await axios({...config, headers: { "Authorization": "Bearer " + authStore.accessToken }});   
-                        return response.data;
-                    } catch (error) {
-                        // refreshAccessToken will report errors
-                    }
-                }
-
-                reportAxiosError('Something went wrong when editing user profile', error)
-                throw new Error;
-            }
-        
-        }
-    }
-
-    // get profile query
-    const { data: profile, status: getProfileStatus, refetch: refetchProfile } = useQuery<UserProfile, Error>(['getProfile', userId], async () => getProfile(userId))
-    // get notifications query
-    const { data: notifications, status: getNotificationsStatus, refetch: refetchNotifications } = useQuery<UserNotification[], Error>(['getNotifications', userId], async () => getNotifications())
-    // edit profile query
-    const { status: updateProfileStatus, mutateAsync: updateProfile, reset: resetUpdateProfile } = useMutation(editProfile, {
-        onSuccess: () => { queryClient.invalidateQueries('getProfile')},  
-        onError: () => queryClient.invalidateQueries('getProfile'),  
-    })
-
-    const { status: deleteAccountStatus, mutateAsync: deleteAccount, reset: resetDeleteAccount } = useMutation(deleteProfile, {
-        onSuccess: () => { queryClient.invalidateQueries('getProfile')},  
-        onError: () => queryClient.invalidateQueries('getProfile'),  
-    })
-
-
-    const { status: markNotificationReadStatus, mutateAsync: markNotificationAsRead, reset: resetMarkNotificationRead } = useMutation(markNotificationRead, {
-        onSuccess: () => { queryClient.invalidateQueries('getNotifications')},  
-        onError: () => queryClient.invalidateQueries('getNotifications'),  
-    })
-
-    const { status: deleteNotificationStatus, mutateAsync: deleteNotification, reset: resetDeleteNotification } = useMutation(removeNotificaiton, {
-        onSuccess: () => { queryClient.invalidateQueries('getNotifications')},  
-        onError: () => queryClient.invalidateQueries('getNotifications'),  
-    })
-
-    // toggle tips
-    const { status: changePasswordStatus, mutateAsync: changePasswordAsync, reset: resetChangePassword } = useMutation(changePassword, {
-        onSuccess: () => { queryClient.invalidateQueries('getProfile')},  
-        onError: () => queryClient.invalidateQueries('getProfile'),  
-    })
-    // toggle tips
-    const { status: toggleTipsStatus, mutateAsync: toggleTipsAsync, reset: resetToggleTips } = useMutation(toggleTips, {
-        onSuccess: () => { queryClient.invalidateQueries('getProfile')},  
-        onError: () => queryClient.invalidateQueries('getProfile'),  
-    })
-
-    return { 
-        profile, getProfileStatus, refetchProfile, //reading 
-        updateProfile, updateProfileStatus, resetUpdateProfile, //editing
-        deleteAccount, deleteAccountStatus, resetDeleteAccount, //deleting
-        notifications, getNotificationsStatus, refetchNotifications, //notifications 
-        deleteNotification, deleteNotificationStatus, resetDeleteNotification, //delete notification
-        markNotificationAsRead, markNotificationReadStatus, resetMarkNotificationRead, //update notifications 
-        changePasswordAsync, changePasswordStatus, resetChangePassword, //changePassword
-        toggleTipsAsync, toggleTipsStatus //toggleTips
-     } 
-}

+ 0 - 197
src/libs/auth/AuthStore.ts

@@ -1,197 +0,0 @@
-/* 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 * as SecureStore from 'expo-secure-store';
-import { getItemAsync } from 'expo-secure-store';
-import { action, makeObservable, observable } from "mobx";
-import { SECURESTORE_ACCESSTOKEN, SECURESTORE_IDTOKEN, SECURESTORE_NOTIFTOKEN, SECURESTORE_REFRESHTOKEN } from '../../utils/GlobalUtils';
-
-/**
- * Represents OIDC ID token.
- */
-export interface IdToken {
-    sub: string
-}
-
-interface CheckAuthStateResult {
-    tokenValue: string
-    tokenType: "access" | "refresh"
-}
-
-/**
- * A mobx store responsible for holding the current access token, refresh token and userId in memory and in Expo's [SecureStore]{@link https://docs.expo.dev/versions/latest/sdk/securestore/}.
- * Used by {@link Atlas}
- * @category Stores
- */
-class AuthStore {
-    /**
-     * A string representing the OAuth2 access token issued to the user when they authenticate. It is sent in the Authorization header of all subsequent XHR requests in order to use authorized API endpoints.
-     * For more information on OAuth2 and OpenIdConnect, visit {@link https://openid.net/connect/} and {@link https://oauth.net/2/}.
-     * @memberOf AuthStore
-     */
-    accessToken?: string | null = null;
-    /**
-     * A string representing the OAuth2 refresh token issued to the user when they authenticate. It is sent to the identity provider to retrieve a new access token when the previous access token expires.
-     * @memberOf AuthStore
-     */
-    refreshToken?: string | null = null;
-    /**
-     * A string representing the OAuth2 refresh token issued to the user when they authenticate. It is used to retrieve the users information from the API and to link objects created by the user to the user.
-     */
-    userId?: string = undefined;
-    /**
-     * A string representing the expo notification token.
-     */
-    notificationToken?: string = undefined;
-
-    /**
-     * Constructor. Uses mobx's [makeObservable]{@linkcode https://mobx.js.org/observable-state.html} to register actions and observable elements.
-     */
-    constructor() {
-        makeObservable(this, {
-            accessToken: observable,
-            refreshToken: observable,
-            userId: observable,
-            notificationToken: observable,
-            setAccessTokenAsync: action,
-            setRefreshTokenAsync: action,
-            setIdAsync: action,
-            setNotificationTokenAsync: action
-        });
-    }
-
-    /**
-     * Uses the given access token value to set {@linkcode accessToken} and the 'access' element in the secure store.
-     */
-    async setAccessTokenAsync(tokenValue: string | null) {
-        this.accessToken = tokenValue;
-        if (this.accessToken) {
-            await SecureStore.setItemAsync(SECURESTORE_ACCESSTOKEN, this.accessToken);    
-        }
-        else 
-        {
-            await SecureStore.deleteItemAsync(SECURESTORE_ACCESSTOKEN);    
-        }
-    }
-
-    /**
-     * Uses the given refresh token value to set {@linkcode refreshToken} and the 'refresh' element in the secure store.
-     */
-    async setRefreshTokenAsync(tokenValue?: string) {
-        this.refreshToken = tokenValue;
-        if (this.refreshToken) {
-            await SecureStore.setItemAsync(SECURESTORE_REFRESHTOKEN, this.accessToken);    
-        }
-        else 
-        {
-            await SecureStore.deleteItemAsync(SECURESTORE_REFRESHTOKEN);    
-        }
-    }
-
-    /**
-     * Uses the given user id value to set {@linkcode userId} and the 'id' element in the secure store.
-     */
-    async setIdAsync(id?: string) {
-        this.userId = id;
-        if (this.userId) {
-            await SecureStore.setItemAsync(SECURESTORE_IDTOKEN, this.userId);    
-        }
-        else 
-        {
-            await SecureStore.deleteItemAsync(SECURESTORE_IDTOKEN);    
-        }
-    }
-
-    /**
-     * Uses the given notification token value to set {@linkcode notificationToken} and the 'notif-token' element in the secure store.
-     */
-     async setNotificationTokenAsync(token?: string) {
-        this.notificationToken = token;
-        if (this.notificationToken) {
-            await SecureStore.setItemAsync(SECURESTORE_NOTIFTOKEN, this.notificationToken);    
-        }
-        else 
-        {
-            await SecureStore.deleteItemAsync(SECURESTORE_NOTIFTOKEN);    
-        }
-    }
-
-    /**
-   * 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.
-   */
-  //   checkAuthState = async (): CheckAuthStateResult => {
-  //   // check both the mobx store and secure storage for the token
-  //   console.log('[Authentication]: Checking for access token in memory...')
-  //   let currentAccessToken = authStore.accessToken;
-  //   if (!currentAccessToken) {
-  //     console.log('[Authentication]: No access token in memory, checking in secure store...')
-  //     currentAccessToken = await getItemAsync(SECURESTORE_ACCESSTOKEN);
-  //   }
-
-  //   if (!currentAccessToken) {
-  //     console.log('[Authentication]: No access token in secure store, attempting to use a refresh token...')
-  //     let refreshToken  = authStore.refreshToken;
-  //     if (!refreshToken) {
-  //       refreshToken = await getItemAsync(SECURESTORE_REFRESHTOKEN)
-  //     }
-
-  //     if (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);
-  //       if (response.status == 200) {
-  //         await authStore.setAccessTokenAsync(currentAccessToken);
-  //         console.log('[Authentication]: Access token valid.')
-  //       }
-  //     } catch (error) {
-  //       // check if access token can be refreshed
-  //       console.log(error)
-  //       if (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);
-  // }
-}
-
-export const authStore = new AuthStore();

+ 0 - 142
src/libs/auth/core.ts

@@ -1,142 +0,0 @@
-
-/* 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 axios, { AxiosError, AxiosRequestConfig } from "axios";
-import { authStore, IdToken } from "./AuthStore";
-import { API_URL } from "../../utils/RequestUtils";
-import { AuthRequestConfig, DiscoveryDocument, IssuerOrDiscovery, loadAsync, ResponseType } from "expo-auth-session";
-import jwt_decode from 'jwt-decode';
-
-export const SECURESTORE_ACCESSTOKEN = "access"
-export const SECURESTORE_REFRESHTOKEN = "refresh"
-export const SECURESTORE_NOTIFTOKEN = 'notif'
-export const SECURESTORE_IDTOKEN = 'id'
-
-interface AuthenticationResult {
-    success: boolean
-    errorMessage?: string
-}
-
-/**
-     * Function that initiates the login flow. It opens up with the in app browser and sends an authorization request to the API, which redirects the user to the backend login page. 
-     * If the credentials entered are valid, the OAuth Authorization Code flow will occur, which results in the user recieving an access token and refresh token which are then stored in {@link AuthStore}.
-     * */
-export const authenticate = async (requestConfig: AuthRequestConfig, discovery: DiscoveryDocument): Promise<AuthenticationResult> => {
-    console.log('[Authentication]: User is attempting to login, opening web portal login...')
-
-    // initiate authentication request to the server 
-    const request = await loadAsync(requestConfig, 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;
-
-            await authStore.setAccessTokenAsync(tokenResponse.access_token);
-            await authStore.setRefreshTokenAsync(tokenResponse.refresh_token);
-            await authStore.setIdAsync(idToken.sub)
-
-            console.log('[Authentication]: Tokens successfully retrieved.')
-
-            return {success: true}
-        } catch (error) {
-            reportAxiosError("Something went wrong when retrieving access token", error);
-            return {success: false, errorMessage: "Something went wrong while logging in. Please try again."}
-        } 
-    }
-    else {
-        return {success: false, errorMessage: response.type}
-    }
-}   
-
-/**
- * Logs the user out
- */
-export const logout = async () => {
-    try {
-        const tokenParams = new URLSearchParams();
-        tokenParams.append('client_id', 'atlas.mobile');
-        tokenParams.append('token', authStore.accessToken as string);
-        
-        const response = await axios.post(API_URL + `/o/revoke-token/`, tokenParams, {
-            headers: {
-                'Content-Type': 'application/x-www-form-urlencoded'
-            },
-        });  
-
-        await authStore.setAccessTokenAsync(null);
-        await authStore.setRefreshTokenAsync(null);
-        await authStore.setNotificationTokenAsync(null);
-        await authStore.setIdAsync(null);
-    } catch (error) {
-        reportAxiosError("Something went wrong when retrieving access token", error);
-    } 
-}
-
-
-
-    /**
-     * If there is a refresh token available, attempts to use it to obtain a new, valid access token.
-     * Used in {@link Atlas}
-     * @memberOf useAuth
-     */
-    const refreshAccessToken = async () => {
-        if (authStore.refreshToken) {
-            try {
-                const tokenData = new URLSearchParams();
-                tokenData.append('grant_type', 'refresh_token');
-                tokenData.append('refresh_token', authStore.refreshToken);
-                tokenData.append('client_id', 'atlas.mobile');
-                console.log('[Authentication]: Attempting to refresh token...')
-                const { data: refreshResponseData } = await axios.post(API_URL + "/o/token/", tokenData, {
-                    headers: { 'Content-Type': "application/x-www-form-urlencoded" }
-                });   
-                
-                await authStore.setRefreshTokenAsync(refreshResponseData.refresh_token);
-                await authStore.setAccessTokenAsync(refreshResponseData.access_token);
-
-                console.info('Successfully refreshed access token, re-attempting initial request...')
-            }
-            catch (error) {
-                reportAxiosError("[Authentication]: Error when trying to refresh access token", error);            
-            }
-        }
-    }
-
-    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;
-        }
-    
-        console.error(errorString);
-    }

+ 0 - 58
src/libs/notfications/helpers.ts

@@ -1,58 +0,0 @@
-export const getNotificationTokenFromServer = async () => {
-    const config: AxiosRequestConfig = {
-        method: 'GET',
-        url: API_URL + `/api/notif-token/${authStore.userId}/`,
-        headers: { "Authorization": "Bearer " + authStore.accessToken, }
-    }
-    
-    try {
-        const response = await axios(config);   
-        return response.data;
-    } catch (error) {
-        if (error.response.status == 401) {
-            try {
-                await refreshAccessToken()    
-                const response = await axios({...config, headers: { "Authorization": "Bearer " + authStore.accessToken }});   
-
-                return response.data;
-            } catch (error) {
-                // refreshAccessToken will report errors
-            }
-        }
-
-        reportAxiosError('Something went wrong when retrieving landmarks', error)
-        throw new Error;
-    }
-}
-
-    const ensureNotificationTokenExistsOnServer = async (notificationToken: string, retries: number) => {
-        if (retries > 0) {
-            const config: AxiosRequestConfig = {
-                method: 'POST',
-                data: {token: notificationToken},
-                url: API_URL + `/api/notif-token/${authStore.userId}/`,
-                headers: { "Authorization": "Bearer " + authStore.accessToken, }
-            } 
-            try {
-                const response = await axios(config);   
-                return response.data;
-            } catch (error) {
-                if (error.response.status == 401) {
-                    try {
-                        await refreshAccessToken()    
-                        const response = await axios({...config, headers: { "Authorization": "Bearer " + authStore.accessToken }});   
-
-                        return response.data;
-                    } catch (error) {
-                        // refreshAccessToken will report errors
-                    }
-                }
-                reportAxiosError('Something went wrong when retrieving landmarks', error)
-            }
-    
-            ensureNotificationTokenExistsOnServer(notificationToken, retries - 1)   
-        }
-        else {
-            throw new Error;
-        }
-    }

+ 0 - 239
src/navigation/AuthorizedNavigator.tsx

@@ -1,239 +0,0 @@
-/* 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 { FontAwesome } from '@expo/vector-icons';
-import { BottomTabNavigationOptions, BottomTabNavigationProp, createBottomTabNavigator } from "@react-navigation/bottom-tabs";
-import { observer } from "mobx-react";
-import React, { useEffect } from "react";
-import { AppState, Platform, SafeAreaView, View, Text, Alert } from 'react-native';
-import OutdoorMap from "../components/Map/MainMapComponent/OutdoorMap";
-import Profile from "../components/Profile/Profile";
-import { colors, SECURESTORE_NOTIFTOKEN } from "../utils/GlobalUtils";
-import  {check, checkMultiple, PERMISSIONS, requestMultiple} from 'react-native-permissions'
-import { Feed } from '../components/Feed/Feed';
-import { authStore } from '../libs/auth/AuthStore';
-import { getItemAsync } from 'expo-secure-store';
-import Constants from 'expo-constants';
-import * as Notifications from 'expo-notifications'
-import { useAuth } from '../hooks/useAuth';
-import { NavigationContainerRef, RouteProp, useNavigation } from '@react-navigation/native';
-import { QueryClient, useQuery } from 'react-query';
-import { useProfile } from '../hooks/useProfile';
-import { NotifType } from '../types';
-import Badge from '../components/Badge';
-import MapNavigator from './MapNavigator';
-
-Notifications.setNotificationHandler({
-    handleNotification: async () => ({
-      shouldShowAlert: true,
-      shouldPlaySound: false,
-      shouldSetBadge: false,
-    }),
-});
-
-const MainTabs = createBottomTabNavigator();
-
-const tabBarOptions: BottomTabNavigationOptions = {
-    //keyboardHidesTabBar: true,
-    tabBarActiveTintColor: 'white',
-    tabBarActiveBackgroundColor: '#e35555',
-    tabBarInactiveTintColor: "lightgray",
-    
-    tabBarStyle: {backgroundColor: colors.red, height: 60, justifyContent: 'center'},
-    tabBarLabelStyle: {marginBottom: 7},
-    tabBarIconStyle: {marginBottom: 7}
-} 
-
-/**
- * Permitted screens for the Auth tabs navigator. 
- * @category Navigation
- * @typedef
- */
- export type AuthTabsParamList = {
-    Map: {selectedLandmark: string, selectedLandmarks: string[]},
-    Profile: React.FC,
-}
-
-export type AuthTabsNavigationProp = BottomTabNavigationProp<AuthTabsParamList>
-/**
- * The root navigator for all authorized screens ({@link Map}, {@link Profile}). It uses a [React Navigation Bottom Tabs Navigator]{@link https://reactnavigation.org/docs/bottom-tab-navigator/} the main navigation mechanism.
- * @category Navigation
- * @component
- */
-const AuthorizedNavigator: React.FC = () => {
-    const {notifications, refetchNotifications, markNotificationAsRead } = useProfile(authStore.userId)
-
-    const {getNotificationTokenFromServer, ensureNotificationTokenExistsOnServer} = useAuth()
-
-    const { profile, toggleTipsAsync } = useProfile(authStore.userId)
-
-    const navigation = useNavigation()
-
-    /**
-     * If the user has their preferences configured to show tips, show them on page load. Recheck every time show_tips changes
-     */
-    useEffect(() => {
-        const showTip = () =>  {
-            if (profile?.show_tips) {
-                console.log('[Profile]: User has tips configured, showing tips.')
-                Alert.alert(
-                    'Welcome!', 
-                    "There are 3 ways to add a landmark to the map: \n\n - Press and hold where you'd like to add it on the map \n\n - Press the add button to add a landmark at your current location \n\n - Press the mic button and use voice commands.", 
-                    [{text: "Don't show this again", onPress: async () => await toggleTipsAsync()}, {text: 'Ok'}]
-                )   
-            }
-            else {
-                console.log('[Profile]: User does not have tips configured, not showing tips')
-            }
-        }
-        showTip();
-    }, [profile?.show_tips])
-
-    const handleNotificationInteraction = async (notifData: any) => {
-        await markNotificationAsRead(notifData.notif_id)
-        await refetchNotifications()
-        if (notifData?.notif_type as NotifType == 'landmark-like' || notifData?.notif_type as NotifType == 'near-landmark')
-            navigation.navigate('Map' as never, {selectedLandmark: notifData.landmark_id} as never)
-        if (notifData?.notif_type as NotifType == 'near-landmarks')
-            navigation.navigate('Map' as never, {selectedLandmarks: notifData.landmarks} as never)
-    }
-
-    const registerForPushNotificationsAsync = 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!');
-                await authStore.setNotificationTokenAsync('')
-                return;
-            }
-
-            let token = authStore.notificationToken
-
-            // try getting from memory
-            if (!token) {
-                // try getting token from SecureStore 
-                console.log('[Notifcations]: Attempting to get token from SecureStore...')
-                token = await getItemAsync(SECURESTORE_NOTIFTOKEN)
-                if (token) {
-                    console.log('[Notifications]: Found notification token in, setting it as current token')
-                    await authStore.setNotificationTokenAsync(token)
-                }
-                else {
-                    // try getting token from server db 
-                    console.log('[Notifcations]: Attempting to get token from SecureStore...')
-                    console.log('[Notifcations]: Couldn\'t find token in SecureStore. Attempting to get notification token from server...')
-                    token = await getNotificationTokenFromServer()
-                    if (token) {
-                        console.log(token)
-                        console.log('[Notificaitons]: Found token on server, setting it as current token')
-                        await authStore.setNotificationTokenAsync(token[0])
-                    }
-                    else {
-                        // get new token from expo and save it to the server
-                        console.log('[Notifcations]: Couldn\'t find token in server. Getting new token from expo...')
-                        token = (await Notifications.getExpoPushTokenAsync()).data;      
-                        await authStore.setNotificationTokenAsync(token)
-                    }
-                }
-            }
-
-            // ensure that notificationToken exists on the server
-            await ensureNotificationTokenExistsOnServer(token, 5)
-
-        } 
-        else {
-            console.warn('[Notifcations]: A physical device must be used for push notifications');
-            authStore.setNotificationTokenAsync('')
-            return 
-        }
-        
-        if (Platform.OS === 'android') {
-            Notifications.setNotificationChannelAsync('default', {
-                name: 'default',
-                importance: Notifications.AndroidImportance.MAX,
-                vibrationPattern: [0, 250, 250, 250],
-                lightColor: '#FF231F7C',
-            });
-        }   
-    };
-      
-    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
-         */
-        const initializePushNotifications = async () => {
-        if (authStore.userId) {
-            await registerForPushNotificationsAsync()
-            if (authStore.notificationToken) {
-                const notifReceivedSubscription = Notifications.addNotificationReceivedListener(async notification => { 
-                    await refetchNotifications()
-                })
-        
-                const notifResponseReceivedSubscription = Notifications.addNotificationResponseReceivedListener(async response => {
-                    const notifData = response.notification.request.content.data
-                    handleNotificationInteraction(notifData)
-                });
-        
-                return () => {
-                    notifReceivedSubscription.remove()
-                    notifResponseReceivedSubscription.remove()
-                }; 
-            }
-        }
-        }
-        initializePushNotifications();
-    }, [authStore.userId]);
-
-    useEffect(() => {
-
-    }, [])
-
-    const getIconSize = (focused: boolean): number => {
-        if (focused) {
-            return 20
-        }
-        else {
-            return 17
-        } 
-    }
-
-    const renderFeedBadge = () => {
-        const newNotifAmount = notifications?.filter(notif => !notif.read).length
-
-        return newNotifAmount > 0 ? <Badge positioning={{top: 8, left: 13,}} value={newNotifAmount}/> : null
-    }
-
-    return(
-        <SafeAreaView style={{height: '100%'}}>
-            {/* <AdMobBanner adUnitID="ca-app-pub-3940256099942544/6300978111" /> */}
-            <MainTabs.Navigator 
-                sceneContainerStyle={{flex:1}}     
-                initialRouteName="Map"
-                screenOptions={tabBarOptions}>
-                <MainTabs.Screen name="Map" component={MapNavigator} options={{tabBarIcon: ({color, focused}) => (<FontAwesome name={focused ? 'map' : 'map-o'} size={getIconSize(focused)} color={color} style={{position: 'absolute', top: 10}}/>)}}/>
-                <MainTabs.Screen name="Feed" options={{tabBarIcon: ({color, focused}) => (
-                    <View style={{position: 'absolute', top: 10}}>
-                        <FontAwesome name={focused ? 'bell' : 'bell-o'} size={getIconSize(focused)} color={color} />
-                        {renderFeedBadge()}
-                    </View>
-                )}}>
-                    {() => <Feed notifications={notifications} handleNotifInteraction={handleNotificationInteraction} />}
-                </MainTabs.Screen>
-                <MainTabs.Screen name="Profile" component={Profile} options={{tabBarIcon: ({color, focused}) => (<FontAwesome name={focused ? 'user' : 'user-o'} size={getIconSize(focused)} color={color} style={{position: 'absolute', top: 10}}/>)}} />
-            </MainTabs.Navigator>
-        </SafeAreaView>
-        
-    )
-}
-
-export default observer(AuthorizedNavigator);

+ 9 - 8
src/navigation/UnauthorizedNavigator.tsx → src/navigation/BaseStackNavigator.tsx

@@ -7,8 +7,8 @@
 
 import { createStackNavigator, StackNavigationProp } from '@react-navigation/stack';
 import React from "react";
-import Intro from "../components/Auth/Intro";
-import { RegisterMain } from "../components/Auth/RegisterMain";
+import { RegisterMain } from '../components/Profile/Registration/RegisterMain';
+import MainTabsNavigator from './MainTabsNavigator';
 ;
 
 /**
@@ -16,15 +16,14 @@ import { RegisterMain } from "../components/Auth/RegisterMain";
  * @category Navigation
  * @typedef
  */
-export type UnAuthStackParamList = {
-    Intro: React.FC,
-    Login: React.FC,
+export type BaseStackParamList = {
+    MainTabs: React.FC,
     Register: React.FC,
 }
 
-export type UnAuthStackNavigationProp = StackNavigationProp<UnAuthStackParamList>;
+export type BaseStackNavigationProp = StackNavigationProp<BaseStackParamList>;
 
-const AuthStack = createStackNavigator<UnAuthStackParamList>();
+const AuthStack = createStackNavigator<BaseStackParamList>();
 
 /**
  * The root navigator for all unauthorized screens ({@link Intro}, {@link RegisterMain}). It uses a [React Navigation Stack Navigator]{@link https://reactnavigation.org/docs/stack-navigator/} the main navigation mechanism.
@@ -37,7 +36,9 @@ const UnauthorizedNavigator : React.FC = () => {
             headerTransparent: true,
             headerTintColor: 'white',
             headerTitle: ""}}>
-            <AuthStack.Screen name="Intro" component={Intro} />
+            <AuthStack.Screen name="MainTabs" >
+                {({navigation}) => <MainTabsNavigator navigation={navigation} />}
+            </AuthStack.Screen>
             <AuthStack.Screen name="Register" component={RegisterMain} />
         </AuthStack.Navigator>
     );

+ 134 - 0
src/navigation/MainTabsNavigator.tsx

@@ -0,0 +1,134 @@
+/* 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 { FontAwesome } from '@expo/vector-icons';
+import { BottomTabNavigationOptions, BottomTabNavigationProp, createBottomTabNavigator } from "@react-navigation/bottom-tabs";
+import * as Notifications from 'expo-notifications';
+import { observer } from "mobx-react";
+import React, { useEffect } from "react";
+import { Alert, SafeAreaView, View } from 'react-native';
+import Badge from '../components/Badge';
+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 { useOwnedProfile, useToggleTips } from '../data/profiles';
+import { colors } from "../utils/GlobalUtils";
+import MapNavigator from './MapNavigator';
+
+Notifications.setNotificationHandler({
+    handleNotification: async () => ({
+      shouldShowAlert: true,
+      shouldPlaySound: false,
+      shouldSetBadge: false,
+    }),
+});
+
+const MainTabs = createBottomTabNavigator();
+
+const tabBarOptions: BottomTabNavigationOptions = {
+    //keyboardHidesTabBar: true,
+    tabBarActiveTintColor: 'white',
+    tabBarInactiveTintColor: "lightgray",
+    headerShown: false,
+    tabBarStyle: {backgroundColor: colors.red, justifyContent: 'center'},
+    tabBarLabelStyle: {marginBottom: 7},
+    tabBarIconStyle: {marginBottom: 7}
+} 
+
+/**
+ * Permitted screens for the Auth tabs navigator. 
+ * @category Navigation
+ * @typedef
+ */
+ export type MainTabsParamList = {
+    Map: {selectedLandmark: string, selectedLandmarks: string[]},
+        Account: React.FC,
+}
+
+export type MainTabsNavigationProp = BottomTabNavigationProp<MainTabsParamList>
+/**
+ * The root navigator for all authorized screens ({@link Map}, {@link Profile}). It uses a [React Navigation Bottom Tabs Navigator]{@link https://reactnavigation.org/docs/bottom-tab-navigator/} the main navigation mechanism.
+ * @category Navigation
+ * @component
+ */
+const MainTabsNavigator: React.FC<{navigation}> = ({navigation}) => {
+    const {profile} = useOwnedProfile()
+    const {userId} = useAuth()
+    const toggleTips = useToggleTips()
+    const notificationsQuery = useNotifications()
+    const {subscribeToNotifications, handleNotificationInteraction} = useRegisterNotifications()
+
+    /**
+     * If the user has their preferences configured to show tips, show them on page load. Recheck every time show_tips changes
+     */
+    useEffect(() => {
+        const showTip = () =>  {
+            if (profile?.show_tips) {
+                console.log('[Profile]: User has tips configured, showing tips.')
+                Alert.alert(
+                    'Welcome!', 
+                    "There are 3 ways to add a landmark to the map: \n\n - Press and hold where you'd like to add it on the map \n\n - Press the add button to add a landmark at your current location \n\n - Press the mic button and use voice commands.", 
+                    [{text: "Don't show this again", onPress: async () => await toggleTips.mutateAsync()}, {text: 'Ok'}]
+                )   
+            }
+            else {
+                console.log('[Profile]: User does not have tips configured, not showing tips')
+            }
+        }
+        showTip();
+    }, [profile?.show_tips])
+
+    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
+         */
+        subscribeToNotifications()
+    }, [userId]);
+
+    const getIconSize = (focused: boolean): number => {
+        if (focused) {
+            return 20
+        }
+        else {
+            return 17
+        } 
+    }
+
+    const renderFeedBadge = () => {
+        const newNotifAmount = notificationsQuery.data?.filter(notif => !notif.read).length
+
+        return newNotifAmount > 0 ? <Badge positioning={{top: 8, left: 13,}} value={newNotifAmount}/> : null
+    }
+
+    return(
+        <SafeAreaView style={{height: '100%'}}>
+            {/* <AdMobBanner adUnitID="ca-app-pub-3940256099942544/6300978111" /> */}
+            <MainTabs.Navigator 
+                sceneContainerStyle={{flex:1}}     
+                initialRouteName="Map"
+                screenOptions={tabBarOptions}>
+                <MainTabs.Screen name="Map" component={MapNavigator} options={{tabBarIcon: ({color, focused}) => (<FontAwesome name={focused ? 'map' : 'map-o'} size={getIconSize(focused)} color={color} style={{position: 'absolute', top: 8}}/>)}}/>
+                <MainTabs.Screen name="Feed" options={{tabBarIcon: ({color, focused}) => (
+                    <View style={{position: 'absolute', top: 8}}>
+                        <FontAwesome name={focused ? 'bell' : 'bell-o'} size={getIconSize(focused)} color={color} />
+                        {renderFeedBadge()}
+                    </View>
+                )}}>
+                    {() => <Feed notifications={notificationsQuery.data} handleNotifInteraction={handleNotificationInteraction} />}
+                </MainTabs.Screen>
+                <MainTabs.Screen name="Account" options={{tabBarIcon: ({color, focused}) => (<FontAwesome name={focused ? 'user' : 'user-o'} size={getIconSize(focused)} color={color} style={{position: 'absolute', top: 8}}/>)}}>
+                    {({navigation}) => <ProfileTemplate navigation={navigation}/>}
+                </MainTabs.Screen>
+            </MainTabs.Navigator>
+        </SafeAreaView>
+        
+    )
+}
+
+export default observer(MainTabsNavigator);

+ 87 - 35
src/navigation/MapNavigator.tsx

@@ -1,10 +1,10 @@
 import { FontAwesome } from "@expo/vector-icons"
 import { useNavigation, useNavigationState, useRoute } from "@react-navigation/native"
-import { createStackNavigator, StackNavigationProp } from "@react-navigation/stack"
 import { createNativeStackNavigator } from "@react-navigation/native-stack"
+import { StackNavigationProp } from "@react-navigation/stack"
 import { observer } from "mobx-react"
 import React, { useEffect, useState } from "react"
-import { Dimensions, Image, ScrollView, View } from "react-native"
+import { Alert, Dimensions, Image, ScrollView, View, Linking, Text } from "react-native"
 import { Chip } from "react-native-paper"
 import { IconButton } from "../components/Buttons"
 import IndoorMap from "../components/Map/MainMapComponent/IndoorMap"
@@ -14,10 +14,16 @@ import { useMapState } from "../components/Map/MainMapComponent/useMapState"
 import AddLandmarkPanel from "../components/Map/Panels/AddLandmarkPanel"
 import { FilterPanel } from "../components/Map/Panels/FilterPanel/FilterPanel"
 import LandmarkDetails from "../components/Map/Panels/LandmarkDetailsPanel/LandmarkDetails"
-import { Landmark, useLandmarks } from "../hooks/useLandmarks"
-import { authStore } from "../libs/auth/AuthStore"
-import { colors, lmTypes } from "../utils/GlobalUtils"
-import { AuthTabsNavigationProp } from "./AuthorizedNavigator"
+import { useAuth } from "../data/Auth/AuthContext"
+import { Landmark, useLandmarks } from '../data/landmarks'
+import { colors, lmTypes, } from "../utils/GlobalUtils"
+import { MainTabsNavigationProp } from "./MainTabsNavigator"
+
+import { Menu, MenuItem, MenuDivider } from 'react-native-material-menu';
+import { navigate } from "./RootNavigator"
+import { Separator } from "../components/Separator"
+
+
 
 const MapStackNavigator = createNativeStackNavigator()
 
@@ -28,25 +34,22 @@ export type MapStackParamList = {
 
 export type MapStackNavigationProp = StackNavigationProp<MapStackParamList>
 
-interface MapNavigatorProps {
-    authNavigation: AuthTabsNavigationProp
-    route: AuthTabsMapRouteProp
-}
-
-
-const MapNavigator: React.FC<MapNavigatorProps> = ({ route }) => {
+const MapNavigator: React.FC = ({ }) => {
+    const { landmarkOwnedByUser } = useAuth()
     const mapState = useMapState()
-    const authNavigation = useNavigation() as AuthTabsNavigationProp
+    const authNavigation = useNavigation() as MainTabsNavigationProp
     const authRoute = useRoute() as AuthTabsMapRouteProp
 
     // bring in all landmarks from useLandmark hook
-    const { landmarks, refetchLandmarks } = useLandmarks({});
+    const landmarksQuery = useLandmarks();
 
     // const currentRouteIndex = useNavigation().getState().routes[0].state.index
     // const currentRouteName = useNavigation().getState().routes[0].state.routeNames[currentRouteIndex]
-
     const navigationState = useNavigationState(state => state)
     const [currentRoute, setCurrentRoute] = useState<string>()
+
+    const [visible, setVisible] = useState(false);
+
     useEffect(() => {
         const currentRouteIndex = navigationState?.routes[0]?.state?.index
         const currentRouteName = navigationState?.routes[0]?.state?.routeNames[currentRouteIndex]
@@ -64,8 +67,7 @@ const MapNavigator: React.FC<MapNavigatorProps> = ({ route }) => {
 
     useEffect(() => {
         const refetchLandmarksOnFilterOptionsChange = async () => {
-            applyFilters(landmarks)
-            console.log('[Map]: Filters changed')
+            applyFilters(landmarksQuery?.data)
         }
         refetchLandmarksOnFilterOptionsChange()
     }, [mapState.lmFilteredTypes, mapState.onlyOwned, mapState.minLmRating])
@@ -81,7 +83,7 @@ const MapNavigator: React.FC<MapNavigatorProps> = ({ route }) => {
             }
 
             if (mapState.onlyOwned) {
-                landmarks = landmarks?.filter(lm => lm.user == authStore.userId);
+                landmarks = landmarks?.filter(lm => landmarkOwnedByUser(lm));
             }
         }
 
@@ -107,7 +109,7 @@ const MapNavigator: React.FC<MapNavigatorProps> = ({ route }) => {
  * Triggers {@link openAddLandmark} via useEffect because the asyncronous nature of useState does not set the coordinates fast enough to toggle the
  * modal directly through this method (this issue shows up in many other parts of the app where a modal is toggled by a boolean, and is solved in the same way).
  */
-    const promptAddLandmark = (longitude?: number, latitude?: number, floor?: number) => {
+    const promptAddLandmark = async (longitude?: number, latitude?: number, floor?: number) => {
         console.log('[Map]: Opening add landmark panel...')
         mapState.setNewLandmark({ latitude: latitude, longitude: longitude, floor: floor });
         mapState.toggleLmAdd(true)
@@ -118,10 +120,10 @@ const MapNavigator: React.FC<MapNavigatorProps> = ({ route }) => {
     return (
         <View style={{ flex: 1 }}>
             <MapStackNavigator.Navigator screenOptions={{ headerShown: false }} initialRouteName="Outdoor">
-
                 <MapStackNavigator.Screen name="Outdoor" >
                     {({ navigation }) =>
                         <OutdoorMap
+                            authNavIndex={navigationState.index}
                             newLandmark={mapState.newLandmark}
                             setNewLandmark={mapState.setNewLandmark}
                             promptAddLandmark={promptAddLandmark}
@@ -132,33 +134,29 @@ const MapNavigator: React.FC<MapNavigatorProps> = ({ route }) => {
                             route={authRoute}
                             mapNavigation={navigation}
                             authNavigation={authNavigation}
-                            landmarks={landmarks}
+                            landmarks={landmarksQuery?.data}
                             selectedLandmarkId={mapState.selectedLandmarkId}
                             setSelectedLandmarkId={mapState.setSelectedLandmarkId} />}
                 </MapStackNavigator.Screen>
 
-                <MapStackNavigator.Screen name="Indoor" listeners={{
-                    
-                }} >
+                <MapStackNavigator.Screen name="Indoor" >
                     {({ navigation }) =>
                         <IndoorMap
                             navigation={navigation}
-                            landmarks={landmarks}
+                            landmarks={landmarksQuery?.data}
                             promptAddLandmark={promptAddLandmark}
                             focusLandmark={focusLandmark}
                             applyFilter={applyFilters}
                         />}
                 </MapStackNavigator.Screen>
 
-
-
             </MapStackNavigator.Navigator>
 
 
 
             {/* Filter chips and button*/}
             {!mapState.filterVisible && currentRoute == 'Indoor' ?
-                <View style={{width:Dimensions.get("window").width*0.8 ,marginLeft:Dimensions.get("window").width*0.1, borderColor:"red" , borderWidth:0, bottom: 50, position: 'absolute', flexDirection: "row-reverse", justifyContent: 'flex-end' }}>
+                <View style={{ top: 60, right: 7.5, position: 'absolute', flexDirection: "row-reverse", justifyContent: 'flex-end' }}>
                     <IconButton size={16} color={colors.red} style={[mapStyles.filterButtonIndoor]} icon="filter" onPress={() => mapState.toggleFilter(true)} />
                     <ScrollView horizontal={true} contentContainerStyle={{ alignItems: 'center' }} style={{ marginHorizontal: 10, flexDirection: 'row' }}>
                         {mapState.onlyOwned ? <Chip avatar={(<FontAwesome name="user" size={20} color='gray' style={{ textAlign: 'center', textAlignVertical: 'center' }} />)} style={{ borderWidth: 1, borderColor: 'lightgray', marginRight: 5, marginLeft: 10 }} onClose={() => mapState.toggleOnlyOwned(false)}>My landmarks</Chip> : null}
@@ -170,7 +168,7 @@ const MapNavigator: React.FC<MapNavigatorProps> = ({ route }) => {
                         {mapState.minLmRating > 0 ? <Chip avatar={(<FontAwesome name="star" size={20} color='gray' style={{ textAlign: 'center', textAlignVertical: 'center' }} />)} style={{ borderWidth: 1, borderColor: 'lightgray', marginLeft: 5, marginRight: 10 }} onClose={() => mapState.setMinLmRating(0)}>Minimum rating: {mapState.minLmRating}</Chip> : null}
                     </ScrollView>
                 </View>
-                 :
+                :
                 <View style={{ top: 10, marginLeft: 40, marginRight: 20, position: 'absolute', flexDirection: "row-reverse", justifyContent: 'flex-end' }}>
                     <IconButton size={20} color={colors.red} style={[mapStyles.filterButtonOutdoor]} icon="filter" onPress={() => mapState.toggleFilter(true)} />
                     <ScrollView horizontal={true} contentContainerStyle={{ alignItems: 'center' }} style={{ marginHorizontal: 10, flexDirection: 'row' }}>
@@ -183,12 +181,65 @@ const MapNavigator: React.FC<MapNavigatorProps> = ({ route }) => {
                         {mapState.minLmRating > 0 ? <Chip avatar={(<FontAwesome name="star" size={20} color='gray' style={{ textAlign: 'center', textAlignVertical: 'center' }} />)} style={{ borderWidth: 1, borderColor: 'lightgray', marginLeft: 5, marginRight: 10 }} onClose={() => mapState.setMinLmRating(0)}>Minimum rating: {mapState.minLmRating}</Chip> : null}
                     </ScrollView>
                 </View>
-                
-                
-                }
-  
+            }
+
+            {/* Create Hamburger icon */}
+            {currentRoute == 'Indoor' ?
+                <View style={{ top: 100, right: 7.5, position: 'absolute', }}>
+                    <Menu
+                        visible={visible}
+                        anchor={<IconButton size={16} color={colors.red} style={[mapStyles.filterButtonIndoor]} icon="bars" onPress={() => setVisible(true)} />}
+                        onRequestClose={() => setVisible(false)}
+                    >
+                        <MenuItem onPress={() => {
+                            setVisible(false)
+                            navigate("Outdoor")
+                            // Alert.alert("Cameron Library")
+                        }}>Go Back Outdoors</MenuItem>
+
+                        <MenuItem onPress={() => {
+                            setVisible(false)
+                            Linking.openURL('https://www.ualberta.ca/facilities-operations/portfolio/emergency-management-office/emergency-procedures/alarms-evacuation.html')
+                            // Alert.alert("Cameron Library")
+                        }}>Emergency Procedures</MenuItem>
+
+                        <MenuItem onPress={() => {
+                            setVisible(false)
+                            Linking.openURL('https://www.library.ualberta.ca/')
+                            // Alert.alert("Cameron Library")
+                        }}>Resources</MenuItem>
+
+                    </Menu>
+                </View>
+                :
+                <View style={{ top: 60, right: 20, position: 'absolute', }}>
+                    <Menu
+                        visible={visible}
+                        anchor={<IconButton size={20} color={colors.red} style={[mapStyles.filterButtonOutdoor]} icon="bars" onPress={() => setVisible(true)} />}
+                        onRequestClose={() => setVisible(false)}
+                    >
+                        <MenuItem disabled={true} disabledTextColor='black' style={{ alignItems: 'center', borderColor: 'black', borderBottomWidth: 1, marginHorizontal: 10, opacity: .7 }}>
+                            <Text>Indoor buildings</Text>
+                        </MenuItem>
+                        
+                        <MenuItem onPress={() => {
+                            setVisible(false)
+                            navigate("Indoor")
+                            // Alert.alert("Cameron Library")
+                        }}>Cameron</MenuItem>
+
+                        <MenuDivider />
+                        <MenuItem disabled>More indoor maps on the way!</MenuItem>
+
+                    </Menu>
+
+                    {/* either "bars" or "list"  */}
+                </View>
+            }
+
+
 
-  {/* {!mapState.filterVisible ?
+            {/* {!mapState.filterVisible ?
                 <View style={{ top: 10, marginLeft: 40, marginRight: 20, position: 'absolute', flexDirection: "row-reverse", justifyContent: 'flex-end' }}>
                     <IconButton size={20} color={colors.red} style={[mapStyles.filterButton]} icon="filter" onPress={() => mapState.toggleFilter(true)} />
                     <ScrollView horizontal={true} contentContainerStyle={{ alignItems: 'center' }} style={{ marginHorizontal: 10, flexDirection: 'row' }}>
@@ -210,6 +261,7 @@ const MapNavigator: React.FC<MapNavigatorProps> = ({ route }) => {
                 newLandmark={mapState.newLandmark}
                 visible={mapState.lmAddVisible} />
             <LandmarkDetails
+                authNavigation={authNavigation}
                 visible={mapState.lmDetailsVisible}
                 toggleLmDetails={mapState.toggleLmDetails}
                 setLandmark={mapState.setSelectedLandmarkId}

+ 13 - 0
src/navigation/RootNavigator.tsx

@@ -0,0 +1,13 @@
+import { createNavigationContainerRef } from "@react-navigation/native";
+
+export const navigationRef = createNavigationContainerRef()
+
+export function navigate(name, params?) {
+  if (navigationRef.isReady()) {
+    console.log("[Navigation]: Using global navigation to switch to screen: " + name)
+    navigationRef.navigate(name as never, params as never);
+  }
+  else {
+    console.log("[Navigation]: Global navigation isn't ready.")
+  }
+}

+ 0 - 0
src/navigation/contexts.tsx


+ 0 - 9
src/types.ts

@@ -1,9 +0,0 @@
-/* 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
- */
-
-// Types that don't belong in any related file are stored here
-export type NotifType = "near-landmark" | 'near-landmarks' | "landmark-like"

+ 52 - 7
src/utils/GlobalUtils.ts

@@ -5,7 +5,7 @@
  * <dev@clicknpush.ca>, January 2022
  */
 
-import { Alert, ImageURISource, Permission, Platform, StyleSheet, View } from "react-native";
+import { Alert, ImageRequireSource, ImageURISource, Permission, Platform, StyleSheet, View } from "react-native";
 import { checkMultiple, PERMISSIONS, requestMultiple, RESULTS, } from "react-native-permissions";
 
 // global constants
@@ -18,7 +18,7 @@ export const SECURESTORE_REFRESHTOKEN = "refresh"
 export const SECURESTORE_NOTIFTOKEN = 'notif'
 export const SECURESTORE_IDTOKEN = 'id'
 
-export const lmTypes: {[key: number]: {image: ImageURISource, label: string}} = {
+export const lmTypes: {[key: number]: {image: ImageRequireSource, label: string}} = {
     // 1: {image: require('../../assets/uneven.png'), label: "rough terrain"}, not currently in use
     2: {image: require('../../assets/stairs.png'), label: "stairs"},
     3: {image: require('../../assets/barrier.png'), label: "barrier"},
@@ -28,8 +28,43 @@ export const lmTypes: {[key: number]: {image: ImageURISource, label: string}} =
     7: {image: require('../../assets/power.png'), label: "power issue"},
     8: {image: require('../../assets/crosswalk.png'), label: "crosswalk issue"},
     9: {image: require('../../assets/ice.png'), label: "ice"},
-    
-} 
+    14: {image: require('../../assets/ramp.png'), label: "ramp"},
+    16: {image: require('../../assets/childfriendly.png'), label: "child friendly area"},
+}
+
+export const lmTypesIndoor: {[key: number]: {image: ImageRequireSource, label: string}} = {
+    2: {image: require('../../assets/stairs.png'), label: "stairs"},
+    5: {image: require('../../assets/information.png'), label: "information"},
+    6: {image: require('../../assets/washroom.png'), label: "accessible washroom"},
+    10: {image: require('../../assets/desk.png'), label: "desk"},
+    11: {image: require('../../assets/elevator.png'), label: "elevator"},
+    12: {image: require('../../assets/kiosk.png'), label: "kiosk"},
+    13: {image: require('../../assets/monitor.png'), label: "monitor"},
+    14: {image: require('../../assets/ramp.png'), label: "ramp"},
+    15: {image: require('../../assets/water.png'), label: "water fountain"},
+    16: {image: require('../../assets/childfriendly.png'), label: "child friendly area"},
+    17: {image: require('../../assets/garbage.png'), label: "Garbage cans"},
+    18: {image: require('../../assets/loudnoise.png'), label: "Loud area"},
+    19: {image: require('../../assets/tripping.png'), label: "Tripping hazard"},
+
+}
+
+// 2: {image: require('../../assets/stairs.png'), label: "stairs"},
+// 3: {image: require('../../assets/barrier.png'), label: "barrier"},
+// 4: {image: require('../../assets/uneven.png'), label: "rough terrain"},
+// 5: {image: require('../../assets/information.png'), label: "information"},
+// 6: {image: require('../../assets/washroom.png'), label: "accessible washroom"},
+// 7: {image: require('../../assets/power.png'), label: "power issue"},
+// 8: {image: require('../../assets/crosswalk.png'), label: "crosswalk issue"},
+// 9: {image: require('../../assets/ice.png'), label: "ice"},
+// 10: {image: require('../../assets/desk.png'), label: "desk"},
+// 11: {image: require('../../assets/elevator.png'), label: "elevator"},
+// 12: {image: require('../../assets/kiosk.png'), label: "kiosk"},
+// 13: {image: require('../../assets/monitor.png'), label: "monitor"},
+// 14: {image: require('../../assets/ramp.png'), label: "ramp"},
+// 15: {image: require('../../assets/water.png'), label: "water fountain"},
+// 16: {image: require('../../assets/childfriendly.png'), label: "child friendly area"},
+
 
 export const GlobalStyles = StyleSheet.create({
     itemRowContainer: {
@@ -79,10 +114,7 @@ export const getMediaPermissions = async () => {
     try {
         const permissionsResult = await checkMultiple(permissions)
         const ungrantedPermissions = permissions.filter(permission => permissionsResult[permission] != RESULTS.GRANTED);
-        console.log(permissionsResult)
-        console.log(ungrantedPermissions)
         if (ungrantedPermissions.length > 0) {
-            console.log('asking for permissions')
             const result = await requestMultiple(ungrantedPermissions)   
             const deniedPermissions = ungrantedPermissions.filter(permission => result[permission] != RESULTS.GRANTED)
             if (deniedPermissions.length > 0) {
@@ -132,3 +164,16 @@ export const getMapPermissions = async () => {
     }
 }
 
+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();
+}

+ 57 - 49
src/utils/RegistrationUtils.ts

@@ -7,8 +7,8 @@
 
 import axios from 'axios';
 import * as Yup from "yup";
+import { useAuth } from '../data/Auth/AuthContext';
 import { API_URL } from '../utils/RequestUtils';
-import {reportAxiosError} from '../libs/auth/core'
 
 /**
  * String enum for type of registration validation request to send to server
@@ -20,56 +20,64 @@ export interface RegisterCredsValues {
     username: string;
 }
 
-const uniqueValidation = async (validationEndpoint: RegisterCredsValidationType, valueToValidate: string): Promise<boolean> => {
-  try {
-    const data = new FormData()
-    data.append('input', valueToValidate)
-    const response = await axios({
-      method: 'POST',
-      url: API_URL + '/api/' + validationEndpoint + '/',
-      data: data,
-    });
-    if (response.data.valid) {
-      return true;
-    }
-    else {
+export const useValidation = () => {
+  const {sendApiRequestAsync} = useAuth();
+  const uniqueValidation = async (validationEndpoint: RegisterCredsValidationType, valueToValidate: string): Promise<boolean> => {
+    try {
+      const data = new FormData()
+      data.append('input', valueToValidate)
+      const response = await sendApiRequestAsync({
+        axiosConfig: {
+          method: 'POST',
+          url: API_URL + '/api/' + validationEndpoint + '/',
+          data: data,
+        },
+        authorized: false,
+        errorMessage: 'Error validating input',
+      });
+      if (response.data.valid) {
+        return true;
+      }
+      else {
+        return false;
+      }
+    } catch (error) {
       return false;
     }
-  } catch (error) {
-    reportAxiosError('Username validation request failed.', error);
-    return false;
   }
-}
-
-export const credsSchema = Yup.object({
-  username: Yup
-    .string()
-    .trim()
-    .required("You must enter a username.")
-    .min(5, "Your username must be atleast 5 characters long.")
-    .test("unique-username", "This username is already in use", async (value) => await uniqueValidation("usernamevalidation", value)),
-  email: Yup
-    .string()
-    .trim()
-    .required("You must enter an email.")
-    .email("You must enter a valid email.")
-    .test("unique-email", "This email is already in use", async (value) => await uniqueValidation("emailvalidation", value)),
-})
 
-export const profileCredsSchema = Yup.object({
-  username: Yup
-    .string()
-    .trim()
-    .required("You must enter a username.")
-    .min(5, "Your username must be atleast 5 characters long."),
-  email: Yup
-    .string()
-    .trim()
-    .required("You must enter an email.")
-    .email("You must enter a valid email.")
-})
+  const credsSchema = Yup.object({
+    username: Yup
+      .string()
+      .trim()
+      .required("You must enter a username.")
+      .min(5, "Your username must be atleast 5 characters long.")
+      .test("unique-username", "This username is already in use", async (value) => await uniqueValidation("usernamevalidation", value)),
+    email: Yup
+      .string()
+      .trim()
+      .required("You must enter an email.")
+      .email("You must enter a valid email.")
+      .test("unique-email", "This email is already in use", async (value) => await uniqueValidation("emailvalidation", value)),
+  })
+  
+  const profileCredsSchema = Yup.object({
+    username: Yup
+      .string()
+      .trim()
+      .required("You must enter a username.")
+      .min(5, "Your username must be atleast 5 characters long."),
+    email: Yup
+      .string()
+      .trim()
+      .required("You must enter an email.")
+      .email("You must enter a valid email.")
+  })
+  
+  const passwordSchema = Yup.object({
+    password: Yup.string().trim().required("You must enter a password.").matches(/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])[\w~@#$%^&*+=`|{}:;!.?\"()\[\]-]{8,}$/, "Your password must be a minimum of 8 characters and contain 1 special character and 1 number."),
+    confirmPassword: Yup.string().trim().required("You must confirm your password.").oneOf([Yup.ref('password'), null], "Your passwords must match.")
+  })
 
-export const passwordSchema = Yup.object({
-  password: Yup.string().trim().required("You must enter a password.").matches(/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])[\w~@#$%^&*+=`|{}:;!.?\"()\[\]-]{8,}$/, "Your password must be a minimum of 8 characters and contain 1 special character and 1 number."),
-  confirmPassword: Yup.string().trim().required("You must confirm your password.").oneOf([Yup.ref('password'), null], "Your passwords must match.")
-})
+  return {credsSchema, profileCredsSchema, passwordSchema}
+}

+ 4 - 6
src/utils/RequestUtils.ts

@@ -17,8 +17,6 @@ import Config from 'react-native-config'
     if (printResponse) {
         errorString + "\nError response: " + error.response;
     }
-
-    console.error(errorString);
 }
 
 /**
@@ -27,8 +25,8 @@ import Config from 'react-native-config'
 //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.162:8000'
-
-// export const API_URL = Config.API_URL
-
+export const API_URL = 'http://192.168.3.102: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 = Config.API_URL

+ 86 - 12
yarn.lock

@@ -4810,6 +4810,11 @@
   "resolved" "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
   "version" "1.0.2"
 
+"base-64@^0.1.0":
+  "integrity" "sha1-eAqZyE59YAJgNhURxId2E78k9rs="
+  "resolved" "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz"
+  "version" "0.1.0"
+
 "base-x@^3.0.8":
   "integrity" "sha512-Rl/1AWP4J/zRrk54hhlxH4drNxPJXYUaKffODVI53/dAsV4t9fBxyxYKAVPU1XBHxYwOWP9h9H0hM2MVw4YfJA=="
   "resolved" "https://registry.npmjs.org/base-x/-/base-x-3.0.8.tgz"
@@ -10613,7 +10618,7 @@
     "inherits" "^2.0.1"
     "safe-buffer" "^5.1.2"
 
-"md5@^2.2.1":
+"md5@^2.2.1", "md5@^2.3.0":
   "integrity" "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g=="
   "resolved" "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz"
   "version" "2.3.0"
@@ -13508,7 +13513,7 @@
     "kleur" "^3.0.3"
     "sisteransi" "^1.0.5"
 
-"prop-types@^15.5.8", "prop-types@^15.6.0", "prop-types@^15.6.2", "prop-types@^15.7.2", "prop-types@15.7.2":
+"prop-types@^15.5.10", "prop-types@^15.5.8", "prop-types@^15.6.0", "prop-types@^15.6.2", "prop-types@^15.7.2", "prop-types@15.7.2":
   "integrity" "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ=="
   "resolved" "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz"
   "version" "15.7.2"
@@ -13896,6 +13901,11 @@
   "resolved" "https://registry.npmjs.org/react-native-config/-/react-native-config-1.4.5.tgz"
   "version" "1.4.5"
 
+"react-native-device-info@^8.6.0":
+  "integrity" "sha512-LxxaHkx2XQDuRFhk6545uBvjbjL/Xq7YNa8ufOCKg/BSPuZZPWmOW8SUwxDn/bteAGOx6Djv1YqGOMolX+bzSw=="
+  "resolved" "https://registry.npmjs.org/react-native-device-info/-/react-native-device-info-8.6.0.tgz"
+  "version" "8.6.0"
+
 "react-native-dialog@^9.2.0":
   "integrity" "sha512-VXdfo+bAi9ER7+w4aKOWypw8K97C2orvnfnxX4Lup/U8iQB/635V00hgfHEE3s0XScQHftXxm34Fa4iIIjojQA=="
   "resolved" "https://registry.npmjs.org/react-native-dialog/-/react-native-dialog-9.2.0.tgz"
@@ -13908,6 +13918,19 @@
   dependencies:
     "dotenv" "^10.0.0"
 
+"react-native-fast-image@^8.5.11":
+  "integrity" "sha512-cNW4bIJg3nvKaheG8vGMfqCt5LMWX9MS5+wMudgKIHbGO51spRr4sgnlhVgwHLcZ5aeNOVJ8CPRxDIWKRq/0QA=="
+  "resolved" "https://registry.npmjs.org/react-native-fast-image/-/react-native-fast-image-8.5.11.tgz"
+  "version" "8.5.11"
+
+"react-native-fs@^2.19.0":
+  "integrity" "sha512-Yl09IbETkV5UJcBtVtBLttyTmiAhJIHpGA/LvredI5dYiw3MXMMVu42bzELiuH2Bwj7F+qd0fMNvgfBDiDxd2A=="
+  "resolved" "https://registry.npmjs.org/react-native-fs/-/react-native-fs-2.19.0.tgz"
+  "version" "2.19.0"
+  dependencies:
+    "base-64" "^0.1.0"
+    "utf8" "^3.0.0"
+
 "react-native-gesture-handler@*", "react-native-gesture-handler@>= 1.0.0", "react-native-gesture-handler@~1.10.2":
   "integrity" "sha512-cBGMi1IEsIVMgoox4RvMx7V2r6bNKw0uR1Mu1o7NbuHS6BRSVLq0dP34l2ecnPlC+jpWd3le6Yg1nrdCjby2Mw=="
   "resolved" "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-1.10.3.tgz"
@@ -13931,13 +13954,26 @@
   "resolved" "https://registry.npmjs.org/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz"
   "version" "1.3.1"
 
-"react-native-maps@0.29.3":
+"react-native-maps-directions@^1.8.0":
+  "integrity" "sha512-7KWfPrvPLU8VP2nEqsnrWlOuylidlRWWDdk3lqT5Nb1q87FeyzNgA7Ib7n6cZlQwH4usTRlJSnzNo/yQ3u4AZw=="
+  "resolved" "https://registry.npmjs.org/react-native-maps-directions/-/react-native-maps-directions-1.8.0.tgz"
+  "version" "1.8.0"
+  dependencies:
+    "lodash.isequal" "^4.5.0"
+    "prop-types" "^15.6.0"
+
+"react-native-maps@>=0.12.1", "react-native-maps@0.29.3":
   "integrity" "sha512-742ipC71NdGHXmrCHPY9SEGDuKoSx2l0RMC1W9WXe/jd6+Eg5OllRBcR6Y+ORnzvuYZrd0Hj6q6bK3QR5ZL8sA=="
   "resolved" "https://registry.npmjs.org/react-native-maps/-/react-native-maps-0.29.3.tgz"
   "version" "0.29.3"
   dependencies:
     "@types/geojson" "^7946.0.7"
 
+"react-native-material-menu@^2.0.0":
+  "integrity" "sha512-SmO9PLE3E469EPbVWZqvdu6JGPPZIm7YjqDcWs2PPoY0k7w2V9tFo3BmmLXNzNZDCVCAi+PPSsL7h/5WkfHcSg=="
+  "resolved" "https://registry.npmjs.org/react-native-material-menu/-/react-native-material-menu-2.0.0.tgz"
+  "version" "2.0.0"
+
 "react-native-modal@^12.0.3":
   "integrity" "sha512-9myTNZ75gz6MPg1Cngm01x59JbaVkTeWoZvmChmW//PbSAdfbP/dFPnzbidL9IDrRh6Ftuuq8WSuM0aoS5m2lg=="
   "resolved" "https://registry.npmjs.org/react-native-modal/-/react-native-modal-12.1.0.tgz"
@@ -13994,6 +14030,19 @@
     "mockdate" "^3.0.2"
     "string-hash-64" "^1.0.3"
 
+"react-native-root-siblings@^4.0.0":
+  "integrity" "sha512-sdmLElNs5PDWqmZmj4/aNH4anyxreaPm61c4ZkRiR8SO/GzLg6KjAbb0e17RmMdnBdD0AIQbS38h/l55YKN4ZA=="
+  "resolved" "https://registry.npmjs.org/react-native-root-siblings/-/react-native-root-siblings-4.1.1.tgz"
+  "version" "4.1.1"
+
+"react-native-root-toast@^3.3.0":
+  "integrity" "sha512-C4Pqu+Ae7kXsYJwTvz8NshyJ9SL5YJd+/vCkvgDAxxR8AYlPFggEcTCMNARIWXuRwthLbuwcakh4z9k6qg95dg=="
+  "resolved" "https://registry.npmjs.org/react-native-root-toast/-/react-native-root-toast-3.3.0.tgz"
+  "version" "3.3.0"
+  dependencies:
+    "prop-types" "^15.5.10"
+    "react-native-root-siblings" "^4.0.0"
+
 "react-native-safe-area-context@>= 3.0.0", "react-native-safe-area-context@3.3.2":
   "integrity" "sha512-yOwiiPJ1rk+/nfK13eafbpW6sKW0jOnsRem2C1LPJjM3tfTof6hlvV5eWHATye3XOpu2cJ7N+HdkUvUDGwFD2Q=="
   "resolved" "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-3.3.2.tgz"
@@ -14011,6 +14060,11 @@
   "resolved" "https://registry.npmjs.org/react-native-sectioned-multi-select/-/react-native-sectioned-multi-select-0.8.1.tgz"
   "version" "0.8.1"
 
+"react-native-sha256@^1.4.7":
+  "integrity" "sha512-VcIjOBGvHG6V2OCgbGnEKOymcatYC7byf1aM6mmCoUDqOUFQrGIjnU9fUMWGoMERAljVqisvsG/M1GdfhilkFg=="
+  "resolved" "https://registry.npmjs.org/react-native-sha256/-/react-native-sha256-1.4.7.tgz"
+  "version" "1.4.7"
+
 "react-native-side-drawer@^1.2.9":
   "integrity" "sha512-hWB1TuOiTGhZ7Fa2r4+vuaPp112AjJxQ5Tb3LAWaKTj8hIH1bFOPvw/AzSaEWS151gBX1cMuN8uPYkK0NmsgYQ=="
   "resolved" "https://registry.npmjs.org/react-native-side-drawer/-/react-native-side-drawer-1.2.9.tgz"
@@ -14047,6 +14101,11 @@
     "css-select" "^2.1.0"
     "css-tree" "^1.0.0-alpha.39"
 
+"react-native-toast-message@^2.1.3":
+  "integrity" "sha512-K3hHSWezWixxOZUDbxPSarEG+tPv2WcaJG4L7dvRUC1TOKNVCEKzXjsx+6ZMxllDrJ0sFczuYGqZibGpFe/ubA=="
+  "resolved" "https://registry.npmjs.org/react-native-toast-message/-/react-native-toast-message-2.1.3.tgz"
+  "version" "2.1.3"
+
 "react-native-vector-icons@*", "react-native-vector-icons@>6.0.0":
   "integrity" "sha512-sHIdBB6Y0dHaot2fMXgy5J/hhCn5YuyN7SKDNFgPzL8KA1oF2/v7mgYMavnK7LIIs2dJoGnDANKf61dsU+TZlg=="
   "resolved" "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-8.1.0.tgz"
@@ -14061,6 +14120,11 @@
     "prop-types" "^15.7.2"
     "yargs" "^16.1.1"
 
+"react-native-view-shot@3.1.2":
+  "integrity" "sha512-9u9fPtp6a52UMoZ/UCPrCjKZk8tnkI9To0Eh6yYnLKFEGkRZ7Chm6DqwDJbYJHeZrheCCopaD5oEOnRqhF4L2Q=="
+  "resolved" "https://registry.npmjs.org/react-native-view-shot/-/react-native-view-shot-3.1.2.tgz"
+  "version" "3.1.2"
+
 "react-native-web@^0.13":
   "integrity" "sha512-WR/0ECAmwLQ2+2cL2Ur+0/swXFAtcSM0URoADJmG6D4MnY+wGc91JO8LoOTlgY0USBOY+qG/beRrjFa+RAuOiA=="
   "resolved" "https://registry.npmjs.org/react-native-web/-/react-native-web-0.13.18.tgz"
@@ -14116,7 +14180,7 @@
     "whatwg-fetch" "^3.0.0"
     "ws" "^6.1.4"
 
-"react-native@*", "react-native@^0.64.0", "react-native@>= 0.51", "react-native@>= 0.62", "react-native@>=0.42.0", "react-native@>=0.50.0", "react-native@>=0.54.0", "react-native@>=0.56", "react-native@>=0.57", "react-native@>=0.59.0", "react-native@>=0.60.0", "react-native@>=0.63.0", "react-native@>=0.63.3", "react-native@>=0.64.0-rc.0 || 0.0.0-*", "react-native@>0.57.0", "react-native@0.64.3":
+"react-native@*", "react-native@^0.64.0", "react-native@>= 0.51", "react-native@>= 0.62", "react-native@>=0.42.0", "react-native@>=0.47.0", "react-native@>=0.50.0", "react-native@>=0.54.0", "react-native@>=0.56", "react-native@>=0.57", "react-native@>=0.59.0", "react-native@>=0.60.0", "react-native@>=0.63.0", "react-native@>=0.63.3", "react-native@>=0.64.0-rc.0 || 0.0.0-*", "react-native@>0.57.0", "react-native@0.64.3":
   "integrity" "sha512-2OEU74U0Ek1/WeBzPbg6XDsCfjF/9fhrNX/5TFgEiBKd5mNc9LOZ/OlMmkb7iues/ZZ/oc51SbEfLRQdcW0fVw=="
   "resolved" "https://registry.npmjs.org/react-native/-/react-native-0.64.3.tgz"
   "version" "0.64.3"
@@ -14178,7 +14242,7 @@
   "resolved" "https://registry.npmjs.org/react-timer-mixin/-/react-timer-mixin-0.13.4.tgz"
   "version" "0.13.4"
 
-"react@*", "react@^16.3.0", "react@^16.8.0 || ^17", "react@^16.8.0 || ^17.0.0", "react@>= 16.0 || < 18.0", "react@>=16.13.1", "react@>=16.5.1", "react@>=16.8.0", "react@>16.6.0", "react@16 || 17", "react@17.0.1":
+"react@*", "react@^16.3.0", "react@^16.8.0 || ^17", "react@^16.8.0 || ^17.0.0", "react@^16.8.6 || ^17.0.0", "react@>= 16.0 || < 18.0", "react@>=16.13.1", "react@>=16.5.1", "react@>=16.8.0", "react@>16.6.0", "react@16 || 17", "react@17.0.1":
   "integrity" "sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w=="
   "resolved" "https://registry.npmjs.org/react/-/react-17.0.1.tgz"
   "version" "17.0.1"
@@ -16660,6 +16724,11 @@
     "execa" "^1.0.0"
     "mem" "^4.3.0"
 
+"utf8@^3.0.0":
+  "integrity" "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ=="
+  "resolved" "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz"
+  "version" "3.0.0"
+
 "util-deprecate@^1.0.1", "util-deprecate@^1.0.2", "util-deprecate@~1.0.1":
   "integrity" "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
   "resolved" "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
@@ -16714,7 +16783,17 @@
   "resolved" "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz"
   "version" "1.0.1"
 
-"uuid@^3.1.0", "uuid@^3.3.2", "uuid@^3.4.0":
+"uuid@^3.1.0":
+  "integrity" "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
+  "resolved" "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz"
+  "version" "3.4.0"
+
+"uuid@^3.3.2":
+  "integrity" "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
+  "resolved" "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz"
+  "version" "3.4.0"
+
+"uuid@^3.4.0":
   "integrity" "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
   "resolved" "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz"
   "version" "3.4.0"
@@ -16724,12 +16803,7 @@
   "resolved" "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz"
   "version" "7.0.3"
 
-"uuid@^8.0.0":
-  "integrity" "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
-  "resolved" "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz"
-  "version" "8.3.2"
-
-"uuid@^8.3.2":
+"uuid@^8.0.0", "uuid@^8.3.2":
   "integrity" "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
   "resolved" "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz"
   "version" "8.3.2"

Some files were not shown because too many files changed in this diff