Skip to main content

Module 8: Native Features and Device APIs

Permissions

Requesting access to device capabilities the right way

🎯 Learning Objectives

  • Understand how mobile permissions work on iOS and Android
  • Know when permissions are required and when they're not
  • Request permissions using Expo's unified API
  • Handle all permission states: granted, denied, and undetermined
  • Implement best practices for permission requests
  • Guide users to settings when permissions are denied
  • Configure permissions in app.json for store submission

Why Permissions Matter

Mobile apps run in a sandboxed environment with limited access to device capabilities. When your app needs to use sensitive features like the camera, location, or contacts, it must explicitly request permission from the user. This protects user privacy and gives them control over their data.

πŸ”’ What Permissions Protect

  • Camera & Microphone: Prevents unauthorized recording
  • Location: Protects physical whereabouts
  • Contacts: Guards personal relationships
  • Photos: Secures private memories
  • Notifications: Controls interruptions
  • Calendar: Protects schedule and events
The Permission Request Flow Your App πŸ“± Needs camera Request "MyApp" wants access πŸ“· to your camera Deny Allow Denied: Feature unavailable Granted Device API βœ“ Access granted πŸ“Έ

πŸ“– The Permission Contract

When users grant permission, they're trusting your app with access to sensitive capabilities. Honor that trust by only requesting permissions you truly need, using them only for their stated purpose, and being transparent about why you need them.

Types of Permissions

Not all device features require explicit permission. Understanding what needs permissionβ€”and what doesn'tβ€”helps you build apps that request only what's necessary.

Features That Require Permission

Feature Expo Package Notes
πŸ“· Camera expo-camera Photo and video capture
πŸ“ Location expo-location Foreground and background
🎀 Microphone expo-av Audio recording
πŸ–ΌοΈ Media Library expo-media-library Read/write photos and videos
πŸ‘€ Contacts expo-contacts Access address book
πŸ“… Calendar expo-calendar Read/write events
πŸ”” Notifications expo-notifications Push and local notifications
πŸƒ Motion/Fitness expo-sensors Pedometer, accelerometer

Features That Don't Require Permission

βœ… No Permission Needed

  • Network access: HTTP requests, WebSockets
  • Local storage: AsyncStorage, SecureStore, MMKV
  • Vibration: Haptic feedback
  • Device info: Model, OS version (basic info)
  • Clipboard: Read/write (mostly)
  • Sharing: Share sheet for content
  • Opening URLs: Linking to websites, apps
  • Flashlight: Camera flash as flashlight

Platform Differences

iOS and Android handle permissions differently in some cases:

Aspect iOS Android
When asked Only when you request At install (old) or runtime (new)
Denial behavior Can only ask once Can ask multiple times
Location options While Using / Always / Once While Using / Always / Deny
Photo access Limited selection option (iOS 14+) All or nothing

Permission States

Every permission exists in one of several states. Understanding these states is crucial for handling permissions correctly.

stateDiagram-v2
    [*] --> Undetermined: App installed
    Undetermined --> Granted: User allows
    Undetermined --> Denied: User denies
    
    Granted --> Denied: User revokes in Settings
    Denied --> Granted: User enables in Settings
    
    note right of Undetermined
        Never asked yet.
        Safe to request.
    end note
    
    note right of Granted
        Permission active.
        Feature available.
    end note
    
    note right of Denied
        User said no.
        Must go to Settings.
    end note
                

Permission Status Values

import { PermissionStatus } from 'expo-modules-core';

// The possible states
PermissionStatus.UNDETERMINED  // Never asked
PermissionStatus.GRANTED       // User allowed
PermissionStatus.DENIED        // User denied

// Some permissions have additional states
// For location on iOS:
// - 'granted' with accuracy: 'full' or 'reduced'
// - Background location has separate permission

⚠️ iOS "Ask Once" Rule

On iOS, you can only show the system permission dialog once. If the user denies, the dialog won't appear againβ€”you must direct them to Settings. This makes the timing of your permission request critical.

Expo Permissions API

Each Expo package that needs permissions includes its own permission hooks and methods. The pattern is consistent across all packages.

The Permission Pattern

// Every Expo package with permissions follows this pattern:
import * as SomeFeature from 'expo-some-feature';

// Check current permission status (doesn't prompt user)
const [status, requestPermission] = SomeFeature.usePermissions();

// Or without hooks:
const { status } = await SomeFeature.getPermissionsAsync();

// Request permission (prompts user if undetermined)
const { status: newStatus } = await SomeFeature.requestPermissionsAsync();

