react-native-permission-gate 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.
Files changed (50) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +225 -0
  3. package/lib/module/adapters/reactNativePermissions.js +97 -0
  4. package/lib/module/adapters/reactNativePermissions.js.map +1 -0
  5. package/lib/module/components/PermissionGate.js +60 -0
  6. package/lib/module/components/PermissionGate.js.map +1 -0
  7. package/lib/module/context.js +29 -0
  8. package/lib/module/context.js.map +1 -0
  9. package/lib/module/core/machine.js +107 -0
  10. package/lib/module/core/machine.js.map +1 -0
  11. package/lib/module/hooks/usePermission.js +162 -0
  12. package/lib/module/hooks/usePermission.js.map +1 -0
  13. package/lib/module/hooks/usePermissions.js +165 -0
  14. package/lib/module/hooks/usePermissions.js.map +1 -0
  15. package/lib/module/imperative.js +24 -0
  16. package/lib/module/imperative.js.map +1 -0
  17. package/lib/module/index.js +15 -0
  18. package/lib/module/index.js.map +1 -0
  19. package/lib/module/package.json +1 -0
  20. package/lib/module/types.js +2 -0
  21. package/lib/module/types.js.map +1 -0
  22. package/lib/typescript/package.json +1 -0
  23. package/lib/typescript/src/adapters/reactNativePermissions.d.ts +8 -0
  24. package/lib/typescript/src/adapters/reactNativePermissions.d.ts.map +1 -0
  25. package/lib/typescript/src/components/PermissionGate.d.ts +34 -0
  26. package/lib/typescript/src/components/PermissionGate.d.ts.map +1 -0
  27. package/lib/typescript/src/context.d.ts +22 -0
  28. package/lib/typescript/src/context.d.ts.map +1 -0
  29. package/lib/typescript/src/core/machine.d.ts +47 -0
  30. package/lib/typescript/src/core/machine.d.ts.map +1 -0
  31. package/lib/typescript/src/hooks/usePermission.d.ts +8 -0
  32. package/lib/typescript/src/hooks/usePermission.d.ts.map +1 -0
  33. package/lib/typescript/src/hooks/usePermissions.d.ts +32 -0
  34. package/lib/typescript/src/hooks/usePermissions.d.ts.map +1 -0
  35. package/lib/typescript/src/imperative.d.ts +13 -0
  36. package/lib/typescript/src/imperative.d.ts.map +1 -0
  37. package/lib/typescript/src/index.d.ts +11 -0
  38. package/lib/typescript/src/index.d.ts.map +1 -0
  39. package/lib/typescript/src/types.d.ts +89 -0
  40. package/lib/typescript/src/types.d.ts.map +1 -0
  41. package/package.json +145 -0
  42. package/src/adapters/reactNativePermissions.ts +119 -0
  43. package/src/components/PermissionGate.tsx +81 -0
  44. package/src/context.tsx +43 -0
  45. package/src/core/machine.ts +98 -0
  46. package/src/hooks/usePermission.ts +205 -0
  47. package/src/hooks/usePermissions.ts +241 -0
  48. package/src/imperative.ts +35 -0
  49. package/src/index.tsx +42 -0
  50. package/src/types.ts +114 -0
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Emre Cengiz
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,225 @@
1
+ # react-native-permission-gate
2
+
3
+ [![npm version](https://img.shields.io/npm/v/react-native-permission-gate.svg)](https://www.npmjs.com/package/react-native-permission-gate)
4
+ [![CI](https://github.com/cengizemre/react-native-permission-gate/actions/workflows/ci.yml/badge.svg)](https://github.com/cengizemre/react-native-permission-gate/actions/workflows/ci.yml)
5
+ [![license](https://img.shields.io/npm/l/react-native-permission-gate.svg)](./LICENSE)
6
+ [![types](https://img.shields.io/npm/types/react-native-permission-gate.svg)](./src/types.ts)
7
+
8
+ Headless permission-flow orchestration for React Native: a state-machine hook, rationale gating, and a `<PermissionGate>` component. Bring your own UI.
9
+
10
+ `react-native-permissions` gives you the native call. The flow around it - show a pre-prompt, request, detect "denied", route to Settings, re-check when the user comes back - is what everyone hand-writes in every app. This library is that flow, as a headless state machine. It renders nothing and ships no modals.
11
+
12
+ ```tsx
13
+ const cam = usePermission('camera', { rationale: true });
14
+
15
+ <Button title="Enable camera" onPress={cam.request} />;
16
+ {cam.phase === 'rationale' && <MyPrePrompt onConfirm={cam.confirmRationale} onCancel={cam.cancelRationale} />}
17
+ {cam.isBlocked && <MyBlockedNotice onOpenSettings={cam.openSettings} />}
18
+ ```
19
+
20
+ ---
21
+
22
+ ## Why
23
+
24
+ - No baked-in modals or styles - it's headless, so you drive your own components off `status` / `phase`.
25
+ - A real, testable state machine (`idle → rationale → requesting → settled`) instead of ad-hoc booleans.
26
+ - One hook covers the whole flow: check, rationale pre-prompt, native prompt, blocked → Settings, and focus re-validation.
27
+ - `request()` resolves with the final status, so it works imperatively (`await`) or declaratively (render on `phase`).
28
+ - The core has zero native dependencies, so your flows unit-test without a device.
29
+ - Full TypeScript with a normalized cross-platform status model.
30
+
31
+ ## Installation
32
+
33
+ ```sh
34
+ yarn add react-native-permission-gate react-native-permissions
35
+ # or
36
+ npm install react-native-permission-gate react-native-permissions
37
+ ```
38
+
39
+ `react-native-permissions` is a peer dependency and needs its own one-time native setup (iOS `Podfile` `setup_permissions` + `Info.plist` usage strings, Android manifest entries). Follow its [setup guide](https://github.com/zoontek/react-native-permissions#setup) - that's the only native step.
40
+
41
+ ## Quick start
42
+
43
+ ```tsx
44
+ import { usePermission } from 'react-native-permission-gate';
45
+
46
+ function CameraButton() {
47
+ const cam = usePermission('camera', { rationale: true });
48
+
49
+ return (
50
+ <>
51
+ <Button title="Enable camera" onPress={() => cam.request()} />
52
+
53
+ <Modal visible={cam.phase === 'rationale'} transparent>
54
+ <PrePrompt
55
+ onConfirm={cam.confirmRationale}
56
+ onCancel={cam.cancelRationale}
57
+ />
58
+ </Modal>
59
+
60
+ {cam.isBlocked && <BlockedNotice onPress={cam.openSettings} />}
61
+ </>
62
+ );
63
+ }
64
+ ```
65
+
66
+ Prefer a declarative gate? Same flow, less wiring:
67
+
68
+ ```tsx
69
+ import { PermissionGate } from 'react-native-permission-gate';
70
+
71
+ <PermissionGate
72
+ name="camera"
73
+ rationale={({ confirm, cancel }) => <PrePrompt onConfirm={confirm} onCancel={cancel} />}
74
+ blocked={({ openSettings }) => <BlockedNotice onPress={openSettings} />}
75
+ fallback={<Spinner />}
76
+ >
77
+ <CameraView />
78
+ </PermissionGate>
79
+ ```
80
+
81
+ ## Core concepts
82
+
83
+ Two independent axes - kept separate on purpose:
84
+
85
+ | `status` (the OS truth) | Meaning |
86
+ | --- | --- |
87
+ | `granted` | Access allowed |
88
+ | `limited` | Partial access (iOS: selected photos/contacts) |
89
+ | `requestable` | Not granted yet, but the OS prompt can still show |
90
+ | `blocked` | Denied; the OS won't prompt again - only Settings helps |
91
+ | `unavailable` | Not supported on this device/build |
92
+
93
+ | `phase` (the flow position) | Meaning |
94
+ | --- | --- |
95
+ | `idle` | Nothing in flight |
96
+ | `rationale` | Your pre-prompt is showing, awaiting the user's choice |
97
+ | `requesting` | The native OS dialog is up |
98
+ | `settled` | The flow finished - read `status` for the outcome |
99
+
100
+ ```mermaid
101
+ stateDiagram-v2
102
+ [*] --> idle
103
+ idle --> rationale: request(), rationale on
104
+ idle --> requesting: request(), rationale off
105
+ rationale --> requesting: confirmRationale()
106
+ rationale --> idle: cancelRationale()
107
+ requesting --> settled: OS responds
108
+ settled --> settled: refresh() / app focus
109
+ ```
110
+
111
+ When `request()` is called on a status that can't be prompted (`granted`, `blocked`, `limited`, `unavailable`), the machine settles immediately and resolves - so a `blocked` result is something you _react to_, never a dead end.
112
+
113
+ ## API
114
+
115
+ ### `usePermission(name, options?)`
116
+
117
+ ```ts
118
+ const controller = usePermission('camera', {
119
+ rationale: false, // gate the native prompt behind a `rationale` phase
120
+ revalidateOnFocus: true, // re-check when the app returns to the foreground
121
+ requestOnMount: false, // run the flow automatically on mount
122
+ onStatusChange: (s) => {}, // called when the resolved status changes
123
+ adapter, // override the backend (defaults to react-native-permissions)
124
+ });
125
+ ```
126
+
127
+ Returns a `PermissionController`:
128
+
129
+ | Field | Type | Description |
130
+ | --- | --- | --- |
131
+ | `status` | `PermissionStatus` | Current OS truth |
132
+ | `phase` | `FlowPhase` | Current flow position |
133
+ | `isGranted` | `boolean` | `status === 'granted'` |
134
+ | `isBlocked` | `boolean` | `status === 'blocked'` |
135
+ | `request()` | `() => Promise<PermissionStatus>` | Run the flow; resolves with the final status |
136
+ | `confirmRationale()` | `() => void` | Proceed from `rationale` to the native prompt |
137
+ | `cancelRationale()` | `() => void` | Abort `rationale`, back to `idle` |
138
+ | `openSettings()` | `() => Promise<void>` | Open this app's OS settings |
139
+ | `refresh()` | `() => Promise<PermissionStatus>` | Re-check on demand |
140
+
141
+ Concurrent/double `request()` calls are de-duplicated - a double-tap never fires two native prompts or orphans a promise.
142
+
143
+ ### `usePermissions(names, options?)`
144
+
145
+ Aggregate variant for permissions acquired together (e.g. camera + microphone for video). One shared rationale gate, one `request()`, one status map. Pass a **stable / memoized** `names` array.
146
+
147
+ ```tsx
148
+ const media = usePermissions(['camera', 'microphone'], { rationale: true });
149
+
150
+ media.statuses; // { camera: 'granted', microphone: 'requestable' }
151
+ media.allGranted; // every requested permission is granted
152
+ media.anyBlocked; // at least one is blocked
153
+
154
+ <Button title="Start recording" onPress={media.request} />;
155
+ {media.phase === 'rationale' && (
156
+ <MediaPrePrompt onConfirm={media.confirmRationale} onCancel={media.cancelRationale} />
157
+ )}
158
+ ```
159
+
160
+ `request()` prompts each not-yet-granted permission in turn (behind the single shared rationale gate) and resolves with the final status map. Already-granted permissions are skipped; `allGranted` counts only `granted` as satisfied.
161
+
162
+ ### `<PermissionGate>`
163
+
164
+ Declarative wrapper over the hook. Renders `children` once granted (or `limited`); otherwise renders your `rationale` / `blocked` / `fallback`. Pass a **function** as `children` to take full control for every state.
165
+
166
+ Unlike the bare hook, the gate starts the request flow automatically on mount (`requestOnMount: true`) — override it via `options` if you want a manual trigger.
167
+
168
+ ```tsx
169
+ <PermissionGate name="microphone" options={{ rationale: true }}>
170
+ {(p) => <Text>{p.status} / {p.phase}</Text>}
171
+ </PermissionGate>
172
+ ```
173
+
174
+ ### `<PermissionProvider>`
175
+
176
+ Optional. Sets a shared adapter and default options for the subtree - the hook works fine without it.
177
+
178
+ ```tsx
179
+ <PermissionProvider defaults={{ rationale: true, revalidateOnFocus: true }}>
180
+ <App />
181
+ </PermissionProvider>
182
+ ```
183
+
184
+ ### Imperative (outside React)
185
+
186
+ For services, sagas, and utilities - the raw calls, no rationale UX:
187
+
188
+ ```ts
189
+ import {
190
+ checkPermission,
191
+ requestPermission,
192
+ openAppSettings,
193
+ } from 'react-native-permission-gate';
194
+
195
+ const status = await checkPermission('camera');
196
+ ```
197
+
198
+ ## Supported permissions
199
+
200
+ **Cross-platform:** `camera`, `microphone`, `photoLibrary`, `contacts`, `calendar`, `calendarWriteOnly`, `locationWhenInUse`, `locationAlways`, `notifications`, `bluetooth`, `motion`.
201
+
202
+ **iOS only:** `photoLibraryAddOnly`, `mediaLibrary`, `reminders`, `speechRecognition`, `appTrackingTransparency`, `faceId`.
203
+
204
+ **Android only:** `storageReadAudio`, `storageReadVideo`, `bodySensors`.
205
+
206
+ Each alias maps to the correct native permission per platform; anything not meaningful on the current platform resolves to `unavailable`. Need more? Provide a custom `adapter` - or open an issue.
207
+
208
+ ## Testing your flows
209
+
210
+ Because the core is adapter-agnostic, you can test permission logic with a mock adapter - no device, no native module:
211
+
212
+ ```ts
213
+ const adapter: PermissionAdapter = {
214
+ isSupported: () => true,
215
+ check: async () => 'requestable',
216
+ request: async () => 'granted',
217
+ openSettings: async () => {},
218
+ };
219
+
220
+ const { result } = renderHook(() => usePermission('camera', { adapter }));
221
+ ```
222
+
223
+ ## License
224
+
225
+ MIT © [Emre Cengiz](https://github.com/cengizemre)
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+
3
+ import { Platform } from 'react-native';
4
+ import { PERMISSIONS, RESULTS, check, request, checkNotifications, requestNotifications, openSettings } from 'react-native-permissions';
5
+ const IOS = {
6
+ camera: PERMISSIONS.IOS.CAMERA,
7
+ microphone: PERMISSIONS.IOS.MICROPHONE,
8
+ photoLibrary: PERMISSIONS.IOS.PHOTO_LIBRARY,
9
+ photoLibraryAddOnly: PERMISSIONS.IOS.PHOTO_LIBRARY_ADD_ONLY,
10
+ mediaLibrary: PERMISSIONS.IOS.MEDIA_LIBRARY,
11
+ contacts: PERMISSIONS.IOS.CONTACTS,
12
+ calendar: PERMISSIONS.IOS.CALENDARS,
13
+ calendarWriteOnly: PERMISSIONS.IOS.CALENDARS_WRITE_ONLY,
14
+ reminders: PERMISSIONS.IOS.REMINDERS,
15
+ locationWhenInUse: PERMISSIONS.IOS.LOCATION_WHEN_IN_USE,
16
+ locationAlways: PERMISSIONS.IOS.LOCATION_ALWAYS,
17
+ bluetooth: PERMISSIONS.IOS.BLUETOOTH,
18
+ motion: PERMISSIONS.IOS.MOTION,
19
+ speechRecognition: PERMISSIONS.IOS.SPEECH_RECOGNITION,
20
+ appTrackingTransparency: PERMISSIONS.IOS.APP_TRACKING_TRANSPARENCY,
21
+ faceId: PERMISSIONS.IOS.FACE_ID
22
+ // Android-only aliases (storageReadAudio, storageReadVideo, bodySensors)
23
+ // resolve to unavailable here.
24
+ };
25
+ const ANDROID = {
26
+ camera: PERMISSIONS.ANDROID.CAMERA,
27
+ microphone: PERMISSIONS.ANDROID.RECORD_AUDIO,
28
+ photoLibrary: PERMISSIONS.ANDROID.READ_MEDIA_IMAGES,
29
+ storageReadAudio: PERMISSIONS.ANDROID.READ_MEDIA_AUDIO,
30
+ storageReadVideo: PERMISSIONS.ANDROID.READ_MEDIA_VIDEO,
31
+ contacts: PERMISSIONS.ANDROID.READ_CONTACTS,
32
+ calendar: PERMISSIONS.ANDROID.READ_CALENDAR,
33
+ calendarWriteOnly: PERMISSIONS.ANDROID.WRITE_CALENDAR,
34
+ locationWhenInUse: PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION,
35
+ locationAlways: PERMISSIONS.ANDROID.ACCESS_BACKGROUND_LOCATION,
36
+ bluetooth: PERMISSIONS.ANDROID.BLUETOOTH_CONNECT,
37
+ motion: PERMISSIONS.ANDROID.ACTIVITY_RECOGNITION,
38
+ bodySensors: PERMISSIONS.ANDROID.BODY_SENSORS
39
+ // iOS-only aliases (photoLibraryAddOnly, mediaLibrary, reminders,
40
+ // speechRecognition, appTrackingTransparency, faceId) → unavailable here.
41
+ };
42
+ const TABLE = Platform.OS === 'ios' ? IOS : Platform.OS === 'android' ? ANDROID : {};
43
+ function toStatus(result) {
44
+ switch (result) {
45
+ case RESULTS.GRANTED:
46
+ return 'granted';
47
+ case RESULTS.LIMITED:
48
+ return 'limited';
49
+ case RESULTS.BLOCKED:
50
+ return 'blocked';
51
+ case RESULTS.DENIED:
52
+ return 'requestable';
53
+ case RESULTS.UNAVAILABLE:
54
+ default:
55
+ return 'unavailable';
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Default adapter, backed by `react-native-permissions`. Notifications are a
61
+ * special case in the native library (a dedicated check/request API) and are
62
+ * routed accordingly.
63
+ */
64
+ export const reactNativePermissionsAdapter = {
65
+ isSupported(name) {
66
+ if (name === 'notifications') {
67
+ return Platform.OS === 'ios' || Platform.OS === 'android';
68
+ }
69
+ return TABLE[name] !== undefined;
70
+ },
71
+ async check(name) {
72
+ if (name === 'notifications') {
73
+ const {
74
+ status
75
+ } = await checkNotifications();
76
+ return toStatus(status);
77
+ }
78
+ const native = TABLE[name];
79
+ if (!native) return 'unavailable';
80
+ return toStatus(await check(native));
81
+ },
82
+ async request(name) {
83
+ if (name === 'notifications') {
84
+ const {
85
+ status
86
+ } = await requestNotifications(['alert', 'sound', 'badge']);
87
+ return toStatus(status);
88
+ }
89
+ const native = TABLE[name];
90
+ if (!native) return 'unavailable';
91
+ return toStatus(await request(native));
92
+ },
93
+ async openSettings() {
94
+ await openSettings();
95
+ }
96
+ };
97
+ //# sourceMappingURL=reactNativePermissions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["Platform","PERMISSIONS","RESULTS","check","request","checkNotifications","requestNotifications","openSettings","IOS","camera","CAMERA","microphone","MICROPHONE","photoLibrary","PHOTO_LIBRARY","photoLibraryAddOnly","PHOTO_LIBRARY_ADD_ONLY","mediaLibrary","MEDIA_LIBRARY","contacts","CONTACTS","calendar","CALENDARS","calendarWriteOnly","CALENDARS_WRITE_ONLY","reminders","REMINDERS","locationWhenInUse","LOCATION_WHEN_IN_USE","locationAlways","LOCATION_ALWAYS","bluetooth","BLUETOOTH","motion","MOTION","speechRecognition","SPEECH_RECOGNITION","appTrackingTransparency","APP_TRACKING_TRANSPARENCY","faceId","FACE_ID","ANDROID","RECORD_AUDIO","READ_MEDIA_IMAGES","storageReadAudio","READ_MEDIA_AUDIO","storageReadVideo","READ_MEDIA_VIDEO","READ_CONTACTS","READ_CALENDAR","WRITE_CALENDAR","ACCESS_FINE_LOCATION","ACCESS_BACKGROUND_LOCATION","BLUETOOTH_CONNECT","ACTIVITY_RECOGNITION","bodySensors","BODY_SENSORS","TABLE","OS","toStatus","result","GRANTED","LIMITED","BLOCKED","DENIED","UNAVAILABLE","reactNativePermissionsAdapter","isSupported","name","undefined","status","native"],"sourceRoot":"../../../src","sources":["adapters/reactNativePermissions.ts"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+
3
+ import { usePermission } from "../hooks/usePermission.js";
4
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
5
+ /**
6
+ * Declarative convenience over {@link usePermission}. Gates its children behind
7
+ * a granted permission and lets you plug in your own rationale / blocked UI.
8
+ * Auto-runs the request flow on mount unless you override `requestOnMount`.
9
+ */
10
+ export function PermissionGate({
11
+ name,
12
+ options,
13
+ children,
14
+ rationale,
15
+ blocked,
16
+ fallback = null
17
+ }) {
18
+ const controller = usePermission(name, {
19
+ requestOnMount: true,
20
+ ...options
21
+ });
22
+
23
+ // Render-prop mode: hand over full control regardless of status.
24
+ if (typeof children === 'function') {
25
+ return /*#__PURE__*/_jsx(_Fragment, {
26
+ children: children(controller)
27
+ });
28
+ }
29
+ const {
30
+ status,
31
+ phase,
32
+ isBlocked
33
+ } = controller;
34
+ if (status === 'granted' || status === 'limited') {
35
+ return /*#__PURE__*/_jsx(_Fragment, {
36
+ children: children
37
+ });
38
+ }
39
+ if (phase === 'rationale' && rationale) {
40
+ return /*#__PURE__*/_jsx(_Fragment, {
41
+ children: rationale({
42
+ confirm: controller.confirmRationale,
43
+ cancel: controller.cancelRationale,
44
+ status
45
+ })
46
+ });
47
+ }
48
+ if (isBlocked && blocked) {
49
+ return /*#__PURE__*/_jsx(_Fragment, {
50
+ children: blocked({
51
+ openSettings: controller.openSettings,
52
+ status
53
+ })
54
+ });
55
+ }
56
+ return /*#__PURE__*/_jsx(_Fragment, {
57
+ children: fallback
58
+ });
59
+ }
60
+ //# sourceMappingURL=PermissionGate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["usePermission","Fragment","_Fragment","jsx","_jsx","PermissionGate","name","options","children","rationale","blocked","fallback","controller","requestOnMount","status","phase","isBlocked","confirm","confirmRationale","cancel","cancelRationale","openSettings"],"sourceRoot":"../../../src","sources":["components/PermissionGate.tsx"],"mappings":";;AACA,SAASA,aAAa,QAAQ,2BAAwB;AAAC,SAAAC,QAAA,IAAAC,SAAA,EAAAC,GAAA,IAAAC,IAAA;AAoCvD;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,cAAcA,CAAC;EAC7BC,IAAI;EACJC,OAAO;EACPC,QAAQ;EACRC,SAAS;EACTC,OAAO;EACPC,QAAQ,GAAG;AACQ,CAAC,EAAE;EACtB,MAAMC,UAAU,GAAGZ,aAAa,CAACM,IAAI,EAAE;IAAEO,cAAc,EAAE,IAAI;IAAE,GAAGN;EAAQ,CAAC,CAAC;;EAE5E;EACA,IAAI,OAAOC,QAAQ,KAAK,UAAU,EAAE;IAClC,oBAAOJ,IAAA,CAAAF,SAAA;MAAAM,QAAA,EAAGA,QAAQ,CAACI,UAAU;IAAC,CAAG,CAAC;EACpC;EAEA,MAAM;IAAEE,MAAM;IAAEC,KAAK;IAAEC;EAAU,CAAC,GAAGJ,UAAU;EAE/C,IAAIE,MAAM,KAAK,SAAS,IAAIA,MAAM,KAAK,SAAS,EAAE;IAChD,oBAAOV,IAAA,CAAAF,SAAA;MAAAM,QAAA,EAAGA;IAAQ,CAAG,CAAC;EACxB;EAEA,IAAIO,KAAK,KAAK,WAAW,IAAIN,SAAS,EAAE;IACtC,oBACEL,IAAA,CAAAF,SAAA;MAAAM,QAAA,EACGC,SAAS,CAAC;QACTQ,OAAO,EAAEL,UAAU,CAACM,gBAAgB;QACpCC,MAAM,EAAEP,UAAU,CAACQ,eAAe;QAClCN;MACF,CAAC;IAAC,CACF,CAAC;EAEP;EAEA,IAAIE,SAAS,IAAIN,OAAO,EAAE;IACxB,oBAAON,IAAA,CAAAF,SAAA;MAAAM,QAAA,EAAGE,OAAO,CAAC;QAAEW,YAAY,EAAET,UAAU,CAACS,YAAY;QAAEP;MAAO,CAAC;IAAC,CAAG,CAAC;EAC1E;EAEA,oBAAOV,IAAA,CAAAF,SAAA;IAAAM,QAAA,EAAGG;EAAQ,CAAG,CAAC;AACxB","ignoreList":[]}
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+
3
+ import { createContext } from 'react';
4
+ import { reactNativePermissionsAdapter } from "./adapters/reactNativePermissions.js";
5
+
6
+ /** Options that can be defaulted for every hook/component under a provider. */
7
+ import { jsx as _jsx } from "react/jsx-runtime";
8
+ export const PermissionContext = /*#__PURE__*/createContext({
9
+ adapter: reactNativePermissionsAdapter,
10
+ defaults: {}
11
+ });
12
+ /**
13
+ * Optional. Sets a shared adapter and default options for the subtree so you
14
+ * don't repeat them at every call site. `usePermission` works without it.
15
+ */
16
+ export function PermissionProvider({
17
+ adapter = reactNativePermissionsAdapter,
18
+ defaults = {},
19
+ children
20
+ }) {
21
+ return /*#__PURE__*/_jsx(PermissionContext.Provider, {
22
+ value: {
23
+ adapter,
24
+ defaults
25
+ },
26
+ children: children
27
+ });
28
+ }
29
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["createContext","reactNativePermissionsAdapter","jsx","_jsx","PermissionContext","adapter","defaults","PermissionProvider","children","Provider","value"],"sourceRoot":"../../src","sources":["context.tsx"],"mappings":";;AAAA,SAASA,aAAa,QAAwB,OAAO;AACrD,SAASC,6BAA6B,QAAQ,sCAAmC;;AAGjF;AAAA,SAAAC,GAAA,IAAAC,IAAA;AAWA,OAAO,MAAMC,iBAAiB,gBAAGJ,aAAa,CAAyB;EACrEK,OAAO,EAAEJ,6BAA6B;EACtCK,QAAQ,EAAE,CAAC;AACb,CAAC,CAAC;AAUF;AACA;AACA;AACA;AACA,OAAO,SAASC,kBAAkBA,CAAC;EACjCF,OAAO,GAAGJ,6BAA6B;EACvCK,QAAQ,GAAG,CAAC,CAAC;EACbE;AACuB,CAAC,EAAE;EAC1B,oBACEL,IAAA,CAACC,iBAAiB,CAACK,QAAQ;IAACC,KAAK,EAAE;MAAEL,OAAO;MAAEC;IAAS,CAAE;IAAAE,QAAA,EACtDA;EAAQ,CACiB,CAAC;AAEjC","ignoreList":[]}
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Pure, framework-free state machine that drives the permission UX flow.
5
+ *
6
+ * It has zero React and zero native dependencies: given the current state and an
7
+ * event, it returns the next state plus a list of side effects for the caller to
8
+ * interpret. This is what makes the whole flow unit-testable without a device.
9
+ */
10
+
11
+ const noop = state => ({
12
+ state,
13
+ effects: []
14
+ });
15
+ export function reduce(state, event, ctx) {
16
+ switch (event.type) {
17
+ case 'REQUEST':
18
+ {
19
+ // Only `requestable` can produce an OS prompt. Every other status is
20
+ // terminal for a request (granted/limited/blocked/unavailable): settle
21
+ // immediately so callers can react (e.g. show a "go to Settings" UI when
22
+ // blocked) without a dead-end.
23
+ if (state.status !== 'requestable') {
24
+ return {
25
+ state: {
26
+ ...state,
27
+ phase: 'settled'
28
+ },
29
+ effects: [{
30
+ type: 'resolve',
31
+ status: state.status
32
+ }]
33
+ };
34
+ }
35
+ if (ctx.rationale) {
36
+ return {
37
+ state: {
38
+ ...state,
39
+ phase: 'rationale'
40
+ },
41
+ effects: []
42
+ };
43
+ }
44
+ return {
45
+ state: {
46
+ ...state,
47
+ phase: 'requesting'
48
+ },
49
+ effects: [{
50
+ type: 'runRequest'
51
+ }]
52
+ };
53
+ }
54
+ case 'CONFIRM_RATIONALE':
55
+ {
56
+ if (state.phase !== 'rationale') return noop(state);
57
+ return {
58
+ state: {
59
+ ...state,
60
+ phase: 'requesting'
61
+ },
62
+ effects: [{
63
+ type: 'runRequest'
64
+ }]
65
+ };
66
+ }
67
+ case 'CANCEL_RATIONALE':
68
+ {
69
+ if (state.phase !== 'rationale') return noop(state);
70
+ return {
71
+ state: {
72
+ ...state,
73
+ phase: 'idle'
74
+ },
75
+ effects: [{
76
+ type: 'resolve',
77
+ status: state.status
78
+ }]
79
+ };
80
+ }
81
+ case 'REQUEST_RESULT':
82
+ {
83
+ return {
84
+ state: {
85
+ status: event.status,
86
+ phase: 'settled'
87
+ },
88
+ effects: [{
89
+ type: 'resolve',
90
+ status: event.status
91
+ }]
92
+ };
93
+ }
94
+ case 'CHECK_RESULT':
95
+ case 'REFRESH_RESULT':
96
+ {
97
+ // Update the known status without disturbing an in-flight phase.
98
+ return noop({
99
+ ...state,
100
+ status: event.status
101
+ });
102
+ }
103
+ default:
104
+ return noop(state);
105
+ }
106
+ }
107
+ //# sourceMappingURL=machine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["noop","state","effects","reduce","event","ctx","type","status","phase","rationale"],"sourceRoot":"../../../src","sources":["core/machine.ts"],"mappings":";;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AA6BA,MAAMA,IAAI,GAAIC,KAAmB,KAAkB;EAAEA,KAAK;EAAEC,OAAO,EAAE;AAAG,CAAC,CAAC;AAE1E,OAAO,SAASC,MAAMA,CACpBF,KAAmB,EACnBG,KAAmB,EACnBC,GAAmB,EACP;EACZ,QAAQD,KAAK,CAACE,IAAI;IAChB,KAAK,SAAS;MAAE;QACd;QACA;QACA;QACA;QACA,IAAIL,KAAK,CAACM,MAAM,KAAK,aAAa,EAAE;UAClC,OAAO;YACLN,KAAK,EAAE;cAAE,GAAGA,KAAK;cAAEO,KAAK,EAAE;YAAU,CAAC;YACrCN,OAAO,EAAE,CAAC;cAAEI,IAAI,EAAE,SAAS;cAAEC,MAAM,EAAEN,KAAK,CAACM;YAAO,CAAC;UACrD,CAAC;QACH;QACA,IAAIF,GAAG,CAACI,SAAS,EAAE;UACjB,OAAO;YAAER,KAAK,EAAE;cAAE,GAAGA,KAAK;cAAEO,KAAK,EAAE;YAAY,CAAC;YAAEN,OAAO,EAAE;UAAG,CAAC;QACjE;QACA,OAAO;UACLD,KAAK,EAAE;YAAE,GAAGA,KAAK;YAAEO,KAAK,EAAE;UAAa,CAAC;UACxCN,OAAO,EAAE,CAAC;YAAEI,IAAI,EAAE;UAAa,CAAC;QAClC,CAAC;MACH;IAEA,KAAK,mBAAmB;MAAE;QACxB,IAAIL,KAAK,CAACO,KAAK,KAAK,WAAW,EAAE,OAAOR,IAAI,CAACC,KAAK,CAAC;QACnD,OAAO;UACLA,KAAK,EAAE;YAAE,GAAGA,KAAK;YAAEO,KAAK,EAAE;UAAa,CAAC;UACxCN,OAAO,EAAE,CAAC;YAAEI,IAAI,EAAE;UAAa,CAAC;QAClC,CAAC;MACH;IAEA,KAAK,kBAAkB;MAAE;QACvB,IAAIL,KAAK,CAACO,KAAK,KAAK,WAAW,EAAE,OAAOR,IAAI,CAACC,KAAK,CAAC;QACnD,OAAO;UACLA,KAAK,EAAE;YAAE,GAAGA,KAAK;YAAEO,KAAK,EAAE;UAAO,CAAC;UAClCN,OAAO,EAAE,CAAC;YAAEI,IAAI,EAAE,SAAS;YAAEC,MAAM,EAAEN,KAAK,CAACM;UAAO,CAAC;QACrD,CAAC;MACH;IAEA,KAAK,gBAAgB;MAAE;QACrB,OAAO;UACLN,KAAK,EAAE;YAAEM,MAAM,EAAEH,KAAK,CAACG,MAAM;YAAEC,KAAK,EAAE;UAAU,CAAC;UACjDN,OAAO,EAAE,CAAC;YAAEI,IAAI,EAAE,SAAS;YAAEC,MAAM,EAAEH,KAAK,CAACG;UAAO,CAAC;QACrD,CAAC;MACH;IAEA,KAAK,cAAc;IACnB,KAAK,gBAAgB;MAAE;QACrB;QACA,OAAOP,IAAI,CAAC;UAAE,GAAGC,KAAK;UAAEM,MAAM,EAAEH,KAAK,CAACG;QAAO,CAAC,CAAC;MACjD;IAEA;MACE,OAAOP,IAAI,CAACC,KAAK,CAAC;EACtB;AACF","ignoreList":[]}