react-native-permission-handler 0.1.0 → 0.2.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 (41) hide show
  1. package/README.md +183 -47
  2. package/dist/chunk-NFEGQTCC.mjs +27 -0
  3. package/dist/chunk-NFEGQTCC.mjs.map +1 -0
  4. package/dist/chunk-WZJOIVOM.mjs +49 -0
  5. package/dist/chunk-WZJOIVOM.mjs.map +1 -0
  6. package/dist/engines/expo.d.mts +18 -0
  7. package/dist/engines/expo.d.ts +18 -0
  8. package/dist/engines/expo.js +58 -0
  9. package/dist/engines/expo.js.map +1 -0
  10. package/dist/engines/expo.mjs +35 -0
  11. package/dist/engines/expo.mjs.map +1 -0
  12. package/dist/engines/rnp.d.mts +5 -0
  13. package/dist/engines/rnp.d.ts +5 -0
  14. package/dist/engines/rnp.js +52 -0
  15. package/dist/engines/rnp.js.map +1 -0
  16. package/dist/engines/rnp.mjs +10 -0
  17. package/dist/engines/rnp.mjs.map +1 -0
  18. package/dist/index.d.mts +7 -107
  19. package/dist/index.d.ts +7 -107
  20. package/dist/index.js +127 -58
  21. package/dist/index.js.map +1 -1
  22. package/dist/index.mjs +98 -70
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/types-QXyq8VnD.d.mts +122 -0
  25. package/dist/types-QXyq8VnD.d.ts +122 -0
  26. package/package.json +27 -2
  27. package/src/components/permission-gate.tsx +10 -3
  28. package/src/engines/expo.test.ts +122 -0
  29. package/src/engines/expo.ts +45 -0
  30. package/src/engines/resolve.test.ts +85 -0
  31. package/src/engines/resolve.ts +11 -0
  32. package/src/engines/rnp-fallback.ts +23 -0
  33. package/src/engines/rnp.test.ts +96 -0
  34. package/src/engines/rnp.ts +33 -0
  35. package/src/engines/use-engine.ts +10 -0
  36. package/src/hooks/use-multiple-permissions.test.ts +94 -54
  37. package/src/hooks/use-multiple-permissions.ts +52 -39
  38. package/src/hooks/use-permission-handler.test.ts +59 -49
  39. package/src/hooks/use-permission-handler.ts +11 -40
  40. package/src/index.ts +3 -0
  41. package/src/types.ts +20 -3
