react-native-permission-handler 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Radu Ghitescu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,407 @@
1
+ # react-native-permission-handler
2
+
3
+ Smart permission UX flows for React Native. Pre-prompts, blocked handling, settings redirect, and foreground re-check — in one hook.
4
+
5
+ Built on [`react-native-permissions`](https://github.com/zoontek/react-native-permissions).
6
+
7
+ ## Why
8
+
9
+ Every React Native app that uses device features needs runtime permissions. The low-level check/request API is solved by `react-native-permissions`. But the **UX flow** — pre-prompts, blocked state recovery, settings redirect, foreground re-check — is not. Every team builds the same 150+ lines of boilerplate for every permission, in every project.
10
+
11
+ This library handles the full flow in a single hook call.
12
+
13
+ ## Quick Start
14
+
15
+ ```bash
16
+ npm install react-native-permission-handler react-native-permissions
17
+ ```
18
+
19
+ Set up `react-native-permissions` for your platform ([iOS](https://github.com/zoontek/react-native-permissions#ios) / [Android](https://github.com/zoontek/react-native-permissions#android) / [Expo](https://github.com/zoontek/react-native-permissions#expo)).
20
+
21
+ Then:
22
+
23
+ ```tsx
24
+ import { usePermissionHandler } from "react-native-permission-handler";
25
+ import { PERMISSIONS } from "react-native-permissions";
26
+
27
+ function QRScannerScreen() {
28
+ const camera = usePermissionHandler({
29
+ permission: PERMISSIONS.IOS.CAMERA,
30
+ prePrompt: {
31
+ title: "Camera Access",
32
+ message: "We need your camera to scan QR codes.",
33
+ },
34
+ blockedPrompt: {
35
+ title: "Camera Blocked",
36
+ message: "Please enable camera in Settings.",
37
+ },
38
+ });
39
+
40
+ if (camera.isChecking) return <LoadingSpinner />;
41
+ if (camera.isGranted) return <QRScanner />;
42
+ if (camera.isUnavailable) return <Text>Camera not available.</Text>;
43
+
44
+ // Pre-prompt and blocked modals are rendered by the hook's
45
+ // default UI. Or build your own using camera.state.
46
+ return null;
47
+ }
48
+ ```
49
+
50
+ That's it. The hook handles checking on mount, showing a pre-prompt before the system dialog, detecting blocked state, opening Settings, re-checking when the app returns, and firing callbacks.
51
+
52
+ ## State Machine
53
+
54
+ The core of the library is a pure state machine that drives the entire permission flow:
55
+
56
+ ```
57
+ +-------+
58
+ | idle |
59
+ +---+---+
60
+ |
61
+ CHECK
62
+ |
63
+ +-----v-----+
64
+ | checking |
65
+ +-----+-----+
66
+ |
67
+ +---------------+---------------+
68
+ | | |
69
+ GRANTED DENIED BLOCKED
70
+ | | |
71
+ +-----v---+ +-----v-----+ +-----v-------+
72
+ | granted | | prePrompt | | blockedPrompt|
73
+ +---------+ +-----+-----+ +------+------+
74
+ | |
75
+ +--------+--------+ OPEN_SETTINGS
76
+ | | |
77
+ CONFIRM DISMISS +----v----------+
78
+ | | | openingSettings|
79
+ +-----v-----+ +-----v+ +----+----------+
80
+ | requesting | |denied| |
81
+ +-----+------+ +------+ SETTINGS_RETURN
82
+ | |
83
+ +--------+--------+ +---------v-----------+
84
+ | | | |recheckingAfterSettings|
85
+ GRANTED DENIED BLOCKED +---------+-----------+
86
+ | | | |
87
+ +-----v-+ +---v--+ +---v--------+ +---+---+
88
+ |granted| |denied| |blockedPrompt| |granted| or back
89
+ +-------+ +------+ +------------+ +-------+ to blockedPrompt
90
+ ```
91
+
92
+ **States:**
93
+
94
+ | State | Description |
95
+ |-------|-------------|
96
+ | `idle` | Initial state. Nothing has happened yet. |
97
+ | `checking` | Permission status is being checked. |
98
+ | `prePrompt` | Permission is requestable. Show a friendly explanation before the system dialog. |
99
+ | `requesting` | System permission dialog is showing. |
100
+ | `granted` | Permission is granted. Show the protected content. |
101
+ | `denied` | User dismissed the pre-prompt ("Not Now") or denied via system dialog. Permission is still requestable next time. |
102
+ | `blocked` | Permission is permanently denied. Only Settings can fix it. |
103
+ | `blockedPrompt` | Showing the "go to Settings" prompt. |
104
+ | `openingSettings` | User tapped "Open Settings". Waiting for app to return to foreground. |
105
+ | `recheckingAfterSettings` | App returned from Settings. Re-checking permission status. |
106
+ | `unavailable` | Device doesn't support this feature. Terminal state. |
107
+
108
+ ## API Reference
109
+
110
+ ### `usePermissionHandler(config)`
111
+
112
+ The main hook. Manages the full permission lifecycle.
113
+
114
+ **Config:**
115
+
116
+ ```typescript
117
+ {
118
+ // The permission to manage (from react-native-permissions PERMISSIONS constants)
119
+ permission: Permission | "notifications";
120
+
121
+ // Pre-prompt shown BEFORE the system dialog
122
+ prePrompt: {
123
+ title: string;
124
+ message: string;
125
+ confirmLabel?: string; // default: "Continue"
126
+ cancelLabel?: string; // default: "Not Now"
127
+ };
128
+
129
+ // Prompt shown when permission is permanently blocked
130
+ blockedPrompt: {
131
+ title: string;
132
+ message: string;
133
+ settingsLabel?: string; // default: "Open Settings"
134
+ };
135
+
136
+ // Callbacks for analytics
137
+ onGrant?: () => void;
138
+ onDeny?: () => void;
139
+ onBlock?: () => void;
140
+ onSettingsReturn?: (granted: boolean) => void;
141
+
142
+ // Options
143
+ autoCheck?: boolean; // default: true — check on mount
144
+ recheckOnForeground?: boolean; // default: false
145
+ }
146
+ ```
147
+
148
+ **Returns:**
149
+
150
+ ```typescript
151
+ {
152
+ state: PermissionFlowState; // current state machine state
153
+ nativeStatus: PermissionStatus | null; // raw status from react-native-permissions
154
+
155
+ // Convenience booleans
156
+ isGranted: boolean;
157
+ isDenied: boolean;
158
+ isBlocked: boolean;
159
+ isChecking: boolean;
160
+ isUnavailable: boolean;
161
+
162
+ // Actions
163
+ request: () => void; // confirm pre-prompt → fire system dialog
164
+ check: () => void; // manually re-check permission status
165
+ dismiss: () => void; // dismiss pre-prompt ("Not Now")
166
+ openSettings: () => void; // open app settings for blocked permissions
167
+ }
168
+ ```
169
+
170
+ **Example — full control over UI:**
171
+
172
+ ```tsx
173
+ function CameraScreen() {
174
+ const camera = usePermissionHandler({
175
+ permission: PERMISSIONS.IOS.CAMERA,
176
+ prePrompt: {
177
+ title: "Camera Access",
178
+ message: "We need your camera to scan QR codes.",
179
+ },
180
+ blockedPrompt: {
181
+ title: "Camera Blocked",
182
+ message: "Please enable camera in Settings.",
183
+ },
184
+ onGrant: () => analytics.track("camera_granted"),
185
+ onDeny: () => analytics.track("camera_denied"),
186
+ });
187
+
188
+ if (camera.isGranted) return <CameraView />;
189
+
190
+ if (camera.state === "prePrompt") {
191
+ return (
192
+ <MyCustomModal>
193
+ <Text>We need camera access</Text>
194
+ <Button title="Allow" onPress={camera.request} />
195
+ <Button title="Not Now" onPress={camera.dismiss} />
196
+ </MyCustomModal>
197
+ );
198
+ }
199
+
200
+ if (camera.state === "blockedPrompt") {
201
+ return (
202
+ <MyCustomModal>
203
+ <Text>Camera is blocked</Text>
204
+ <Button title="Open Settings" onPress={camera.openSettings} />
205
+ </MyCustomModal>
206
+ );
207
+ }
208
+
209
+ return <LoadingSpinner />;
210
+ }
211
+ ```
212
+
213
+ ---
214
+
215
+ ### `useMultiplePermissions(config)`
216
+
217
+ Orchestrates flows for features needing multiple permissions (e.g., video call = camera + microphone).
218
+
219
+ **Config:**
220
+
221
+ ```typescript
222
+ {
223
+ permissions: Array<{
224
+ permission: Permission | "notifications";
225
+ prePrompt: PrePromptConfig;
226
+ blockedPrompt: BlockedPromptConfig;
227
+ onGrant?: () => void;
228
+ onDeny?: () => void;
229
+ onBlock?: () => void;
230
+ }>;
231
+
232
+ strategy: "sequential" | "parallel";
233
+ // sequential: ask one at a time, stop on denial/block
234
+ // parallel: check all at once, then request denied ones
235
+
236
+ onAllGranted?: () => void;
237
+ }
238
+ ```
239
+
240
+ **Returns:**
241
+
242
+ ```typescript
243
+ {
244
+ statuses: Record<string, PermissionFlowState>; // keyed by permission identifier
245
+ allGranted: boolean;
246
+ request: () => void; // start the multi-permission flow
247
+ }
248
+ ```
249
+
250
+ **Example:**
251
+
252
+ ```tsx
253
+ function VideoCallScreen() {
254
+ const perms = useMultiplePermissions({
255
+ permissions: [
256
+ {
257
+ permission: PERMISSIONS.IOS.CAMERA,
258
+ prePrompt: { title: "Camera", message: "Needed for video." },
259
+ blockedPrompt: { title: "Camera Blocked", message: "Enable in Settings." },
260
+ },
261
+ {
262
+ permission: PERMISSIONS.IOS.MICROPHONE,
263
+ prePrompt: { title: "Microphone", message: "Needed for audio." },
264
+ blockedPrompt: { title: "Mic Blocked", message: "Enable in Settings." },
265
+ },
266
+ ],
267
+ strategy: "sequential",
268
+ onAllGranted: () => startCall(),
269
+ });
270
+
271
+ if (perms.allGranted) return <VideoCallUI />;
272
+
273
+ return <Button title="Start Call" onPress={perms.request} />;
274
+ }
275
+ ```
276
+
277
+ ---
278
+
279
+ ### `<PermissionGate>`
280
+
281
+ Declarative component that renders children only when permission is granted.
282
+
283
+ **Props:**
284
+
285
+ ```typescript
286
+ {
287
+ permission: Permission | "notifications";
288
+ prePrompt: PrePromptConfig;
289
+ blockedPrompt: BlockedPromptConfig;
290
+ children: ReactNode; // shown when granted
291
+ fallback?: ReactNode; // shown while checking
292
+ onGrant?: () => void;
293
+ onDeny?: () => void;
294
+ onBlock?: () => void;
295
+ onSettingsReturn?: (granted: boolean) => void;
296
+
297
+ // Custom UI (optional — default modals are used if omitted)
298
+ renderPrePrompt?: (props: {
299
+ config: PrePromptConfig;
300
+ onConfirm: () => void;
301
+ onCancel: () => void;
302
+ }) => ReactNode;
303
+ renderBlockedPrompt?: (props: {
304
+ config: BlockedPromptConfig;
305
+ onOpenSettings: () => void;
306
+ }) => ReactNode;
307
+ }
308
+ ```
309
+
310
+ **Example:**
311
+
312
+ ```tsx
313
+ <PermissionGate
314
+ permission={PERMISSIONS.IOS.CAMERA}
315
+ prePrompt={{ title: "Camera", message: "We need camera access." }}
316
+ blockedPrompt={{ title: "Blocked", message: "Enable in Settings." }}
317
+ fallback={<LoadingSpinner />}
318
+ >
319
+ <CameraView />
320
+ </PermissionGate>
321
+ ```
322
+
323
+ ---
324
+
325
+ ### `<DefaultPrePrompt>` / `<DefaultBlockedPrompt>`
326
+
327
+ The default modal components used by `usePermissionHandler` and `PermissionGate`. You can import and use them directly if you want the default look with custom behavior.
328
+
329
+ ```tsx
330
+ import { DefaultPrePrompt, DefaultBlockedPrompt } from "react-native-permission-handler";
331
+
332
+ <DefaultPrePrompt
333
+ visible={showPrePrompt}
334
+ title="Camera Access"
335
+ message="We need your camera."
336
+ confirmLabel="Allow"
337
+ cancelLabel="Not Now"
338
+ onConfirm={handleConfirm}
339
+ onCancel={handleCancel}
340
+ />
341
+
342
+ <DefaultBlockedPrompt
343
+ visible={showBlocked}
344
+ title="Camera Blocked"
345
+ message="Please enable in Settings."
346
+ settingsLabel="Open Settings"
347
+ onOpenSettings={handleOpenSettings}
348
+ />
349
+ ```
350
+
351
+ These use only React Native primitives (`Modal`, `View`, `Text`, `TouchableOpacity`) — no third-party UI dependencies.
352
+
353
+ ---
354
+
355
+ ### `transition(state, event)`
356
+
357
+ The raw state machine function. For advanced use cases where you want to build your own hook or integrate with a state management library.
358
+
359
+ ```typescript
360
+ import { transition } from "react-native-permission-handler";
361
+
362
+ const next = transition("prePrompt", { type: "PRE_PROMPT_CONFIRM" });
363
+ // → "requesting"
364
+ ```
365
+
366
+ Pure function, no side effects, no React dependency.
367
+
368
+ ## Platform Notes
369
+
370
+ **iOS:**
371
+ - The system permission dialog can only be shown **once** per permission. If the user denies it, you can never show it again programmatically. This is why the pre-prompt is critical — it preserves the one-time system dialog.
372
+ - `check()` returns `DENIED` for both "never asked" and "denied once" — both are still requestable.
373
+ - `BLOCKED` means the user denied via the system dialog or disabled the permission in Settings.
374
+
375
+ **Android:**
376
+ - After 2 denials, Android 11+ auto-blocks the permission. No more system dialogs.
377
+ - For notification permissions on Android 13+, `checkNotifications()` never returns `BLOCKED` — the library handles this by using `requestNotifications()` for accurate status.
378
+
379
+ **Notifications:**
380
+ - Pass `"notifications"` as the permission identifier. The library automatically routes to `checkNotifications`/`requestNotifications` instead of `check`/`request`.
381
+
382
+ ## Requirements
383
+
384
+ - React Native >= 0.76
385
+ - React >= 18
386
+ - `react-native-permissions` >= 4.0.0
387
+
388
+ **Expo:** Add the config plugin to your `app.json`:
389
+
390
+ ```json
391
+ {
392
+ "plugins": [
393
+ [
394
+ "react-native-permissions",
395
+ {
396
+ "iosPermissions": ["Camera", "Microphone", "LocationWhenInUse"]
397
+ }
398
+ ]
399
+ ]
400
+ }
401
+ ```
402
+
403
+ **Bare React Native:** Follow the [react-native-permissions setup guide](https://github.com/zoontek/react-native-permissions#setup).
404
+
405
+ ## License
406
+
407
+ MIT
@@ -0,0 +1,146 @@
1
+ import { Permission, PermissionStatus } from 'react-native-permissions';
2
+ import * as react_jsx_runtime from 'react/jsx-runtime';
3
+ import { ReactNode } from 'react';
4
+
5
+ /**
6
+ * States of the permission flow state machine.
7
+ */
8
+ type PermissionFlowState = "idle" | "checking" | "prePrompt" | "requesting" | "granted" | "denied" | "blocked" | "blockedPrompt" | "openingSettings" | "recheckingAfterSettings" | "unavailable";
9
+ /**
10
+ * Events that drive state transitions.
11
+ */
12
+ type PermissionFlowEvent = {
13
+ type: "CHECK";
14
+ } | {
15
+ type: "CHECK_RESULT";
16
+ status: PermissionStatus;
17
+ } | {
18
+ type: "PRE_PROMPT_CONFIRM";
19
+ } | {
20
+ type: "PRE_PROMPT_DISMISS";
21
+ } | {
22
+ type: "REQUEST_RESULT";
23
+ status: PermissionStatus;
24
+ } | {
25
+ type: "OPEN_SETTINGS";
26
+ } | {
27
+ type: "SETTINGS_RETURN";
28
+ } | {
29
+ type: "RECHECK_RESULT";
30
+ status: PermissionStatus;
31
+ };
32
+ /**
33
+ * Configuration for the pre-prompt modal.
34
+ */
35
+ interface PrePromptConfig {
36
+ title: string;
37
+ message: string;
38
+ confirmLabel?: string;
39
+ cancelLabel?: string;
40
+ }
41
+ /**
42
+ * Configuration for the blocked-prompt modal.
43
+ */
44
+ interface BlockedPromptConfig {
45
+ title: string;
46
+ message: string;
47
+ settingsLabel?: string;
48
+ }
49
+ /**
50
+ * Callbacks for analytics and side effects.
51
+ */
52
+ interface PermissionCallbacks {
53
+ onGrant?: () => void;
54
+ onDeny?: () => void;
55
+ onBlock?: () => void;
56
+ onSettingsReturn?: (granted: boolean) => void;
57
+ }
58
+ /**
59
+ * Configuration for usePermissionHandler.
60
+ */
61
+ interface PermissionHandlerConfig extends PermissionCallbacks {
62
+ permission: Permission | "notifications";
63
+ prePrompt: PrePromptConfig;
64
+ blockedPrompt: BlockedPromptConfig;
65
+ autoCheck?: boolean;
66
+ recheckOnForeground?: boolean;
67
+ }
68
+ /**
69
+ * Return type of usePermissionHandler.
70
+ */
71
+ interface PermissionHandlerResult {
72
+ state: PermissionFlowState;
73
+ nativeStatus: PermissionStatus | null;
74
+ isGranted: boolean;
75
+ isDenied: boolean;
76
+ isBlocked: boolean;
77
+ isChecking: boolean;
78
+ isUnavailable: boolean;
79
+ request: () => void;
80
+ check: () => void;
81
+ dismiss: () => void;
82
+ openSettings: () => void;
83
+ }
84
+ /**
85
+ * Configuration for a single permission within useMultiplePermissions.
86
+ */
87
+ interface MultiPermissionEntry extends PermissionCallbacks {
88
+ permission: Permission | "notifications";
89
+ prePrompt: PrePromptConfig;
90
+ blockedPrompt: BlockedPromptConfig;
91
+ }
92
+ /**
93
+ * Configuration for useMultiplePermissions.
94
+ */
95
+ interface MultiplePermissionsConfig {
96
+ permissions: MultiPermissionEntry[];
97
+ strategy: "sequential" | "parallel";
98
+ onAllGranted?: () => void;
99
+ }
100
+ /**
101
+ * Return type of useMultiplePermissions.
102
+ */
103
+ interface MultiplePermissionsResult {
104
+ statuses: Record<string, PermissionFlowState>;
105
+ allGranted: boolean;
106
+ request: () => void;
107
+ }
108
+
109
+ declare function transition(state: PermissionFlowState, event: PermissionFlowEvent): PermissionFlowState;
110
+
111
+ declare function usePermissionHandler(config: PermissionHandlerConfig): PermissionHandlerResult;
112
+
113
+ declare function useMultiplePermissions(config: MultiplePermissionsConfig): MultiplePermissionsResult;
114
+
115
+ interface PermissionGateProps extends PermissionCallbacks {
116
+ permission: Permission | "notifications";
117
+ prePrompt: PrePromptConfig;
118
+ blockedPrompt: BlockedPromptConfig;
119
+ children: ReactNode;
120
+ fallback?: ReactNode;
121
+ renderPrePrompt?: (props: {
122
+ config: PrePromptConfig;
123
+ onConfirm: () => void;
124
+ onCancel: () => void;
125
+ }) => ReactNode;
126
+ renderBlockedPrompt?: (props: {
127
+ config: BlockedPromptConfig;
128
+ onOpenSettings: () => void;
129
+ }) => ReactNode;
130
+ }
131
+ declare function PermissionGate({ permission, prePrompt, blockedPrompt, children, fallback, renderPrePrompt, renderBlockedPrompt, onGrant, onDeny, onBlock, onSettingsReturn, }: PermissionGateProps): react_jsx_runtime.JSX.Element;
132
+
133
+ interface DefaultPrePromptProps extends PrePromptConfig {
134
+ visible: boolean;
135
+ onConfirm: () => void;
136
+ onCancel: () => void;
137
+ }
138
+ declare function DefaultPrePrompt({ visible, title, message, confirmLabel, cancelLabel, onConfirm, onCancel, }: DefaultPrePromptProps): react_jsx_runtime.JSX.Element;
139
+
140
+ interface DefaultBlockedPromptProps extends BlockedPromptConfig {
141
+ visible: boolean;
142
+ onOpenSettings: () => void;
143
+ }
144
+ declare function DefaultBlockedPrompt({ visible, title, message, settingsLabel, onOpenSettings, }: DefaultBlockedPromptProps): react_jsx_runtime.JSX.Element;
145
+
146
+ export { type BlockedPromptConfig, DefaultBlockedPrompt, type DefaultBlockedPromptProps, DefaultPrePrompt, type DefaultPrePromptProps, type MultiPermissionEntry, type MultiplePermissionsConfig, type MultiplePermissionsResult, type PermissionCallbacks, type PermissionFlowEvent, type PermissionFlowState, PermissionGate, type PermissionGateProps, type PermissionHandlerConfig, type PermissionHandlerResult, type PrePromptConfig, transition, useMultiplePermissions, usePermissionHandler };