Request a device permission in context and keep the app useful when the user says no.
Why it matters
Cameras, location, contacts, notifications and the photo library make mobile apps powerful, and they are also private. Both iOS and Android make apps ask permission before using them, and users can say no, or say yes and change their mind later in Settings. An app that crashes, nags or dead-ends when permission is denied gets uninstalled and earns bad reviews. Good apps ask at the right moment, explain why, and keep working without the permission.
How permissions work
- You declare what you might use. On iOS each permission needs a usage description string (for example
NSCameraUsageDescription) shown in the system dialog; on Android permissions are listed in the manifest. With Expo you set these inapp.json, usually through a library's config plugin. - At runtime you check the current status and request it. The system shows its own dialog; you cannot change its wording, but you control when it appears.
- The result can be granted, denied, or (on iOS for photos and location) limited or approximate. If the user denies and the system will not ask again (
canAskAgainis false), only the Settings app can change it. - Ask in context: request the camera when the user taps "Scan receipt", not on first launch. Many teams show a short explanation screen first ("We use the camera to scan receipts. Nothing is uploaded without your OK.") and then trigger the system prompt.
Worked example: camera with a fallback
expo-camera gives you a hook that returns the current permission and a function to request it:
import { CameraView, useCameraPermissions } from "expo-camera";
import { Linking, Pressable, Text, View } from "react-native";
export default function ScanScreen({ onPickFromLibrary }) {
const [permission, requestPermission] = useCameraPermissions();
if (!permission) return null; // still loading the status
if (!permission.granted) {
return (
<View>
<Text>We use the camera to scan receipts. Photos stay on your phone.</Text>
{permission.canAskAgain ? (
<Pressable onPress={requestPermission}><Text>Allow camera</Text></Pressable>
) : (
<Pressable onPress={() => Linking.openSettings()}><Text>Open Settings</Text></Pressable>
)}
<Pressable onPress={onPickFromLibrary}><Text>Choose a photo instead</Text></Pressable>
</View>
);
}
return <CameraView style={{ flex: 1 }} facing="back" />;
}
And the matching declaration in app.json:
{
"expo": {
"plugins": [
["expo-camera", { "cameraPermission": "Allow $(PRODUCT_NAME) to use the camera to scan receipts." }]
]
}
}
The fallback is the important part. Denied users can still pick a photo from the library (which, with the system photo picker, often needs no permission at all) or type the details by hand. The feature degrades; the app does not break.
In Expo Go the permission prompt names Expo Go rather than your app, because Expo Go is the app on the phone. In your own build it names your app and uses your description.
Other device APIs, same pattern
- Location (
expo-location): ask for "while using the app" first; background location needs a strong reason and extra store review. - Notifications (
expo-notifications): ask after the user has seen value, for example after they create their first reminder. - Photos (
expo-image-picker): the picker lets users choose specific photos, often without full library access. - Haptics, sharing, clipboard: usually no permission needed.
Common mistakes
- Asking for everything at first launch. Users deny permissions they do not understand.
- No usage description on iOS. Store review rejects it, and the app can crash when it requests the permission.
- Dead ends after denial. Always offer a fallback or a way to Settings.
- Assuming granted stays granted. Users can revoke permissions at any time; check status each time the feature opens.
- Collecting more than you need. Approximate location is often enough; privacy labels in the stores must list what you collect.
Quick recap
- Declare permissions (usage strings, manifest entries) and request them at runtime, in context.
- Handle granted, denied and "cannot ask again"; offer
Linking.openSettings()and a fallback path. - The same pattern covers camera, location, notifications and photos.
Try it
The Do tab has you add a camera feature to your app with a proper explanation, a denial fallback and a Settings link, then test all three paths on your phone.