Engines
An engine is the pluggable adapter that this library uses to talk to the actual permissions backend. Every hook and component reaches state through an engine — the library itself ships zero native code.
The PermissionEngine interface
interface PermissionEngine {
check(permission: string): Promise<PermissionStatus>;
request(permission: string): Promise<PermissionStatus>;
openSettings(permission?: string): Promise<void>;
requestFullAccess?(permission: string): Promise<PermissionStatus>;
}
type PermissionStatus = "granted" | "denied" | "blocked" | "limited" | "unavailable";An engine is responsible for:
- Mapping its backend's native status values to the library's
PermissionStatus. - Routing special cases like notifications to the correct API (e.g.,
checkNotificationson RNP). - Opening the correct settings screen for the platform. On iOS, the optional
permissionparameter enables best-effort deep-linking into the per-permission Settings sub-page; engines fall back to generic Settings if the deep-link fails. - Optionally, implementing
requestFullAccessfor the iOS 14+ photo-library upgrade flow. Hooks call this viaPermissionHandlerResult.requestFullAccess()and throw a clear error if it is not implemented.
iOS Settings deep-linking
When openSettings(permission) is called with a permission identifier, the RNP and Expo engines build an iOS App-Prefs:root=Privacy&path=<PATH> URL and attempt to open it. The mapping is substring-based and accepts RNP constants, Expo keys, and plain strings:
| Input (case-insensitive, substring match) | iOS Settings path |
|---|---|
camera | CAMERA |
microphone / record_audio | MICROPHONE |
photo / mediaLibrary / read_media_* | PHOTOS |
location (including foreground/background variants) | LOCATION |
contacts | CONTACTS |
calendar | CALENDARS |
reminders | REMINDERS |
motion | MOTION |
bluetooth | BLUETOOTH |
| anything else (notifications, tracking, etc.) | fall back to generic Settings |
The App-Prefs: URL scheme is unofficial — iOS may reject it on some versions. Every deep-link attempt is wrapped in try/catch, so a failed openURL falls through to the generic openSettings() path without throwing. Treat the deep-link as a best-effort UX enhancement, not a guarantee. On Android, the permission parameter is ignored because RNP's openSettings() already lands on the app-specific permissions page.
Engine resolution order
When a hook or PermissionGate needs an engine, it resolves in this order:
- The
engineprop passed directly to the hook/component (highest precedence). - The global default set via
setDefaultEngine(). - A lazy RNP fallback that loads
react-native-permissionsif it is installed (zero config).
If none of the above resolves, the hook throws an error that explains the three options.
import { setDefaultEngine } from "react-native-permission-handler";
setDefaultEngine(myEngine); // call once at app startupcreateRNPEngine(options?)
Adapter for react-native-permissions. Also re-exports the Permissions constants — see types.md for the full list.
import { createRNPEngine, Permissions } from "react-native-permission-handler/rnp";
import { setDefaultEngine } from "react-native-permission-handler";
setDefaultEngine(createRNPEngine());If react-native-permissions is installed, this is auto-wired by the RNP fallback and you don't need to call it explicitly. Call createRNPEngine({...}) explicitly when you need to pass options:
| Option | Type | Description |
|---|---|---|
normalizePhotoLibrary | boolean | Opt-in. Rewrites unavailable → blocked for photo library permissions. Useful when iOS reports unavailable in edge cases where the user could still recover through Settings. See the android-normalization recipe for when to enable it. |
normalizeAndroid | boolean | Opt-in. Applies a set of Android-specific fixes: (1) rewrites POST_NOTIFICATIONS denied → granted on API < 33, (2) treats dialog-dismiss misreported as blocked as denied until the 2nd request, and (3) replays the last request() result when check() lies about notifications state. |
The RNP adapter handles "notifications" internally by routing to checkNotifications and requestNotifications.
Permissions.BUNDLES
Platform-aware presets that resolve to string[] at runtime. Designed to be passed to useMultiplePermissions when a single logical feature requires multiple native permissions.
import { Permissions } from "react-native-permission-handler/rnp";
Permissions.BUNDLES.BLUETOOTH; // iOS: [BLUETOOTH]; Android 12+: [SCAN, CONNECT]; else [FINE_LOCATION]
Permissions.BUNDLES.LOCATION_BACKGROUND; // iOS: [LOCATION_WHEN_IN_USE]; Android: [ACCESS_FINE_LOCATION, ACCESS_BACKGROUND_LOCATION]
Permissions.BUNDLES.CALENDARS_WRITE_ONLY;// iOS 17+: dedicated write-only; else full calendarSee ble-device-pairing and background-location for full-flow examples.
createExpoEngine(config?)
Adapter for Expo permission modules. Zero-config: with no arguments it auto-discovers installed Expo modules and maps them to standard permission keys.
import { createExpoEngine } from "react-native-permission-handler/expo";
import { setDefaultEngine } from "react-native-permission-handler";
setDefaultEngine(createExpoEngine());Auto-discovered keys include: camera, microphone, locationForeground, locationBackground, notifications, contacts, calendar, reminders, mediaLibrary, imagePickerCamera, imagePickerMediaLibrary, tracking, brightness, audioRecording, audio, screenCapture, cellular, pedometer, accelerometer.
Override or add custom permissions by passing config.permissions:
import * as Camera from "expo-camera";
setDefaultEngine(
createExpoEngine({
permissions: {
// Non-standard method names: use { get, request }
camera: {
get: () => Camera.getCameraPermissionsAsync(),
request: () => Camera.requestCameraPermissionsAsync(),
},
// Standard modules: pass the module directly
myCustom: myModule,
},
}),
);Expo status mapping:
Expo status | canAskAgain | Mapped to |
|---|---|---|
"granted" | — | "granted" |
"undetermined" | — | "denied" |
"denied" | true | "denied" |
"denied" | false | "blocked" |
createTestingEngine(initialStatuses?, options?)
A controllable engine for unit tests. Records every check / request call and lets you rewrite statuses mid-test.
import { createTestingEngine } from "react-native-permission-handler/testing";
const engine = createTestingEngine({ "ios.permission.CAMERA": "denied" });
// drive tests
engine.setStatus("ios.permission.CAMERA", "granted");
engine.getRequestHistory();
engine.reset();Defaults for unseeded permissions. Both check() and request() return "denied" for permissions you have not explicitly seeded via initialStatuses or setStatus. This keeps test behavior symmetric and predictable — a permission you forgot to set up won't silently grant on request().
options.autoGrantUnset — pass { autoGrantUnset: true } to restore the happy-path shortcut where request() returns "granted" for unseeded permissions (while check() still returns "denied"). Useful when you want to test grant flows without enumerating every permission up front.
// Symmetric default: both check and request return "denied" for unseeded permissions.
const strict = createTestingEngine({ camera: "denied" });
// Happy-path shortcut: request() auto-grants anything not seeded.
const lenient = createTestingEngine({}, { autoGrantUnset: true });See the testing recipe for a full example.
createNoopEngine(defaultStatus?)
A no-op engine useful for web builds and Storybook. Returns defaultStatus (default: "granted") for every check and request, and silently resolves openSettings.
import { createNoopEngine } from "react-native-permission-handler/noop";
import { setDefaultEngine } from "react-native-permission-handler";
if (Platform.OS === "web") {
setDefaultEngine(createNoopEngine("granted"));
}Custom engines
Implement the interface directly when you need to wrap a bespoke backend:
import type { PermissionEngine } from "react-native-permission-handler";
const engine: PermissionEngine = {
async check(permission) {
const status = await myBackend.check(permission);
return status === "ok" ? "granted" : "denied";
},
async request(permission) {
return myBackend.request(permission);
},
async openSettings() {
await myBackend.openSettings();
},
};Pass it per-hook via the engine prop, or globally via setDefaultEngine(engine).