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
@@ -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
- // Mocks must be before imports that use them
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
- vi.mock("react-native-permissions", () => ({
15
- check: vi.fn(),
16
- request: vi.fn(),
17
- openSettings: vi.fn(),
18
- checkNotifications: vi.fn(),
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(baseConfig.permission);
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({ ...baseConfig, autoCheck: false }));
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({ ...baseConfig, onGrant }));
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({ ...baseConfig, onDeny }));
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({ ...baseConfig, onBlock }));
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("uses notification API when permission is 'notifications'", async () => {
156
- vi.mocked(checkNotifications).mockResolvedValue({ status: "denied", settings: {} });
157
- vi.mocked(requestNotifications).mockResolvedValue({ status: "granted", settings: {} });
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({ ...baseConfig, permission: "notifications" }),
171
+ usePermissionHandler(baseConfig({ permission: "notifications" })),
161
172
  );
162
173
 
163
174
  await act(async () => {});
164
- expect(checkNotifications).toHaveBeenCalled();
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(requestNotifications).toHaveBeenCalledWith(["alert", "badge", "sound"]);
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({ ...baseConfig, onDeny }));
208
+ const { result } = renderHook(() => usePermissionHandler(baseConfig({ onDeny })));
199
209
 
200
210
  await act(async () => {});
201
211
  expect(result.current.state).toBe("prePrompt");
@@ -1,27 +1,16 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
2
  import { AppState } from "react-native";
3
- import {
4
- type PermissionStatus,
5
- check,
6
- checkNotifications,
7
- openSettings,
8
- request,
9
- requestNotifications,
10
- } from "react-native-permissions";
11
3
  import { transition } from "../core/state-machine";
4
+ import { resolveEngine } from "../engines/use-engine";
12
5
  import type {
13
6
  PermissionFlowState,
14
7
  PermissionHandlerConfig,
15
8
  PermissionHandlerResult,
9
+ PermissionStatus,
16
10
  } from "../types";
17
11
 
18
- function isNotifications(
19
- permission: PermissionHandlerConfig["permission"],
20
- ): permission is "notifications" {
21
- return permission === "notifications";
22
- }
23
-
24
12
  export function usePermissionHandler(config: PermissionHandlerConfig): PermissionHandlerResult {
13
+ const engine = resolveEngine(config.engine);
25
14
  const [flowState, setFlowState] = useState<PermissionFlowState>("idle");
26
15
  const [nativeStatus, setNativeStatus] = useState<PermissionStatus | null>(null);
27
16
  const isRequesting = useRef(false);
@@ -33,13 +22,7 @@ export function usePermissionHandler(config: PermissionHandlerConfig): Permissio
33
22
  const checkPermission = useCallback(async () => {
34
23
  setFlowState((s) => transition(s, { type: "CHECK" }));
35
24
  try {
36
- let status: PermissionStatus;
37
- if (isNotifications(permission)) {
38
- const result = await checkNotifications();
39
- status = result.status;
40
- } else {
41
- status = await check(permission);
42
- }
25
+ const status = await engine.check(permission);
43
26
  setNativeStatus(status);
44
27
  setFlowState((s) => {
45
28
  const next = transition(s, { type: "CHECK_RESULT", status });
@@ -49,7 +32,7 @@ export function usePermissionHandler(config: PermissionHandlerConfig): Permissio
49
32
  } catch {
50
33
  setFlowState("idle");
51
34
  }
52
- }, [permission, onGrant]);
35
+ }, [engine, permission, onGrant]);
53
36
 
54
37
  const requestPermission = useCallback(async () => {
55
38
  if (isRequesting.current) return;
@@ -57,13 +40,7 @@ export function usePermissionHandler(config: PermissionHandlerConfig): Permissio
57
40
 
58
41
  setFlowState((s) => transition(s, { type: "PRE_PROMPT_CONFIRM" }));
59
42
  try {
60
- let status: PermissionStatus;
61
- if (isNotifications(permission)) {
62
- const result = await requestNotifications(["alert", "badge", "sound"]);
63
- status = result.status;
64
- } else {
65
- status = await request(permission);
66
- }
43
+ const status = await engine.request(permission);
67
44
  setNativeStatus(status);
68
45
  setFlowState((s) => {
69
46
  const next = transition(s, { type: "REQUEST_RESULT", status });
@@ -77,7 +54,7 @@ export function usePermissionHandler(config: PermissionHandlerConfig): Permissio
77
54
  } finally {
78
55
  isRequesting.current = false;
79
56
  }
80
- }, [permission, onGrant, onDeny, onBlock]);
57
+ }, [engine, permission, onGrant, onDeny, onBlock]);
81
58
 
82
59
  const dismiss = useCallback(() => {
83
60
  setFlowState((s) => transition(s, { type: "PRE_PROMPT_DISMISS" }));
@@ -88,23 +65,17 @@ export function usePermissionHandler(config: PermissionHandlerConfig): Permissio
88
65
  setFlowState((s) => transition(s, { type: "OPEN_SETTINGS" }));
89
66
  waitingForSettings.current = true;
90
67
  try {
91
- await openSettings();
68
+ await engine.openSettings();
92
69
  } catch {
93
70
  waitingForSettings.current = false;
94
71
  setFlowState("blockedPrompt");
95
72
  }
96
- }, []);
73
+ }, [engine]);
97
74
 