// The status object includes:
// - status: 'granted' | 'denied' | 'undetermined'
// - granted: boolean
// - canAskAgain: boolean (Android)
// - expires: 'never' | number (some permissions)

Common Permission Packages

// Camera
import * as Camera from 'expo-camera';
const [permission, requestPermission] = Camera.useCameraPermissions();

// Location
import * as Location from 'expo-location';
const [permission, requestPermission] = Location.useForegroundPermissions();
const [bgPermission, requestBgPermission] = Location.useBackgroundPermissions();

// Media Library
import * as MediaLibrary from 'expo-media-library';
const [permission, requestPermission] = MediaLibrary.usePermissions();

// Contacts
import * as Contacts from 'expo-contacts';
const { status } = await Contacts.getPermissionsAsync();
const { status } = await Contacts.requestPermissionsAsync();

// Notifications
import * as Notifications from 'expo-notifications';
const { status } = await Notifications.getPermissionsAsync();
const { status } = await Notifications.requestPermissionsAsync();

// Audio Recording
import { Audio } from 'expo-av';
const { status } = await Audio.getPermissionsAsync();
const { status } = await Audio.requestPermissionsAsync();

Using Permission Hooks

import { useState, useEffect } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import * as Camera from 'expo-camera';

export default function CameraScreen() {
  // The hook returns [PermissionResponse, requestPermission]
  const [permission, requestPermission] = Camera.useCameraPermissions();

  // Handle loading state
  if (!permission) {
    return <View />; // Loading
  }

  // Handle denied state
  if (!permission.granted) {
    return (
      <View style={styles.container}>
        <Text style={styles.message}>
          We need your permission to show the camera
        </Text>
        <Button onPress={requestPermission} title="Grant Permission" />
      </View>
    );
  }

  // Permission granted - show camera
  return (
    <View style={styles.container}>
      <Camera.CameraView style={styles.camera} />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
  },
  message: {
    textAlign: 'center',
    paddingBottom: 10,
  },
  camera: {
    flex: 1,
  },
});

Requesting Permissions

How and when you request permissions significantly impacts user experience and grant rates.

The Wrong Way: Ask Immediately

// ❌ Don't do this - asking on app launch
function App() {
  useEffect(() => {
    // User just opened the app, has no context
    Camera.requestPermissionsAsync();
    Location.requestForegroundPermissionsAsync();
    Notifications.requestPermissionsAsync();
  }, []);
  
  return <MainApp />;
}

❌ Why This Is Bad

  • User doesn't understand why you need the permission
  • Feels invasive and suspicious
  • Higher denial rates
  • On iOS, you've wasted your one chance to ask

The Right Way: Ask in Context

// βœ… Ask when the user tries to use the feature
function ProfileScreen() {
  const [photo, setPhoto] = useState<string | null>(null);
  const [permission, requestPermission] = Camera.useCameraPermissions();

  const handleTakePhoto = async () => {
    // Ask when user taps "Take Photo"
    if (!permission?.granted) {
      const result = await requestPermission();
      if (!result.granted) {
        // Handle denial gracefully
        return;
      }
    }
    
    // Now open camera
    const result = await Camera.takePictureAsync();
    setPhoto(result.uri);
  };

  return (
    <View>
      <Image source={{ uri: photo }} />
      <Button title="Take Photo" onPress={handleTakePhoto} />
    </View>
  );
}

Pre-Permission Explanation

Before showing the system dialog, explain why you need the permission:

import { Alert } from 'react-native';

