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
@@ -0,0 +1,205 @@
1
+ import {
2
+ useCallback,
3
+ useContext,
4
+ useEffect,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ } from 'react';
9
+ import { AppState, type AppStateStatus } from 'react-native';
10
+ import { reduce, type MachineEvent, type MachineState } from '../core/machine';
11
+ import { PermissionContext } from '../context';
12
+ import type {
13
+ PermissionController,
14
+ PermissionName,
15
+ PermissionStatus,
16
+ UsePermissionOptions,
17
+ } from '../types';
18
+
19
+ interface Pending {
20
+ promise: Promise<PermissionStatus>;
21
+ resolve: (status: PermissionStatus) => void;
22
+ }
23
+
24
+ /**
25
+ * Headless permission-flow controller. Owns the state machine and wires its
26
+ * effects to the native adapter; renders nothing. Bring your own rationale and
27
+ * "blocked" UI and drive it off `phase` / `status`.
28
+ */
29
+ export function usePermission(
30
+ name: PermissionName,
31
+ options: UsePermissionOptions = {}
32
+ ): PermissionController {
33
+ const ctx = useContext(PermissionContext);
34
+
35
+ const rationale = options.rationale ?? ctx.defaults.rationale ?? false;
36
+ const revalidateOnFocus =
37
+ options.revalidateOnFocus ?? ctx.defaults.revalidateOnFocus ?? true;
38
+ const requestOnMount =
39
+ options.requestOnMount ?? ctx.defaults.requestOnMount ?? false;
40
+ const onStatusChange = options.onStatusChange;
41
+ const adapter = options.adapter ?? ctx.adapter;
42
+
43
+ const supported = adapter.isSupported(name);
44
+
45
+ const neutral = (): MachineState => ({
46
+ status: supported ? 'requestable' : 'unavailable',
47
+ phase: 'idle',
48
+ });
49
+
50
+ const [state, setState] = useState<MachineState>(neutral);
51
+
52
+ // Mirror volatile values in refs so async effect callbacks read fresh data
53
+ // and the public callbacks stay referentially stable.
54
+ const stateRef = useRef(state);
55
+ stateRef.current = state;
56
+ const rationaleRef = useRef(rationale);
57
+ rationaleRef.current = rationale;
58
+ const onStatusChangeRef = useRef(onStatusChange);
59
+ onStatusChangeRef.current = onStatusChange;
60
+ const requestOnMountRef = useRef(requestOnMount);
61
+ requestOnMountRef.current = requestOnMount;
62
+
63
+ // Single in-flight request. Dedupes concurrent/double `request()` calls so a
64
+ // double-tap can never orphan a promise or fire two native prompts.
65
+ const pendingRef = useRef<Pending | null>(null);
66
+ const mountedRef = useRef(true);
67
+
68
+ const dispatch = useCallback(
69
+ (event: MachineEvent) => {
70
+ const { state: next, effects } = reduce(stateRef.current, event, {
71
+ rationale: rationaleRef.current,
72
+ });
73
+ stateRef.current = next;
74
+ if (mountedRef.current) setState(next);
75
+
76
+ for (const effect of effects) {
77
+ if (effect.type === 'resolve') {
78
+ const pending = pendingRef.current;
79
+ pendingRef.current = null;
80
+ pending?.resolve(effect.status);
81
+ } else if (effect.type === 'runRequest') {
82
+ adapter
83
+ .request(name)
84
+ .then((status) =>
85
+ dispatchRef.current({ type: 'REQUEST_RESULT', status })
86
+ )
87
+ .catch(() =>
88
+ dispatchRef.current({
89
+ type: 'REQUEST_RESULT',
90
+ status: stateRef.current.status,
91
+ })
92
+ );
93
+ }
94
+ }
95
+ },
96
+ [adapter, name]
97
+ );
98
+ const dispatchRef = useRef(dispatch);
99
+ dispatchRef.current = dispatch;
100
+
101
+ const request = useCallback(() => {
102
+ // Reuse the in-flight promise instead of starting a second flow.
103
+ if (pendingRef.current) return pendingRef.current.promise;
104
+ let resolve!: (status: PermissionStatus) => void;
105
+ const promise = new Promise<PermissionStatus>((r) => {
106
+ resolve = r;
107
+ });
108
+ pendingRef.current = { promise, resolve };
109
+ dispatchRef.current({ type: 'REQUEST' });
110
+ return promise;
111
+ }, []);
112
+ const requestRef = useRef(request);
113
+ requestRef.current = request;
114
+
115
+ const confirmRationale = useCallback(
116
+ () => dispatchRef.current({ type: 'CONFIRM_RATIONALE' }),
117
+ []
118
+ );
119
+
120
+ const cancelRationale = useCallback(
121
+ () => dispatchRef.current({ type: 'CANCEL_RATIONALE' }),
122
+ []
123
+ );
124
+
125
+ const refresh = useCallback(async () => {
126
+ if (!supported) return 'unavailable' as PermissionStatus;
127
+ const status = await adapter.check(name);
128
+ dispatchRef.current({ type: 'REFRESH_RESULT', status });
129
+ return status;
130
+ }, [adapter, name, supported]);
131
+ const refreshRef = useRef(refresh);
132
+ refreshRef.current = refresh;
133
+
134
+ const openSettings = useCallback(() => adapter.openSettings(), [adapter]);
135
+
136
+ // Initial check on mount and whenever the permission identity changes. On a
137
+ // change we reset to a neutral state first so a stale status never lingers.
138
+ const firstRunRef = useRef(true);
139
+ useEffect(() => {
140
+ mountedRef.current = true;
141
+ if (!firstRunRef.current) {
142
+ const reset = neutral();
143
+ stateRef.current = reset;
144
+ setState(reset);
145
+ pendingRef.current = null;
146
+ }
147
+ firstRunRef.current = false;
148
+
149
+ if (supported) {
150
+ adapter
151
+ .check(name)
152
+ .then((status) => {
153
+ dispatchRef.current({ type: 'CHECK_RESULT', status });
154
+ if (requestOnMountRef.current) requestRef.current();
155
+ })
156
+ .catch(() => {});
157
+ }
158
+ return () => {
159
+ mountedRef.current = false;
160
+ };
161
+ // `neutral` is derived from supported; requestOnMount is read via ref.
162
+ // eslint-disable-next-line react-hooks/exhaustive-deps
163
+ }, [adapter, name, supported]);
164
+
165
+ // Re-check when the app returns to the foreground.
166
+ useEffect(() => {
167
+ if (!revalidateOnFocus || !supported) return;
168
+ const sub = AppState.addEventListener('change', (next: AppStateStatus) => {
169
+ if (next === 'active') refreshRef.current();
170
+ });
171
+ return () => sub.remove();
172
+ }, [revalidateOnFocus, supported]);
173
+
174
+ // Notify on resolved-status changes.
175
+ const prevStatusRef = useRef(state.status);
176
+ useEffect(() => {
177
+ if (prevStatusRef.current !== state.status) {
178
+ prevStatusRef.current = state.status;
179
+ onStatusChangeRef.current?.(state.status);
180
+ }
181
+ }, [state.status]);
182
+
183
+ return useMemo<PermissionController>(
184
+ () => ({
185
+ status: state.status,
186
+ phase: state.phase,
187
+ isGranted: state.status === 'granted',
188
+ isBlocked: state.status === 'blocked',
189
+ request,
190
+ confirmRationale,
191
+ cancelRationale,
192
+ openSettings,
193
+ refresh,
194
+ }),
195
+ [
196
+ state.status,
197
+ state.phase,
198
+ request,
199
+ confirmRationale,
200
+ cancelRationale,
201
+ openSettings,
202
+ refresh,
203
+ ]
204
+ );
205
+ }
@@ -0,0 +1,241 @@
1
+ import {
2
+ useCallback,
3
+ useContext,
4
+ useEffect,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ } from 'react';
9
+ import { AppState, type AppStateStatus } from 'react-native';
10
+ import { PermissionContext } from '../context';
11
+ import type {
12
+ FlowPhase,
13
+ PermissionName,
14
+ PermissionStatus,
15
+ UsePermissionOptions,
16
+ } from '../types';
17
+
18
+ export type PermissionStatusMap = Partial<
19
+ Record<PermissionName, PermissionStatus>
20
+ >;
21
+
22
+ export interface PermissionsController {
23
+ /** Current status per requested permission. */
24
+ statuses: PermissionStatusMap;
25
+ /** Shared flow position for the batch. */
26
+ phase: FlowPhase;
27
+ /** Every requested permission is `granted`. */
28
+ allGranted: boolean;
29
+ /** At least one requested permission is `blocked`. */
30
+ anyBlocked: boolean;
31
+ /**
32
+ * Run the flow for every not-yet-granted permission behind a single shared
33
+ * rationale gate. Prompts are shown one after another. Resolves with the
34
+ * final status map.
35
+ */
36
+ request: () => Promise<PermissionStatusMap>;
37
+ /** Proceed from the shared `rationale` phase to the prompts. */
38
+ confirmRationale: () => void;
39
+ /** Abort the shared `rationale` phase. */
40
+ cancelRationale: () => void;
41
+ openSettings: () => Promise<void>;
42
+ refresh: () => Promise<PermissionStatusMap>;
43
+ }
44
+
45
+ interface Pending {
46
+ promise: Promise<PermissionStatusMap>;
47
+ resolve: (map: PermissionStatusMap) => void;
48
+ }
49
+
50
+ /**
51
+ * Aggregate variant of {@link usePermission} for permissions acquired together
52
+ * (e.g. camera + microphone for video). One shared rationale gate, one
53
+ * `request()`, one status map. The `names` array is treated as a stable set -
54
+ * pass a constant or memoized array.
55
+ */
56
+ export function usePermissions(
57
+ names: PermissionName[],
58
+ options: UsePermissionOptions = {}
59
+ ): PermissionsController {
60
+ const ctx = useContext(PermissionContext);
61
+ const rationale = options.rationale ?? ctx.defaults.rationale ?? false;
62
+ const revalidateOnFocus =
63
+ options.revalidateOnFocus ?? ctx.defaults.revalidateOnFocus ?? true;
64
+ const requestOnMount =
65
+ options.requestOnMount ?? ctx.defaults.requestOnMount ?? false;
66
+ const adapter = options.adapter ?? ctx.adapter;
67
+
68
+ // Stable identity for the set so effects don't re-run on every new array.
69
+ const key = names.join(',');
70
+
71
+ const buildInitial = useCallback((): PermissionStatusMap => {
72
+ const map: PermissionStatusMap = {};
73
+ for (const name of names) {
74
+ map[name] = adapter.isSupported(name) ? 'requestable' : 'unavailable';
75
+ }
76
+ return map;
77
+ // eslint-disable-next-line react-hooks/exhaustive-deps
78
+ }, [adapter, key]);
79
+
80
+ const [statuses, setStatuses] = useState<PermissionStatusMap>(buildInitial);
81
+ const [phase, setPhase] = useState<FlowPhase>('idle');
82
+
83
+ const statusesRef = useRef(statuses);
84
+ statusesRef.current = statuses;
85
+ const phaseRef = useRef(phase);
86
+ phaseRef.current = phase;
87
+ const rationaleRef = useRef(rationale);
88
+ rationaleRef.current = rationale;
89
+ const requestOnMountRef = useRef(requestOnMount);
90
+ requestOnMountRef.current = requestOnMount;
91
+
92
+ const pendingRef = useRef<Pending | null>(null);
93
+ const requestableRef = useRef<PermissionName[]>([]);
94
+ const mountedRef = useRef(true);
95
+
96
+ const commit = useCallback((next: PermissionStatusMap) => {
97
+ statusesRef.current = next;
98
+ if (mountedRef.current) setStatuses(next);
99
+ }, []);
100
+
101
+ const settle = useCallback(() => {
102
+ if (mountedRef.current) setPhase('settled');
103
+ const pending = pendingRef.current;
104
+ pendingRef.current = null;
105
+ pending?.resolve(statusesRef.current);
106
+ }, []);
107
+
108
+ const runBatch = useCallback(
109
+ async (list: PermissionName[]) => {
110
+ if (mountedRef.current) setPhase('requesting');
111
+ for (const name of list) {
112
+ try {
113
+ const status = await adapter.request(name);
114
+ commit({ ...statusesRef.current, [name]: status });
115
+ } catch {
116
+ // Keep the previous status for this permission on failure.
117
+ }
118
+ }
119
+ settle();
120
+ },
121
+ [adapter, commit, settle]
122
+ );
123
+ const runBatchRef = useRef(runBatch);
124
+ runBatchRef.current = runBatch;
125
+
126
+ const request = useCallback(() => {
127
+ if (pendingRef.current) return pendingRef.current.promise;
128
+
129
+ let resolve!: (map: PermissionStatusMap) => void;
130
+ const promise = new Promise<PermissionStatusMap>((r) => {
131
+ resolve = r;
132
+ });
133
+ pendingRef.current = { promise, resolve };
134
+
135
+ const requestable = names.filter(
136
+ (name) => statusesRef.current[name] === 'requestable'
137
+ );
138
+ requestableRef.current = requestable;
139
+
140
+ if (requestable.length === 0) {
141
+ settle();
142
+ } else if (rationaleRef.current) {
143
+ if (mountedRef.current) setPhase('rationale');
144
+ } else {
145
+ runBatchRef.current(requestable);
146
+ }
147
+ return promise;
148
+ // eslint-disable-next-line react-hooks/exhaustive-deps
149
+ }, [key, settle]);
150
+ const requestRef = useRef(request);
151
+ requestRef.current = request;
152
+
153
+ const confirmRationale = useCallback(() => {
154
+ if (phaseRef.current !== 'rationale') return;
155
+ runBatchRef.current(requestableRef.current);
156
+ }, []);
157
+
158
+ const cancelRationale = useCallback(() => {
159
+ if (phaseRef.current !== 'rationale') return;
160
+ if (mountedRef.current) setPhase('idle');
161
+ const pending = pendingRef.current;
162
+ pendingRef.current = null;
163
+ pending?.resolve(statusesRef.current);
164
+ }, []);
165
+
166
+ const refresh = useCallback(async () => {
167
+ const entries = await Promise.all(
168
+ names.map(async (name) => {
169
+ if (!adapter.isSupported(name)) {
170
+ return [name, 'unavailable'] as const;
171
+ }
172
+ return [name, await adapter.check(name)] as const;
173
+ })
174
+ );
175
+ const next: PermissionStatusMap = {};
176
+ for (const [name, status] of entries) next[name] = status;
177
+ commit(next);
178
+ return next;
179
+ // eslint-disable-next-line react-hooks/exhaustive-deps
180
+ }, [adapter, key, commit]);
181
+ const refreshRef = useRef(refresh);
182
+ refreshRef.current = refresh;
183
+
184
+ const openSettings = useCallback(() => adapter.openSettings(), [adapter]);
185
+
186
+ // Initial check on mount / when the set changes.
187
+ const firstRunRef = useRef(true);
188
+ useEffect(() => {
189
+ mountedRef.current = true;
190
+ if (!firstRunRef.current) {
191
+ commit(buildInitial());
192
+ setPhase('idle');
193
+ pendingRef.current = null;
194
+ }
195
+ firstRunRef.current = false;
196
+
197
+ refreshRef.current().then(() => {
198
+ if (requestOnMountRef.current) requestRef.current();
199
+ });
200
+
201
+ return () => {
202
+ mountedRef.current = false;
203
+ };
204
+ // eslint-disable-next-line react-hooks/exhaustive-deps
205
+ }, [key]);
206
+
207
+ // Re-check on foreground.
208
+ useEffect(() => {
209
+ if (!revalidateOnFocus) return;
210
+ const sub = AppState.addEventListener('change', (next: AppStateStatus) => {
211
+ if (next === 'active') refreshRef.current();
212
+ });
213
+ return () => sub.remove();
214
+ }, [revalidateOnFocus]);
215
+
216
+ return useMemo<PermissionsController>(() => {
217
+ const allGranted = names.every((name) => statuses[name] === 'granted');
218
+ const anyBlocked = names.some((name) => statuses[name] === 'blocked');
219
+ return {
220
+ statuses,
221
+ phase,
222
+ allGranted,
223
+ anyBlocked,
224
+ request,
225
+ confirmRationale,
226
+ cancelRationale,
227
+ openSettings,
228
+ refresh,
229
+ };
230
+ // eslint-disable-next-line react-hooks/exhaustive-deps
231
+ }, [
232
+ key,
233
+ statuses,
234
+ phase,
235
+ request,
236
+ confirmRationale,
237
+ cancelRationale,
238
+ openSettings,
239
+ refresh,
240
+ ]);
241
+ }
@@ -0,0 +1,35 @@
1
+ import { reactNativePermissionsAdapter } from './adapters/reactNativePermissions';
2
+ import type {
3
+ PermissionAdapter,
4
+ PermissionName,
5
+ PermissionStatus,
6
+ } from './types';
7
+
8
+ /**
9
+ * Imperative helpers for use outside React (services, sagas, utilities). These
10
+ * are the raw check/request/openSettings calls with no rationale UX - for the
11
+ * full flow with pre-prompts, use {@link usePermission} or {@link PermissionGate}.
12
+ */
13
+
14
+ /** Read current status without prompting. */
15
+ export function checkPermission(
16
+ name: PermissionName,
17
+ adapter: PermissionAdapter = reactNativePermissionsAdapter
18
+ ): Promise<PermissionStatus> {
19
+ return adapter.check(name);
20
+ }
21
+
22
+ /** Trigger the native OS prompt and resolve with the resulting status. */
23
+ export function requestPermission(
24
+ name: PermissionName,
25
+ adapter: PermissionAdapter = reactNativePermissionsAdapter
26
+ ): Promise<PermissionStatus> {
27
+ return adapter.request(name);
28
+ }
29
+
30
+ /** Open the OS settings screen for this app. */
31
+ export function openAppSettings(
32
+ adapter: PermissionAdapter = reactNativePermissionsAdapter
33
+ ): Promise<void> {
34
+ return adapter.openSettings();
35
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,42 @@
1
+ // Hooks - the primary, headless API.
2
+ export { usePermission } from './hooks/usePermission';
3
+ export { usePermissions } from './hooks/usePermissions';
4
+ export type {
5
+ PermissionsController,
6
+ PermissionStatusMap,
7
+ } from './hooks/usePermissions';
8
+
9
+ // Declarative convenience.
10
+ export { PermissionGate } from './components/PermissionGate';
11
+ export type {
12
+ PermissionGateProps,
13
+ RationaleControls,
14
+ BlockedControls,
15
+ } from './components/PermissionGate';
16
+
17
+ // Provider - optional shared adapter + default options.
18
+ export { PermissionProvider, PermissionContext } from './context';
19
+ export type {
20
+ PermissionProviderProps,
21
+ PermissionContextValue,
22
+ PermissionDefaults,
23
+ } from './context';
24
+
25
+ // Imperative helpers for use outside React.
26
+ export {
27
+ checkPermission,
28
+ requestPermission,
29
+ openAppSettings,
30
+ } from './imperative';
31
+
32
+ // Default adapter (override via options.adapter or PermissionProvider).
33
+ export { reactNativePermissionsAdapter } from './adapters/reactNativePermissions';
34
+
35
+ export type {
36
+ PermissionName,
37
+ PermissionStatus,
38
+ FlowPhase,
39
+ PermissionController,
40
+ PermissionAdapter,
41
+ UsePermissionOptions,
42
+ } from './types';
package/src/types.ts ADDED
@@ -0,0 +1,114 @@
1
+ /**
2
+ * The OS-level truth about a permission, normalized across platforms.
3
+ *
4
+ * - `granted` - access is allowed.
5
+ * - `limited` - partial access (iOS: e.g. selected photos / contacts).
6
+ * - `requestable` - not granted yet, but the OS prompt can still be shown.
7
+ * - `blocked` - denied and the OS will no longer prompt; only Settings helps.
8
+ * - `unavailable` - the capability does not exist on this device/build.
9
+ */
10
+ export type PermissionStatus =
11
+ 'granted' | 'limited' | 'requestable' | 'blocked' | 'unavailable';
12
+
13
+ /**
14
+ * Where the request flow currently sits. Independent from {@link PermissionStatus}:
15
+ * `status` is the OS truth, `phase` is the position in the UX state machine.
16
+ *
17
+ * - `idle` - nothing in flight.
18
+ * - `rationale` - pre-prompt is shown, awaiting the user's decision.
19
+ * - `requesting` - the native OS dialog is up.
20
+ * - `settled` - the flow finished; read `status` for the outcome.
21
+ */
22
+ export type FlowPhase = 'idle' | 'rationale' | 'requesting' | 'settled';
23
+
24
+ /**
25
+ * Cross-platform permission aliases. Each maps to the correct native permission
26
+ * per platform inside the adapter. Aliases with no meaning on the current
27
+ * platform resolve to `unavailable`.
28
+ */
29
+ export type PermissionName =
30
+ | 'camera'
31
+ | 'microphone'
32
+ | 'photoLibrary'
33
+ | 'photoLibraryAddOnly'
34
+ | 'mediaLibrary'
35
+ | 'contacts'
36
+ | 'calendar'
37
+ | 'calendarWriteOnly'
38
+ | 'reminders'
39
+ | 'locationWhenInUse'
40
+ | 'locationAlways'
41
+ | 'notifications'
42
+ | 'bluetooth'
43
+ | 'motion'
44
+ | 'speechRecognition'
45
+ | 'appTrackingTransparency'
46
+ | 'bodySensors'
47
+ | 'storageReadAudio'
48
+ | 'storageReadVideo'
49
+ | 'faceId';
50
+
51
+ /**
52
+ * Backend that performs the actual native permission calls. The core state
53
+ * machine is adapter-agnostic, which keeps it fully unit-testable without any
54
+ * native module. The default adapter is built on `react-native-permissions`.
55
+ */
56
+ export interface PermissionAdapter {
57
+ /** Whether the alias is meaningful on the current platform. */
58
+ isSupported(name: PermissionName): boolean;
59
+ /** Read current status without prompting. */
60
+ check(name: PermissionName): Promise<PermissionStatus>;
61
+ /** Trigger the native OS prompt and resolve with the resulting status. */
62
+ request(name: PermissionName): Promise<PermissionStatus>;
63
+ /** Open the OS settings screen for this app. */
64
+ openSettings(): Promise<void>;
65
+ }
66
+
67
+ export interface UsePermissionOptions {
68
+ /**
69
+ * Gate the native prompt behind a `rationale` phase so you can show a
70
+ * pre-prompt explanation first. When `true`, `request()` moves to
71
+ * `phase: 'rationale'` and waits for `confirmRationale()` / `cancelRationale()`.
72
+ * @default false
73
+ */
74
+ rationale?: boolean;
75
+ /**
76
+ * Re-check status when the app returns to the foreground - catches changes the
77
+ * user made in the OS Settings screen.
78
+ * @default true
79
+ */
80
+ revalidateOnFocus?: boolean;
81
+ /**
82
+ * Run the full request flow automatically on mount.
83
+ * @default false
84
+ */
85
+ requestOnMount?: boolean;
86
+ /** Called whenever the resolved {@link PermissionStatus} changes. */
87
+ onStatusChange?: (status: PermissionStatus) => void;
88
+ /** Override the backend. Defaults to the `react-native-permissions` adapter. */
89
+ adapter?: PermissionAdapter;
90
+ }
91
+
92
+ /** The headless controller returned by {@link usePermission}. */
93
+ export interface PermissionController {
94
+ status: PermissionStatus;
95
+ phase: FlowPhase;
96
+ /** Convenience: `status === 'granted'`. */
97
+ isGranted: boolean;
98
+ /** Convenience: `status === 'blocked'`. */
99
+ isBlocked: boolean;
100
+ /**
101
+ * Run the flow: check → (rationale gate) → native prompt → settle. The promise
102
+ * resolves with the final status once the whole flow finishes, so it works
103
+ * both imperatively (`await request()`) and declaratively (render on `phase`).
104
+ */
105
+ request: () => Promise<PermissionStatus>;
106
+ /** Proceed from the `rationale` phase to the native prompt. */
107
+ confirmRationale: () => void;
108
+ /** Abort the `rationale` phase and return to `idle`. */
109
+ cancelRationale: () => void;
110
+ /** Open the OS settings screen for this app. */
111
+ openSettings: () => Promise<void>;
112
+ /** Re-check status on demand (also happens automatically on focus). */
113
+ refresh: () => Promise<PermissionStatus>;
114
+ }