Bladeren bron

background location

Viktoriia 4 maanden geleden
bovenliggende
commit
2951b27294

+ 12 - 3
app.config.ts

@@ -71,7 +71,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
     },
     associatedDomains: ['applinks:nomadmania.com'],
     infoPlist: {
-      UIBackgroundModes: ['fetch'],
+      UIBackgroundModes: ['location', 'fetch'],
       NSLocationAlwaysUsageDescription:
         'Turn on location service to allow NomadMania.com find friends nearby.',
       NSPhotoLibraryUsageDescription:
@@ -84,6 +84,8 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
         'Nomadmania app needs access to the documents folder to select files.',
       NSCameraUsageDescription: 'Nomadmania app needs access to the camera to record video.',
       NSLocationWhenInUseUsageDescription:
+        'NomadMania app needs access to your location to show relevant data.',
+      NSLocationAlwaysAndWhenInUseUsageDescription:
         'NomadMania app needs access to your location to show relevant data.'
     },
     privacyManifests: {
@@ -117,7 +119,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
     ],
     googleServicesFile: './google-services.json',
     permissions: [
-      // 'ACCESS_BACKGROUND_LOCATION',
+      'ACCESS_BACKGROUND_LOCATION',
       'ACCESS_FINE_LOCATION',
       'ACCESS_COARSE_LOCATION',
       'READ_EXTERNAL_STORAGE',
@@ -173,6 +175,13 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
         microphonePermission: 'Allow Nomadmania to access your microphone.'
       }
     ],
-    ['@maplibre/maplibre-react-native']
+    ['@maplibre/maplibre-react-native'],
+    [
+      'expo-location',
+      {
+        isIosBackgroundLocationEnabled: true,
+        isAndroidBackgroundLocationEnabled: true
+      }
+    ]
   ]
 });

+ 1 - 0
package.json

@@ -49,6 +49,7 @@
     "expo-splash-screen": "~0.27.5",
     "expo-sqlite": "^14.0.6",
     "expo-status-bar": "~1.12.1",
+    "expo-task-manager": "~11.8.2",
     "expo-updates": "~0.25.24",
     "formik": "^2.4.5",
     "moment": "^2.29.4",

+ 43 - 3
src/screens/InAppScreens/MapScreen/FilterModal/index.tsx

@@ -34,6 +34,10 @@ import SharingIcon from 'assets/icons/location-sharing.svg';
 import UsersIcon from 'assets/icons/bottom-navigation/travellers.svg';
 import LocationIcon from 'assets/icons/location.svg';
 import formatNumber from '../../TravelsScreen/utils/formatNumber';