async function requestCameraWithExplanation() {
  const { status: existingStatus } = await Camera.getPermissionsAsync();
  
  if (existingStatus === 'granted') {
    return true;
  }
  
  // Show explanation BEFORE the system dialog
  return new Promise((resolve) => {
    Alert.alert(
      'Camera Permission',
      'We need camera access to let you take profile photos. Your photos are stored only on your device.',
      [
        {
          text: 'Not Now',
          style: 'cancel',
          onPress: () => resolve(false),
        },
        {
          text: 'Continue',
          onPress: async () => {
            const { status } = await Camera.requestPermissionsAsync();
            resolve(status === 'granted');
          },
        },
      ]
    );
  });
}
The Pre-Permission Pattern 1. User Action πŸ“Έ Taps "Take Photo" 2. Your Dialog "We need camera access to take your profile photo" [Continue] 3. System Dialog "MyApp" would like to access the camera [Don't Allow] [OK] 4. Access βœ“ Why This Works Better: βœ“ User understands the context βœ“ You explain privacy practices βœ“ Higher grant rates βœ“ User feels in control

Best Practices

βœ… Permission Best Practices

  1. Ask in context: Request when the user tries to use the feature
  2. Explain first: Show a custom dialog explaining why before the system dialog
  3. Be specific: Tell users exactly what you'll use the permission for
  4. Request minimal: Only ask for permissions you actually need
  5. Degrade gracefully: App should work (with reduced features) without permissions
  6. Don't block: Never prevent app usage until permissions are granted
  7. Provide alternatives: Offer manual input if permission is denied
  8. Respect denial: Don't repeatedly nag users who said no

Creating a Permission Helper

// utils/permissions.ts
import { Alert, Linking, Platform } from 'react-native';

interface PermissionConfig {
  title: string;
  message: string;
  checkPermission: () => Promise<{ status: string }>;
  requestPermission: () => Promise<{ status: string }>;
}

export async function requestWithExplanation(
  config: PermissionConfig
): Promise<boolean> {
  // First check current status
  const { status: existingStatus } = await config.checkPermission();
  
  if (existingStatus === 'granted') {
    return true;
  }
  
  // Show explanation dialog
  return new Promise((resolve) => {
    Alert.alert(
      config.title,
      config.message,
      [
        {
          text: 'Not Now',
          style: 'cancel',
          onPress: () => resolve(false),
        },
        {
          text: 'Continue',
          onPress: async () => {
            const { status } = await config.requestPermission();
            
            if (status === 'granted') {
              resolve(true);
            } else {
              // Offer to open settings
              showSettingsAlert(config.title);
              resolve(false);
            }
          },
        },
      ]
    );
  });
}

function showSettingsAlert(permissionName: string) {
  Alert.alert(
    'Permission Required',
    `To use this feature, please enable ${permissionName} in your device settings.`,
    [
      { text: 'Cancel', style: 'cancel' },
      { 
        text: 'Open Settings', 
        onPress: () => Linking.openSettings() 
      },
    ]
  );
}

// Usage
import * as Camera from 'expo-camera';

const granted = await requestWithExplanation({
  title: 'Camera Access',
  message: 'We need camera access to take photos for your posts. Photos are stored locally.',
  checkPermission: Camera.getCameraPermissionsAsync,
  requestPermission: Camera.requestCameraPermissionsAsync,
});

Handling Denied Permissions

When users deny permissions, your app should handle it gracefully without breaking. Here's how to provide a good experience even without full permissions.

Detecting Permanent Denial

import * as Camera from 'expo-camera';
import { Alert, Linking, Platform } from 'react-native';

async function checkCameraPermission() {
  const { status, canAskAgain } = await Camera.getCameraPermissionsAsync();
  
  if (status === 'granted') {
    return 'granted';
  }
  
  if (status === 'denied' && !canAskAgain) {
    // On Android, canAskAgain is false after "Don't ask again"
    // On iOS, it's always false after first denial
    return 'permanently-denied';
  }
  
  if (status === 'denied') {
    return 'denied';
  }
  
  return 'undetermined';
}

// Handle based on status
async function handleCameraAccess() {
  const permissionState = await checkCameraPermission();
  
  switch (permissionState) {
    case 'granted':
      // Proceed to camera
      openCamera();
      break;
      
    case 'undetermined':
      // Can still request
      const { status } = await Camera.requestCameraPermissionsAsync();
      if (status === 'granted') openCamera();
      break;
      
    case 'denied':
      // On Android, can request again
      if (Platform.OS === 'android') {
        const { status } = await Camera.requestCameraPermissionsAsync();
        if (status === 'granted') openCamera();
      } else {
        // iOS - must go to settings
        promptOpenSettings();
      }
      break;
      
    case 'permanently-denied':
      // Must go to settings on both platforms
      promptOpenSettings();
      break;
  }
}

function promptOpenSettings() {
  Alert.alert(
    'Camera Permission Required',
    'Please enable camera access in Settings to take photos.',
    [
      { text: 'Cancel', style: 'cancel' },
      { text: 'Open Settings', onPress: () => Linking.openSettings() },
    ]
  );
}

Providing Alternatives

// When camera is denied, offer to pick from library instead
import * as ImagePicker from 'expo-image-picker';

function PhotoUpload() {
  const [cameraPermission] = Camera.useCameraPermissions();
  const [libraryPermission] = ImagePicker.useMediaLibraryPermissions();

  const handleAddPhoto = async () => {
    // Try camera first
    if (cameraPermission?.granted) {
      const result = await ImagePicker.launchCameraAsync();
      if (!result.canceled) {
        setPhoto(result.assets[0].uri);
        return;
      }
    }
    
    // Fall back to library
    if (libraryPermission?.granted) {
      const result = await ImagePicker.launchImageLibraryAsync();
      if (!result.canceled) {
        setPhoto(result.assets[0].uri);
        return;
      }
    }
    
    // Neither available - show options
    Alert.alert(
      'Add Photo',
      'Choose how to add a photo',
      [
        {
          text: 'Take Photo',
          onPress: async () => {
            const granted = await requestCameraPermission();
            if (granted) {
              // Try again
              handleAddPhoto();
            }
          },
        },
        {
          text: 'Choose from Library',
          onPress: async () => {
            const granted = await requestLibraryPermission();
            if (granted) {
              handleAddPhoto();
            }
          },
        },
        { text: 'Cancel', style: 'cancel' },
      ]
    );
  };

  return (
    <Pressable onPress={handleAddPhoto}>
      <Text>Add Photo</Text>
    </Pressable>
  );
}

Complete Permission Component

import { useState, useEffect } from 'react';
import { View, Text, Pressable, StyleSheet, Linking } from 'react-native';
import * as Camera from 'expo-camera';

type PermissionState = 'loading' | 'granted' | 'undetermined' | 'denied' | 'blocked';

export function CameraPermissionGate({ children }: { children: React.ReactNode }) {
  const [permission, requestPermission] = Camera.useCameraPermissions();
  const [state, setState] = useState<PermissionState>('loading');

  useEffect(() => {
    if (!permission) {
      setState('loading');
    } else if (permission.granted) {
      setState('granted');
    } else if (permission.status === 'undetermined') {
      setState('undetermined');
    } else if (!permission.canAskAgain) {
      setState('blocked');
    } else {
      setState('denied');
    }
  }, [permission]);

  const handleRequest = async () => {
    await requestPermission();
  };

  const handleOpenSettings = () => {
    Linking.openSettings();
  };

  if (state === 'loading') {
    return (
      <View style={styles.container}>
        <Text>Loading...</Text>
      </View>
    );
  }

  if (state === 'granted') {
    return <>{children}</>;
  }

  if (state === 'blocked') {
    return (
      <View style={styles.container}>
        <Text style={styles.icon}>πŸ”’</Text>
        <Text style={styles.title}>Camera Access Blocked</Text>
        <Text style={styles.message}>
          Camera access was previously denied. Please enable it in your device settings.
        </Text>
        <Pressable style={styles.button} onPress={handleOpenSettings}>
          <Text style={styles.buttonText}>Open Settings</Text>
        </Pressable>
      </View>
    );
  }

  // undetermined or denied (can ask again)
  return (
    <View style={styles.container}>
      <Text style={styles.icon}>πŸ“·</Text>
      <Text style={styles.title}>Camera Permission Needed</Text>
      <Text style={styles.message}>
        We need access to your camera to take photos. Your photos stay on your device.
      </Text>
      <Pressable style={styles.button} onPress={handleRequest}>
        <Text style={styles.buttonText}>Enable Camera</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 40,
  },
  icon: {
    fontSize: 64,
    marginBottom: 20,
  },
  title: {
    fontSize: 20,
    fontWeight: 'bold',
    marginBottom: 12,
    textAlign: 'center',
  },
  message: {
    fontSize: 16,
    color: '#666',
    textAlign: 'center',
    marginBottom: 24,
    lineHeight: 24,
  },
  button: {
    backgroundColor: '#667eea',
    paddingHorizontal: 32,
    paddingVertical: 14,
    borderRadius: 8,
  },
  buttonText: {
    color: 'white',
    fontSize: 16,
    fontWeight: '600',
  },
});

