React Native Background Location Services: Beyond the Basics
Step-by-step guide to setting up background location tracking with react-native-geolocation-service and Notifee.
Photo by Priscilla Du Preez 🇨🇦 on Unsplash
Imagine building a payment platform where merchants need to be discoverable by nearby customers, even when they're not actively using their phones. Or consider developing a ride-hailing service where drivers must track passengers' real-time location changes after booking a ride. Both scenarios share a critical requirement: reliable background location tracking.
For our payment platform, merchants must continuously broadcast their location, with updates triggered every 5 meters to ensure precise discovery by potential customers. In the ride-hailing scenario, accurate passenger location updates are crucial for drivers to provide efficient service, especially when passengers are on the move after booking.
These real-world applications present unique challenges in React Native development. They require not just periodic location updates, but consistent background tracking that works reliably across both iOS and Android platforms. While this might seem straightforward at first glance, implementing robust background location tracking involves navigating platform-specific behaviours, battery optimisation challenges, and different implementation approaches.
Setting Up Background Location for a Payment Platform
To get started with background location tracking in our payment platform, we need to configure both Android and iOS platforms properly.
npm install react-native-geolocation-service @notifee/react-native
# or using yarn
yarn add react-native-geolocation-service @notifee/react-native
Android Configuration
Add these permissions to your AndroidManifest.xml
:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
iOS Configuration
Add these keys to your
Info.plist
:<key>NSLocationWhenInUseUsageDescription</key> <string>We need your location to find nearby payment opportunities</string> <key>NSLocationAlwaysAndWhenInUseUsageDescription</key> <string>We need your location in the background to notify you of nearby payment opportunities</string> <key>NSLocationAlwaysUsageDescription</key> <string>We need your location in the background to notify you of nearby payment opportunities</string> <key>UIBackgroundModes</key> <array> <string>location</string> <string>fetch</string> </array>
Enable ‘Background Modes’ in Xcode:
Open your project in Xcode
Go to your target’s ‘Signing & Capabilities‘
Click'+ Capability’ and add ‘Background Modes’
Check ‘Location updates’
Permission Handling
import { Platform, PermissionsAndroid } from 'react-native';
import Geolocation from 'react-native-geolocation-service';
import { requestNotifications } from 'react-native-permissions';
const requestLocationPermissions = async () => {
if (Platform.OS === 'ios') {
const status = await Geolocation.requestAuthorization('always');
return status === 'granted';
}
if (Platform.OS === 'android') {
// Request ACCESS_FINE_LOCATION
const fineLocationPermission = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
{
title: "Location Permission",
message: "We need your location to find nearby payment opportunities",
buttonNeutral: "Ask Me Later",
buttonNegative: "Cancel",
buttonPositive: "OK",
}
);
if (fineLocationPermission === PermissionsAndroid.RESULTS.GRANTED) {
// Request ACCESS_BACKGROUND_LOCATION
const backgroundPermission = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.ACCESS_BACKGROUND_LOCATION,
{
title: "Background Location Permission",
message:
"To continue receiving nearby payment opportunities, please allow background location access. " +
"You can change this anytime in your device settings.",
buttonNeutral: "Ask Me Later",
buttonNegative: "Cancel",
buttonPositive: "OK",
}
);
return backgroundPermission === PermissionsAndroid.RESULTS.GRANTED;
}
return false;
}
};
const requestNotificationPermissions = async () => {
if (Platform.OS === 'ios') {
// Request notifications (alert, sound, badge)
const { status } = await requestNotifications(['alert', 'sound', 'badge']);
return status === 'granted';
} else if (Platform.OS === 'android') {
// Android 13 (API level 33) and above require POST_NOTIFICATIONS permission
if (Platform.Version >= 33) {
const notificationPermission = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS,
{
title: "Notification Permission",
message: "We need your notification permission to keep you updated on payment opportunities.",
buttonNeutral: "Ask Me Later",
buttonNegative: "Cancel",
buttonPositive: "OK",
}
);
return notificationPermission === PermissionsAndroid.RESULTS.GRANTED;
}
// For Android versions below 33, notifications are granted by default.
return true;
}
return false;
};
const requestAllPermissions = async () => {
try {
const locationGranted = await requestLocationPermissions();
const notificationsGranted = await requestNotificationPermissions();
return locationGranted && notificationsGranted;
} catch (error) {
//throw 'Error requesting permissions:', error);
return false;
}
};
// permission for location
const checkAndRequestPermissions = async () => {
try {
const hasPermissions = await requestAllPermissions();
if (hasPermissions) {
//All required permissions granted
// Start location tracking or other functionality here
} else {
//'Required permissions not granted
}
} catch (err) {
//'throw Error requesting permissions:'
}
};
//permission request notifiction
checkAndRequestPermissions();
The above setup is for the two use cases mentioned earlier and applies to both Android and iOS platforms. The following section will focus on the use case of a payment platform, specifically on Android. Currently, Notifee does not support foreground services on iOS. In the next part, we will discuss how to handle background location reading using react-native-background-actions on both iOS and Android.
Location Tracking Implementation on Android
Getting current location and continuous location tracking with watchPosition
//utils.ts
// Utility Functions for Location Tracking
// 1. getCurrentLocationPosition Function
// Purpose: Retrieve the user's current geographic coordinates
export const getCurrentLocationPosition = () => new Promise((resolve, reject) => {
Geolocation.getCurrentPosition(
// Success Callback
position => {
// Extract precise location details
const cords = {
latitude: position.coords.latitude, // Exact latitude
longitude: position.coords.longitude, // Exact longitude
heading: position?.coords?.heading, // Optional: movement direction
};
resolve(cords); // Resolve promise with coordinates
},
// Error Callback
error => {
reject(error.message); // Reject with error message if location retrieval fails
},
// Configuration Options
{
enableHighAccuracy: true, // Use GPS for most precise location
timeout: 15000, // 15 seconds timeout for location request
maximumAge: 10000 // Accept cached location up to 10 seconds old
}
);
});
// 2. watchMerchantPosition Function
// Purpose: Continuously track location changes with high precision
export const watchMerchantPosition = (
successFn: SuccessCallback, // Function called on successful location update
errorFn: ErrorCallback, // Function called on location tracking error
) => {
const watchID = Geolocation.watchPosition(
successFn, // Success handler
errorFn, // Error handler
{
// Platform-specific accuracy settings
accuracy: {
android: 'high', // High accuracy mode for Android
ios: 'best' // Best accuracy mode for iOS
},
enableHighAccuracy: true, // Force high-precision tracking
distanceFilter: 5, // Update location even on minimal movement
interval: 5000, // Check for updates every 5 seconds (Android)
fastestInterval: 2000, // Minimum update interval (Android)
showLocationDialog: true, // Show system location settings dialog if needed
forceRequestLocation: true // Attempt location request even if previously denied
}
);
return watchID; // Return tracking ID for potential cancellation
};
// 3. watchMerchantPositionSuccess Function
// Purpose: Handle successful location updates
export function watchMerchantPositionSuccess(position: GeoPosition) {
// Extract latitude and longitude
const {latitude, longitude} = position.coords;
// Placeholder for backend update logic
// Typically, you'd send these coordinates to your server
}
// 4. watchMerchantPositionError Function
// Purpose: Handle location tracking errors
export function watchMerchantPositionError(error: GeoError) {
return error; // Simply return the error for logging/handling
}
// 5. startForegroundService Function
// Purpose: Create a foreground service notification for continuous location tracking
export async function startForegroundService() {
// Create a notification channel
const channelId = await notifee.createChannel({
id: 'location..',
name: 'Location Tracking',
importance: AndroidImportance.HIGH,
});
// Display persistent foreground service notification
await notifee.displayNotification({
title: 'Location Tracking Active',
body: 'Your location is being tracked',
android: {
channelId,
asForegroundService: true, // Run as a foreground service
ongoing: true, // Persistent notification
importance: AndroidImportance.HIGH,
},
});
}
We also want to add the following code to our index.js
to register the foreground service. Additionally, include the services below in your AndroidManifest.xml
to enable Notifee to handle our foreground service.
Notifee foreground notification
//index.js
notifee.registerForegroundService(notification => {
return new Promise(async () => {
await startForegroundService().catch(err => err);
});
});
<service android:name="app.notifee.core.ForegroundService" android:foregroundServiceType="location" />
Let me explain each part of the code implementation:
We can read the user's current location once they grant permission. We have access to both the user's location and background location.
To implement continuous tracking, we use some utility functions to help manage each part of the process. We'll create a utility function to monitor changes in the user's location when it changes by 5 meters after getting the user's current location. We handle both success responses and errors. We also include a function to start the foreground process and display a notification to the user. It's necessary to include the service <service android:name="app.notifee.core.ForegroundService" android:foregroundServiceType="location" />
in our AndroidManifest.xml
to access the user's location when the Notifee foreground service is running. registerForegroundService
is used to register the foreground service and also serves as a cleanup function. Once we stop the service or the device stops it, the foreground notification service is automatically removed.
Now, let's move on to how to use this implementation.
import React, {useEffect, useRef} from 'react';
import {AppState, Button, StyleSheet, Text, View} from 'react-native';
import {Platform} from 'react-native';
import Geolocation from 'react-native-geolocation-service';
import {
getCurrentLocationPosition,
startForegroundService,
watchMerchantPosition,
watchMerchantPositionError,
watchMerchantPositionSuccess,
} from './app/utils';
import notifee from '@notifee/react-native';
function App(): React.JSX.Element {
//when user is on
useEffect(() => {
let id: number = 0;
(async function updateUserLocation() {
id = watchMerchantPosition(
position => watchMerchantPositionSuccess(position),
error => watchMerchantPositionError(error as any),
);
})();
return () => {
Geolocation.clearWatch(id);
};
}, []);
let watchId = useRef<number>(0);
async function startLocationTracking() {
// Start foreground service first
await startForegroundService();
watchId.current = watchMerchantPosition(
position => watchMerchantPositionSuccess(position),
error => watchMerchantPositionError(error as any),
);
}
function stopLocationTracking(watchId: number) {
Geolocation.clearWatch(watchId);
notifee.stopForegroundService();
}
useEffect(() => {
const subscription = AppState.addEventListener(
'change',
async nextAppState => {
if (nextAppState === 'background' || nextAppState === 'inactive') {
await startLocationTracking();
} else {
stopLocationTracking(watchId.current);
}
},
);
return () => {
// clearTimeout(setTimeoutId);
subscription.remove();
};
}, []);
return (
<View style={styles.sectionContainer}>
<Text style={styles.sectionTitle}>Payment App using GEO location </Text>
<Button title="Request Permission" onPress={checkAndRequestPermissions} />
</View>
);
}
const styles = StyleSheet.create({
sectionContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
sectionTitle: {
fontSize: 20,
fontWeight: '600',
},
sectionDescription: {
marginTop: 8,
fontSize: 18,
fontWeight: '400',
},
highlight: {
fontWeight: '700',
},
});
export default App;
This useEffect
hook is designed to immediately start tracking the user's location when the component mounts. Here's a detailed explanation:
let id: number = 0;
initializes a variable to store the watch ID returned bywatchMerchantPosition
.The immediately invoked async function
(async function updateUserLocation() { ... })()
is used to callwatchMerchantPosition
immediately.watchMerchantPosition
is called with two callback functions:The first callback (
position => watchMerchantPositionSuccess(position)
) handles successful location updates, allowing you to update the backend with the new location coordinates.The second callback (
error => watchMerchantPositionError(error as any)
) handles any errors during location tracking
The return function in the useEffect
is a cleanup mechanism. When the component unmounts, Geolocation.clearWatch(id)
is called to stop location tracking and prevent memory leaks.
watchPosition
is designed to run in the foreground and report changes in the user's location. So, whether it's in auseEffect
, it will still run when the conditions are met.
The startLocationTracking
function adds an important feature for tracking in the background. It starts by initiating a foreground service, which is needed for continuous location monitoring on Android. This method creates a persistent notification, allowing the app to keep tracking location even when it's not actively being used. The function captures the watch ID, which is essential for stopping the location tracking later.
The second useEffect
hook is quite advanced, using the AppState
API to detect changes in the app's state. When the app goes to the background or becomes inactive, it automatically calls the startLocationTracking
method to keep monitoring the location. On the other hand, when the app becomes active again, it stops the foreground service and location tracking.
The stopLocationTracking
function provides a clean way to halt location tracking, clearing the watch and stopping the foreground service. This is essential for managing resources and respecting user privacy when location tracking is no longer necessary.
useEffect(() => {
let setTimeoutId: any = null;
const subscription = AppState.addEventListener(
'change',
async nextAppState => {
if (nextAppState === 'background' || nextAppState === 'inactive') {
await startLocationTracking();
setTimeoutId = setTimeout(
() => {
stopLocationTracking(watchId.current);
},
1000 * 60 * 30,
);
} else {
stopLocationTracking(watchId.current);
}
},
);
return () => {
clearTimeout(setTimeoutId);
subscription.remove();
};
}, []);
Note: The Notifee foreground service will continue running until the user reopens the app or the battery dies. To prevent user frustration and ensure a smooth experience, it's important to set a timeout—such as 30 minutes—to automatically stop the service. Additionally, you can display a dismissible notification informing users that location access is essential for the best experience, prompting them to reopen the app and restart the process if needed.
In an E-hail app scenario, the background service is typically stopped once the driver reaches the user's destination and the ride begins. A new background service is then started to track the driver's progress from the ride's start until the destination, and it is stopped again once the ride is completed.
Conclusion
We've explored one method of implementing location tracking in React Native using Notifee and react-native-geolocation-service. While powerful, this approach has its limitations, especially when it comes to background tracking.
Check the link to the github repo here
In our next article, we'll dive deeper into location tracking by using react-native-background-actions
with react-native-geolocation-service
. We'll uncover a more robust solution for tracking user location across different platforms.
Happy Coding!