This is one of my first posts ever. When I'm stuck I usually just hit my head against a wall, but today I'm asking for help. I've got a ReactNative app built using Expo which does some realtime location monitoring. Which works when the component is active on the screen, but when the app goes into the background, it stops functioning. I've attempted to implement a background task using expo-task-manager, but for the life of me I can't seem to get it to work. I've implemented it to use haptics to notify me that it's functioning properly while testing in the field, and while it seems to work ok on the simulator (I get the console.log at least), the real device only buzzes when the component is active and on screen. I am able to see the blue indicator when the app is not active, but no haptics.
Can someone figure out what I've missed here? Thank you so much in advance.
Here's my code:
import React, { useState, useEffect } from 'react';import { Text, View, Button } from 'react-native';import * as Location from 'expo-location';import * as TaskManager from 'expo-task-manager';import * as Haptics from 'expo-haptics';const DESTINATION_COORDS = { latitude: 44.0041179865438, longitude: -121.68169920997431,};const BACKGROUND_LOCATION_TASK = 'background-location-task-buzz-v2';TaskManager.defineTask(BACKGROUND_LOCATION_TASK, async ({ data, error }) => { if (error) { console.error('Background location task error:', error.message); return; } const { locations } = data; if (locations && locations.length > 0) { const { latitude, longitude } = locations[0].coords; const distance = calculateDistance( latitude, longitude, DESTINATION_COORDS.latitude, DESTINATION_COORDS.longitude ); if (distance < 10) { await triggerHaptics(); console.log('found it.', Date.now()); } }});const calculateDistance = (lat1, lon1, lat2, lon2) => { const R = 6371; // Radius of the earth in km const dLat = deg2rad(lat2 - lat1); // deg2rad below const dLon = deg2rad(lon2 - lon1); const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2) ; const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); const d = R * c; // Distance in km return d * 1000; // Convert to meters};const deg2rad = (deg) => { return deg * (Math.PI / 180);};const triggerHaptics = async () => { await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);};const BuzzOnArrival = () => { const [currentLocation, setCurrentLocation] = useState(null); const [distanceToDestination, setDistanceToDestination] = useState(null); useEffect(() => { // Start background location updates when component mounts Location.startLocationUpdatesAsync(BACKGROUND_LOCATION_TASK, { accuracy: Location.Accuracy.BestForNavigation, timeInterval: 10000, // Check every 10 seconds distanceInterval: 0, showsBackgroundLocationIndicator: true, }); // Clean up function to stop background location updates when component unmounts return () => { Location.stopLocationUpdatesAsync(BACKGROUND_LOCATION_TASK); }; }, []); useEffect(() => { // Fetch current location every second const interval = setInterval(() => { fetchCurrentLocation(); }, 1000); // Clean up function to clear the interval when component unmounts return () => clearInterval(interval); }, []); const requestPermissions = async () => { const { status: foregroundStatus } = await Location.requestForegroundPermissionsAsync(); if (foregroundStatus === 'granted') { const { status: backgroundStatus } = await Location.requestBackgroundPermissionsAsync(); if (backgroundStatus === 'granted') { await Location.startLocationUpdatesAsync(LOCATION_TASK_NAME, { accuracy: Location.Accuracy.Balanced, }); } } }; const fetchCurrentLocation = async () => { const location = await Location.getCurrentPositionAsync({}); setCurrentLocation(location.coords); // Calculate distance to destination const distance = calculateDistance( location.coords.latitude, location.coords.longitude, DESTINATION_COORDS.latitude, DESTINATION_COORDS.longitude ); setDistanceToDestination(distance); }; const handleButtonPress = async () => { await triggerHaptics(); }; return (<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}><Text>Listening for arrival...</Text> {currentLocation && (<Text>Current Location: {currentLocation.latitude}, {currentLocation.longitude}</Text> )}<Text>Destination: {DESTINATION_COORDS.latitude}, {DESTINATION_COORDS.longitude}</Text><Text>Distance to Destination: {distanceToDestination?.toFixed(2)} meters</Text><Button title="Test Haptics" onPress={handleButtonPress} /><Button onPress={requestPermissions} title="Enable background location" /></View> );};export default BuzzOnArrival;
And here is my package.json:
{"name": "geocaster","version": "1.0.0","main": "expo-router/entry","scripts": {"start": "expo start","android": "expo run:android","ios": "expo run:ios","web": "expo start --web" },"dependencies": {"@gorhom/bottom-sheet": "^4.6.1","@react-native-async-storage/async-storage": "1.21.0","@reduxjs/toolkit": "^2.0.1","@supabase/supabase-js": "^2.39.3","base64-arraybuffer": "^1.0.2","expo": "~50.0.2","expo-av": "~13.10.3","expo-background-fetch": "^11.8.1","expo-camera": "~14.0.1","expo-constants": "~15.4.5","expo-crypto": "~12.8.0","expo-file-system": "~16.0.6","expo-haptics": "~12.8.1","expo-image-picker": "~14.7.1","expo-linking": "~6.2.2","expo-location": "~16.5.2","expo-media-library": "~15.9.1","expo-notifications": "~0.27.5","expo-router": "~3.4.4","expo-secure-store": "~12.8.1","expo-sensors": "~12.9.1","expo-status-bar": "~1.11.1","expo-task-manager": "~11.7.0","expo-updates": "~0.24.12","expo-web-browser": "~12.8.2","firebase": "^10.7.1","geolib": "^3.3.4","jszip": "^3.10.1","jszip-utils": "^0.1.0","lucide-react-native": "^0.314.0","pullstate": "^1.25.0","react": "18.2.0","react-dom": "18.2.0","react-native": "0.73.2","react-native-draggable-flatlist": "^4.0.1","react-native-easy-grid": "^0.2.2","react-native-gesture-handler": "^2.16.0","react-native-maps": "1.8.0","react-native-paper": "^5.12.1","react-native-progress": "^5.0.1","react-native-reanimated": "^3.8.1","react-native-safe-area-context": "4.8.2","react-native-screens": "~3.29.0","react-native-snap-carousel": "^3.9.1","react-native-svg": "14.1.0","react-native-web": "~0.19.6","react-redux": "^9.1.0","redux": "^5.0.1" },"devDependencies": {"@babel/core": "^7.20.0" },"private": true}
I also am providing what I believe is the relevant parts of my app.json:
{"expo": { ..."ios": { ...,"infoPlist": {"UIBackgroundModes": ["location", "fetch"],"NSLocationAlwaysAndWhenInUseUsageDescription": "Placeholder","NSLocationAlwaysUsageDescription": "Placeholder","NSLocationWhenInUseUsageDescription": "Placeholder", } }, ...,"plugins": ["expo-router","expo-secure-store", ["expo-location", {"locationAlwaysAndWhenInUsePermission": "Placeholder","isAndroidBackgroundLocationEnabled": true,"isIosBackgroundLocationEnabled": true } ], ], ... }}
and even my Info.plist:
<key>NSLocationUsageDescription</key><string>Use of location for determining waypoints and proximity</string><key>NSLocationWhenInUseUsageDescription</key><string>Use of location for determing waypoints and proximity</string><key>NSMicrophoneUsageDescription</key><string>The Microphone will be used to record videos for waypoints.</string><key>NSMotionUsageDescription</key><string>The of motion sensors required for the compass.</string><key>NSPhotoLibraryUsageDescription</key><string>The photo library can be used to select images and videos for upload.</string><key>NSUserActivityTypes</key><array><string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string></array><key>UIBackgroundModes</key><array><string>location</string><string>fetch</string></array>
I'm focused on iOS for the moment so have only tested it there. I've tried working with it in the simulator, and on real devices. The code above buzzes when the component is active, but not when it's in the background.
I would expect it to buzz as I get within about ten meters of the DESTINATION_COORDS, whether I have the app in the foreground OR in the background.
Right now it only buzzes when the app is in the foreground and the component is actively on the screen.