// Usage
function CameraScreen() {
  return (
    <CameraPermissionGate>
      <Camera.CameraView style={{ flex: 1 }} />
    </CameraPermissionGate>
  );
}

App Configuration

To pass app store review, you must configure permissions properly in your app.json. Each platform requires specific descriptions explaining why you need each permission.

iOS Permission Descriptions

iOS requires a usage description string for every permission. These appear in the system dialog and must clearly explain why you need the permission.

// app.json
{
  "expo": {
    "ios": {
      "infoPlist": {
        "NSCameraUsageDescription": "This app uses the camera to take profile photos and scan documents.",
        "NSMicrophoneUsageDescription": "This app uses the microphone to record voice messages.",
        "NSPhotoLibraryUsageDescription": "This app accesses your photos to upload images to your posts.",
        "NSPhotoLibraryAddUsageDescription": "This app saves photos you take to your photo library.",
        "NSLocationWhenInUseUsageDescription": "This app uses your location to show nearby places.",
        "NSLocationAlwaysAndWhenInUseUsageDescription": "This app uses your location in the background to track your runs.",
        "NSContactsUsageDescription": "This app accesses your contacts to help you invite friends.",
        "NSCalendarsUsageDescription": "This app accesses your calendar to schedule events.",
        "NSFaceIDUsageDescription": "This app uses Face ID to securely unlock your account.",
        "NSMotionUsageDescription": "This app uses motion sensors to count your steps."
      }
    }
  }
}