@@ -0,0 +1,85 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import type { PermissionEngine } from "../types";
3
+
4
+ // Mock the RNP fallback
5
+ const mockFallbackEngine: PermissionEngine = {
6
+ check: vi.fn().mockResolvedValue("granted"),
7
+ request: vi.fn().mockResolvedValue("granted"),
8
+ openSettings: vi.fn().mockResolvedValue(undefined),
9
+ };
10
+
11
+ vi.mock("./rnp-fallback", () => ({
12
+ getRNPFallbackEngine: vi.fn(() => mockFallbackEngine),
13
+ }));
14
+
15
+ import { getDefaultEngine, setDefaultEngine } from "./resolve";
16
+ import { resolveEngine } from "./use-engine";
17
+
18
+ describe("resolveEngine", () => {
19
+ afterEach(() => {
20
+ // Reset global default by setting it to a known state
21
+ // We can't truly reset since there's no clearDefaultEngine, but tests
22
+ // run in order so we test the cascade explicitly
23
+ });
24
+
25
+ it("uses config engine when provided", () => {
26
+ const configEngine: PermissionEngine = {
27
+ check: vi.fn(),
28
+ request: vi.fn(),
29
+ openSettings: vi.fn(),
30
+ };
31
+
32
+ expect(resolveEngine(configEngine)).toBe(configEngine);
33
+ });
34
+
35
+ it("falls back to RNP fallback when no config or global engine", () => {
36
+ // getDefaultEngine() returns null by default (no setDefaultEngine called yet in fresh module)
37
+ // But since we can't guarantee module state across tests, we test the cascade
38
+ const result = resolveEngine(undefined);
39
+ // Should either be the global default or the fallback
40
+ expect(result).toBeDefined();
41
+ expect(result.check).toBeDefined();
42
+ expect(result.request).toBeDefined();
43
+ expect(result.openSettings).toBeDefined();
44
+ });
45
+ });
46
+
47
+ describe("setDefaultEngine / getDefaultEngine", () => {
48
+ it("stores and retrieves a default engine", () => {
49
+ const engine: PermissionEngine = {
50
+ check: vi.fn(),
51
+ request: vi.fn(),
52
+ openSettings: vi.fn(),
53
+ };
54
+
55
+ setDefaultEngine(engine);
56
+ expect(getDefaultEngine()).toBe(engine);
57
+ });
58
+
59
+ it("resolveEngine uses global default over fallback", () => {
60
+ const globalEngine: PermissionEngine = {
61
+ check: vi.fn(),
62
+ request: vi.fn(),
63
+ openSettings: vi.fn(),
64
+ };
65
+
66
+ setDefaultEngine(globalEngine);
67
+ expect(resolveEngine(undefined)).toBe(globalEngine);
68
+ });
69
+
70
+ it("config engine takes priority over global default", () => {
71
+ const globalEngine: PermissionEngine = {
72
+ check: vi.fn(),
73
+ request: vi.fn(),
74
+ openSettings: vi.fn(),
75
+ };
76
+ const configEngine: PermissionEngine = {
77
+ check: vi.fn(),
78
+ request: vi.fn(),
79
+ openSettings: vi.fn(),
80
+ };
81
+
82
+ setDefaultEngine(globalEngine);
83
+ expect(resolveEngine(configEngine)).toBe(configEngine);
84
+ });
85
+ });
@@ -0,0 +1,11 @@
1
+ import type { PermissionEngine } from "../types";
2
+
3
+ let defaultEngine: PermissionEngine | null = null;
4
+
5
+ export function setDefaultEngine(engine: PermissionEngine): void {
6
+ defaultEngine = engine;
7
+ }
8
+
9
+ export function getDefaultEngine(): PermissionEngine | null {
10
+ return defaultEngine;
11
+ }
@@ -0,0 +1,23 @@
1
+ import type { PermissionEngine } from "../types";
2
+
3
+ let cachedFallback: PermissionEngine | null = null;
4
+
5
+ export function getRNPFallbackEngine(): PermissionEngine {
6
+ if (cachedFallback) return cachedFallback;
7
+
8
+ try {
9
+ // Dynamic require to avoid hard dependency — only resolves if
10
+ // react-native-permissions is installed by the consumer.
11
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
12
+ const mod = require("./rnp") as { createRNPEngine: () => PermissionEngine };
13
+ const { createRNPEngine } = mod;
14
+ cachedFallback = createRNPEngine();
15
+ return cachedFallback;
16
+ } catch {
17
+ throw new Error(
18
+ "react-native-permission-handler: No permission engine configured. " +
19
+ "Either pass an `engine` in your hook config, call setDefaultEngine(), " +
20
+ "or install react-native-permissions as a peer dependency.",
21
+ );
22
+ }
23
+ }
@@ -0,0 +1,96 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ vi.mock("react-native-permissions", () => ({
4
+ check: vi.fn(),
5
+ request: vi.fn(),
6
+ openSettings: vi.fn(),
7
+ checkNotifications: vi.fn(),
8
+ requestNotifications: vi.fn(),
9
+ }));
10
+
11
+ import {
12
+ check,
13
+ checkNotifications,
14
+ openSettings,
15
+ request,
16
+ requestNotifications,
17
+ } from "react-native-permissions";
18
+ import { createRNPEngine } from "./rnp";
19
+
20
+ describe("createRNPEngine", () => {
21
+ beforeEach(() => {
22
+ vi.clearAllMocks();
23
+ });
24
+
25
+ describe("check", () => {
26
+ it("delegates to check() for regular permissions", async () => {
27
+ vi.mocked(check).mockResolvedValue("granted");
28
+ const engine = createRNPEngine();
29
+
30
+ const result = await engine.check("ios.permission.CAMERA");
31
+
32
+ expect(check).toHaveBeenCalledWith("ios.permission.CAMERA");
33
+ expect(result).toBe("granted");
34
+ });
35
+
36
+ it("delegates to checkNotifications() for 'notifications'", async () => {
37
+ vi.mocked(checkNotifications).mockResolvedValue({
38
+ status: "denied",
39
+ settings: {},
40
+ });
41
+ const engine = createRNPEngine();
42
+
43
+ const result = await engine.check("notifications");
44
+
45
+ expect(checkNotifications).toHaveBeenCalled();
46
+ expect(check).not.toHaveBeenCalled();
47
+ expect(result).toBe("denied");
48
+ });
49
+
50
+ it("returns all status values correctly", async () => {
51
+ const engine = createRNPEngine();
52
+
53
+ for (const status of ["granted", "denied", "blocked", "limited", "unavailable"] as const) {
54
+ vi.mocked(check).mockResolvedValue(status);
55
+ expect(await engine.check("some.permission")).toBe(status);
56
+ }
57
+ });
58
+ });
59
+
60
+ describe("request", () => {
61
+ it("delegates to request() for regular permissions", async () => {
62
+ vi.mocked(request).mockResolvedValue("granted");
63
+ const engine = createRNPEngine();
64
+
65
+ const result = await engine.request("ios.permission.CAMERA");
66
+
67
+ expect(request).toHaveBeenCalledWith("ios.permission.CAMERA");
68
+ expect(result).toBe("granted");
69
+ });
70
+
71
+ it("delegates to requestNotifications() for 'notifications'", async () => {
72
+ vi.mocked(requestNotifications).mockResolvedValue({
73
+ status: "granted",
74
+ settings: {},
75
+ });
76
+ const engine = createRNPEngine();
77
+
78
+ const result = await engine.request("notifications");
79
+
80
+ expect(requestNotifications).toHaveBeenCalledWith(["alert", "badge", "sound"]);
81
+ expect(request).not.toHaveBeenCalled();
82
+ expect(result).toBe("granted");
83
+ });
84
+ });
85
+
86
+ describe("openSettings", () => {
87
+ it("delegates to openSettings()", async () => {
88
+ vi.mocked(openSettings).mockResolvedValue(undefined as never);
89
+ const engine = createRNPEngine();
90
+
91
+ await engine.openSettings();
92
+
93
+ expect(openSettings).toHaveBeenCalled();
94
+ });
95
+ });
96
+ });
@@ -0,0 +1,33 @@
1
+ import {
2
+ type Permission,
3
+ check,
4
+ checkNotifications,
5
+ openSettings,
6
+ request,
7
+ requestNotifications,
8
+ } from "react-native-permissions";
9
+ import type { PermissionEngine, PermissionStatus } from "../types";
10
+
11
+ export function createRNPEngine(): PermissionEngine {
12
+ return {
13
+ async check(permission: string): Promise<PermissionStatus> {
14
+ if (permission === "notifications") {
15
+ const result = await checkNotifications();
16
+ return result.status as PermissionStatus;
17
+ }
18
+ return (await check(permission as Permission)) as PermissionStatus;
19
+ },
20
+
21
+ async request(permission: string): Promise<PermissionStatus> {
22
+ if (permission === "notifications") {
23
+ const result = await requestNotifications(["alert", "badge", "sound"]);
24
+ return result.status as PermissionStatus;
25
+ }
26
+ return (await request(permission as Permission)) as PermissionStatus;
27
+ },
28
+
29
+ async openSettings(): Promise<void> {
30
+ await openSettings();
31
+ },
32
+ };
33
+ }
@@ -0,0 +1,10 @@
1
+ import type { PermissionEngine } from "../types";
2
+ import { getDefaultEngine } from "./resolve";
3
+ import { getRNPFallbackEngine } from "./rnp-fallback";
4
+
5
+ export function resolveEngine(configEngine?: PermissionEngine): PermissionEngine {
6
+ if (configEngine) return configEngine;
7
+ const global = getDefaultEngine();
8
+ if (global) return global;
9
+ return getRNPFallbackEngine();
10
+ }
@@ -1,7 +1,11 @@
1
1
  import { createElement } from "react";
