Skip to main content

Overview

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.

Permission Types

Common permissions in Expo apps:
  • Camera: Take photos and videos
  • Microphone: Record audio
  • Location: Access device location
  • Notifications: Send push notifications
  • Photos/Media Library: Access saved photos
  • Contacts: Access user contacts
  • Calendar: Access calendar events
  • Reminders: Access iOS reminders

Installation

Most Expo SDK packages include their own permission methods:
npx expo install expo-camera expo-location expo-notifications expo-media-library

Requesting Permissions

Basic pattern

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;
}

Permission hook

Create a reusable hook for any permission:
hooks/usePermission.ts
import { useState, useEffect } from 'react';
import { PermissionResponse } from 'expo-modules-core';

type PermissionHook = {
  getAsync: () => Promise<PermissionResponse>;
  requestAsync: () => Promise<PermissionResponse>;
};

export function usePermission(permission: PermissionHook) {
  const [status, setStatus] = useState<PermissionResponse | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    (async () => {
      const result = await permission.getAsync();
      setStatus(result);
      setLoading(false);
    })();
  }, []);

  const request = async () => {
    setLoading(true);
    const result = await permission.requestAsync();
    setStatus(result);
    setLoading(false);
    return result;
  };

  return {
    status: status?.status,
    granted: status?.status === 'granted',
    canAskAgain: status?.canAskAgain,
    loading,
    request,
  };
}
Usage:
components/CameraButton.tsx
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'}
    />
  );
}

Common Permissions

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,
  });
}

Permission States

Understanding permission status

type PermissionStatus = 
  | 'undetermined'  // Not yet asked
  | 'denied'        // User denied
  | 'granted'       // User granted
  | 'restricted';   // OS restriction (iOS)

Handling different states

async function handlePermission() {
  const { status, canAskAgain } = await Location.getForegroundPermissionsAsync();

  switch (status) {
    case 'granted':
      // Permission granted, proceed
      return true;
      
    case 'undetermined':
      // First time, ask for permission
      const result = await Location.requestForegroundPermissionsAsync();
      return result.status === 'granted';
      
    case 'denied':
      if (canAskAgain) {
        // Can ask again
        const result = await Location.requestForegroundPermissionsAsync();
        return result.status === 'granted';
      } else {
        // User selected "Don't ask again", direct to settings
        Alert.alert(
          'Permission Required',
          'Please enable location access in Settings',
          [
            { text: 'Cancel', style: 'cancel' },
            { text: 'Open Settings', onPress: () => Linking.openSettings() },
          ]
        );
        return false;
      }
      
    case 'restricted':
      // OS restriction (parental controls, etc.)
      Alert.alert(
        'Access Restricted',
        'Location access is restricted on this device'
      );
      return false;
  }
}

Platform Differences

iOS-specific considerations

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 enabled
const accuracy = await Location.getProviderStatusAsync();
console.log(accuracy.locationServicesEnabled);

Best Practices

1. Request permissions in context

// Bad: Request all permissions upfront
useEffect(() => {
  requestAllPermissions();
}, []);

// Good: Request when needed
function handleTakePhoto() {
  requestCameraPermission().then(granted => {
    if (granted) openCamera();
  });
}

2. Explain why you need permission

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>
  );
}

3. Handle all permission states

function LocationPermissionScreen() {
  const { status, request } = usePermission(Location.useForegroundPermissions());

  if (status === 'granted') {
    return <MapView />;
  }

  if (status === 'denied') {
    return (
      <View>
        <Text>Location access was denied</Text>
        <Button onPress={() => Linking.openSettings()} title="Open Settings" />
      </View>
    );
  }

  return (
    <View>
      <Text>We need your location to show nearby places</Text>
      <Button onPress={request} title="Allow Location Access" />
    </View>
  );
}

4. Graceful degradation

async function shareWithContacts() {
  const { status } = await Contacts.requestPermissionsAsync();
  
  if (status === 'granted') {
    // Show contact picker
    const contacts = await Contacts.getContactsAsync();
    return contacts;
  } else {
    // Fallback: manual phone number entry
    return showPhoneNumberInput();
  }
}

5. Check before requesting

async function ensurePermission() {
  // Check current status first
  const { status } = await Camera.getCameraPermissionsAsync();
  
  if (status === 'granted') {
    return true;
  }
  
  // Only request if needed
  const { status: newStatus } = await Camera.requestCameraPermissionsAsync();
  return newStatus === 'granted';
}

Debugging Permissions

Log permission status

async function debugPermissions() {
  const camera = await Camera.getCameraPermissionsAsync();
  const location = await Location.getForegroundPermissionsAsync();
  const notifications = await Notifications.getPermissionsAsync();
  
  console.log('Permissions:', {
    camera: camera.status,
    location: location.status,
    notifications: notifications.status,
  });
}

Reset permissions (development)

# Simulator
xcrun simctl privacy booted reset all com.company.myapp

# Or: Settings > General > Reset > Reset Location & Privacy

Troubleshooting

iOS: Ensure you’ve added the usage description to app.json:
"NSCameraUsageDescription": "This app needs camera access"
Rebuild after adding: npx expo prebuild --clean
  • User previously denied with “Don’t ask again” (Android)
  • User denied twice already (iOS)
  • Direct user to Settings with Linking.openSettings()
  • Check for additional permissions (e.g., location + background location)
  • Verify device settings (Location Services enabled)
  • Check for OS restrictions (parental controls)
  • Verify you’re calling requestAsync() not just getAsync()
  • Check permission isn’t already granted or permanently denied
  • Ensure you’ve declared the permission in app.json (Android)
Always rebuild your app after modifying permissions in app.json: npx expo prebuild --clean

Permission Reference

Quick reference for common permission methods:
FeaturePackageGet PermissionRequest Permission
Cameraexpo-cameragetCameraPermissionsAsync()requestCameraPermissionsAsync()
Locationexpo-locationgetForegroundPermissionsAsync()requestForegroundPermissionsAsync()
Media Libraryexpo-media-librarygetPermissionsAsync()requestPermissionsAsync()
Notificationsexpo-notificationsgetPermissionsAsync()requestPermissionsAsync()
Audioexpo-avgetPermissionsAsync()requestPermissionsAsync()
Contactsexpo-contactsgetPermissionsAsync()requestPermissionsAsync()
Calendarexpo-calendargetCalendarPermissionsAsync()requestCalendarPermissionsAsync()