⚠️ App Store Rejection Warning

Apple rejects apps with:

  • Vague descriptions like "This app needs camera access"
  • Missing descriptions for used permissions
  • Permissions requested but never used

Be specific: "This app uses the camera to take profile photos" is much better than "This app needs camera."

Android Permissions

Android permissions are added to AndroidManifest.xml. Expo handles most of this automatically based on the packages you use, but you can customize them:

// app.json
{
  "expo": {
    "android": {
      "permissions": [
        "CAMERA",
        "RECORD_AUDIO",
        "READ_EXTERNAL_STORAGE",
        "WRITE_EXTERNAL_STORAGE",
        "ACCESS_FINE_LOCATION",
        "ACCESS_COARSE_LOCATION",
        "ACCESS_BACKGROUND_LOCATION",
        "READ_CONTACTS",
        "WRITE_CONTACTS",
        "READ_CALENDAR",
        "WRITE_CALENDAR",
        "VIBRATE"
      ],
      // Block permissions you don't want included
      "blockedPermissions": [
        "android.permission.RECORD_AUDIO"
      ]
    }
  }
}

Complete Example Configuration

// app.json for a photo-sharing app
{
  "expo": {
    "name": "PhotoShare",
    "slug": "photoshare",
    "version": "1.0.0",
    "plugins": [
      [
        "expo-camera",
        {
          "cameraPermission": "PhotoShare needs camera access to take photos for your posts."
        }
      ],
      [
        "expo-media-library",
        {
          "photosPermission": "PhotoShare needs access to your photos to upload images.",
          "savePhotosPermission": "PhotoShare saves photos you take to your gallery.",
          "isAccessMediaLocationEnabled": true
        }
      ],
      [
        "expo-location",
        {
          "locationAlwaysAndWhenInUsePermission": "PhotoShare uses your location to tag photos with where they were taken."
        }
      ],
      [
        "expo-notifications",
        {
          "icon": "./assets/notification-icon.png",
          "color": "#667eea"
        }
      ]
    ],
    "ios": {
      "supportsTablet": true,
      "bundleIdentifier": "com.example.photoshare",
      "infoPlist": {
        "NSCameraUsageDescription": "PhotoShare needs camera access to take photos for your posts.",
        "NSPhotoLibraryUsageDescription": "PhotoShare needs access to your photos to upload images.",
        "NSPhotoLibraryAddUsageDescription": "PhotoShare saves photos you take to your gallery.",
        "NSLocationWhenInUseUsageDescription": "PhotoShare uses your location to tag photos with where they were taken."
      }
    },
    "android": {
      "package": "com.example.photoshare",
      "adaptiveIcon": {
        "foregroundImage": "./assets/adaptive-icon.png",
        "backgroundColor": "#667eea"
      },
      "permissions": [
        "CAMERA",
        "READ_EXTERNAL_STORAGE",
        "WRITE_EXTERNAL_STORAGE",
        "ACCESS_FINE_LOCATION",
        "ACCESS_COARSE_LOCATION"
      ]
    }
  }
}

Plugin Configuration Reference

πŸ“¦ Common Plugin Configurations

Each Expo package with permissions can be configured via plugins:

// expo-camera
["expo-camera", {
  "cameraPermission": "Custom camera message",
  "microphonePermission": "Custom microphone message",
  "recordAudioAndroid": true
}]

// expo-location
["expo-location", {
  "locationAlwaysAndWhenInUsePermission": "Background location message",
  "locationAlwaysPermission": "Always location message",
  "locationWhenInUsePermission": "Foreground location message"
}]

// expo-media-library
["expo-media-library", {
  "photosPermission": "Photo access message",
  "savePhotosPermission": "Save photos message",
  "isAccessMediaLocationEnabled": true
}]

// expo-contacts
["expo-contacts", {
  "contactsPermission": "Contacts access message"
}]

// expo-calendar
["expo-calendar", {
  "calendarPermission": "Calendar access message"
}]

Hands-On Exercises

Exercise 1: Camera Permission Flow

Build a complete camera permission flow with pre-permission explanation.

Requirements:

  • Show a custom explanation dialog before requesting
  • Handle all permission states (granted, denied, blocked)
  • Provide a button to open Settings when blocked
  • Only show camera when permission is granted
Show Hint