+import {
+  startBackgroundLocationUpdates,
+  stopBackgroundLocationUpdates
+} from 'src/utils/backgroundLocation';
 
 const FilterModal = ({
   isFilterVisible,
@@ -90,6 +94,8 @@ const FilterModal = ({
   const [isSharing, setIsSharing] = useState(false);
   const [askLocationVisible, setAskLocationVisible] = useState<boolean>(false);
   const [openSettingsVisible, setOpenSettingsVisible] = useState<boolean>(false);
+  const [openSettingsBackgroundVisible, setOpenSettingsBackgroundVisible] =
+    useState<boolean>(false);
 
   useEffect(() => {
     const syncSettings = async () => {
@@ -413,6 +419,7 @@ const FilterModal = ({
       setSettings({ token, sharing: 0 });
       setShowNomads && setShowNomads(false);
       storage.set('showNomads', false);
+      await stopBackgroundLocationUpdates();
     }
   };
 
@@ -421,6 +428,19 @@ const FilterModal = ({
     const isServicesEnabled = await Location.hasServicesEnabledAsync();
 
     if (status === 'granted' && isServicesEnabled) {
+      const bgStatus = await Location.getBackgroundPermissionsAsync();
+      if (bgStatus.status !== 'granted') {
+        const { status } = await Location.requestBackgroundPermissionsAsync();
+        if (status === Location.PermissionStatus.GRANTED) {
+          await startBackgroundLocationUpdates();
+        } else {
+          await stopBackgroundLocationUpdates();
+          setOpenSettingsBackgroundVisible(true);
+        }
+      } else {
+        await startBackgroundLocationUpdates();
+      }
+
       getLocation();
     } else if (!canAskAgain || !isServicesEnabled) {
       setOpenSettingsVisible(true);
@@ -455,6 +475,19 @@ const FilterModal = ({
     const isServicesEnabled = await Location.hasServicesEnabledAsync();
 
     if (status === 'granted' && isServicesEnabled) {
+      const bgStatus = await Location.getBackgroundPermissionsAsync();
+
+      if (bgStatus.status !== 'granted') {
+        const { status } = await Location.requestBackgroundPermissionsAsync();
+        if (status === Location.PermissionStatus.GRANTED) {
+          await startBackgroundLocationUpdates();
+        } else {
+          await stopBackgroundLocationUpdates();
+          setOpenSettingsBackgroundVisible(true);
+        }
+      } else {
+        await startBackgroundLocationUpdates();
+      }
       getLocation();
     } else if (!canAskAgain || !isServicesEnabled) {
       setOpenSettingsVisible(true);
@@ -538,8 +571,11 @@ const FilterModal = ({
         />
         <WarningModal
           type={'success'}
-          isVisible={openSettingsVisible}
-          onClose={() => setOpenSettingsVisible(false)}
+          isVisible={openSettingsVisible || openSettingsBackgroundVisible}
+          onClose={() => {
+            setOpenSettingsVisible(false);
+            setOpenSettingsBackgroundVisible(false);
+          }}
           action={async () => {
             const isServicesEnabled = await Location.hasServicesEnabledAsync();
 
@@ -551,7 +587,11 @@ const FilterModal = ({
               Platform.OS === 'ios' ? Linking.openURL('app-settings:') : Linking.openSettings();
             }
           }}
-          message="NomadMania app needs location permissions to function properly. Open settings?"
+          message={
+            openSettingsBackgroundVisible
+              ? "NomadMania app needs background location access to update your location automatically. Please select 'Always' in location permissions. Open settings?"
+              : 'NomadMania app needs location permissions to function properly. Open settings?'
+          }
         />
       </View>
     );

+ 29 - 0
src/screens/InAppScreens/MapScreen/index.tsx

@@ -84,6 +84,10 @@ import { useAvatarStore } from 'src/stores/avatarVersionStore';
 import _ from 'lodash';
 import ScaleBar from 'src/components/ScaleBar';
 import MessagesDot from 'src/components/MessagesDot';
+import {
+  startBackgroundLocationUpdates,
+  stopBackgroundLocationUpdates
+} from 'src/utils/backgroundLocation';
 
 const clusteredUsersIcon = require('assets/icons/icon-clustered-users.png');
 const defaultUserAvatar = require('assets/icon-user-share-location-solid.png');
@@ -622,9 +626,22 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
       ) {
         setShowNomads(false);
         storage.set('showNomads', false);
+        await stopBackgroundLocationUpdates();
         return;
       }
 
+      const bgStatus = await Location.getBackgroundPermissionsAsync();
+      if (bgStatus.status !== 'granted') {
+        const { status } = await Location.requestBackgroundPermissionsAsync();
+        if (status === Location.PermissionStatus.GRANTED) {
+          await startBackgroundLocationUpdates();
+        } else {
+          await stopBackgroundLocationUpdates();
+        }
+      } else {
+        await startBackgroundLocationUpdates();
+      }
+
       try {
         let currentLocation = await Location.getCurrentPositionAsync({
           accuracy: Location.Accuracy.Balanced
@@ -893,6 +910,18 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
       const isServicesEnabled = await Location.hasServicesEnabledAsync();
 
       if (status === 'granted' && isServicesEnabled) {
+        const bgStatus = await Location.getBackgroundPermissionsAsync();
+        if (bgStatus.status !== 'granted') {
+          const { status } = await Location.requestBackgroundPermissionsAsync();
+          if (status === Location.PermissionStatus.GRANTED) {
+            await startBackgroundLocationUpdates();
+          } else {
+            await stopBackgroundLocationUpdates();
+          }
+        } else {
+          await startBackgroundLocationUpdates();
+        }
+
         await getLocation();
       } else if (!canAskAgain || !isServicesEnabled) {
         setOpenSettingsVisible(true);

+ 43 - 3
src/screens/LocationSharingScreen/index.tsx

@@ -24,6 +24,10 @@ import {
   usePostUpdateLocationMutation
 } from '@api/location';
 import LocationIcon from 'assets/icons/location.svg';
+import {
+  startBackgroundLocationUpdates,
+  stopBackgroundLocationUpdates
+} from 'src/utils/backgroundLocation';
 
 const LocationSharingScreen = ({ navigation }: { navigation: any }) => {
   const token = storage.get('token', StoreType.STRING) as string;
@@ -37,6 +41,8 @@ const LocationSharingScreen = ({ navigation }: { navigation: any }) => {
   const [isSharingWithEveryone, setIsSharingWithEveryone] = useState(false);
   const [askLocationVisible, setAskLocationVisible] = useState<boolean>(false);
   const [openSettingsVisible, setOpenSettingsVisible] = useState<boolean>(false);
+  const [openSettingsBackgroundVisible, setOpenSettingsBackgroundVisible] =
+    useState<boolean>(false);
 
   useEffect(() => {
     const syncSettings = async () => {
@@ -67,6 +73,7 @@ const LocationSharingScreen = ({ navigation }: { navigation: any }) => {
           setSettings({ token, sharing: 0 });
           storage.set('showNomads', false);
           setIsSharingWithEveryone(false);
+          await stopBackgroundLocationUpdates();
         }
       }
     });
@@ -85,6 +92,7 @@ const LocationSharingScreen = ({ navigation }: { navigation: any }) => {
         setSettings({ token, sharing: 0 });
         storage.set('showNomads', false);
         setIsSharingWithEveryone(false);
+        await stopBackgroundLocationUpdates();
       }
       setInitialPermissionStatus(status);
     };
@@ -112,6 +120,7 @@ const LocationSharingScreen = ({ navigation }: { navigation: any }) => {
       setSettings({ token, sharing: 0 });
       storage.set('showNomads', false);
       setIsSharingWithEveryone(false);
+      await stopBackgroundLocationUpdates();
     }
   };
 
@@ -121,6 +130,18 @@ const LocationSharingScreen = ({ navigation }: { navigation: any }) => {
 
     if (status === 'granted' && isServicesEnabled) {
       getLocation();
+      const bgStatus = await Location.getBackgroundPermissionsAsync();
+      if (bgStatus.status !== 'granted') {
+        const { status } = await Location.requestBackgroundPermissionsAsync();
+        if (status === Location.PermissionStatus.GRANTED) {
+          await startBackgroundLocationUpdates();
+        } else {
+          await stopBackgroundLocationUpdates();
+          setOpenSettingsBackgroundVisible(true);
+        }
+      } else {
+        await startBackgroundLocationUpdates();
+      }
     } else if (!canAskAgain || !isServicesEnabled) {
       setOpenSettingsVisible(true);
     } else {
@@ -148,6 +169,18 @@ const LocationSharingScreen = ({ navigation }: { navigation: any }) => {
 
     if (status === 'granted' && isServicesEnabled) {
       getLocation();
+      const bgStatus = await Location.getBackgroundPermissionsAsync();
+      if (bgStatus.status !== 'granted') {
+        const { status } = await Location.requestBackgroundPermissionsAsync();
+        if (status === Location.PermissionStatus.GRANTED) {
+          await startBackgroundLocationUpdates();
+        } else {
+          await stopBackgroundLocationUpdates();
+          setOpenSettingsBackgroundVisible(true);
+        }
+      } else {
+        await startBackgroundLocationUpdates();
+      }
     } else if (!canAskAgain || !isServicesEnabled) {
       setOpenSettingsVisible(true);
     }
@@ -201,8 +234,11 @@ const LocationSharingScreen = ({ navigation }: { navigation: any }) => {
       />
       <WarningModal
         type={'success'}
-        isVisible={openSettingsVisible}
-        onClose={() => setOpenSettingsVisible(false)}
+        isVisible={openSettingsVisible || openSettingsBackgroundVisible}
+        onClose={() => {
+          setOpenSettingsVisible(false);
+          setOpenSettingsBackgroundVisible(false);
+        }}
         action={async () => {
           const isServicesEnabled = await Location.hasServicesEnabledAsync();
 
@@ -214,7 +250,11 @@ const LocationSharingScreen = ({ navigation }: { navigation: any }) => {
             Platform.OS === 'ios' ? Linking.openURL('app-settings:') : Linking.openSettings();
           }
         }}
-        message="NomadMania app needs location permissions to function properly. Open settings?"
+        message={
+          openSettingsBackgroundVisible
+            ? "NomadMania app needs background location access to update your location automatically. Please select 'Always' in location permissions. Open settings?"
+            : 'NomadMania app needs location permissions to function properly. Open settings?'
+        }
       />
     </PageWrapper>
   );

+ 77 - 0
src/utils/backgroundLocation.ts

@@ -0,0 +1,77 @@
+import * as TaskManager from 'expo-task-manager';
+import * as Location from 'expo-location';
+import axios from 'axios';
+import { storage, StoreType } from 'src/storage';
+import { Platform } from 'react-native';
+import { API_URL, APP_VERSION } from 'src/constants';
+import { API } from 'src/types';
+
+const LOCATION_TASK_NAME = 'BACKGROUND_LOCATION_TASK';
+
+TaskManager.defineTask(LOCATION_TASK_NAME, async ({ data, error }) => {
+  if (error) {
+    console.error('[BackgroundLocation] Task error:', error);
+    return;
+  }
+  if (data) {
+    const { locations } = data as any;
+    if (locations && locations.length > 0) {
+      const { coords } = locations[0];
+      const token = storage.get('token', StoreType.STRING);
+
+      if (!token) {
+        return;
+      }
+
+      try {
+        const response = await axios.postForm(
+          API_URL + '/' + API.UPDATE_LOCATION,
+          {
+            token,
+            lat: coords.latitude,
+            lng: coords.longitude,
+            background: LOCATION_TASK_NAME
+          },
+          {
+            headers: {
+              Platform: Platform.OS,
+              'App-Version': APP_VERSION
+            }
+          }
+        );
+      } catch (sendError) {
+        console.error('[BackgroundLocation] Sending location failed:', sendError);
+      }
+    }
+  }
+});
+
+export const startBackgroundLocationUpdates = async () => {
+  const hasStarted = await Location.hasStartedLocationUpdatesAsync(LOCATION_TASK_NAME);
+  if (hasStarted) {
+    return;
+  }
+
+  await Location.startLocationUpdatesAsync(LOCATION_TASK_NAME, {
+    accuracy: Location.Accuracy.Highest,
+    // 30 minutes for Android
+    timeInterval: 30 * 60 * 1000,
+    showsBackgroundLocationIndicator: false,
+    pausesUpdatesAutomatically: false,
+    // banner on Android
+    foregroundService: {
+      notificationTitle: 'NomadMania tracking your location',
+      notificationBody: 'Location is used in background every 30 minutes.'
+      // notificationColor: '#0F3F4F'
+    },
+    // iOS only
+    activityType: Location.ActivityType.Other
+  });
+};
+
+export const stopBackgroundLocationUpdates = async () => {
+  const hasStarted = await Location.hasStartedLocationUpdatesAsync(LOCATION_TASK_NAME);
+  if (hasStarted) {
+    await Location.stopLocationUpdatesAsync(LOCATION_TASK_NAME);
+  }
+};