Skip to content

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

ts
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., checkNotifications on RNP).
  • Opening the correct settings screen for the platform. On iOS, the optional permission parameter 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 requestFullAccess for the iOS 14+ photo-library upgrade flow. Hooks call this via PermissionHandlerResult.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
cameraCAMERA
microphone / record_audioMICROPHONE
photo / mediaLibrary / read_media_*PHOTOS
location (including foreground/background variants)LOCATION
contactsCONTACTS
calendarCALENDARS
remindersREMINDERS
motionMOTION
bluetoothBLUETOOTH
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:

  1. The engine prop passed directly to the hook/component (highest precedence).
  2. The global default set via setDefaultEngine().
  3. A lazy RNP fallback that loads react-native-permissions if it is installed (zero config).

If none of the above resolves, the hook throws an error that explains the three options.

ts
import { setDefaultEngine } from "react-native-permission-handler";

setDefaultEngine(myEngine); // call once at app startup

createRNPEngine(options?)

Adapter for react-native-permissions. Also re-exports the Permissions constants — see types.md for the full list.

ts
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:

OptionTypeDescription
normalizePhotoLibrarybooleanOpt-in. Rewrites unavailableblocked 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.
normalizeAndroidbooleanOpt-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.

ts
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 calendar

See 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.

ts
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:

ts
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 statuscanAskAgainMapped 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.

ts
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.

ts
// 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.

ts
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:

ts
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).