Use Camera.useCameraPermissions() hook. Check permission.canAskAgain to detect if permission is blocked. Use Alert.alert() for the pre-permission dialog and Linking.openSettings() to open device settings.

Show Solution
import { useState } from 'react';
import { View, Text, Pressable, Alert, Linking, StyleSheet } from 'react-native';
import * as Camera from 'expo-camera';

export default function CameraPermissionDemo() {
  const [permission, requestPermission] = Camera.useCameraPermissions();
  const [showCamera, setShowCamera] = useState(false);

  const handleRequestCamera = async () => {
    // Already granted
    if (permission?.granted) {
      setShowCamera(true);
      return;
    }

    // Blocked - go to settings
    if (permission?.status === 'denied' && !permission?.canAskAgain) {
      Alert.alert(
        'Permission Blocked',
        'Camera access is blocked. Please enable it in Settings.',
        [
          { text: 'Cancel', style: 'cancel' },
          { text: 'Open Settings', onPress: () => Linking.openSettings() },
        ]
      );
      return;
    }

    // Show pre-permission dialog
    Alert.alert(
      'Camera Access',
      'We need camera access to take photos. Your photos are stored only on your device and are never uploaded without your consent.',
      [
        { text: 'Not Now', style: 'cancel' },
        {
          text: 'Continue',
          onPress: async () => {
            const result = await requestPermission();
            if (result.granted) {
              setShowCamera(true);
            } else if (!result.canAskAgain) {
              Alert.alert(
                'Permission Denied',
                'You can enable camera access later in Settings.',
                [{ text: 'OK' }]
              );
            }
          },
        },
      ]
    );
  };

  if (showCamera && permission?.granted) {
    return (
      <View style={styles.container}>
        <Camera.CameraView style={styles.camera}>
          <View style={styles.overlay}>
            <Pressable 
              style={styles.closeButton}
              onPress={() => setShowCamera(false)}
            >
              <Text style={styles.closeText}>βœ• Close</Text>
            </Pressable>
          </View>
        </Camera.CameraView>
      </View>
    );
  }

  return (
    <View style={styles.container}>
      <Text style={styles.icon}>πŸ“·</Text>
      <Text style={styles.title}>Camera Demo</Text>
      <Text style={styles.status}>
        Status: {permission?.status ?? 'loading'}
        {permission?.canAskAgain === false && ' (blocked)'}
      </Text>
      <Pressable style={styles.button} onPress={handleRequestCamera}>
        <Text style={styles.buttonText}>Open Camera</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  camera: { flex: 1, width: '100%' },
  overlay: { flex: 1, backgroundColor: 'transparent', padding: 20 },
  closeButton: { alignSelf: 'flex-end', padding: 10, backgroundColor: 'rgba(0,0,0,0.5)', borderRadius: 8 },
  closeText: { color: 'white', fontSize: 16 },
  icon: { fontSize: 64, marginBottom: 20 },
  title: { fontSize: 24, fontWeight: 'bold', marginBottom: 10 },
  status: { fontSize: 14, color: '#666', marginBottom: 30 },
  button: { backgroundColor: '#667eea', paddingHorizontal: 32, paddingVertical: 14, borderRadius: 8 },
  buttonText: { color: 'white', fontSize: 16, fontWeight: '600' },
});

Exercise 2: Multi-Permission Request

Create a function that requests multiple permissions in sequence with proper handling.

Requirements:

  • Request camera, microphone, and location permissions
  • Show progress/status for each permission
  • Return a summary of which permissions were granted
  • Handle partial grants (some allowed, some denied)
Show Hint

Create an async function that requests permissions one at a time. Store results in an object. Use a state variable to show which permission is currently being requested.

Show Solution
import { useState } from 'react';
import { View, Text, Pressable, StyleSheet } from 'react-native';
import * as Camera from 'expo-camera';
import { Audio } from 'expo-av';
import * as Location from 'expo-location';

interface PermissionResults {
  camera: boolean;
  microphone: boolean;
  location: boolean;
}

type CurrentStep = 'idle' | 'camera' | 'microphone' | 'location' | 'done';