98
75
  const recheckAfterSettings = useCallback(async () => {
99
76
  setFlowState((s) => transition(s, { type: "SETTINGS_RETURN" }));
100
77
  try {
101
- let status: PermissionStatus;
102
- if (isNotifications(permission)) {
103
- const result = await checkNotifications();
104
- status = result.status;
105
- } else {
106
- status = await check(permission);
107
- }
78
+ const status = await engine.check(permission);
108
79
  setNativeStatus(status);
109
80
  setFlowState((s) => {
110
81
  const next = transition(s, { type: "RECHECK_RESULT", status });
@@ -115,7 +86,7 @@ export function usePermissionHandler(config: PermissionHandlerConfig): Permissio
115
86
  } catch {
116
87
  setFlowState("blockedPrompt");
117
88
  }
118
- }, [permission, onGrant, onSettingsReturn]);
89
+ }, [engine, permission, onGrant, onSettingsReturn]);
119
90
 
120
91
  // Auto-check on mount
121
92
  // biome-ignore lint/correctness/useExhaustiveDependencies: intentional mount-only effect
package/src/index.ts CHANGED
@@ -4,13 +4,16 @@ export type {
4
4
  MultiplePermissionsConfig,
5
5
  MultiplePermissionsResult,
6
6
  PermissionCallbacks,
7
+ PermissionEngine,
7
8
  PermissionFlowEvent,
8
9
  PermissionFlowState,
9
10
  PermissionHandlerConfig,
10
11
  PermissionHandlerResult,
12
+ PermissionStatus,
11
13
  PrePromptConfig,
12
14
  } from "./types";
13
15
 
16
+ export { setDefaultEngine } from "./engines/resolve";
14
17
  export { transition } from "./core/state-machine";
15
18
  export { usePermissionHandler } from "./hooks/use-permission-handler";
16
19
  export { useMultiplePermissions } from "./hooks/use-multiple-permissions";
package/src/types.ts CHANGED
@@ -1,4 +1,18 @@
1
- import type { Permission, PermissionStatus } from "react-native-permissions";
1
+ /**
2
+ * Permission status values owned by this library.
3
+ * Engines must map their native statuses to these values.
4
+ */
5
+ export type PermissionStatus = "granted" | "denied" | "blocked" | "limited" | "unavailable";
6
+
7
+ /**
8
+ * The pluggable permission engine interface.
9
+ * Implement this to use a custom permissions backend.
10
+ */
11
+ export interface PermissionEngine {
12
+ check(permission: string): Promise<PermissionStatus>;
13
+ request(permission: string): Promise<PermissionStatus>;
14
+ openSettings(): Promise<void>;
15
+ }
2
16
 
3
17
  /**
4
18
  * States of the permission flow state machine.
@@ -62,7 +76,8 @@ export interface PermissionCallbacks {
62
76
  * Configuration for usePermissionHandler.
63
77
  */
64
78
  export interface PermissionHandlerConfig extends PermissionCallbacks {
65
- permission: Permission | "notifications";
79
+ permission: string;
80
+ engine?: PermissionEngine;
66
81
  prePrompt: PrePromptConfig;
67
82
  blockedPrompt: BlockedPromptConfig;
68
83
  autoCheck?: boolean;
@@ -90,7 +105,7 @@ export interface PermissionHandlerResult {
90
105
  * Configuration for a single permission within useMultiplePermissions.
91
106
  */
92
107
  export interface MultiPermissionEntry extends PermissionCallbacks {
93
- permission: Permission | "notifications";
108
+ permission: string;
94
109
  prePrompt: PrePromptConfig;
95
110
  blockedPrompt: BlockedPromptConfig;
96
111
  }
@@ -101,6 +116,8 @@ export interface MultiPermissionEntry extends PermissionCallbacks {
101
116
  export interface MultiplePermissionsConfig {
102
117
  permissions: MultiPermissionEntry[];
103
118
  strategy: "sequential" | "parallel";
119
+ engine?: PermissionEngine;
120
+ autoCheck?: boolean;
104
121
  onAllGranted?: () => void;
105
122
  }
106
123