Reputation: 966
I am developing an app where my users will sometimes need to be tracked as they are outside of the app. I have gone through the expo docs and it appears I am supposed to work with Foreground and Background Permissions Async functions but my expo app does not recognize either version. Has anyone else encountered this? How do I fix this? Below is some of my code:
import * as Permissions from 'expo-permissions';
import * as Request from 'expo-permissions';
import * as Location from 'expo-location';
import Constants from 'expo-constants'
useEffect(() => {
(async () => {
if (Platform.OS === 'android' && !Constants.isDevice) {
setErrorMsg(
'Oops, this will not work on Snack in an Android emulator. Try it on your device!'
);
return;
}
let { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
setErrorMsg('Permission to access location was denied');
return;
}
let location = await Location.getCurrentPositionAsync({});
setLocation(location);
})();
}, []);
useEffect (() =>{
(async () => {
if (Platform.OS === 'android' && !Constants.isDevice){
setErrorMsg2(
'Whoops'
);
return;
}
let { status2 } = await Location.requestBackgroundPermissionsAsync();
if (status2 !== 'granted') {
setErrorMsg2('Permission to access background location was denied');
return;
}
let location2 = await Location.getCurrentPositionAsync({})
setLocation2(location2)
})()
},[])
let text = 'Waiting..';
if (errorMsg) {
text = errorMsg;
}
else if (location) {
text = JSON.stringify(location);
}
let text2 = 'Also Wating...';
if (errorMsg2){
text2 = errorMsg2;
}
else if (location) {
text2 = JSON.stringify(location)
}
I am running Expo in managed workflow, currently using Expo version 3.23.2, but have also tested it on version 4.3.2, and have been thrown the same errors.
Upvotes: 2
Views: 8876
Reputation: 2922
Instead of task runner from expo, I use notifee for dedicated foreground service to have more granular control. Then I simply create a simple function with setTimeout to run things periodically and use expo-location to grab the location.
import * as Location from 'expo-location';
import Constants from 'expo-constants';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { startActivityAsync, ActivityAction } from 'expo-intent-launcher';
import notifee, { type Event, AndroidColor, EventType } from '@notifee/react-native';
import type { LocationObject } from 'expo-location';
export { register, unregister }
async function waitUntil(
conditionPromise: () => Promise<boolean>,
loopFunc: () => Promise<any>,
timeout: number
) {
return await new Promise(resolve => {
console.log(`Creating interval task at date: ${new Date().toISOString()}`);
const interval = setInterval(
async () => {
const condition = await conditionPromise();
if (condition) {
console.log(`Stopping interval task at date: ${new Date().toISOString()}`);
resolve('stopping');
clearInterval(interval);
return;
} else {
return loopFunc();
}
},
timeout
);
});
}
async function register(func: (locations: LocationObject[]) => Promise<void>) {
// Request permissions (required for iOS)
await notifee.requestPermission()
// Create a channel (required for Android)
const channelId = await notifee.createChannel({
id: 'default',
name: 'Default Channel',
});
notifee.registerForegroundService((notification) => {
console.log('register foreground', notification);
return new Promise(() => {
waitUntil(async () => {
const value = await AsyncStorage.getItem('@Notification_id');
//console.log(`AsyncStorage @Notification_id: ${value}`);
return !value;
}, async () => {
let locationObject;
try {
locationObject = await Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.Highest,
});
//console.log(`Got background fetch call at date: ${new Date().toISOString()}`);
const prevLocations = JSON.parse((await AsyncStorage.getItem('@Locations')) || '[]') as LocationObject[];
const locations = [...prevLocations, locationObject]
await func(locations); // callback to application layer
// cleanup
await AsyncStorage.removeItem('@Locations');
// update our live status of notification to indicate it is working
const notificationId = await AsyncStorage.getItem('@Notification_id');
if (notificationId) {
await notifee.displayNotification({
id: notificationId,
title: notification.title,
body: notification.body,
android: {
...notification.android,
color: AndroidColor.NAVY,
ongoing: true,
progress: {
indeterminate: true,
},
pressAction: {
id: 'stop',
},
},
});
}
} catch (e) {
//console.log('Inside try catch', e);
if (e instanceof Error) {
const notificationId = await AsyncStorage.getItem('@Notification_id');
// update our foreground notification to indicate error
if (notificationId) {
await notifee.displayNotification({
id: notificationId,
title: notification.title,
body: e.message,
android: {
...notification.android,
color: AndroidColor.OLIVE,
ongoing: true,
progress: undefined,
pressAction: {
id: 'error',
},
}
});
}
if (/network request failed/i.test(e.message)) {
// save
await AsyncStorage.setItem('@Locations', JSON.stringify(locations));
}
}
}
}, 30000).catch(console.log)
const eventCallback = async ({ type, detail }: Event) => {
if (type === EventType.ACTION_PRESS || type === EventType.PRESS) {
if (detail?.pressAction?.id === 'stop') {
await notifee.stopForegroundService();
await AsyncStorage.removeItem('@Notification_id');
} else if (detail?.pressAction?.id === 'error') {
// Go to location sources settings page, regardless of error
await startActivityAsync(ActivityAction.LOCATION_SOURCE_SETTINGS);
}
}
}
notifee.onForegroundEvent(async ({ type, detail }) => {
//console.log(`onForegroundEvent`, type, JSON.stringify(detail, null, 4));
await eventCallback({ type, detail }).catch(console.log);
});
notifee.onBackgroundEvent(async ({ type, detail }) => {
//console.log(`onBackgroundEvent`, type, JSON.stringify(detail, null, 4));
await eventCallback({ type, detail });
});
});
});
const notificationId = await notifee.displayNotification({
title: 'App',
body: 'Tracking your location in the background. Click notification to disable.',
android: {
channelId,
asForegroundService: true,
color: AndroidColor.NAVY,
ongoing: true,
colorized: true,
pressAction: {
id: 'stop',
},
},
});
await AsyncStorage.setItem('@Notification_id', notificationId)
console.log(`Registered notification at date: ${new Date().toISOString()}`);
return notificationId;
}
async function unregister(notificationId?: string) {
await notifee.stopForegroundService();
notificationId && await notifee.cancelNotification(notificationId);
await AsyncStorage.removeItem('@Notification_id');
console.log(`Unregister notification at date: ${new Date().toISOString()}`);
}
Upvotes: 0
Reputation: 4992
While using expo-location
, you have to keep these points in mind :
Location.requestBackgroundPermissionsAsync()
Asks the user to grant permissions for location while the app is in the background. On Android 11 or higher: this method will open the system settings page - before that happens you should explain to the user why your application needs background location permission.
Foreground permissions should be granted before asking for the background permissions (your app can't obtain background permission without foreground permission).
Refer to the docs here
Note : This has been tested on SDK 41 and expo-location
's version 12.2.1
Working Example for Background Location here
How I've implemented it
import { useState, useEffect } from "react";
import { Text, View, StyleSheet } from "react-native";
import * as Location from "expo-location";
export default function App() {
const [location, setLocation] = useState(null);
const [errorMsg, setErrorMsg] = useState(null);
useEffect(() => {
(async () => {
let { status } = await Location.requestForegroundPermissionsAsync();
if (status !== "granted") {
setErrorMsg("Permission to access location was denied");
return;
}
let location = await Location.getCurrentPositionAsync({});
setLocation(location);
let backPerm = await Location.requestBackgroundPermissionsAsync();
console.log(backPerm);
})();
}, []);
let text = "Waiting..";
if (errorMsg) {
text = errorMsg;
} else if (location) {
text = JSON.stringify(location);
}
return (
<View style={styles.container}>
<Text style={styles.paragraph}>{text}</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
paddingTop: 50,
backgroundColor: "#ecf0f1",
padding: 8,
},
paragraph: {
margin: 24,
fontSize: 18,
fontWeight: "bold",
textAlign: "center",
},
});
First, it asks me the foreground Permission, I tapped on While Using the app
Then, for background location permission it takes me to the device settings page to allow location access in the background
If by any chance this doesn't work as expected, try these steps
Expo Go
app.Expo go
appUpvotes: 7