Modern mobile apps require explicit user permission to access sensitive features like camera, location, and notifications. This guide shows you how to properly request and handle permissions across platforms.
All Expo permission APIs follow a similar pattern:
import * as Camera from 'expo-camera';async function requestCameraPermission() { // Check current status const { status: existingStatus } = await Camera.getCameraPermissionsAsync(); let finalStatus = existingStatus; // Request if not already granted if (existingStatus !== 'granted') { const { status } = await Camera.requestCameraPermissionsAsync(); finalStatus = status; } // Handle the result if (finalStatus !== 'granted') { alert('Camera permission is required to take photos'); return false; } return true;}
import * as Camera from 'expo-camera';import { usePermission } from '../hooks/usePermission';export function CameraButton() { const camera = usePermission(Camera.useCameraPermissions()); const handlePress = async () => { if (!camera.granted) { const result = await camera.request(); if (!result.granted) { alert('Camera permission is required'); return; } } // Open camera openCamera(); }; return ( <Button onPress={handlePress} title={camera.granted ? 'Take Photo' : 'Allow Camera Access'} /> );}
import * as Camera from 'expo-camera';async function takePicture() { const { status } = await Camera.requestCameraPermissionsAsync(); if (status !== 'granted') { alert('Sorry, we need camera permissions to take photos!'); return; } // Use camera const result = await ImagePicker.launchCameraAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images, quality: 1, });}
import * as Location from 'expo-location';async function getLocation() { // Request foreground permission const { status } = await Location.requestForegroundPermissionsAsync(); if (status !== 'granted') { alert('Permission to access location was denied'); return; } const location = await Location.getCurrentPositionAsync({}); console.log(location);}// For background location (iOS/Android 10+)async function requestBackgroundLocation() { const { status: foregroundStatus } = await Location.requestForegroundPermissionsAsync(); if (foregroundStatus === 'granted') { const { status: backgroundStatus } = await Location.requestBackgroundPermissionsAsync(); return backgroundStatus === 'granted'; } return false;}
import * as MediaLibrary from 'expo-media-library';async function savePhoto(uri: string) { const { status } = await MediaLibrary.requestPermissionsAsync(); if (status !== 'granted') { alert('Permission to access media library is required'); return; } await MediaLibrary.saveToLibraryAsync(uri); alert('Photo saved!');}async function getPhotos() { const { status } = await MediaLibrary.requestPermissionsAsync(); if (status === 'granted') { const { assets } = await MediaLibrary.getAssetsAsync({ first: 20, mediaType: 'photo', }); return assets; }}
import * as Notifications from 'expo-notifications';async function registerForPushNotifications() { const { status: existingStatus } = await Notifications.getPermissionsAsync(); let finalStatus = existingStatus; if (existingStatus !== 'granted') { const { status } = await Notifications.requestPermissionsAsync(); finalStatus = status; } if (finalStatus !== 'granted') { alert('Failed to get push notification permissions'); return; } const token = await Notifications.getExpoPushTokenAsync(); return token.data;}
import * as Contacts from 'expo-contacts';async function getContacts() { const { status } = await Contacts.requestPermissionsAsync(); if (status !== 'granted') { alert('Permission to access contacts was denied'); return; } const { data } = await Contacts.getContactsAsync({ fields: [Contacts.Fields.PhoneNumbers, Contacts.Fields.Emails], }); return data;}
type PermissionStatus = | 'undetermined' // Not yet asked | 'denied' // User denied | 'granted' // User granted | 'restricted'; // OS restriction (iOS)
Permission descriptions required:Add usage descriptions in app.json:
app.json
{ "expo": { "ios": { "infoPlist": { "NSCameraUsageDescription": "This app uses the camera to take photos for your profile.", "NSMicrophoneUsageDescription": "This app uses the microphone to record videos.", "NSLocationWhenInUseUsageDescription": "This app uses your location to show nearby places.", "NSLocationAlwaysAndWhenInUseUsageDescription": "This app uses your location to track your runs.", "NSPhotoLibraryUsageDescription": "This app accesses your photos to let you share them.", "NSPhotoLibraryAddUsageDescription": "This app saves photos to your library.", "NSCalendarsUsageDescription": "This app needs access to your calendar.", "NSRemindersUsageDescription": "This app needs access to your reminders.", "NSContactsUsageDescription": "This app needs access to your contacts." } } }}
Your app will crash if you request a permission without the corresponding usage description.
Permission timing:
iOS only allows asking twice
After second denial, must use Settings
“Don’t Allow” vs “Ask Next Time”
Location precision (iOS 14+):
const { status } = await Location.requestForegroundPermissionsAsync();// Check if precise location is enabledconst accuracy = await Location.getProviderStatusAsync();console.log(accuracy.locationServicesEnabled);
function PermissionPrompt() { return ( <View> <Text>We need access to your camera to:</Text> <Text>• Take profile photos</Text> <Text>• Scan QR codes</Text> <Text>• Upload documents</Text> <Button onPress={requestPermission} title="Allow Camera Access" /> </View> );}