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,89 @@
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 = 'granted' | 'limited' | 'requestable' | 'blocked' | 'unavailable';
11
+ /**
12
+ * Where the request flow currently sits. Independent from {@link PermissionStatus}:
13
+ * `status` is the OS truth, `phase` is the position in the UX state machine.
14
+ *
15
+ * - `idle` - nothing in flight.
16
+ * - `rationale` - pre-prompt is shown, awaiting the user's decision.
17
+ * - `requesting` - the native OS dialog is up.
18
+ * - `settled` - the flow finished; read `status` for the outcome.
19
+ */
20
+ export type FlowPhase = 'idle' | 'rationale' | 'requesting' | 'settled';
21
+ /**
22
+ * Cross-platform permission aliases. Each maps to the correct native permission
23
+ * per platform inside the adapter. Aliases with no meaning on the current
24
+ * platform resolve to `unavailable`.
25
+ */
26
+ export type PermissionName = 'camera' | 'microphone' | 'photoLibrary' | 'photoLibraryAddOnly' | 'mediaLibrary' | 'contacts' | 'calendar' | 'calendarWriteOnly' | 'reminders' | 'locationWhenInUse' | 'locationAlways' | 'notifications' | 'bluetooth' | 'motion' | 'speechRecognition' | 'appTrackingTransparency' | 'bodySensors' | 'storageReadAudio' | 'storageReadVideo' | 'faceId';
27
+ /**
28
+ * Backend that performs the actual native permission calls. The core state
29
+ * machine is adapter-agnostic, which keeps it fully unit-testable without any
30
+ * native module. The default adapter is built on `react-native-permissions`.
31
+ */
32
+ export interface PermissionAdapter {
33
+ /** Whether the alias is meaningful on the current platform. */
34
+ isSupported(name: PermissionName): boolean;
35
+ /** Read current status without prompting. */
36
+ check(name: PermissionName): Promise<PermissionStatus>;
37
+ /** Trigger the native OS prompt and resolve with the resulting status. */
38
+ request(name: PermissionName): Promise<PermissionStatus>;
39
+ /** Open the OS settings screen for this app. */
40
+ openSettings(): Promise<void>;
41
+ }
42
+ export interface UsePermissionOptions {
43
+ /**
44
+ * Gate the native prompt behind a `rationale` phase so you can show a
45
+ * pre-prompt explanation first. When `true`, `request()` moves to
46
+ * `phase: 'rationale'` and waits for `confirmRationale()` / `cancelRationale()`.
47
+ * @default false
48
+ */
49
+ rationale?: boolean;
50
+ /**
51
+ * Re-check status when the app returns to the foreground - catches changes the
52
+ * user made in the OS Settings screen.
53
+ * @default true
54
+ */
55
+ revalidateOnFocus?: boolean;
56
+ /**
57
+ * Run the full request flow automatically on mount.
58
+ * @default false
59
+ */
60
+ requestOnMount?: boolean;
61
+ /** Called whenever the resolved {@link PermissionStatus} changes. */
62
+ onStatusChange?: (status: PermissionStatus) => void;
63
+ /** Override the backend. Defaults to the `react-native-permissions` adapter. */
64
+ adapter?: PermissionAdapter;
65
+ }
66
+ /** The headless controller returned by {@link usePermission}. */
67
+ export interface PermissionController {
68
+ status: PermissionStatus;
69
+ phase: FlowPhase;
70
+ /** Convenience: `status === 'granted'`. */
71
+ isGranted: boolean;
72
+ /** Convenience: `status === 'blocked'`. */
73
+ isBlocked: boolean;
74
+ /**
75
+ * Run the flow: check → (rationale gate) → native prompt → settle. The promise
76
+ * resolves with the final status once the whole flow finishes, so it works
77
+ * both imperatively (`await request()`) and declaratively (render on `phase`).
78
+ */
79
+ request: () => Promise<PermissionStatus>;
80
+ /** Proceed from the `rationale` phase to the native prompt. */
81
+ confirmRationale: () => void;
82
+ /** Abort the `rationale` phase and return to `idle`. */
83
+ cancelRationale: () => void;
84
+ /** Open the OS settings screen for this app. */
85
+ openSettings: () => Promise<void>;
86
+ /** Re-check status on demand (also happens automatically on focus). */
87
+ refresh: () => Promise<PermissionStatus>;
88
+ }
89
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,MAAM,gBAAgB,GAC1B,SAAS,GAAG,SAAS,GAAG,aAAa,GAAG,SAAS,GAAG,aAAa,CAAC;AAEpE;;;;;;;;GAQG;AACH,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,WAAW,GAAG,YAAY,GAAG,SAAS,CAAC;AAExE;;;;GAIG;AACH,MAAM,MAAM,cAAc,GACtB,QAAQ,GACR,YAAY,GACZ,cAAc,GACd,qBAAqB,GACrB,cAAc,GACd,UAAU,GACV,UAAU,GACV,mBAAmB,GACnB,WAAW,GACX,mBAAmB,GACnB,gBAAgB,GAChB,eAAe,GACf,WAAW,GACX,QAAQ,GACR,mBAAmB,GACnB,yBAAyB,GACzB,aAAa,GACb,kBAAkB,GAClB,kBAAkB,GAClB,QAAQ,CAAC;AAEb;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,+DAA+D;IAC/D,WAAW,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC;IAC3C,6CAA6C;IAC7C,KAAK,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACvD,0EAA0E;IAC1E,OAAO,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACzD,gDAAgD;IAChD,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,qEAAqE;IACrE,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;IACpD,gFAAgF;IAChF,OAAO,CAAC,EAAE,iBAAiB,CAAC;CAC7B;AAED,iEAAiE;AACjE,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,gBAAgB,CAAC;IACzB,KAAK,EAAE,SAAS,CAAC;IACjB,2CAA2C;IAC3C,SAAS,EAAE,OAAO,CAAC;IACnB,2CAA2C;IAC3C,SAAS,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,OAAO,EAAE,MAAM,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACzC,+DAA+D;IAC/D,gBAAgB,EAAE,MAAM,IAAI,CAAC;IAC7B,wDAAwD;IACxD,eAAe,EAAE,MAAM,IAAI,CAAC;IAC5B,gDAAgD;IAChD,YAAY,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC,uEAAuE;IACvE,OAAO,EAAE,MAAM,OAAO,CAAC,gBAAgB,CAAC,CAAC;CAC1C"}
package/package.json ADDED
@@ -0,0 +1,145 @@
1
+ {
2
+ "name": "react-native-permission-gate",
3
+ "version": "0.1.0",
4
+ "description": "Headless permission flow orchestration for React Native. A state-machine hook, rationale gating, and a PermissionGate component. Bring your own UI.",
5
+ "main": "./lib/module/index.js",
6
+ "types": "./lib/typescript/src/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "react-native-permission-gate-source": "./src/index.tsx",
10
+ "types": "./lib/typescript/src/index.d.ts",
11
+ "default": "./lib/module/index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "src",
17
+ "lib",
18
+ "android",
19
+ "ios",
20
+ "cpp",
21
+ "*.podspec",
22
+ "react-native.config.js",
23
+ "!ios/build",
24
+ "!android/build",
25
+ "!android/gradle",
26
+ "!android/gradlew",
27
+ "!android/gradlew.bat",
28
+ "!android/local.properties",
29
+ "!**/__tests__",
30
+ "!**/__fixtures__",
31
+ "!**/__mocks__",
32
+ "!**/.*"
33
+ ],
34
+ "scripts": {
35
+ "example": "yarn workspace react-native-permission-gate-example",
36
+ "test": "jest",
37
+ "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
38
+ "prepare": "bob build",
39
+ "typecheck": "tsc",
40
+ "lint": "eslint \"**/*.{js,ts,tsx}\""
41
+ },
42
+ "keywords": [
43
+ "react-native",
44
+ "ios",
45
+ "android",
46
+ "permissions",
47
+ "permission",
48
+ "hooks",
49
+ "headless",
50
+ "rationale",
51
+ "permission-flow",
52
+ "react-native-permissions",
53
+ "expo"
54
+ ],
55
+ "repository": {
56
+ "type": "git",
57
+ "url": "git+https://github.com/cengizemre/react-native-permission-gate.git"
58
+ },
59
+ "author": "Emre Cengiz <34921701+cengizemre@users.noreply.github.com> (https://github.com/cengizemre)",
60
+ "license": "MIT",
61
+ "bugs": {
62
+ "url": "https://github.com/cengizemre/react-native-permission-gate/issues"
63
+ },
64
+ "homepage": "https://github.com/cengizemre/react-native-permission-gate#readme",
65
+ "publishConfig": {
66
+ "registry": "https://registry.npmjs.org/"
67
+ },
68
+ "devDependencies": {
69
+ "@eslint/compat": "^2.1.0",
70
+ "@eslint/eslintrc": "^3.3.5",
71
+ "@eslint/js": "^10.0.1",
72
+ "@react-native/babel-preset": "0.85.0",
73
+ "@react-native/eslint-config": "0.85.0",
74
+ "@react-native/jest-preset": "0.85.0",
75
+ "@testing-library/react-native": "^13.2.0",
76
+ "@types/jest": "^29.5.12",
77
+ "@types/react": "^19.2.0",
78
+ "del-cli": "^7.0.0",
79
+ "eslint": "^9.39.4",
80
+ "eslint-config-prettier": "^10.1.8",
81
+ "eslint-plugin-ft-flow": "^3.0.11",
82
+ "eslint-plugin-prettier": "^5.5.6",
83
+ "jest": "^29.7.0",
84
+ "prettier": "^3.8.3",
85
+ "react": "19.2.3",
86
+ "react-native": "0.85.0",
87
+ "react-native-builder-bob": "^0.43.0",
88
+ "react-native-permissions": "^5.6.0",
89
+ "react-test-renderer": "19.2.3",
90
+ "turbo": "^2.9.16",
91
+ "typescript": "^6.0.3"
92
+ },
93
+ "peerDependencies": {
94
+ "react": "*",
95
+ "react-native": "*",
96
+ "react-native-permissions": ">=4.1.5"
97
+ },
98
+ "workspaces": [
99
+ "example"
100
+ ],
101
+ "packageManager": "yarn@4.11.0",
102
+ "jest": {
103
+ "preset": "@react-native/jest-preset",
104
+ "setupFiles": [
105
+ "<rootDir>/jest.setup.js"
106
+ ],
107
+ "modulePathIgnorePatterns": [
108
+ "<rootDir>/example/node_modules",
109
+ "<rootDir>/lib/"
110
+ ]
111
+ },
112
+ "react-native-builder-bob": {
113
+ "source": "src",
114
+ "output": "lib",
115
+ "targets": [
116
+ [
117
+ "module",
118
+ {
119
+ "esm": true
120
+ }
121
+ ],
122
+ [
123
+ "typescript",
124
+ {
125
+ "project": "tsconfig.build.json"
126
+ }
127
+ ]
128
+ ]
129
+ },
130
+ "prettier": {
131
+ "quoteProps": "consistent",
132
+ "singleQuote": true,
133
+ "tabWidth": 2,
134
+ "trailingComma": "es5",
135
+ "useTabs": false
136
+ },
137
+ "create-react-native-library": {
138
+ "type": "library",
139
+ "languages": "js",
140
+ "tools": [
141
+ "eslint"
142
+ ],
143
+ "version": "0.63.0"
144
+ }
145
+ }
@@ -0,0 +1,119 @@
1
+ import { Platform } from 'react-native';
2
+ import {
3
+ PERMISSIONS,
4
+ RESULTS,
5
+ check,
6
+ request,
7
+ checkNotifications,
8
+ requestNotifications,
9
+ openSettings,
10
+ type Permission,
11
+ type PermissionStatus as RNPermissionStatus,
12
+ } from 'react-native-permissions';
13
+ import type {
14
+ PermissionAdapter,
15
+ PermissionName,
16
+ PermissionStatus,
17
+ } from '../types';
18
+
19
+ type NativeMap = Partial<Record<PermissionName, Permission>>;
20
+
21
+ const IOS: NativeMap = {
22
+ camera: PERMISSIONS.IOS.CAMERA,
23
+ microphone: PERMISSIONS.IOS.MICROPHONE,
24
+ photoLibrary: PERMISSIONS.IOS.PHOTO_LIBRARY,
25
+ photoLibraryAddOnly: PERMISSIONS.IOS.PHOTO_LIBRARY_ADD_ONLY,
26
+ mediaLibrary: PERMISSIONS.IOS.MEDIA_LIBRARY,
27
+ contacts: PERMISSIONS.IOS.CONTACTS,
28
+ calendar: PERMISSIONS.IOS.CALENDARS,
29
+ calendarWriteOnly: PERMISSIONS.IOS.CALENDARS_WRITE_ONLY,
30
+ reminders: PERMISSIONS.IOS.REMINDERS,
31
+ locationWhenInUse: PERMISSIONS.IOS.LOCATION_WHEN_IN_USE,
32
+ locationAlways: PERMISSIONS.IOS.LOCATION_ALWAYS,
33
+ bluetooth: PERMISSIONS.IOS.BLUETOOTH,
34
+ motion: PERMISSIONS.IOS.MOTION,
35
+ speechRecognition: PERMISSIONS.IOS.SPEECH_RECOGNITION,
36
+ appTrackingTransparency: PERMISSIONS.IOS.APP_TRACKING_TRANSPARENCY,
37
+ faceId: PERMISSIONS.IOS.FACE_ID,
38
+ // Android-only aliases (storageReadAudio, storageReadVideo, bodySensors)
39
+ // resolve to unavailable here.
40
+ };
41
+
42
+ const ANDROID: NativeMap = {
43
+ camera: PERMISSIONS.ANDROID.CAMERA,
44
+ microphone: PERMISSIONS.ANDROID.RECORD_AUDIO,
45
+ photoLibrary: PERMISSIONS.ANDROID.READ_MEDIA_IMAGES,
46
+ storageReadAudio: PERMISSIONS.ANDROID.READ_MEDIA_AUDIO,
47
+ storageReadVideo: PERMISSIONS.ANDROID.READ_MEDIA_VIDEO,
48
+ contacts: PERMISSIONS.ANDROID.READ_CONTACTS,
49
+ calendar: PERMISSIONS.ANDROID.READ_CALENDAR,
50
+ calendarWriteOnly: PERMISSIONS.ANDROID.WRITE_CALENDAR,
51
+ locationWhenInUse: PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION,
52
+ locationAlways: PERMISSIONS.ANDROID.ACCESS_BACKGROUND_LOCATION,
53
+ bluetooth: PERMISSIONS.ANDROID.BLUETOOTH_CONNECT,
54
+ motion: PERMISSIONS.ANDROID.ACTIVITY_RECOGNITION,
55
+ bodySensors: PERMISSIONS.ANDROID.BODY_SENSORS,
56
+ // iOS-only aliases (photoLibraryAddOnly, mediaLibrary, reminders,
57
+ // speechRecognition, appTrackingTransparency, faceId) → unavailable here.
58
+ };
59
+
60
+ const TABLE: NativeMap =
61
+ Platform.OS === 'ios' ? IOS : Platform.OS === 'android' ? ANDROID : {};
62
+
63
+ function toStatus(result: RNPermissionStatus): PermissionStatus {
64
+ switch (result) {
65
+ case RESULTS.GRANTED:
66
+ return 'granted';
67
+ case RESULTS.LIMITED:
68
+ return 'limited';
69
+ case RESULTS.BLOCKED:
70
+ return 'blocked';
71
+ case RESULTS.DENIED:
72
+ return 'requestable';
73
+ case RESULTS.UNAVAILABLE:
74
+ default:
75
+ return 'unavailable';
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Default adapter, backed by `react-native-permissions`. Notifications are a
81
+ * special case in the native library (a dedicated check/request API) and are
82
+ * routed accordingly.
83
+ */
84
+ export const reactNativePermissionsAdapter: PermissionAdapter = {
85
+ isSupported(name) {
86
+ if (name === 'notifications') {
87
+ return Platform.OS === 'ios' || Platform.OS === 'android';
88
+ }
89
+ return TABLE[name] !== undefined;
90
+ },
91
+
92
+ async check(name) {
93
+ if (name === 'notifications') {
94
+ const { status } = await checkNotifications();
95
+ return toStatus(status);
96
+ }
97
+ const native = TABLE[name];
98
+ if (!native) return 'unavailable';
99
+ return toStatus(await check(native));
100
+ },
101
+
102
+ async request(name) {
103
+ if (name === 'notifications') {
104
+ const { status } = await requestNotifications([
105
+ 'alert',
106
+ 'sound',
107
+ 'badge',
108
+ ]);
109
+ return toStatus(status);
110
+ }
111
+ const native = TABLE[name];
112
+ if (!native) return 'unavailable';
113
+ return toStatus(await request(native));
114
+ },
115
+
116
+ async openSettings() {
117
+ await openSettings();
118
+ },
119
+ };
@@ -0,0 +1,81 @@
1
+ import { type ReactNode } from 'react';
2
+ import { usePermission } from '../hooks/usePermission';
3
+ import type {
4
+ PermissionController,
5
+ PermissionName,
6
+ PermissionStatus,
7
+ UsePermissionOptions,
8
+ } from '../types';
9
+
10
+ export interface RationaleControls {
11
+ confirm: () => void;
12
+ cancel: () => void;
13
+ status: PermissionStatus;
14
+ }
15
+
16
+ export interface BlockedControls {
17
+ openSettings: () => Promise<void>;
18
+ status: PermissionStatus;
19
+ }
20
+
21
+ export interface PermissionGateProps {
22
+ name: PermissionName;
23
+ /** Forwarded to the underlying `usePermission`. `requestOnMount` defaults to true here. */
24
+ options?: UsePermissionOptions;
25
+ /**
26
+ * Rendered once access is granted (or limited). Pass a function to take full
27
+ * control and render for every state yourself.
28
+ */
29
+ children: ReactNode | ((controller: PermissionController) => ReactNode);
30
+ /** Your pre-prompt UI, shown while `phase === 'rationale'`. */
31
+ rationale?: (controls: RationaleControls) => ReactNode;
32
+ /** Your "go to Settings" UI, shown while the permission is blocked. */
33
+ blocked?: (controls: BlockedControls) => ReactNode;
34
+ /** Shown while not yet granted and no rationale/blocked UI applies. */
35
+ fallback?: ReactNode;
36
+ }
37
+
38
+ /**
39
+ * Declarative convenience over {@link usePermission}. Gates its children behind
40
+ * a granted permission and lets you plug in your own rationale / blocked UI.
41
+ * Auto-runs the request flow on mount unless you override `requestOnMount`.
42
+ */
43
+ export function PermissionGate({
44
+ name,
45
+ options,
46
+ children,
47
+ rationale,
48
+ blocked,
49
+ fallback = null,
50
+ }: PermissionGateProps) {
51
+ const controller = usePermission(name, { requestOnMount: true, ...options });
52
+
53
+ // Render-prop mode: hand over full control regardless of status.
54
+ if (typeof children === 'function') {
55
+ return <>{children(controller)}</>;
56
+ }
57
+
58
+ const { status, phase, isBlocked } = controller;
59
+
60
+ if (status === 'granted' || status === 'limited') {
61
+ return <>{children}</>;
62
+ }
63
+
64
+ if (phase === 'rationale' && rationale) {
65
+ return (
66
+ <>
67
+ {rationale({
68
+ confirm: controller.confirmRationale,
69
+ cancel: controller.cancelRationale,
70
+ status,
71
+ })}
72
+ </>
73
+ );
74
+ }
75
+
76
+ if (isBlocked && blocked) {
77
+ return <>{blocked({ openSettings: controller.openSettings, status })}</>;
78
+ }
79
+
80
+ return <>{fallback}</>;
81
+ }
@@ -0,0 +1,43 @@
1
+ import { createContext, type ReactNode } from 'react';
2
+ import { reactNativePermissionsAdapter } from './adapters/reactNativePermissions';
3
+ import type { PermissionAdapter, UsePermissionOptions } from './types';
4
+
5
+ /** Options that can be defaulted for every hook/component under a provider. */
6
+ export type PermissionDefaults = Pick<
7
+ UsePermissionOptions,
8
+ 'rationale' | 'revalidateOnFocus' | 'requestOnMount'
9
+ >;
10
+
11
+ export interface PermissionContextValue {
12
+ adapter: PermissionAdapter;
13
+ defaults: PermissionDefaults;
14
+ }
15
+
16
+ export const PermissionContext = createContext<PermissionContextValue>({
17
+ adapter: reactNativePermissionsAdapter,
18
+ defaults: {},
19
+ });
20
+
21
+ export interface PermissionProviderProps {
22
+ /** Override the backend for the whole tree. */
23
+ adapter?: PermissionAdapter;
24
+ /** Default options applied to every `usePermission` / `PermissionGate`. */
25
+ defaults?: PermissionDefaults;
26
+ children: ReactNode;
27
+ }
28
+
29
+ /**
30
+ * Optional. Sets a shared adapter and default options for the subtree so you
31
+ * don't repeat them at every call site. `usePermission` works without it.
32
+ */
33
+ export function PermissionProvider({
34
+ adapter = reactNativePermissionsAdapter,
35
+ defaults = {},
36
+ children,
37
+ }: PermissionProviderProps) {
38
+ return (
39
+ <PermissionContext.Provider value={{ adapter, defaults }}>
40
+ {children}
41
+ </PermissionContext.Provider>
42
+ );
43
+ }
@@ -0,0 +1,98 @@
1
+ import type { FlowPhase, PermissionStatus } from '../types';
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
+ export interface MachineState {
11
+ status: PermissionStatus;
12
+ phase: FlowPhase;
13
+ }
14
+
15
+ export type MachineEvent =
16
+ | { type: 'REQUEST' }
17
+ | { type: 'CONFIRM_RATIONALE' }
18
+ | { type: 'CANCEL_RATIONALE' }
19
+ | { type: 'REQUEST_RESULT'; status: PermissionStatus }
20
+ | { type: 'CHECK_RESULT'; status: PermissionStatus }
21
+ | { type: 'REFRESH_RESULT'; status: PermissionStatus };
22
+
23
+ export type MachineEffect =
24
+ /** Fire the adapter's native `request()`. */
25
+ | { type: 'runRequest' }
26
+ /** Resolve the pending `request()` promise with a final status. */
27
+ | { type: 'resolve'; status: PermissionStatus };
28
+
29
+ export interface MachineContext {
30
+ rationale: boolean;
31
+ }
32
+
33
+ export interface Transition {
34
+ state: MachineState;
35
+ effects: MachineEffect[];
36
+ }
37
+
38
+ const noop = (state: MachineState): Transition => ({ state, effects: [] });
39
+
40
+ export function reduce(
41
+ state: MachineState,
42
+ event: MachineEvent,
43
+ ctx: MachineContext
44
+ ): Transition {
45
+ switch (event.type) {
46
+ case 'REQUEST': {
47
+ // Only `requestable` can produce an OS prompt. Every other status is
48
+ // terminal for a request (granted/limited/blocked/unavailable): settle
49
+ // immediately so callers can react (e.g. show a "go to Settings" UI when
50
+ // blocked) without a dead-end.
51
+ if (state.status !== 'requestable') {
52
+ return {
53
+ state: { ...state, phase: 'settled' },
54
+ effects: [{ type: 'resolve', status: state.status }],
55
+ };
56
+ }
57
+ if (ctx.rationale) {
58
+ return { state: { ...state, phase: 'rationale' }, effects: [] };
59
+ }
60
+ return {
61
+ state: { ...state, phase: 'requesting' },
62
+ effects: [{ type: 'runRequest' }],
63
+ };
64
+ }
65
+
66
+ case 'CONFIRM_RATIONALE': {
67
+ if (state.phase !== 'rationale') return noop(state);
68
+ return {
69
+ state: { ...state, phase: 'requesting' },
70
+ effects: [{ type: 'runRequest' }],
71
+ };
72
+ }
73
+
74
+ case 'CANCEL_RATIONALE': {
75
+ if (state.phase !== 'rationale') return noop(state);
76
+ return {
77
+ state: { ...state, phase: 'idle' },
78
+ effects: [{ type: 'resolve', status: state.status }],
79
+ };
80
+ }
81
+
82
+ case 'REQUEST_RESULT': {
83
+ return {
84
+ state: { status: event.status, phase: 'settled' },
85
+ effects: [{ type: 'resolve', status: event.status }],
86
+ };
87
+ }
88
+
89
+ case 'CHECK_RESULT':
90
+ case 'REFRESH_RESULT': {
91
+ // Update the known status without disturbing an in-flight phase.
92
+ return noop({ ...state, status: event.status });
93
+ }
94
+
95
+ default:
96
+ return noop(state);
97
+ }
98
+ }