| name | mobile-permissions |
|---|---|
| description | Handle runtime permissions in a React Native/Expo app. Covers camera, location, contacts, media library, notifications, and microphone with iOS rationale strings and Android manifest config. Use when the user needs to request device permissions. |
| standards-version | 1.7.0 |
Use this skill when the user:
- Needs to request a device permission (camera, location, contacts, etc.)
- Gets a permission denied error
- Asks about iOS usage description strings or Android manifest permissions
- Wants to handle the "don't ask again" / permanently blocked state
- Mentions "permission", "Info.plist", "NSCameraUsageDescription", "rationale", or "settings"
- Permission type: camera, location, contacts, media-library, notifications, microphone, or calendar
- Use case: Why the app needs this permission (used for the rationale string)
- When to request: On app launch, on first use of the feature, or on a specific screen
-
Understand the permission lifecycle. Every permission follows this flow:
Not Determined --> Request --> Granted --> Denied --> Can Ask Again? Yes: request again No: link to SettingsOn iOS, you get one chance to show the system dialog. After the user denies, you must send them to Settings. On Android 11+, after two denials, the system permanently blocks the dialog.
-
Install the Expo module. Each permission comes from its own package:
Permission Package Hook Camera expo-camerauseCameraPermissions()Location expo-locationN/A, use requestForegroundPermissionsAsync()Contacts expo-contactsN/A, use requestPermissionsAsync()Media Library expo-media-libraryusePermissions()Notifications expo-notificationsN/A, use requestPermissionsAsync()Microphone expo-avN/A, use Audio.requestPermissionsAsync()Calendar expo-calendarN/A, use requestCalendarPermissionsAsync()Install with:
npx expo install expo-camera # or whichever package you need -
Request permission with pre-check. Always check current status before requesting:
import * as Location from "expo-location"; import { Alert, Linking, Platform } from "react-native"; async function requestLocationPermission(): Promise<boolean> { const { status: existing } = await Location.getForegroundPermissionsAsync(); if (existing === "granted") return true; const { status, canAskAgain } = await Location.requestForegroundPermissionsAsync(); if (status === "granted") return true; if (!canAskAgain) { Alert.alert( "Location Permission Required", "Location access was denied. Open Settings to enable it.", [ { text: "Cancel", style: "cancel" }, { text: "Open Settings", onPress: () => { if (Platform.OS === "ios") { Linking.openURL("app-settings:"); } else { Linking.openSettings(); } }, }, ], ); } return false; }
-
Create a reusable permission hook. Generalize the pattern:
import { useState, useCallback, useEffect } from "react"; import { Alert, Linking, Platform } from "react-native"; interface PermissionResult { status: "undetermined" | "granted" | "denied"; canAskAgain: boolean; } interface UsePermissionOptions { name: string; getPermission: () => Promise<PermissionResult>; requestPermission: () => Promise<PermissionResult>; } export function usePermission({ name, getPermission, requestPermission, }: UsePermissionOptions) { const [granted, setGranted] = useState(false); const [checked, setChecked] = useState(false); useEffect(() => { getPermission().then((result) => { setGranted(result.status === "granted"); setChecked(true); }); }, []); const request = useCallback(async () => { const result = await requestPermission(); if (result.status === "granted") { setGranted(true); return true; } if (!result.canAskAgain) { Alert.alert( `${name} Permission Required`, `${name} access was denied. Open Settings to enable it.`, [ { text: "Cancel", style: "cancel" }, { text: "Open Settings", onPress: () => { Platform.OS === "ios" ? Linking.openURL("app-settings:") : Linking.openSettings(); }, }, ], ); } return false; }, [name, requestPermission]); return { granted, checked, request }; }
-
Add iOS rationale strings. Configure in
app.jsonvia Expo config plugins:{ "expo": { "plugins": [ [ "expo-camera", { "cameraPermission": "$(PRODUCT_NAME) needs camera access to take photos." } ], [ "expo-location", { "locationAlwaysAndWhenInUsePermission": "$(PRODUCT_NAME) uses your location to show nearby places.", "locationAlwaysPermission": "$(PRODUCT_NAME) uses your location in the background for navigation.", "locationWhenInUsePermission": "$(PRODUCT_NAME) uses your location to show nearby places." } ], [ "expo-media-library", { "photosPermission": "$(PRODUCT_NAME) needs photo library access to save and select photos.", "savePhotosPermission": "$(PRODUCT_NAME) saves photos to your library." } ], [ "expo-contacts", { "contactsPermission": "$(PRODUCT_NAME) accesses contacts to help you share with friends." } ] ] } }iOS requires specific
NS*UsageDescriptionkeys. Expo config plugins generate these automatically from the strings above. -
Android-specific considerations.
- Android 13+ requires
POST_NOTIFICATIONSpermission for push notifications. Older versions grant it automatically. ACCESS_FINE_LOCATIONvsACCESS_COARSE_LOCATION: fine gives GPS-level accuracy, coarse gives city-block accuracy. Request the minimum you need.- Background location (
ACCESS_BACKGROUND_LOCATION) requires a separate request after foreground location is granted. Google Play requires a privacy policy justifying it.
- Android 13+ requires
-
Best practices for timing. When to ask matters for approval rates:
- Request permission at the moment the user tries to use the feature, not on app launch
- Show a custom "pre-permission" screen explaining why before triggering the system dialog
- If denied, do not block the entire app. Degrade gracefully and offer the feature again later with context
- Expo permissions overview
- expo-camera permissions
- expo-location permissions
- expo-notifications permissions
- Apple: Requesting access to protected resources
User: "My app needs location and camera. How do I handle permissions properly?"
Agent:
- Installs
expo-cameraandexpo-locationwithmobile_installDependency - Adds permission rationale strings with
mobile_addPermissionfor both - Creates the reusable
usePermissionhook - Implements a pre-permission screen explaining why the app needs these
- Handles denied and permanently blocked states with Settings links
- Notes that both require dev builds (not Expo Go)
| Step | MCP Tool | Description |
|---|---|---|
| Add permission config | mobile_addPermission |
Add permission rationale to app.json for any supported type |
| Install permission package | mobile_installDependency |
Install the Expo package for the needed permission |
| Scaffold permission screen | mobile_generateScreen |
Create a pre-permission explanation screen |
| Verify build | mobile_checkBuildHealth |
Confirm the project builds after adding native modules |
- Requesting all permissions on launch - Users deny permissions they do not understand. Ask at the moment they try to use the feature, with context about why.
- Not handling "permanently denied" - After the user selects "Don't ask again" on Android (or denies on iOS), the system dialog will never appear again. You must link to Settings.
- Missing iOS rationale strings - Apple rejects apps without
NS*UsageDescriptionkeys. Add them via Expo config plugins inapp.json. - Requesting background location upfront - Google Play rejects apps that request
ACCESS_BACKGROUND_LOCATIONwithout justification. Request foreground first, then background only if needed. - Confusing permission status values -
undeterminedmeans never asked.deniedmeans asked and refused.grantedmeans approved. CheckcanAskAgainto know if the system dialog will show. - Testing only on simulators - iOS Simulator auto-grants some permissions. Always test permission flows on a physical device.
- Mobile Camera Integration - uses camera permissions
- Mobile AI Features - uses camera and microphone permissions