react-native-permission-handler
v0.8.2
Published
Smart permission UX flows for React Native — pre-prompts, blocked handling, settings redirect & foreground re-check. Pluggable engine: works with react-native-permissions, Expo, or your own.
Downloads
1,451
Maintainers
Readme
react-native-permission-handler
Smart permission UX flows for React Native. Pre-prompts, blocked handling, settings redirect, and
foreground re-check — in one hook, one component, and a pluggable engine. Works with
react-native-permissions, Expo modules,
or any custom backend.
import { PermissionGate } from "react-native-permission-handler";
import { Permissions } from "react-native-permission-handler/rnp";
export function QRScannerScreen() {
return (
<PermissionGate
permission={Permissions.CAMERA}
prePrompt={{ title: "Camera", message: "We need your camera to scan QR codes." }}
blockedPrompt={{ title: "Camera blocked", message: "Enable camera in Settings." }}
fallback={<Spinner />}
>
<QRScanner />
</PermissionGate>
);
}That's the whole flow: pre-prompt modal, system dialog, blocked recovery, Settings round-trip, and AppState re-check on return. No state machine to wire up, no foreground listener to juggle.
Why this library
- Pure state machine at the core. 12 states, pure transitions, no React or native code underneath. Every hook and component is a thin side-effect layer on top.
- Pluggable engines. Bring your own permissions backend. Zero-config with
react-native-permissions, auto-discovery with Expo modules, pluscreateTestingEnginefor unit tests andcreateNoopEnginefor web/Storybook. - Declarative components, RN primitives only.
PermissionGate,DefaultPrePrompt,DefaultBlockedPrompt, andLimitedUpgradePrompt— no third-party UI dependencies, override any of them with a render prop.
Installation
npm install react-native-permission-handlerThen pick a permissions backend:
# Option A: react-native-permissions (recommended, auto-detected)
npm install react-native-permissions
# Option B: Expo modules (auto-discovered from installed expo-* packages)
# Option C: custom — implement the PermissionEngine interface yourselfWith react-native-permissions installed, the library auto-creates an engine the first time a
hook runs. Zero config needed.
Quick start
With PermissionGate (declarative)
import { PermissionGate } from "react-native-permission-handler";
import { Permissions } from "react-native-permission-handler/rnp";
<PermissionGate
permission={Permissions.CAMERA}
prePrompt={{ title: "Camera", message: "We need your camera." }}
blockedPrompt={{ title: "Blocked", message: "Enable in Settings." }}
fallback={<Spinner />}
>
<CameraView />
</PermissionGate>With usePermissionHandler (imperative)
import { usePermissionHandler } from "react-native-permission-handler";
import { Permissions } from "react-native-permission-handler/rnp";
function CameraScreen() {
const camera = usePermissionHandler({
permission: Permissions.CAMERA,
prePrompt: { title: "Camera", message: "We need your camera." },
blockedPrompt: { title: "Blocked", message: "Enable in Settings." },
onGrant: () => analytics.track("camera_granted"),
});
if (camera.isChecking) return <Spinner />;
if (camera.isGranted) return <CameraView />;
if (camera.isUnavailable) return <Text>Camera not available.</Text>;
return null; // default pre-prompt / blocked modals render on top
}The state machine
The core of the library is a pure state machine that drives the entire permission flow:
+-------+
| idle |
+---+---+
|
CHECK
|
+-----v-----+
| checking |
+-----+-----+
|
+---------------+---------------+
| | |
GRANTED DENIED BLOCKED
| | |
+-----v---+ +-----v-----+ +-----v-------+
| granted | | prePrompt | | blockedPrompt|
+---------+ +-----+-----+ +------+------+
| |
+--------+--------+ OPEN_SETTINGS
| | |
CONFIRM DISMISS +----v----------+
| | | openingSettings|
+-----v-----+ +-----v+ +----+----------+
| requesting | |denied| |
+-----+------+ +------+ SETTINGS_RETURN
| |
+--------+--------+ +---------v-----------+
| | | |recheckingAfterSettings|
GRANTED DENIED BLOCKED +---------+-----------+
| | | |
+-----v-+ +---v--+ +---v--------+ +---+---+
|granted| |denied| |blockedPrompt| |granted| or back
+-------+ +------+ +------------+ +-------+ to blockedPromptlimited is a sibling of granted — iOS 14+ partial photo access. isGranted is true for
both, but isLimited lets you surface an upgrade prompt. See the full state list in
docs/api/types.md.
Core APIs
Every API is documented in depth under docs/api/.
usePermissionHandler— single-permission hook. Full lifecycle: check, pre-prompt, request, blocked recovery, settings round-trip, optionalrequestFullAccessfor limited → granted upgrade.useMultiplePermissions— sequential or parallel multi-permission orchestration. Per-permission handlers, stableidkeys,resume()after a Settings trip,blockedPermissionssummary.PermissionGate— declarative component.renderPrePrompt,renderBlockedPrompt,renderDenied, andrenderLimitedrender props.transition(state, event)— raw pure state machine function. Build your own hook or integrate with a state library.
Engines
An engine is the pluggable adapter between this library and the actual permissions backend. See
docs/api/engines.md for the full reference, including resolution order
(engine prop > setDefaultEngine() > auto RNP fallback).
createRNPEngine({ normalizePhotoLibrary?, normalizeAndroid? })—react-native-permissionsadapter with opt-in Android and photo-library status normalization.createExpoEngine()— Expo modules adapter. Zero config, auto-discovers installedexpo-camera,expo-location,expo-notifications, etc.createTestingEngine(initialStatuses?, { autoGrantUnset? })— controllable engine for unit tests. Defaults are symmetric: bothcheck()andrequest()return"denied"for unseeded permissions. Pass{ autoGrantUnset: true }for a happy-path shortcut that auto-grants onrequest().createNoopEngine(defaultStatus?)— always-granted stub for web builds and Storybook.- Any object implementing
PermissionEngine— roll your own.
Recipes
Drop-in solutions to real problems. See docs/recipes/.
- Limited photo access + upgrade — iOS 14+ partial
grants,
renderLimited, andrequestFullAccess(). - Background location — sequential
Permissions.BUNDLES.LOCATION_BACKGROUNDflow. - Onboarding permission wall — sequential wall with
idkeys, per-row handlers, andresume()after Settings. - Bluetooth device pairing —
Permissions.BUNDLES.BLUETOOTHhandles Android 12+ scan/connect and older-Android location fallback automatically. - Voice note composer — inline mic access with
skipPrePrompt: "android". - Android status normalization — when and why to
enable
normalizeAndroidandnormalizePhotoLibrary. - Testing with
createTestingEngine— fake engines for fast unit tests without native mocks.
Platform gotchas
iOS:
- Before shipping to the App Store, make sure your app declares a
PrivacyInfo.xcprivacyat the target level. See the iOS Privacy Manifest guide for a boilerplate template and the full list of permission usage-description keys. - The system permission dialog only shows once per permission, ever. Once denied via the system dialog, there is no programmatic path back — only Settings. Always show a pre-prompt first to warm the user up.
check()returnsdeniedfor both "never asked" and "denied once" — both are still requestable.limitedis an iOS 14+ state for photo library partial access.isGrantedistruefor it (backward compatible),isLimiteddistinguishes it.- App Tracking Transparency (
APP_TRACKING_TRANSPARENCY) can only be requested while the app is in the.activestate. Calling it from a mount effect or cold-start path often fires while the app is still.inactive, and iOS silently returnsdeniedwith no system prompt — the user never gets a chance to allow tracking. Defer the request until after your first user interaction, or gate it on anAppStatelistener that waits foractive. Rule of thumb: never callrequest("ios.permission.APP_TRACKING_TRANSPARENCY")fromuseEffect(..., [])on a screen the user is navigating to for the first time.
Android:
- After 2 denials, Android 11+ auto-blocks the permission. No more system dialogs.
checkNotifications()never returnsblockedon Android 13+ — the RNP engine handles this internally. EnablenormalizeAndroid: trueto also cache the lastrequest()result for accuratecheck()reads.- "One-time" permission grants (location, camera, mic) auto-revoke after ~30–60s of backgrounding.
- Android 16 (API 36+) hang.
request()can hang indefinitely when a permission is innever_ask_againstate (facebook/react-native#53887). The library auto-applies a 5 srequestTimeoutdefault on Android 16 and routes to the blocked prompt on expiry. Override with an explicitrequestTimeoutper-hook if you need different behavior.
Notifications: pass "notifications" as the permission identifier. The RNP engine routes to
checkNotifications/requestNotifications. For Expo, map "notifications" to expo-notifications
in the engine config (or rely on auto-discovery).
What this library doesn't cover
The library wraps device permission prompts — the OS-level CAMERA, PHOTO_LIBRARY, LOCATION,
etc. dialogs. It deliberately does not cover the following adjacent problems:
- HealthKit / Google Fit / Apple Health — these use per-data-type authorization with a
fundamentally different model (no "denied" signal is returned to the app by design). Use
react-native-health,@kingstinct/react-native-healthkit, or native modules. - OAuth and social login scopes — Google Sign-In, Facebook Login, "Sign in with Apple," etc.
These are auth grants, not device permissions. Use
@react-native-google-signin/google-signinor the provider-specific SDK. - IAP / StoreKit entitlements — use
react-native-iap. - Push notification provider tokens (APNs / FCM registration) — this library handles the
user-facing notification permission. Device-token registration and routing belong to
@react-native-firebase/messagingor similar. - HomeKit, Local Network, NFC, Contacts groups, Gamekit, CarPlay — domain-specific permission models. Some map cleanly via a custom engine; most are better served by dedicated libraries.
If you're unsure whether your use case fits, a good test: does your permission prompt show a system-standard "Allow / Don't Allow" dialog? If yes, the library probably fits. If it's a custom auth flow or a per-scope consent screen, look elsewhere.
What's new in v0.7.0
requestFullAccess()on the hook result — upgrade from limited → granted without leaving the app. Engine-routed, throws with a clear error if unsupported.renderLimitedonPermissionGate— custom UI during iOS 14+ partial photo access.Permissions.BUNDLES—BLUETOOTH,LOCATION_BACKGROUND, andCALENDARS_WRITE_ONLYpresets that resolve to the correctstring[]per platform and OS version.MultiPermissionEntry.id— stable cross-platform keys forstatuses/handlersrecords.resume()onuseMultiplePermissions— restart a stopped sequential flow from current ungranted statuses, preserving already-granted progress.skipPrePrompt: boolean | "android"— one-tap composer flows without a pre-prompt modal, safely scoped to Android only.- Optional
prePrompt/blockedPromptconfig — custom-UI users no longer need dummy configs to satisfy types. - Android 16 auto-recovery — 5 s default
requestTimeouton API 36+, routed to the blocked prompt on expiry. createRNPEngine({ normalizeAndroid, normalizePhotoLibrary })— opt-in Android status normalization (pre-13POST_NOTIFICATIONS, dialog-dismiss misreports, stale check cache) and iOS photo-libraryunavailable → blockedrewrite.
See docs/api/ and docs/recipes/ for full details.
Requirements
- React Native >= 0.76
- React >= 18
- One of:
react-native-permissions>= 4.0.0 (auto-detected, zero config)- Expo permission modules (use
createExpoEngine) - Custom
PermissionEngineimplementation
Contributing
Issues and PRs welcome. The project uses Biome for linting, Vitest for tests, and tsup for
builds. Run npm test && npm run lint && npm run typecheck before submitting a PR.
License
MIT
