Section 4 · Data and the Device
Permissions: Asking at the Right Moment
17 min3 min readCode playground
Lesson at a glance
Ask for location and camera access at the moment it makes sense, handle every permission state including permanent denial, and write the usage descriptions iOS and app review require.
- 1The states a permission can be in
- 2Ask in context, once you’ve explained
- 3Usage descriptions and config plugins
- 4Foreground first, background only if you must
No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.
Trailhead’s best feature is live tracking: start a hike, and the app records your route. It needs the phone’s location, and the phone won’t give it without asking the user. That request is the most fragile moment in the app. Ask badly and you lose the feature for that user, sometimes for good.
The states a permission can be in
Every Expo module that needs a permission (location, camera, media library, notifications) returns the same response shape:
{
status: 'undetermined' | 'granted' | 'denied',
granted: boolean,
canAskAgain: boolean,
expires: 'never' | number,
}- undetermined: you’ve never asked. A request will show the system prompt.
- granted: go ahead. On recent iOS and Android versions, users may grant access only once (for this session) or only approximate location, so be ready to ask again another day and to receive less precise positions.
- denied with
canAskAgain: true: Android after a single denial. You can ask once more, ideally after explaining better. - denied with
canAskAgain: false: the system won’t show the prompt again. On iOS that’s the state right after the first denial; on Android, after the user denies twice. Only the Settings app can change it now.
Ask in context, once you’ve explained
The worst pattern is the one many apps ship: a cascade of system prompts at first launch, before the user knows what the app does. People deny what they don’t understand, and on iOS that first “Don’t Allow” is permanent unless they dig into Settings.
Trailhead asks for location when the user taps Start tracking, the first moment the request is obviously connected to something they want. Before the system prompt, it shows its own short explanation: “Trailhead records your route while you hike. Your location stays on this phone unless you sync.” Then a Continue button triggers the real request. Your own screen can be dismissed without consequence; the system prompt can’t.
The same rule applies to every permission in the app. Ask for the camera when the user taps Add photo, and for notifications when they turn on reminders, not before. Each request then arrives with its reason already visible on screen, and acceptance rates go up noticeably.
import * as Location from 'expo-location';
import { useFocusEffect } from 'expo-router';
import { useCallback, useState } from 'react';
import { Linking } from 'react-native';
export function useLocationPermission() {
const [permission, setPermission] = useState<Location.LocationPermissionResponse | null>(null);
// Re-check whenever the screen regains focus: the user may have
// changed the setting in the Settings app while Trailhead was in the background.
useFocusEffect(
useCallback(() => {
Location.getForegroundPermissionsAsync().then(setPermission);
}, []),
);
const request = useCallback(async () => {
const result = await Location.requestForegroundPermissionsAsync();
setPermission(result);
return result.granted;
}, []);
return { permission, request, openSettings: () => Linking.openSettings() };
}The tracking screen then reads permission and chooses what to render: a spinner while it’s null, the explanation and Continue button while asking is still possible, a “Location is off for Trailhead” message with an Open Settings button once canAskAgain is false, and the live map once it’s granted. Turning the response into one of those four actions is a small pure function, and it’s this lesson’s exercise.
Usage descriptions and config plugins
iOS shows a sentence from your app in every permission prompt, and the App Store rejects apps whose descriptions are missing or vague (“This app needs your location”). Say what the feature does with the data. You set these strings through each module’s config plugin in app.json:
{
"expo": {
"plugins": [
[
"expo-location",
{ "locationWhenInUsePermission": "Trailhead records your route on the map while you track a hike." }
],
[
"expo-image-picker",
{
"photosPermission": "Trailhead lets you attach photos from your library to a hike.",
"cameraPermission": "Trailhead lets you take trail photos to attach to a hike."
}
]
]
}
}Config plugins write native configuration (the iOS Info.plist, the Android manifest) when the app is built. That makes them native changes: Fast Refresh won’t apply them, so rebuild your development build after editing them. Expo Go shows its own generic strings, which is one more reason Trailhead moves to a development build now.
Foreground first, background only if you must
Tracking a hike with the screen off needs background location, a separate, more sensitive permission with extra platform requirements and closer review on both stores. Start with foreground (“while using the app”) access, which covers tracking while Trailhead is open. Add background access only when users ask for it, with a clear explanation of why, and expect the review to ask you the same question.
Next you’ll use these permissions for real: the hiker’s position, trail photos, and a reminder notification.
Key takeaways
- Permission responses have a
statusof undetermined, granted or denied, pluscanAskAgain, which tells you whether a system prompt can still appear. - Ask when the user starts the feature that needs it, after a short explanation in your own UI, never as a wall of prompts at launch.
- Once
canAskAgainis false, only the system Settings app can change the answer, so offer a button that callsLinking.openSettings(). - iOS requires a usage description for each permission; set them through config plugin options in app.json and rebuild.
- Re-check permissions when a screen regains focus, because users change them in Settings while your app is in the background.