export default function MultiPermissionRequest() {
  const [currentStep, setCurrentStep] = useState<CurrentStep>('idle');
  const [results, setResults] = useState<PermissionResults | null>(null);

  const requestAllPermissions = async () => {
    const permissionResults: PermissionResults = {
      camera: false,
      microphone: false,
      location: false,
    };

    // Camera
    setCurrentStep('camera');
    const cameraResult = await Camera.requestCameraPermissionsAsync();
    permissionResults.camera = cameraResult.granted;

    // Microphone
    setCurrentStep('microphone');
    const audioResult = await Audio.requestPermissionsAsync();
    permissionResults.microphone = audioResult.granted;

    // Location
    setCurrentStep('location');
    const locationResult = await Location.requestForegroundPermissionsAsync();
    permissionResults.location = locationResult.granted;

    setCurrentStep('done');
    setResults(permissionResults);
  };

  const getStepIcon = (step: keyof PermissionResults) => {
    if (!results) {
      if (currentStep === step) return '⏳';
      return '⬜';
    }
    return results[step] ? 'βœ…' : '❌';
  };

  const allGranted = results && 
    results.camera && 
    results.microphone && 
    results.location;

  const grantedCount = results ? 
    Object.values(results).filter(Boolean).length : 0;

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Permission Setup</Text>
      
      <View style={styles.permissionList}>
        <View style={styles.permissionRow}>
          <Text style={styles.permissionIcon}>{getStepIcon('camera')}</Text>
          <Text style={styles.permissionText}>Camera</Text>
        </View>
        <View style={styles.permissionRow}>
          <Text style={styles.permissionIcon}>{getStepIcon('microphone')}</Text>
          <Text style={styles.permissionText}>Microphone</Text>
        </View>
        <View style={styles.permissionRow}>
          <Text style={styles.permissionIcon}>{getStepIcon('location')}</Text>
          <Text style={styles.permissionText}>Location</Text>
        </View>
      </View>

      {results && (
        <View style={[
          styles.resultCard, 
          allGranted ? styles.successCard : styles.warningCard
        ]}>
          <Text style={styles.resultText}>
            {allGranted 
              ? 'πŸŽ‰ All permissions granted!' 
              : `⚠️ ${grantedCount}/3 permissions granted`}
          </Text>
        </View>
      )}

      {currentStep === 'idle' && (
        <Pressable style={styles.button} onPress={requestAllPermissions}>
          <Text style={styles.buttonText}>Request Permissions</Text>
        </Pressable>
      )}

      {currentStep !== 'idle' && currentStep !== 'done' && (
        <Text style={styles.statusText}>
          Requesting {currentStep} permission...
        </Text>
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', padding: 20 },
  title: { fontSize: 24, fontWeight: 'bold', textAlign: 'center', marginBottom: 30 },
  permissionList: { marginBottom: 30 },
  permissionRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 12 },
  permissionIcon: { fontSize: 24, marginRight: 16 },
  permissionText: { fontSize: 18 },
  resultCard: { padding: 16, borderRadius: 8, marginBottom: 20 },
  successCard: { backgroundColor: '#e8f5e9' },
  warningCard: { backgroundColor: '#fff3e0' },
  resultText: { fontSize: 16, textAlign: 'center' },
  button: { backgroundColor: '#667eea', padding: 16, borderRadius: 8, alignItems: 'center' },
  buttonText: { color: 'white', fontSize: 16, fontWeight: '600' },
  statusText: { textAlign: 'center', color: '#666', fontSize: 14 },
});

Exercise 3: Permission Status Monitor

Build a settings-style screen that shows real-time permission status for all app permissions.

Requirements:

  • Display all permission types with their current status
  • Show granted/denied/not-determined status
  • Provide a button to request each permission
  • Refresh status when returning from Settings
Show Hint

Use useEffect with AppState listener to refresh permissions when the app comes back to foreground. Call all getPermissionsAsync() functions in a single refresh function.

Show Solution
import { useState, useEffect, useCallback } from 'react';
import { View, Text, Pressable, ScrollView, AppState, Linking, StyleSheet } from 'react-native';
import * as Camera from 'expo-camera';
import * as Location from 'expo-location';
import * as MediaLibrary from 'expo-media-library';
import * as Notifications from 'expo-notifications';

interface PermissionInfo {
  name: string;
  icon: string;
  status: 'granted' | 'denied' | 'undetermined' | 'loading';
  request: () => Promise<void>;
}

