react-native-permission-manager
v0.2.5
Published
React Native permissions helper for Android and iOS — check, request, ensure, hooks, and a PermissionGate.
Maintainers
Readme
react-native-permission-manager
Cross-platform permissions for React Native (Android + iOS). One API for check / request / ensure, plus hooks and a small gate component. TurboModule when available, old bridge as fallback.
Table of contents
Features
PermissionManager.request('camera')and friends — same call on both platforms- Android rationale dialog before the system prompt (optional / customizable)
usePermission()for status + request / ensure / settings<PermissionGate>— render kids only when granted- Refreshes when the app comes back from Settings
- Groups:
requestGroup('media')or pass your own list - Typed permission names and results
- Camera, mic, photos, notifications, location, contacts, calendar, bluetooth, storage, SMS, call phone
Requirements
- React Native 0.73+ (0.79+ recommended)
- Node 18+
Installation
npm install react-native-permission-manager
# or
yarn add react-native-permission-managerRebuild the native app after install (npx pod-install on iOS, then run Android/iOS).
iOS (Info.plist)
Add a usage string for every permission you request. Missing keys cause iOS to
abort the app (abort_with_payload) when you call request() — the library
detects this and returns UNAVAILABLE with a console warning instead of crashing,
but real prompts only appear after you add the keys:
<!-- Camera / Mic / Photos -->
<key>NSCameraUsageDescription</key>
<string>Camera access is needed to take photos.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Microphone access is needed for audio.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Photo library access is needed to pick images.</string>
<!-- Contacts -->
<key>NSContactsUsageDescription</key>
<string>Contacts access is needed to find friends.</string>
<!-- Location (foreground) -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>Location access is needed to show nearby content.</string>
<!-- Location (background / always) — also keep the WhenInUse key above -->
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Background location is needed for continuous updates.</string>
<!-- Optional legacy: NSLocationAlwaysUsageDescription -->
<!-- Calendar (iOS 17+ prefers FullAccess) -->
<key>NSCalendarsUsageDescription</key>
<string>Calendar access is needed to manage events.</string>
<key>NSCalendarsFullAccessUsageDescription</key>
<string>Calendar access is needed to manage events.</string>
<!-- Bluetooth -->
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Bluetooth access is needed to connect to devices.</string>cd ios && pod install && cd ..Android (AndroidManifest.xml)
The library does not inject permissions (avoids Manifest Merger conflicts).
Declare only what your app uses:
<!-- Camera / Mic / Notifications -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- Contacts -->
<uses-permission android:name="android.permission.READ_CONTACTS" />
<!-- Location -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Background location (Android 10+) — also declare Fine/Coarse above -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!-- Photos / Storage (API 33+) -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<!-- Older devices -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<!-- Calendar / Bluetooth / Phone (as needed) -->
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.READ_SMS" />
<uses-permission android:name="android.permission.CALL_PHONE" />| Friendly name | Android permission(s) | iOS Info.plist key |
|---------------|----------------------|--------------------|
| camera | CAMERA | NSCameraUsageDescription |
| microphone | RECORD_AUDIO | NSMicrophoneUsageDescription |
| photos | READ_MEDIA_IMAGES + READ_MEDIA_VIDEO (API 33+) | NSPhotoLibraryUsageDescription |
| notification | POST_NOTIFICATIONS (API 33+) | (none — system prompt) |
| contacts | READ_CONTACTS | NSContactsUsageDescription |
| location | ACCESS_FINE_LOCATION + ACCESS_COARSE_LOCATION | NSLocationWhenInUseUsageDescription |
| locationBackground | ACCESS_BACKGROUND_LOCATION | NSLocationAlwaysAndWhenInUseUsageDescription |
| calendar | READ_CALENDAR + WRITE_CALENDAR | NSCalendarsUsageDescription / FullAccess |
| bluetooth | BLUETOOTH_SCAN + BLUETOOTH_CONNECT (API 31+) | NSBluetoothAlwaysUsageDescription |
| sms | READ_SMS | (unavailable on iOS) |
| callPhone | CALL_PHONE | (unavailable on iOS) |
Usage
import {
PermissionManager,
PermissionProvider,
PermissionGate,
usePermission,
} from 'react-native-permission-manager';1. Imperative (PermissionManager)
const result = await PermissionManager.check('camera');
// { type, status, canAskAgain }
await PermissionManager.request('camera', {
title: 'Camera',
message: 'Needed for your profile photo',
});
await PermissionManager.ensure('camera'); // request → Settings if blocked
await PermissionManager.openSettings();
// Permission groups — built-in name or an ad-hoc list
const { allGranted, results } = await PermissionManager.requestGroup('media');
await PermissionManager.checkGroup(['camera', 'microphone']);2. Hook (usePermission)
function CameraButton() {
const { status, granted, loading, request, ensure, openSettings } =
usePermission('camera', {
title: 'Camera',
message: 'Needed for profile photo',
});
if (loading) return null;
if (granted) return <OpenCamera />;
return (
<Button
title={status === 'BLOCKED' ? 'Open Settings' : 'Allow Camera'}
onPress={() => (status === 'BLOCKED' ? openSettings() : request())}
/>
);
}3. Provider (recommended at app root)
function App() {
return (
<PermissionProvider watchedPermissions={['camera', 'microphone', 'notification']}>
<RootNavigator />
</PermissionProvider>
);
}Caches statuses and refreshes when the app returns from Settings.
4. PermissionGate
<PermissionGate
permission="camera"
autoRequest
renderFallback={({ ensure, openSettings }) => (
<>
<Button title="Grant camera" onPress={() => ensure()} />
<Button title="Open Settings" onPress={() => openSettings()} />
</>
)}
>
<CameraScreen />
</PermissionGate>5. Permission Groups
Bundle related permissions and check/request them together, using a built-in group name or your own list:
import { usePermissionGroup } from 'react-native-permission-manager';
function MediaScreen() {
const { results, allGranted, anyGranted, loading, request } = usePermissionGroup('media');
// 'media' = ['camera', 'microphone', 'photos']
if (loading) return null;
return (
<Button
title={allGranted ? 'All set' : 'Grant media access'}
onPress={() => request()}
/>
);
}Built-in groups (PERMISSION_GROUPS):
| Group | Permissions |
|-------|-------------|
| media | camera, microphone, photos |
| location | location, locationBackground |
| contactsAndCalendar | contacts, calendar |
| communication | sms, callPhone, contacts |
| connectivity | notification, bluetooth |
You can also pass an ad-hoc array instead of a group name: usePermissionGroup(['camera', 'location']).
Props
<PermissionProvider />
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| children | ReactNode | — | App tree |
| watchedPermissions | PermissionInput[] | [] | Permissions to check on mount / resume |
| refreshOnForeground | boolean | true | Re-check when app becomes active |
<PermissionGate />
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| permission | PermissionInput | — | Permission to gate on |
| children | ReactNode | — | Rendered when granted / limited |
| config | PermissionOptions | — | Rationale / settings options |
| autoRequest | boolean | true | Request on mount if not granted |
| fallback | ReactNode | null | Simple fallback UI |
| renderFallback | (args) => ReactNode | — | Custom fallback with request / ensure / openSettings |
| renderLoading | () => ReactNode | spinner | Loading UI |
usePermission(permission, options?) returns
| Field | Type | Description |
|------|------|-------------|
| status | PermissionStatus \| undefined | Current status |
| granted | boolean | true if granted or limited |
| loading | boolean | Check/request in progress |
| error | Error \| undefined | Last error |
| result | PermissionResult \| undefined | Full result object |
| check() | () => Promise<PermissionResult> | Re-check |
| request(options?) | () => Promise<PermissionResult> | Request |
| ensure(options?) | () => Promise<PermissionResult> | Request + Settings flow |
| openSettings() | () => Promise<void> | Open OS settings |
usePermissionGroup(groupNameOrList) returns
| Field | Type | Description |
|------|------|-------------|
| results | PermissionResult[] | One result per permission, in group order |
| allGranted | boolean | true only if every permission is granted/limited |
| anyGranted | boolean | true if at least one permission is granted/limited |
| loading | boolean | Check/request in progress |
| error | Error \| undefined | Last error |
| check() | () => Promise<PermissionGroupResult> | Re-check the whole group |
| request(options?) | () => Promise<PermissionGroupResult> | Request the whole group |
PermissionOptions (request / ensure / config)
| Option | Type | Description |
|--------|------|-------------|
| title | string | Rationale dialog title (content only — does not force the dialog to appear) |
| message | string | Rationale dialog body (content only — does not force the dialog to appear) |
| confirmLabel | string | Confirm button |
| cancelLabel | string | Cancel button |
| showRationale | boolean | Enable/disable the automatic rationale flow entirely (default: true on Android). When enabled, the dialog is only ever shown if Android's shouldShowRequestPermissionRationale returns true (i.e. after a prior denial) — never on the very first request. |
| redirectToSettingsOnBlocked | boolean | Open Settings when blocked |
| settingsTitle | string | Settings dialog title |
| settingsMessage | string | Settings dialog body |
| renderRationale | (args) => Promise<boolean> | Custom rationale UI |
| renderSettingsPrompt | (args) => Promise<boolean> | Custom Settings UI |
Permission names
| Name | Enum |
|------|------|
| camera | PermissionType.CAMERA |
| microphone | PermissionType.MICROPHONE |
| photos | PermissionType.PHOTO_LIBRARY |
| notification | PermissionType.NOTIFICATIONS |
| contacts | PermissionType.CONTACTS |
| location | PermissionType.LOCATION_FOREGROUND |
| calendar | PermissionType.CALENDAR |
| bluetooth | PermissionType.BLUETOOTH |
| storage | PermissionType.STORAGE |
| locationBackground | PermissionType.LOCATION_BACKGROUND |
| sms | PermissionType.SMS |
| callPhone | PermissionType.CALL_PHONE |
API
PermissionManager
| Method | Description |
|--------|-------------|
| check(permission) | Read current status (no prompt) |
| request(permission, options?) | Request access |
| requestMultiple(permissions, options?) | Batch request |
| checkGroup(groupNameOrList) | Check every permission in a group (no prompt) |
| requestGroup(groupNameOrList, options?) | Request every permission in a group |
| ensure(permission, options?) | Request; if blocked → dialog → Settings → re-check |
| openSettings() | Open OS app settings |
| shouldShowRationale(permission) | Android rationale hint |
| addListener(fn) / removeListener(fn) | Status change events |
| getCached(permission) | In-memory snapshot |
Status values
GRANTED · DENIED · BLOCKED · LIMITED · UNAVAILABLE · NOT_DETERMINED
| Status | Meaning | Typical next step |
|--------|---------|-------------------|
| NOT_DETERMINED | Never asked | request() / ensure() |
| DENIED | Refused, but OS may allow asking again (mainly Android after first deny) | Show rationale, then request() again |
| BLOCKED | Permanently denied / cannot re-prompt from the app | openSettings() / ensure() |
| GRANTED / LIMITED | Usable | Proceed |
| UNAVAILABLE | Not supported, not declared in manifest/Info.plist, or native module not linked | Fix app config / rebuild |
iOS note: For Contacts, Camera, Microphone, Photos, and Notifications, a first Deny from the system dialog maps to BLOCKED (not DENIED). That matches iOS reality — the OS will not show that prompt again; only Settings can change it. Android often returns DENIED first (rationale / re-prompt possible) and upgrades to BLOCKED only after “Don’t ask again” / permanent deny.
How to run the example
# clone repo
git clone https://github.com/pawan3008/react-native-permission-manager.git
cd react-native-permission-manager
yarn install
yarn example # install example deps
cd example
yarn android # or: yarn iosExample screens: Camera, Microphone, Photos, Notification, Location, Contacts, Permission Groups.
More detail: example/README.md.
FAQ
Does this support the New Architecture?
Yes — TurboModule Spec + codegen, with old bridge fallback.
What is ensure?
Requests if needed; if permanently denied (BLOCKED), shows a dialog, opens Settings, then re-checks.
Can I customize the rationale UI?
Yes — renderRationale / renderSettingsPrompt, or default Alert dialogs.
Will passing title/message to request() always pop a dialog?
No. Those strings only change the text if Android wants a rationale (after a prior deny). First request usually goes straight to the system prompt. Use showRationale: false to turn the flow off.
Do I need every permission in the manifest / Info.plist?
Only what you use. Missing Android <uses-permission> → UNAVAILABLE + a warning like READ_MEDIA_IMAGES not declared in AndroidManifest.xml. Missing iOS usage string used to crash (abort_with_payload); we catch that and return UNAVAILABLE instead — still add the key if you want a real prompt.
Why does Contacts return BLOCKED right after the first Deny on iOS?
iOS won't show that dialog again from the app. BLOCKED here means "go to Settings". Android usually gives DENIED first so you can ask again.
What if the native module isn’t linked / I forgot to rebuild?
You get UNAVAILABLE and one console warning (no JS crash). Rebuild the native app (pod install on iOS).
What happens if request() is called twice at once?
Same permission (or same batch) shares one in-flight native call, so you don't get stacked system dialogs.
Architecture
See docs/ARCHITECTURE.md and docs/API.md.
Contributing
See CONTRIBUTING.md.
yarn install
yarn lint
yarn typecheck
yarn test
yarn buildLicense
MIT © Pawan Vishwakarma
