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.
- package/README.md +183 -47
- package/dist/chunk-NFEGQTCC.mjs +27 -0
- package/dist/chunk-NFEGQTCC.mjs.map +1 -0
- package/dist/chunk-WZJOIVOM.mjs +49 -0
- package/dist/chunk-WZJOIVOM.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 +5 -0
- package/dist/engines/rnp.d.ts +5 -0
- package/dist/engines/rnp.js +52 -0
- package/dist/engines/rnp.js.map +1 -0
- package/dist/engines/rnp.mjs +10 -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 +127 -58
- 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 +96 -0
- package/src/engines/rnp.ts +33 -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,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 {
|
|
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");
|