2
2
  import { type ReactTestRenderer, act, create } from "react-test-renderer";
3
3
  import { beforeEach, describe, expect, it, vi } from "vitest";
4
- import type { MultiplePermissionsConfig, MultiplePermissionsResult } from "../types";
4
+ import type {
5
+ MultiplePermissionsConfig,
6
+ MultiplePermissionsResult,
7
+ PermissionEngine,
8
+ } from "../types";
5
9
 
6
10
  vi.mock("react-native", () => ({
7
11
  AppState: {
@@ -10,17 +14,24 @@ vi.mock("react-native", () => ({
10
14
  },
11
15
  }));
12
16
 
13
- vi.mock("react-native-permissions", () => ({
14
- check: vi.fn(),
15
- request: vi.fn(),
16
- openSettings: vi.fn(),
17
- checkNotifications: vi.fn(),
18
- requestNotifications: vi.fn(),
17
+ // Mock the RNP fallback so hooks don't try to require react-native-permissions
18
+ vi.mock("../engines/rnp-fallback", () => ({
19
+ getRNPFallbackEngine: vi.fn(() => {
20
+ throw new Error("No engine configured");
21
+ }),
19
22
  }));
20
23
 
21
- import { check, request } from "react-native-permissions";
22
24
  import { useMultiplePermissions } from "./use-multiple-permissions";
23
25
 
26
+ function createMockEngine(overrides?: Partial<PermissionEngine>): PermissionEngine {
27
+ return {
28
+ check: vi.fn().mockResolvedValue("granted"),
29
+ request: vi.fn().mockResolvedValue("granted"),
30
+ openSettings: vi.fn().mockResolvedValue(undefined),
31
+ ...overrides,
32
+ };
33
+ }
34
+
24
35
  function renderHook(hookFn: () => MultiplePermissionsResult) {
25
36
  const results: { current: MultiplePermissionsResult } = {} as {
26
37
  current: MultiplePermissionsResult;
@@ -39,41 +50,58 @@ function renderHook(hookFn: () => MultiplePermissionsResult) {
39
50
  };
40
51
  }
41
52
 
42
- const baseConfig: MultiplePermissionsConfig = {
43
- permissions: [
44
- {
45
- permission:
46
- "ios.permission.CAMERA" as MultiplePermissionsConfig["permissions"][0]["permission"],
47
- prePrompt: { title: "Camera", message: "Need camera" },
48
- blockedPrompt: { title: "Blocked", message: "Camera blocked" },
49
- },
50
- {
51
- permission:
52
- "ios.permission.MICROPHONE" as MultiplePermissionsConfig["permissions"][0]["permission"],
53
- prePrompt: { title: "Mic", message: "Need mic" },
54
- blockedPrompt: { title: "Blocked", message: "Mic blocked" },
55
- },
56
- ],
57
- strategy: "sequential",
58
- };
59
-
60
53
  describe("useMultiplePermissions", () => {
54
+ let engine: PermissionEngine;
55
+
56
+ const baseConfig = (
57
+ overrides?: Partial<MultiplePermissionsConfig>,
58
+ ): MultiplePermissionsConfig => ({
59
+ permissions: [
60
+ {
61
+ permission: "camera",
62
+ prePrompt: { title: "Camera", message: "Need camera" },
63
+ blockedPrompt: { title: "Blocked", message: "Camera blocked" },
64
+ },
65
+ {
66
+ permission: "microphone",
67
+ prePrompt: { title: "Mic", message: "Need mic" },
68
+ blockedPrompt: { title: "Blocked", message: "Mic blocked" },
69
+ },
70
+ ],
71
+ strategy: "sequential",
72
+ engine,
73
+ ...overrides,
74
+ });
75
+
61
76
  beforeEach(() => {
62
77
  vi.clearAllMocks();
78
+ engine = createMockEngine();
63
79
  });
64
80
 
65
- it("initializes all permissions as idle", () => {
66
- const { result } = renderHook(() => useMultiplePermissions(baseConfig));
81
+ it("initializes all permissions as idle when autoCheck is false", () => {
82
+ const { result } = renderHook(() => useMultiplePermissions(baseConfig({ autoCheck: false })));
67
83
 
68
84
  expect(result.current.allGranted).toBe(false);
69
85
  expect(Object.values(result.current.statuses)).toEqual(["idle", "idle"]);
70
86
  });
71
87
 
88
+ it("auto-checks all permissions on mount", async () => {
89
+ vi.mocked(engine.check).mockResolvedValueOnce("granted").mockResolvedValueOnce("denied");
90
+
91
+ const { result } = renderHook(() => useMultiplePermissions(baseConfig()));
92
+
93
+ await act(async () => {});
94
+
95
+ expect(engine.check).toHaveBeenCalledTimes(2);
96
+ expect(result.current.statuses.camera).toBe("granted");
97
+ expect(result.current.statuses.microphone).toBe("prePrompt");
98
+ });
99
+
72
100
  it("grants all permissions sequentially when already granted", async () => {
73
- vi.mocked(check).mockResolvedValue("granted");
101
+ vi.mocked(engine.check).mockResolvedValue("granted");
74
102
  const onAllGranted = vi.fn();
75
103
 
76
- const { result } = renderHook(() => useMultiplePermissions({ ...baseConfig, onAllGranted }));
104
+ const { result } = renderHook(() => useMultiplePermissions(baseConfig({ onAllGranted })));
77
105
 
78
106
  await act(async () => {
79
107
  await result.current.request();
@@ -84,52 +112,65 @@ describe("useMultiplePermissions", () => {
84
112
  });
85
113
 
86
114
  it("requests denied permissions sequentially", async () => {
87
- // Initial checks return denied, final re-checks return granted
88
- vi.mocked(check)
89
- .mockResolvedValueOnce("denied") // camera initial check
90
- .mockResolvedValueOnce("denied") // mic initial check
115
+ // Auto-check on mount returns denied for both, then requestAll flow
116
+ // checks again (denied), requests (granted), and final re-checks (granted)
117
+ vi.mocked(engine.check)
118
+ .mockResolvedValueOnce("denied") // auto-check: camera
119
+ .mockResolvedValueOnce("denied") // auto-check: mic
120
+ .mockResolvedValueOnce("denied") // requestAll: camera check
121
+ .mockResolvedValueOnce("denied") // requestAll: mic check
91
122
  .mockResolvedValue("granted"); // final re-checks
92
- vi.mocked(request).mockResolvedValue("granted");
123
+ vi.mocked(engine.request).mockResolvedValue("granted");
93
124
  const onAllGranted = vi.fn();
94
125
 
95
- const { result } = renderHook(() => useMultiplePermissions({ ...baseConfig, onAllGranted }));
126
+ const { result } = renderHook(() => useMultiplePermissions(baseConfig({ onAllGranted })));
96
127
 
128
+ await act(async () => {});
97
129
  await act(async () => {
98
130
  await result.current.request();
99
131
  });
100
132
 
101
133
  expect(result.current.allGranted).toBe(true);
102
- expect(request).toHaveBeenCalledTimes(2);
134
+ expect(engine.request).toHaveBeenCalledTimes(2);
103
135
  expect(onAllGranted).toHaveBeenCalledOnce();
104
136
  });
105
137
 
106
138
  it("stops sequential flow when permission is denied", async () => {
107
- vi.mocked(check).mockResolvedValue("denied");
108
- vi.mocked(request).mockResolvedValueOnce("denied");
139
+ vi.mocked(engine.check).mockResolvedValue("denied");
140
+ vi.mocked(engine.request).mockResolvedValueOnce("denied");
109
141
 
110
- const { result } = renderHook(() => useMultiplePermissions(baseConfig));
142
+ const { result } = renderHook(() => useMultiplePermissions(baseConfig()));
111
143
 
112
144
  await act(async () => {
113
145
  await result.current.request();
114
146
  });
115
147
 
116
148
  expect(result.current.allGranted).toBe(false);
117
- expect(request).toHaveBeenCalledTimes(1);
149
+ expect(engine.request).toHaveBeenCalledTimes(1);
118
150
  });
119
151
 
120
152
  it("fires per-permission callbacks", async () => {
121
- vi.mocked(check).mockResolvedValue("denied");
122
- vi.mocked(request).mockResolvedValueOnce("granted").mockResolvedValueOnce("denied");
153
+ vi.mocked(engine.check).mockResolvedValue("denied");
154
+ vi.mocked(engine.request).mockResolvedValueOnce("granted").mockResolvedValueOnce("denied");
123
155
  const onGrant = vi.fn();
124
156
  const onDeny = vi.fn();
125
157
 
126
- const config: MultiplePermissionsConfig = {
127
- ...baseConfig,
158
+ const config = baseConfig({
128
159
  permissions: [
129
- { ...baseConfig.permissions[0], onGrant },
130
- { ...baseConfig.permissions[1], onDeny },
160
+ {
161
+ permission: "camera",
162
+ prePrompt: { title: "Camera", message: "Need camera" },
163
+ blockedPrompt: { title: "Blocked", message: "Camera blocked" },
164
+ onGrant,
165
+ },
166
+ {
167
+ permission: "microphone",
168
+ prePrompt: { title: "Mic", message: "Need mic" },
169
+ blockedPrompt: { title: "Blocked", message: "Mic blocked" },
170
+ onDeny,
171
+ },
131
172
  ],
132
- };
173
+ });
133
174
 
134
175
  const { result } = renderHook(() => useMultiplePermissions(config));
135
176
 
@@ -142,18 +183,17 @@ describe("useMultiplePermissions", () => {
142
183
  });
143
184
 
144
185
  it("handles parallel strategy — checks all, then requests denied", async () => {
145
- vi.mocked(check)
186
+ vi.mocked(engine.check)
146
187
  .mockResolvedValueOnce("granted") // camera already granted
147
188
  .mockResolvedValueOnce("denied") // mic needs request
148
189
  .mockResolvedValue("granted"); // final re-checks
149
- vi.mocked(request).mockResolvedValue("granted");
190
+ vi.mocked(engine.request).mockResolvedValue("granted");
150
191
  const onAllGranted = vi.fn();
151
192
 
152
- const config: MultiplePermissionsConfig = {
153
- ...baseConfig,
193
+ const config = baseConfig({
154
194
  strategy: "parallel",
155
195
  onAllGranted,
156
- };
196
+ });
157
197
 
158
198
  const { result } = renderHook(() => useMultiplePermissions(config));
159
199
 
@@ -161,7 +201,7 @@ describe("useMultiplePermissions", () => {
161
201
  await result.current.request();
162
202
  });
163
203
 
164
- expect(request).toHaveBeenCalledTimes(1); // only mic was requested
204
+ expect(engine.request).toHaveBeenCalledTimes(1); // only mic was requested
165
205
  expect(onAllGranted).toHaveBeenCalledOnce();
166
206
  });
167
207
  });
@@ -1,40 +1,14 @@
1
- import { useCallback, useRef, useState } from "react";
2
- import {
3
- type PermissionStatus,
4
- check,
5
- checkNotifications,
6
- request,
7
- requestNotifications,
8
- } from "react-native-permissions";
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import { resolveEngine } from "../engines/use-engine";
9
3
  import type {
10
4
  MultiPermissionEntry,
11
5
  MultiplePermissionsConfig,
12
6
  MultiplePermissionsResult,
7
+ PermissionEngine,
13
8
  PermissionFlowState,
9
+ PermissionStatus,
14
10
  } from "../types";
15
11
 
16
- function isNotifications(
17
- permission: MultiPermissionEntry["permission"],
18
- ): permission is "notifications" {
19
- return permission === "notifications";
20
- }
21
-
22
- async function checkOne(entry: MultiPermissionEntry): Promise<PermissionStatus> {
23
- if (isNotifications(entry.permission)) {
24
- const result = await checkNotifications();
25
- return result.status;
26
- }
27
- return check(entry.permission);
28
- }
29
-
30
- async function requestOne(entry: MultiPermissionEntry): Promise<PermissionStatus> {
31
- if (isNotifications(entry.permission)) {
32
- const result = await requestNotifications(["alert", "badge", "sound"]);
33
- return result.status;
34
- }
35
- return request(entry.permission);
36
- }
37
-
38
12
  function permissionKey(entry: MultiPermissionEntry): string {
39
13
  return String(entry.permission);
40
14
  }
@@ -43,10 +17,27 @@ function isGrantedStatus(status: PermissionStatus): boolean {
43
17
  return status === "granted" || status === "limited";
44
18
  }
45
19
 
20
+ function statusToFlowState(status: PermissionStatus): PermissionFlowState {
21
+ switch (status) {
22
+ case "granted":
23
+ case "limited":
24
+ return "granted";
25
+ case "blocked":
26
+ return "blockedPrompt";
27
+ case "unavailable":
28
+ return "unavailable";
29
+ case "denied":
30
+ return "prePrompt";
31
+ default:
32
+ return "idle";
33
+ }
34
+ }
35
+
46
36
  export function useMultiplePermissions(
47
37
  config: MultiplePermissionsConfig,
48
38
  ): MultiplePermissionsResult {
49
- const { permissions, strategy, onAllGranted } = config;
39
+ const engine = resolveEngine(config.engine);
40
+ const { permissions, strategy, autoCheck = true, onAllGranted } = config;
50
41
  const [statuses, setStatuses] = useState<Record<string, PermissionFlowState>>(() => {
51
42
  const initial: Record<string, PermissionFlowState> = {};
52
43
  for (const entry of permissions) {
@@ -68,15 +59,15 @@ export function useMultiplePermissions(
68
59
 
69
60
  try {
70
61
  if (strategy === "sequential") {
71
- await runSequential(permissions, update);
62
+ await runSequential(permissions, engine, update);
72
63
  } else {
73
- await runParallel(permissions, update);
64
+ await runParallel(permissions, engine, update);
74
65
  }
75
66
 
76
67
  // Final check: are all granted?
77
68
  let allDone = true;
78
69
  for (const entry of permissions) {
79
- const finalStatus = await checkOne(entry);
70
+ const finalStatus = await engine.check(entry.permission);
80
71
  if (!isGrantedStatus(finalStatus)) {
81
72
  allDone = false;
82
73
  break;
@@ -88,7 +79,27 @@ export function useMultiplePermissions(
88
79
  } finally {
89
80
  isRunning.current = false;
90
81
  }
91
- }, [permissions, strategy, onAllGranted]);
82
+ }, [permissions, strategy, engine, onAllGranted]);
83
+
84
+ // Auto-check on mount
85
+ // biome-ignore lint/correctness/useExhaustiveDependencies: intentional mount-only effect
86
+ useEffect(() => {
87
+ if (!autoCheck) return;
88
+
89
+ let cancelled = false;
90
+ async function checkAll() {
91
+ for (const entry of permissions) {
92
+ const key = permissionKey(entry);
93
+ const status = await engine.check(entry.permission);
94
+ if (cancelled) return;
95
+ setStatuses((prev) => ({ ...prev, [key]: statusToFlowState(status) }));
96
+ }
97
+ }
98
+ checkAll();
99
+ return () => {
100
+ cancelled = true;
101
+ };
102
+ }, []);
92
103
 
93
104
  return {
94
105
  statuses,
@@ -99,13 +110,14 @@ export function useMultiplePermissions(
99
110
 
100
111
  async function runSequential(
101
112
  permissions: MultiPermissionEntry[],
113
+ engine: PermissionEngine,
102
114
  updateStatus: (key: string, state: PermissionFlowState) => void,
103
115
  ): Promise<void> {
104
116
  for (const entry of permissions) {
105
117
  const key = permissionKey(entry);
106
118
 
107
119
  updateStatus(key, "checking");
108
- const checkStatus = await checkOne(entry);
120
+ const checkStatus = await engine.check(entry.permission);
109
121
 
110
122
  if (isGrantedStatus(checkStatus)) {
111
123
  updateStatus(key, "granted");
@@ -126,7 +138,7 @@ async function runSequential(
126
138
 
127
139
  // Denied — request it
128
140
  updateStatus(key, "requesting");
129
- const requestStatus = await requestOne(entry);
141
+ const requestStatus = await engine.request(entry.permission);
130
142
 
131
143
  if (isGrantedStatus(requestStatus)) {
132
144
  updateStatus(key, "granted");
@@ -145,6 +157,7 @@ async function runSequential(
145
157
 
146
158
  async function runParallel(
147
159
  permissions: MultiPermissionEntry[],
160
+ engine: PermissionEngine,
148
161
  updateStatus: (key: string, state: PermissionFlowState) => void,
149
162
  ): Promise<void> {
150
163
  // Check all in parallel
@@ -152,7 +165,7 @@ async function runParallel(
152
165
  permissions.map(async (entry) => {
153
166
  const key = permissionKey(entry);
154
167
  updateStatus(key, "checking");
155
- const status = await checkOne(entry);
168
+ const status = await engine.check(entry.permission);
156
169
  return { entry, key, status };
157
170
  }),
158
171
  );
@@ -180,7 +193,7 @@ async function runParallel(
180
193
  }
181
194
 
182
195
  updateStatus(key, "requesting");
183
- const requestStatus = await requestOne(entry);
196
+ const requestStatus = await engine.request(entry.permission);
184
197
 
185
198
  if (isGrantedStatus(requestStatus)) {
186
199
  updateStatus(key, "granted");