react-native-permission-handler 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +212 -53
- package/dist/chunk-EU3KPRTI.mjs +81 -0
- package/dist/chunk-EU3KPRTI.mjs.map +1 -0
- package/dist/chunk-NFEGQTCC.mjs +27 -0
- package/dist/chunk-NFEGQTCC.mjs.map +1 -0
- package/dist/engines/expo.d.mts +18 -0
- package/dist/engines/expo.d.ts +18 -0
- package/dist/engines/expo.js +58 -0
- package/dist/engines/expo.js.map +1 -0
- package/dist/engines/expo.mjs +35 -0
- package/dist/engines/expo.mjs.map +1 -0
- package/dist/engines/rnp.d.mts +22 -0
- package/dist/engines/rnp.d.ts +22 -0
- package/dist/engines/rnp.js +83 -0
- package/dist/engines/rnp.js.map +1 -0
- package/dist/engines/rnp.mjs +12 -0
- package/dist/engines/rnp.mjs.map +1 -0
- package/dist/index.d.mts +7 -107
- package/dist/index.d.ts +7 -107
- package/dist/index.js +175 -76
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +98 -70
- package/dist/index.mjs.map +1 -1
- package/dist/types-QXyq8VnD.d.mts +122 -0
- package/dist/types-QXyq8VnD.d.ts +122 -0
- package/package.json +27 -2
- package/src/components/permission-gate.tsx +10 -3
- package/src/engines/expo.test.ts +122 -0
- package/src/engines/expo.ts +45 -0
- package/src/engines/resolve.test.ts +85 -0
- package/src/engines/resolve.ts +11 -0
- package/src/engines/rnp-fallback.ts +23 -0
- package/src/engines/rnp.test.ts +122 -0
- package/src/engines/rnp.ts +68 -0
- package/src/engines/use-engine.ts +10 -0
- package/src/hooks/use-multiple-permissions.test.ts +94 -54
- package/src/hooks/use-multiple-permissions.ts +52 -39
- package/src/hooks/use-permission-handler.test.ts +59 -49
- package/src/hooks/use-permission-handler.ts +11 -40
- package/src/index.ts +3 -0
- package/src/types.ts +20 -3
|
@@ -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 {
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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({
|
|
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
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
.mockResolvedValueOnce("denied") //
|
|
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({
|
|
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
|
|
127
|
-
...baseConfig,
|
|
158
|
+
const config = baseConfig({
|
|
128
159
|
permissions: [
|
|
129
|
-
{
|
|
130
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
196
|
+
const requestStatus = await engine.request(entry.permission);
|
|
184
197
|
|
|
185
198
|
if (isGrantedStatus(requestStatus)) {
|
|
186
199
|
updateStatus(key, "granted");
|
|
@@ -1,9 +1,9 @@
|
|
|
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 { PermissionHandlerConfig, PermissionHandlerResult } from "../types";
|
|
4
|
+
import type { PermissionEngine, PermissionHandlerConfig, PermissionHandlerResult } from "../types";
|
|
5
5
|
|
|
6
|
-
//
|
|
6
|
+
// Mock react-native AppState
|
|
7
7
|
vi.mock("react-native", () => ({
|
|
8
8
|
AppState: {
|
|
9
9
|
currentState: "active",
|
|
@@ -11,17 +11,24 @@ vi.mock("react-native", () => ({
|
|
|
11
11
|
},
|
|
12
12
|
}));
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
requestNotifications: vi.fn(),
|
|
14
|
+
// Mock the RNP fallback so hooks don't try to require react-native-permissions
|
|
15
|
+
vi.mock("../engines/rnp-fallback", () => ({
|
|
16
|
+
getRNPFallbackEngine: vi.fn(() => {
|
|
17
|
+
throw new Error("No engine configured");
|
|
18
|
+
}),
|
|
20
19
|
}));
|
|
21
20
|
|
|
22
|
-
import { check, checkNotifications, request, requestNotifications } from "react-native-permissions";
|
|
23
21
|
import { usePermissionHandler } from "./use-permission-handler";
|
|
24
22
|
|
|
23
|
+
function createMockEngine(overrides?: Partial<PermissionEngine>): PermissionEngine {
|
|
24
|
+
return {
|
|
25
|
+
check: vi.fn().mockResolvedValue("granted"),
|
|
26
|
+
request: vi.fn().mockResolvedValue("granted"),
|
|
27
|
+
openSettings: vi.fn().mockResolvedValue(undefined),
|
|
28
|
+
...overrides,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
25
32
|
// Minimal renderHook using react-test-renderer
|
|
26
33
|
function renderHook(hookFn: () => PermissionHandlerResult) {
|
|
27
34
|
const results: { current: PermissionHandlerResult } = {} as {
|
|
@@ -41,32 +48,36 @@ function renderHook(hookFn: () => PermissionHandlerResult) {
|
|
|
41
48
|
};
|
|
42
49
|
}
|
|
43
50
|
|
|
44
|
-
const baseConfig: PermissionHandlerConfig = {
|
|
45
|
-
permission: "ios.permission.CAMERA" as PermissionHandlerConfig["permission"],
|
|
46
|
-
prePrompt: { title: "Camera", message: "We need camera access" },
|
|
47
|
-
blockedPrompt: { title: "Blocked", message: "Camera is blocked" },
|
|
48
|
-
};
|
|
49
|
-
|
|
50
51
|
describe("usePermissionHandler", () => {
|
|
52
|
+
let engine: PermissionEngine;
|
|
53
|
+
|
|
54
|
+
const baseConfig = (overrides?: Partial<PermissionHandlerConfig>): PermissionHandlerConfig => ({
|
|
55
|
+
permission: "camera",
|
|
56
|
+
engine,
|
|
57
|
+
prePrompt: { title: "Camera", message: "We need camera access" },
|
|
58
|
+
blockedPrompt: { title: "Blocked", message: "Camera is blocked" },
|
|
59
|
+
...overrides,
|
|
60
|
+
});
|
|
61
|
+
|
|
51
62
|
beforeEach(() => {
|
|
52
63
|
vi.clearAllMocks();
|
|
64
|
+
engine = createMockEngine();
|
|
53
65
|
});
|
|
54
66
|
|
|
55
67
|
it("auto-checks on mount and transitions to granted", async () => {
|
|
56
|
-
vi.mocked(check).mockResolvedValue("granted");
|
|
57
|
-
const { result } = renderHook(() => usePermissionHandler(baseConfig));
|
|
68
|
+
vi.mocked(engine.check).mockResolvedValue("granted");
|
|
69
|
+
const { result } = renderHook(() => usePermissionHandler(baseConfig()));
|
|
58
70
|
|
|
59
|
-
// After mount, should be checking
|
|
60
71
|
await act(async () => {});
|
|
61
72
|
|
|
62
73
|
expect(result.current.isGranted).toBe(true);
|
|
63
74
|
expect(result.current.state).toBe("granted");
|
|
64
|
-
expect(check).toHaveBeenCalledWith(
|
|
75
|
+
expect(engine.check).toHaveBeenCalledWith("camera");
|
|
65
76
|
});
|
|
66
77
|
|
|
67
78
|
it("transitions to prePrompt when permission is denied", async () => {
|
|
68
|
-
vi.mocked(check).mockResolvedValue("denied");
|
|
69
|
-
const { result } = renderHook(() => usePermissionHandler(baseConfig));
|
|
79
|
+
vi.mocked(engine.check).mockResolvedValue("denied");
|
|
80
|
+
const { result } = renderHook(() => usePermissionHandler(baseConfig()));
|
|
70
81
|
|
|
71
82
|
await act(async () => {});
|
|
72
83
|
|
|
@@ -74,8 +85,8 @@ describe("usePermissionHandler", () => {
|
|
|
74
85
|
});
|
|
75
86
|
|
|
76
87
|
it("transitions to blockedPrompt when permission is blocked", async () => {
|
|
77
|
-
vi.mocked(check).mockResolvedValue("blocked");
|
|
78
|
-
const { result } = renderHook(() => usePermissionHandler(baseConfig));
|
|
88
|
+
vi.mocked(engine.check).mockResolvedValue("blocked");
|
|
89
|
+
const { result } = renderHook(() => usePermissionHandler(baseConfig()));
|
|
79
90
|
|
|
80
91
|
await act(async () => {});
|
|
81
92
|
|
|
@@ -84,8 +95,8 @@ describe("usePermissionHandler", () => {
|
|
|
84
95
|
});
|
|
85
96
|
|
|
86
97
|
it("transitions to unavailable", async () => {
|
|
87
|
-
vi.mocked(check).mockResolvedValue("unavailable");
|
|
88
|
-
const { result } = renderHook(() => usePermissionHandler(baseConfig));
|
|
98
|
+
vi.mocked(engine.check).mockResolvedValue("unavailable");
|
|
99
|
+
const { result } = renderHook(() => usePermissionHandler(baseConfig()));
|
|
89
100
|
|
|
90
101
|
await act(async () => {});
|
|
91
102
|
|
|
@@ -94,20 +105,20 @@ describe("usePermissionHandler", () => {
|
|
|
94
105
|
});
|
|
95
106
|
|
|
96
107
|
it("skips auto-check when autoCheck is false", async () => {
|
|
97
|
-
const { result } = renderHook(() => usePermissionHandler({
|
|
108
|
+
const { result } = renderHook(() => usePermissionHandler(baseConfig({ autoCheck: false })));
|
|
98
109
|
|
|
99
110
|
await act(async () => {});
|
|
100
111
|
|
|
101
112
|
expect(result.current.state).toBe("idle");
|
|
102
|
-
expect(check).not.toHaveBeenCalled();
|
|
113
|
+
expect(engine.check).not.toHaveBeenCalled();
|
|
103
114
|
});
|
|
104
115
|
|
|
105
116
|
it("requests permission and fires onGrant", async () => {
|
|
106
|
-
vi.mocked(check).mockResolvedValue("denied");
|
|
107
|
-
vi.mocked(request).mockResolvedValue("granted");
|
|
117
|
+
vi.mocked(engine.check).mockResolvedValue("denied");
|
|
118
|
+
vi.mocked(engine.request).mockResolvedValue("granted");
|
|
108
119
|
const onGrant = vi.fn();
|
|
109
120
|
|
|
110
|
-
const { result } = renderHook(() => usePermissionHandler({
|
|
121
|
+
const { result } = renderHook(() => usePermissionHandler(baseConfig({ onGrant })));
|
|
111
122
|
|
|
112
123
|
await act(async () => {});
|
|
113
124
|
expect(result.current.state).toBe("prePrompt");
|
|
@@ -121,11 +132,11 @@ describe("usePermissionHandler", () => {
|
|
|
121
132
|
});
|
|
122
133
|
|
|
123
134
|
it("fires onDeny when request is denied", async () => {
|
|
124
|
-
vi.mocked(check).mockResolvedValue("denied");
|
|
125
|
-
vi.mocked(request).mockResolvedValue("denied");
|
|
135
|
+
vi.mocked(engine.check).mockResolvedValue("denied");
|
|
136
|
+
vi.mocked(engine.request).mockResolvedValue("denied");
|
|
126
137
|
const onDeny = vi.fn();
|
|
127
138
|
|
|
128
|
-
const { result } = renderHook(() => usePermissionHandler({
|
|
139
|
+
const { result } = renderHook(() => usePermissionHandler(baseConfig({ onDeny })));
|
|
129
140
|
|
|
130
141
|
await act(async () => {});
|
|
131
142
|
await act(async () => {
|
|
@@ -137,11 +148,11 @@ describe("usePermissionHandler", () => {
|
|
|
137
148
|
});
|
|
138
149
|
|
|
139
150
|
it("fires onBlock when request results in blocked", async () => {
|
|
140
|
-
vi.mocked(check).mockResolvedValue("denied");
|
|
141
|
-
vi.mocked(request).mockResolvedValue("blocked");
|
|
151
|
+
vi.mocked(engine.check).mockResolvedValue("denied");
|
|
152
|
+
vi.mocked(engine.request).mockResolvedValue("blocked");
|
|
142
153
|
const onBlock = vi.fn();
|
|
143
154
|
|
|
144
|
-
const { result } = renderHook(() => usePermissionHandler({
|
|
155
|
+
const { result } = renderHook(() => usePermissionHandler(baseConfig({ onBlock })));
|
|
145
156
|
|
|
146
157
|
await act(async () => {});
|
|
147
158
|
await act(async () => {
|
|
@@ -152,33 +163,32 @@ describe("usePermissionHandler", () => {
|
|
|
152
163
|
expect(onBlock).toHaveBeenCalled();
|
|
153
164
|
});
|
|
154
165
|
|
|
155
|
-
it("
|
|
156
|
-
vi.mocked(
|
|
157
|
-
vi.mocked(
|
|
166
|
+
it("passes permission string to engine (notification routing is engine's job)", async () => {
|
|
167
|
+
vi.mocked(engine.check).mockResolvedValue("denied");
|
|
168
|
+
vi.mocked(engine.request).mockResolvedValue("granted");
|
|
158
169
|
|
|
159
170
|
const { result } = renderHook(() =>
|
|
160
|
-
usePermissionHandler({
|
|
171
|
+
usePermissionHandler(baseConfig({ permission: "notifications" })),
|
|
161
172
|
);
|
|
162
173
|
|
|
163
174
|
await act(async () => {});
|
|
164
|
-
expect(
|
|
165
|
-
expect(check).not.toHaveBeenCalled();
|
|
175
|
+
expect(engine.check).toHaveBeenCalledWith("notifications");
|
|
166
176
|
|
|
167
177
|
await act(async () => {
|
|
168
178
|
result.current.request();
|
|
169
179
|
});
|
|
170
180
|
|
|
171
|
-
expect(
|
|
181
|
+
expect(engine.request).toHaveBeenCalledWith("notifications");
|
|
172
182
|
expect(result.current.isGranted).toBe(true);
|
|
173
183
|
});
|
|
174
184
|
|
|
175
185
|
it("guards against double-tap race condition", async () => {
|
|
176
|
-
vi.mocked(check).mockResolvedValue("denied");
|
|
177
|
-
vi.mocked(request).mockImplementation(
|
|
186
|
+
vi.mocked(engine.check).mockResolvedValue("denied");
|
|
187
|
+
vi.mocked(engine.request).mockImplementation(
|
|
178
188
|
() => new Promise((resolve) => setTimeout(() => resolve("granted"), 50)),
|
|
179
189
|
);
|
|
180
190
|
|
|
181
|
-
const { result } = renderHook(() => usePermissionHandler(baseConfig));
|
|
191
|
+
const { result } = renderHook(() => usePermissionHandler(baseConfig()));
|
|
182
192
|
|
|
183
193
|
await act(async () => {});
|
|
184
194
|
expect(result.current.state).toBe("prePrompt");
|
|
@@ -188,14 +198,14 @@ describe("usePermissionHandler", () => {
|
|
|
188
198
|
result.current.request(); // double-tap
|
|
189
199
|
});
|
|
190
200
|
|
|
191
|
-
expect(request).toHaveBeenCalledTimes(1);
|
|
201
|
+
expect(engine.request).toHaveBeenCalledTimes(1);
|
|
192
202
|
});
|
|
193
203
|
|
|
194
204
|
it("dismiss fires onDeny and transitions to denied", async () => {
|
|
195
|
-
vi.mocked(check).mockResolvedValue("denied");
|
|
205
|
+
vi.mocked(engine.check).mockResolvedValue("denied");
|
|
196
206
|
const onDeny = vi.fn();
|
|
197
207
|
|
|
198
|
-
const { result } = renderHook(() => usePermissionHandler({
|
|
208
|
+
const { result } = renderHook(() => usePermissionHandler(baseConfig({ onDeny })));
|
|
199
209
|
|
|
200
210
|
await act(async () => {});
|
|
201
211
|
expect(result.current.state).toBe("prePrompt");
|