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 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');
},
},
]
);
});
}
Best Practices
β Permission Best Practices
- Ask in context: Request when the user tries to use the feature
- Explain first: Show a custom dialog explaining why before the system dialog
- Be specific: Tell users exactly what you'll use the permission for
- Request minimal: Only ask for permissions you actually need
- Degrade gracefully: App should work (with reduced features) without permissions
- Don't block: Never prevent app usage until permissions are granted
- Provide alternatives: Offer manual input if permission is denied
- 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.