Skip to content

Types, flow states, and Permissions

PermissionStatus

The unified permission status owned by this library. Engines must map their native statuses to these values.

ts
type PermissionStatus = "granted" | "denied" | "blocked" | "limited" | "unavailable";
ValueMeaning
grantedPermission is fully granted. Show the protected content.
limitediOS 14+ partial grant. Reliably emitted for the iOS photo library ("Selected Photos"); feature works, upgrade flow is available via requestFullAccess(). iOS 18+ caveat: Apple added a conceptually similar limited-contacts mode, but whether engines return limited for contacts on iOS 18 is RNP/Expo-dependent. If you're shipping limited-contacts UX, test on a real iOS 18 device first — the state machine and isLimited helper handle it correctly when the engine emits it, but neither RNP nor Expo guarantees emission at the time of v0.8.0.
deniedNot granted but still requestable — system dialog can still be shown.
blockedPermanently denied. Only Settings can fix it.
unavailableThe device doesn't support this feature. Terminal state.

PermissionMetadata

Engine-specific metadata captured alongside the PermissionStatus. Exposed via PermissionHandlerResult.metadata. Fields are optional and engine-dependent — not every engine populates every field, and the object is always present (empty when there's nothing to report) so consumers don't have to null-check the top-level field.

ts
interface PermissionMetadata {
  locationAccuracy?: "full" | "reduced";
}
FieldPopulated byMeaning
locationAccuracyExpo engine (SDK 55+)iOS 14+ Core Location authorization level captured from the most recent check/request on a location permission. "full" = precise location, "reduced" = approximate (the user toggled off "Precise Location" in the system prompt). Not populated by the RNP engine.

The shape is intentionally tight — new fields are added only when there is a concrete engine + use case that needs them. Do not treat it as an open-ended bag of strings.

PermissionFlowState

The 12 states of the state machine driving every hook.

ts
type PermissionFlowState =
  | "idle"
  | "checking"
  | "prePrompt"
  | "requesting"
  | "granted"
  | "limited"
  | "denied"
  | "blocked"
  | "blockedPrompt"
  | "openingSettings"
  | "recheckingAfterSettings"
  | "unavailable";
StateDescription
idleInitial state. No check has happened yet.
checkingengine.check() is in flight.
prePromptRequestable. Show a friendly explanation before the system dialog.
requestingSystem permission dialog is visible.
grantedPermission fully granted.
limitedPartial grant (iOS 14+ photo library).
deniedUser dismissed the pre-prompt or the system dialog returned denied. Still requestable.
blockedTerminal-until-Settings state.
blockedPromptShowing the "open Settings" modal.
openingSettingsUser tapped "Open Settings". Waiting for the app to return.
recheckingAfterSettingsApp returned from Settings. Re-checking status.
unavailableDevice doesn't support the feature. Terminal.

PermissionFlowEvent

The events that drive the pure state machine. Expose the raw transition(state, event) function from react-native-permission-handler if you want to build your own hook.

ts
type PermissionFlowEvent =
  | { type: "CHECK" }
  | { type: "CHECK_RESULT"; status: PermissionStatus }
  | { type: "PRE_PROMPT_CONFIRM" }
  | { type: "PRE_PROMPT_DISMISS" }
  | { type: "REQUEST_RESULT"; status: PermissionStatus }
  | { type: "BLOCKED_PROMPT_DISMISS" }
  | { type: "RESET" }
  | { type: "OPEN_SETTINGS" }
  | { type: "SETTINGS_RETURN" }
  | { type: "RECHECK_RESULT"; status: PermissionStatus };
ts
import { transition } from "react-native-permission-handler";

transition("prePrompt", { type: "PRE_PROMPT_CONFIRM" }); // -> "requesting"
transition("blockedPrompt", { type: "BLOCKED_PROMPT_DISMISS" }); // -> "denied"

RESET is a universal transition to idle from any state.

Permissions constants

Imported from the RNP entry point. Cross-platform keys resolve to the correct native permission string per platform (and per API level for scoped-storage-aware keys).

ts
import { Permissions } from "react-native-permission-handler/rnp";

Cross-platform

Permissions.CAMERA
Permissions.MICROPHONE
Permissions.CONTACTS
Permissions.CALENDARS
Permissions.CALENDARS_WRITE_ONLY
Permissions.LOCATION_WHEN_IN_USE
Permissions.LOCATION_ALWAYS
Permissions.PHOTO_LIBRARY           // Android 13+: READ_MEDIA_IMAGES; below: READ_EXTERNAL_STORAGE
Permissions.PHOTO_LIBRARY_ADD_ONLY
Permissions.MEDIA_LIBRARY           // Android 13+: READ_MEDIA_AUDIO
Permissions.VIDEO_LIBRARY           // Android 13+: READ_MEDIA_VIDEO
Permissions.BLUETOOTH
Permissions.SPEECH_RECOGNITION
Permissions.MOTION
Permissions.NOTIFICATIONS           // routed to checkNotifications / requestNotifications

iOS-only

Permissions.IOS.APP_TRACKING_TRANSPARENCY
Permissions.IOS.FACE_ID
Permissions.IOS.REMINDERS
Permissions.IOS.SIRI
Permissions.IOS.STOREKIT

Android-only

Includes (non-exhaustive): BODY_SENSORS, CALL_PHONE, READ_SMS, SEND_SMS, RECEIVE_SMS, BLUETOOTH_SCAN, BLUETOOTH_ADVERTISE, NEARBY_WIFI_DEVICES, READ_MEDIA_IMAGES, READ_MEDIA_VIDEO, READ_MEDIA_AUDIO, READ_MEDIA_VISUAL_USER_SELECTED, UWB_RANGING, ACCESS_COARSE_LOCATION, and more.

Permissions.BUNDLES

Platform-aware presets that resolve to string[] for features requiring multiple native permissions. Designed to be spread into useMultiplePermissions entries.

BundleResolves to
BUNDLES.BLUETOOTHiOS: [BLUETOOTH]. Android 12+: [BLUETOOTH_SCAN, BLUETOOTH_CONNECT]. Android 11 and below: [ACCESS_FINE_LOCATION].
BUNDLES.LOCATION_BACKGROUNDiOS: [LOCATION_WHEN_IN_USE] — iOS models Core Location as a single authorization, and upgrading to "Always" is a follow-up step on the already-granted permission rather than a separate requestable permission (tracked as future upgradeToAlways API). Android: [ACCESS_FINE_LOCATION, ACCESS_BACKGROUND_LOCATION] — foreground must be granted first.
BUNDLES.CALENDARS_WRITE_ONLYiOS 17+: dedicated CALENDARS_WRITE_ONLY. Older iOS: full CALENDARS (since write-only is not separable). Android: WRITE_CALENDAR.
tsx
import { useMultiplePermissions } from "react-native-permission-handler";
import { Permissions } from "react-native-permission-handler/rnp";

const perms = useMultiplePermissions({
  strategy: "sequential",
  permissions: Permissions.BUNDLES.BLUETOOTH.map((permission, i) => ({
    id: `ble-${i}`,
    permission,
    prePrompt: { title: "Bluetooth", message: "We need Bluetooth to pair your device." },
    blockedPrompt: { title: "Blocked", message: "Enable Bluetooth in Settings." },
  })),
});

See ble-device-pairing and background-location for full recipes.