export default function PermissionMonitor() {
  const [permissions, setPermissions] = useState<PermissionInfo[]>([]);
  const [loading, setLoading] = useState(true);

  const refreshPermissions = useCallback(async () => {
    setLoading(true);
    
    const [camera, location, mediaLibrary, notifications] = await Promise.all([
      Camera.getCameraPermissionsAsync(),
      Location.getForegroundPermissionsAsync(),
      MediaLibrary.getPermissionsAsync(),
      Notifications.getPermissionsAsync(),
    ]);

    setPermissions([
      {
        name: 'Camera',
        icon: 'πŸ“·',
        status: camera.status as any,
        request: async () => {
          await Camera.requestCameraPermissionsAsync();
          refreshPermissions();
        },
      },
      {
        name: 'Location',
        icon: 'πŸ“',
        status: location.status as any,
        request: async () => {
          await Location.requestForegroundPermissionsAsync();
          refreshPermissions();
        },
      },
      {
        name: 'Photos',
        icon: 'πŸ–ΌοΈ',
        status: mediaLibrary.status as any,
        request: async () => {
          await MediaLibrary.requestPermissionsAsync();
          refreshPermissions();
        },
      },
      {
        name: 'Notifications',
        icon: 'πŸ””',
        status: notifications.status as any,
        request: async () => {
          await Notifications.requestPermissionsAsync();
          refreshPermissions();
        },
      },
    ]);
    
    setLoading(false);
  }, []);

  // Refresh on mount
  useEffect(() => {
    refreshPermissions();
  }, [refreshPermissions]);

  // Refresh when returning from background (e.g., Settings)
  useEffect(() => {
    const subscription = AppState.addEventListener('change', (state) => {
      if (state === 'active') {
        refreshPermissions();
      }
    });
    return () => subscription.remove();
  }, [refreshPermissions]);

  const getStatusColor = (status: string) => {
    switch (status) {
      case 'granted': return '#4CAF50';
      case 'denied': return '#f44336';
      default: return '#FF9800';
    }
  };

  const getStatusText = (status: string) => {
    switch (status) {
      case 'granted': return 'Allowed';
      case 'denied': return 'Denied';
      default: return 'Not Asked';
    }
  };

  return (
    <ScrollView style={styles.container}>
      <Text style={styles.title}>Permissions</Text>
      <Text style={styles.subtitle}>
        Manage app permissions. Changes take effect immediately.
      </Text>

      {permissions.map((perm) => (
        <View key={perm.name} style={styles.permissionCard}>
          <View style={styles.permissionInfo}>
            <Text style={styles.permissionIcon}>{perm.icon}</Text>
            <View>
              <Text style={styles.permissionName}>{perm.name}</Text>
              <Text style={[styles.permissionStatus, { color: getStatusColor(perm.status) }]}>
                {getStatusText(perm.status)}
              </Text>
            </View>
          </View>
          
          {perm.status !== 'granted' && (
            <Pressable 
              style={styles.requestButton} 
              onPress={perm.status === 'denied' ? () => Linking.openSettings() : perm.request}
            >
              <Text style={styles.requestButtonText}>
                {perm.status === 'denied' ? 'Settings' : 'Enable'}
              </Text>
            </Pressable>
          )}
        </View>
      ))}

      <Pressable style={styles.settingsButton} onPress={() => Linking.openSettings()}>
        <Text style={styles.settingsButtonText}>Open Device Settings</Text>
      </Pressable>
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 20 },
  title: { fontSize: 28, fontWeight: 'bold', marginBottom: 8 },
  subtitle: { fontSize: 14, color: '#666', marginBottom: 24 },
  permissionCard: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', backgroundColor: '#fff', padding: 16, borderRadius: 12, marginBottom: 12, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 2 },
  permissionInfo: { flexDirection: 'row', alignItems: 'center' },
  permissionIcon: { fontSize: 28, marginRight: 16 },
  permissionName: { fontSize: 16, fontWeight: '600' },
  permissionStatus: { fontSize: 13, marginTop: 2 },
  requestButton: { backgroundColor: '#667eea', paddingHorizontal: 16, paddingVertical: 8, borderRadius: 6 },
  requestButtonText: { color: 'white', fontSize: 14, fontWeight: '600' },
  settingsButton: { marginTop: 20, padding: 16, borderRadius: 8, borderWidth: 1, borderColor: '#ddd', alignItems: 'center' },
  settingsButtonText: { color: '#667eea', fontSize: 16, fontWeight: '600' },
});

Summary

Permissions are a critical part of mobile app development. Handling them correctly builds user trust and ensures a smooth experience.

🎯 Key Takeaways

  • Ask in context: Request permissions when the user tries to use the feature
  • Explain first: Show a custom dialog before the system dialog
  • Handle all states: granted, denied, undetermined, and blocked
  • Degrade gracefully: App should work with reduced features if permission denied
  • iOS is strict: You only get one chance to askβ€”make it count
  • Use hooks: useXxxPermissions() is the cleanest pattern
  • Direct to Settings: When blocked, help users find the setting
  • Configure properly: Add usage descriptions to app.json for store approval
  • Refresh on foreground: Re-check permissions when app becomes active

In the next lesson, we'll use these permission patterns to work with the Camera and Media Libraryβ€”taking photos, recording videos, and accessing the user's photo library.