react-native-permission-handler 0.2.0 → 0.3.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/dist/index.js CHANGED
@@ -23,8 +23,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
23
23
  // src/engines/rnp.ts
24
24
  var rnp_exports = {};
25
25
  __export(rnp_exports, {
26
+ Permissions: () => Permissions,
26
27
  createRNPEngine: () => createRNPEngine
27
28
  });
29
+ function p(ios, android) {
30
+ return import_react_native.Platform.select({ ios, android, default: ios }) ?? ios;
31
+ }
28
32
  function createRNPEngine() {
29
33
  return {
30
34
  async check(permission) {
@@ -46,11 +50,37 @@ function createRNPEngine() {
46
50
  }
47
51
  };
48
52
  }
49
- var import_react_native_permissions;
53
+ var import_react_native, import_react_native_permissions, Permissions;
50
54
  var init_rnp = __esm({
51
55
  "src/engines/rnp.ts"() {
52
56
  "use strict";
57
+ import_react_native = require("react-native");
53
58
  import_react_native_permissions = require("react-native-permissions");
59
+ Permissions = {
60
+ CAMERA: p("ios.permission.CAMERA", "android.permission.CAMERA"),
61
+ MICROPHONE: p("ios.permission.MICROPHONE", "android.permission.RECORD_AUDIO"),
62
+ CONTACTS: p("ios.permission.CONTACTS", "android.permission.READ_CONTACTS"),
63
+ CALENDARS: p("ios.permission.CALENDARS", "android.permission.READ_CALENDAR"),
64
+ CALENDARS_WRITE_ONLY: p(
65
+ "ios.permission.CALENDARS_WRITE_ONLY",
66
+ "android.permission.WRITE_CALENDAR"
67
+ ),
68
+ LOCATION_WHEN_IN_USE: p(
69
+ "ios.permission.LOCATION_WHEN_IN_USE",
70
+ "android.permission.ACCESS_FINE_LOCATION"
71
+ ),
72
+ LOCATION_ALWAYS: p(
73
+ "ios.permission.LOCATION_ALWAYS",
74
+ "android.permission.ACCESS_BACKGROUND_LOCATION"
75
+ ),
76
+ PHOTO_LIBRARY: p("ios.permission.PHOTO_LIBRARY", "android.permission.READ_MEDIA_IMAGES"),
77
+ PHOTO_LIBRARY_ADD_ONLY: p(
78
+ "ios.permission.PHOTO_LIBRARY_ADD_ONLY",
79
+ "android.permission.WRITE_EXTERNAL_STORAGE"
80
+ ),
81
+ BLUETOOTH: p("ios.permission.BLUETOOTH", "android.permission.BLUETOOTH_CONNECT"),
82
+ NOTIFICATIONS: "notifications"
83
+ };
54
84
  }
55
85
  });
56
86
 
@@ -153,7 +183,56 @@ function transition(state, event) {
153
183
 
154
184
  // src/hooks/use-permission-handler.ts
155
185
  var import_react = require("react");
156
- var import_react_native = require("react-native");
186
+ var import_react_native2 = require("react-native");
187
+
188
+ // src/core/debug-logger.ts
189
+ var PREFIX = "[permission-handler]";
190
+ var NOOP_LOGGER = {
191
+ transition: () => {
192
+ },
193
+ info: () => {
194
+ }
195
+ };
196
+ function createDebugLogger(debug, permission) {
197
+ if (!debug) return NOOP_LOGGER;
198
+ const log = typeof debug === "function" ? debug : (msg) => console.log(msg);
199
+ return {
200
+ transition(from, to, event) {
201
+ log(`${PREFIX} ${permission}: ${from} \u2192 ${to}${event ? ` (${event})` : ""}`);
202
+ },
203
+ info(msg) {
204
+ log(`${PREFIX} ${permission}: ${msg}`);
205
+ }
206
+ };
207
+ }
208
+
209
+ // src/core/with-timeout.ts
210
+ var PermissionTimeoutError = class extends Error {
211
+ constructor(permission, timeoutMs) {
212
+ super(`Permission request for "${permission}" timed out after ${timeoutMs}ms`);
213
+ this.name = "PermissionTimeoutError";
214
+ this.permission = permission;
215
+ this.timeoutMs = timeoutMs;
216
+ }
217
+ };
218
+ function withTimeout(promise, timeoutMs, permission) {
219
+ return new Promise((resolve, reject) => {
220
+ const timer = setTimeout(
221
+ () => reject(new PermissionTimeoutError(permission, timeoutMs)),
222
+ timeoutMs
223
+ );
224
+ promise.then(
225
+ (val) => {
226
+ clearTimeout(timer);
227
+ resolve(val);
228
+ },
229
+ (err) => {
230
+ clearTimeout(timer);
231
+ reject(err);
232
+ }
233
+ );
234
+ });
235
+ }
157
236
 
158
237
  // src/engines/rnp-fallback.ts
159
238
  var cachedFallback = null;
@@ -186,48 +265,84 @@ function usePermissionHandler(config) {
186
265
  const [nativeStatus, setNativeStatus] = (0, import_react.useState)(null);
187
266
  const isRequesting = (0, import_react.useRef)(false);
188
267
  const waitingForSettings = (0, import_react.useRef)(false);
189
- const appStateRef = (0, import_react.useRef)(import_react_native.AppState.currentState);
190
- const { permission, autoCheck = true, onGrant, onDeny, onBlock, onSettingsReturn } = config;
268
+ const appStateRef = (0, import_react.useRef)(import_react_native2.AppState.currentState);
269
+ const {
270
+ permission,
271
+ autoCheck = true,
272
+ requestTimeout,
273
+ onTimeout,
274
+ debug,
275
+ onGrant,
276
+ onDeny,
277
+ onBlock,
278
+ onSettingsReturn
279
+ } = config;
280
+ const logger = createDebugLogger(debug, permission);
191
281
  const checkPermission = (0, import_react.useCallback)(async () => {
192
- setFlowState((s) => transition(s, { type: "CHECK" }));
282
+ setFlowState((s) => {
283
+ const next = transition(s, { type: "CHECK" });
284
+ logger.transition(s, next, "CHECK");
285
+ return next;
286
+ });
193
287
  try {
194
288
  const status = await engine.check(permission);
195
289
  setNativeStatus(status);
196
290
  setFlowState((s) => {
197
291
  const next = transition(s, { type: "CHECK_RESULT", status });
292
+ logger.transition(s, next, `CHECK_RESULT:${status}`);
198
293
  if (next === "granted" && s !== "granted") onGrant?.();
199
294
  return next;
200
295
  });
201
296
  } catch {
202
297
  setFlowState("idle");
203
298
  }
204
- }, [engine, permission, onGrant]);
299
+ }, [engine, permission, logger, onGrant]);
205
300
  const requestPermission = (0, import_react.useCallback)(async () => {
206
301
  if (isRequesting.current) return;
207
302
  isRequesting.current = true;
208
- setFlowState((s) => transition(s, { type: "PRE_PROMPT_CONFIRM" }));
303
+ setFlowState((s) => {
304
+ const next = transition(s, { type: "PRE_PROMPT_CONFIRM" });
305
+ logger.transition(s, next, "PRE_PROMPT_CONFIRM");
306
+ return next;
307
+ });
209
308
  try {
210
- const status = await engine.request(permission);
309
+ const requestPromise = engine.request(permission);
310
+ const status = requestTimeout ? await withTimeout(requestPromise, requestTimeout, permission) : await requestPromise;
211
311
  setNativeStatus(status);
212
312
  setFlowState((s) => {
213
313
  const next = transition(s, { type: "REQUEST_RESULT", status });
314
+ logger.transition(s, next, `REQUEST_RESULT:${status}`);
214
315
  if (next === "granted") onGrant?.();
215
316
  if (next === "denied") onDeny?.();
216
317
  if (next === "blockedPrompt") onBlock?.();
217
318
  return next;
218
319
  });
219
- } catch {
220
- setFlowState("denied");
320
+ } catch (err) {
321
+ if (err instanceof PermissionTimeoutError) {
322
+ logger.info(`request timed out after ${requestTimeout}ms`);
323
+ onTimeout?.();
324
+ setFlowState("blockedPrompt");
325
+ } else {
326
+ setFlowState("denied");
327
+ }
221
328
  } finally {
222
329
  isRequesting.current = false;
223
330
  }
224
- }, [engine, permission, onGrant, onDeny, onBlock]);
331
+ }, [engine, permission, requestTimeout, onTimeout, logger, onGrant, onDeny, onBlock]);
225
332
  const dismiss = (0, import_react.useCallback)(() => {
226
- setFlowState((s) => transition(s, { type: "PRE_PROMPT_DISMISS" }));
333
+ setFlowState((s) => {
334
+ const next = transition(s, { type: "PRE_PROMPT_DISMISS" });
335
+ logger.transition(s, next, "PRE_PROMPT_DISMISS");
336
+ return next;
337
+ });
227
338
  onDeny?.();
228
- }, [onDeny]);
339
+ }, [logger, onDeny]);
229
340
  const goToSettings = (0, import_react.useCallback)(async () => {
230
- setFlowState((s) => transition(s, { type: "OPEN_SETTINGS" }));
341
+ setFlowState((s) => {
342
+ const next = transition(s, { type: "OPEN_SETTINGS" });
343
+ logger.transition(s, next, "OPEN_SETTINGS");
344
+ return next;
345
+ });
231
346
  waitingForSettings.current = true;
232
347
  try {
233
348
  await engine.openSettings();
@@ -235,14 +350,19 @@ function usePermissionHandler(config) {
235
350
  waitingForSettings.current = false;
236
351
  setFlowState("blockedPrompt");
237
352
  }
238
- }, [engine]);
353
+ }, [engine, logger]);
239
354
  const recheckAfterSettings = (0, import_react.useCallback)(async () => {
240
- setFlowState((s) => transition(s, { type: "SETTINGS_RETURN" }));
355
+ setFlowState((s) => {
356
+ const next = transition(s, { type: "SETTINGS_RETURN" });
357
+ logger.transition(s, next, "SETTINGS_RETURN");
358
+ return next;
359
+ });
241
360
  try {
242
361
  const status = await engine.check(permission);
243
362
  setNativeStatus(status);
244
363
  setFlowState((s) => {
245
364
  const next = transition(s, { type: "RECHECK_RESULT", status });
365
+ logger.transition(s, next, `RECHECK_RESULT:${status}`);
246
366
  if (next === "granted") onGrant?.();
247
367
  onSettingsReturn?.(next === "granted");
248
368
  return next;
@@ -250,14 +370,14 @@ function usePermissionHandler(config) {
250
370
  } catch {
251
371
  setFlowState("blockedPrompt");
252
372
  }
253
- }, [engine, permission, onGrant, onSettingsReturn]);
373
+ }, [engine, permission, logger, onGrant, onSettingsReturn]);
254
374
  (0, import_react.useEffect)(() => {
255
375
  if (autoCheck) {
256
376
  checkPermission();
257
377
  }
258
378
  }, []);
259
379
  (0, import_react.useEffect)(() => {
260
- const subscription = import_react_native.AppState.addEventListener("change", (nextAppState) => {
380
+ const subscription = import_react_native2.AppState.addEventListener("change", (nextAppState) => {
261
381
  if (appStateRef.current.match(/inactive|background/) && nextAppState === "active" && waitingForSettings.current) {
262
382
  waitingForSettings.current = false;
263
383
  recheckAfterSettings();
@@ -306,7 +426,16 @@ function statusToFlowState(status) {
306
426
  }
307
427
  function useMultiplePermissions(config) {
308
428
  const engine = resolveEngine(config.engine);
309
- const { permissions, strategy, autoCheck = true, onAllGranted } = config;
429
+ const {
430
+ permissions,
431
+ strategy,
432
+ autoCheck = true,
433
+ requestTimeout,
434
+ onTimeout,
435
+ debug,
436
+ onAllGranted
437
+ } = config;
438
+ const logger = createDebugLogger(debug, "multi");
310
439
  const [statuses, setStatuses] = (0, import_react2.useState)(() => {
311
440
  const initial = {};
312
441
  for (const entry of permissions) {
@@ -320,13 +449,16 @@ function useMultiplePermissions(config) {
320
449
  if (isRunning.current) return;
321
450
  isRunning.current = true;
322
451
  const update = (key, state) => {
323
- setStatuses((prev) => ({ ...prev, [key]: state }));
452
+ setStatuses((prev) => {
453
+ logger.transition(prev[key] ?? "idle", state, key);
454
+ return { ...prev, [key]: state };
455
+ });
324
456
  };
325
457
  try {
326
458
  if (strategy === "sequential") {
327
- await runSequential(permissions, engine, update);
459
+ await runSequential(permissions, engine, update, requestTimeout, onTimeout);
328
460
  } else {
329
- await runParallel(permissions, engine, update);
461
+ await runParallel(permissions, engine, update, requestTimeout, onTimeout);
330
462
  }
331
463
  let allDone = true;
332
464
  for (const entry of permissions) {
@@ -342,7 +474,7 @@ function useMultiplePermissions(config) {
342
474
  } finally {
343
475
  isRunning.current = false;
344
476
  }
345
- }, [permissions, strategy, engine, onAllGranted]);
477
+ }, [permissions, strategy, engine, requestTimeout, onTimeout, logger, onAllGranted]);
346
478
  (0, import_react2.useEffect)(() => {
347
479
  if (!autoCheck) return;
348
480
  let cancelled = false;
@@ -365,7 +497,7 @@ function useMultiplePermissions(config) {
365
497
  request: requestAll
366
498
  };
367
499
  }
368
- async function runSequential(permissions, engine, updateStatus) {
500
+ async function runSequential(permissions, engine, updateStatus, requestTimeout, onTimeout) {
369
501
  for (const entry of permissions) {
370
502
  const key = permissionKey(entry);
371
503
  updateStatus(key, "checking");
@@ -385,22 +517,32 @@ async function runSequential(permissions, engine, updateStatus) {
385
517
  break;
386
518
  }
387
519
  updateStatus(key, "requesting");
388
- const requestStatus = await engine.request(entry.permission);
389
- if (isGrantedStatus(requestStatus)) {
390
- updateStatus(key, "granted");
391
- entry.onGrant?.();
392
- } else if (requestStatus === "blocked") {
393
- updateStatus(key, "blockedPrompt");
394
- entry.onBlock?.();
395
- break;
396
- } else {
397
- updateStatus(key, "denied");
398
- entry.onDeny?.();
399
- break;
520
+ try {
521
+ const requestPromise = engine.request(entry.permission);
522
+ const requestStatus = requestTimeout ? await withTimeout(requestPromise, requestTimeout, entry.permission) : await requestPromise;
523
+ if (isGrantedStatus(requestStatus)) {
524
+ updateStatus(key, "granted");
525
+ entry.onGrant?.();
526
+ } else if (requestStatus === "blocked") {
527
+ updateStatus(key, "blockedPrompt");
528
+ entry.onBlock?.();
529
+ break;
530
+ } else {
531
+ updateStatus(key, "denied");
532
+ entry.onDeny?.();
533
+ break;
534
+ }
535
+ } catch (err) {
536
+ if (err instanceof PermissionTimeoutError) {
537
+ onTimeout?.();
538
+ updateStatus(key, "blockedPrompt");
539
+ break;
540
+ }
541
+ throw err;
400
542
  }
401
543
  }
402
544
  }
403
- async function runParallel(permissions, engine, updateStatus) {
545
+ async function runParallel(permissions, engine, updateStatus, requestTimeout, onTimeout) {
404
546
  const checkResults = await Promise.all(
405
547
  permissions.map(async (entry) => {
406
548
  const key = permissionKey(entry);
@@ -427,22 +569,32 @@ async function runParallel(permissions, engine, updateStatus) {
427
569
  continue;
428
570
  }
429
571
  updateStatus(key, "requesting");
430
- const requestStatus = await engine.request(entry.permission);
431
- if (isGrantedStatus(requestStatus)) {
432
- updateStatus(key, "granted");
433
- entry.onGrant?.();
434
- } else if (requestStatus === "blocked") {
435
- updateStatus(key, "blockedPrompt");
436
- entry.onBlock?.();
437
- } else {
438
- updateStatus(key, "denied");
439
- entry.onDeny?.();
572
+ try {
573
+ const requestPromise = engine.request(entry.permission);
574
+ const requestStatus = requestTimeout ? await withTimeout(requestPromise, requestTimeout, entry.permission) : await requestPromise;
575
+ if (isGrantedStatus(requestStatus)) {
576
+ updateStatus(key, "granted");
577
+ entry.onGrant?.();
578
+ } else if (requestStatus === "blocked") {
579
+ updateStatus(key, "blockedPrompt");
580
+ entry.onBlock?.();
581
+ } else {
582
+ updateStatus(key, "denied");
583
+ entry.onDeny?.();
584
+ }
585
+ } catch (err) {
586
+ if (err instanceof PermissionTimeoutError) {
587
+ onTimeout?.();
588
+ updateStatus(key, "blockedPrompt");
589
+ } else {
590
+ throw err;
591
+ }
440
592
  }
441
593
  }
442
594
  }
443
595
 
444
596
  // src/components/default-blocked-prompt.tsx
445
- var import_react_native2 = require("react-native");
597
+ var import_react_native3 = require("react-native");
446
598
  var import_jsx_runtime = require("react/jsx-runtime");
447
599
  function DefaultBlockedPrompt({
448
600
  visible,
@@ -451,21 +603,21 @@ function DefaultBlockedPrompt({
451
603
  settingsLabel = "Open Settings",
452
604
  onOpenSettings
453
605
  }) {
454
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Modal, { visible, transparent: true, animationType: "fade", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.View, { style: styles.overlay, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native2.View, { style: styles.modal, children: [
455
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: styles.title, children: title }),
456
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: styles.message, children: message }),
606
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native3.Modal, { visible, transparent: true, animationType: "fade", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native3.View, { style: styles.overlay, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native3.View, { style: styles.modal, children: [
607
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native3.Text, { style: styles.title, children: title }),
608
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native3.Text, { style: styles.message, children: message }),
457
609
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
458
- import_react_native2.TouchableOpacity,
610
+ import_react_native3.TouchableOpacity,
459
611
  {
460
612
  style: styles.settingsButton,
461
613
  onPress: onOpenSettings,
462
614
  accessibilityRole: "button",
463
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native2.Text, { style: styles.settingsText, children: settingsLabel })
615
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native3.Text, { style: styles.settingsText, children: settingsLabel })
464
616
  }
465
617
  )
466
618
  ] }) }) });
467
619
  }
468
- var styles = import_react_native2.StyleSheet.create({
620
+ var styles = import_react_native3.StyleSheet.create({
469
621
  overlay: {
470
622
  flex: 1,
471
623
  backgroundColor: "rgba(0,0,0,0.5)",
@@ -508,7 +660,7 @@ var styles = import_react_native2.StyleSheet.create({
508
660
  });
509
661
 
510
662
  // src/components/default-pre-prompt.tsx
511
- var import_react_native3 = require("react-native");
663
+ var import_react_native4 = require("react-native");
512
664
  var import_jsx_runtime2 = require("react/jsx-runtime");
513
665
  function DefaultPrePrompt({
514
666
  visible,
@@ -519,22 +671,22 @@ function DefaultPrePrompt({
519
671
  onConfirm,
520
672
  onCancel
521
673
  }) {
522
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Modal, { visible, transparent: true, animationType: "fade", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.View, { style: styles2.overlay, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native3.View, { style: styles2.modal, children: [
523
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: styles2.title, children: title }),
524
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: styles2.message, children: message }),
674
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native4.Modal, { visible, transparent: true, animationType: "fade", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native4.View, { style: styles2.overlay, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_native4.View, { style: styles2.modal, children: [
675
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native4.Text, { style: styles2.title, children: title }),
676
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native4.Text, { style: styles2.message, children: message }),
525
677
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
526
- import_react_native3.TouchableOpacity,
678
+ import_react_native4.TouchableOpacity,
527
679
  {
528
680
  style: styles2.confirmButton,
529
681
  onPress: onConfirm,
530
682
  accessibilityRole: "button",
531
- children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: styles2.confirmText, children: confirmLabel })
683
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native4.Text, { style: styles2.confirmText, children: confirmLabel })
532
684
  }
533
685
  ),
534
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.TouchableOpacity, { onPress: onCancel, accessibilityRole: "button", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native3.Text, { style: styles2.cancelText, children: cancelLabel }) })
686
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native4.TouchableOpacity, { onPress: onCancel, accessibilityRole: "button", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native4.Text, { style: styles2.cancelText, children: cancelLabel }) })
535
687
  ] }) }) });
536
688
  }
537
- var styles2 = import_react_native3.StyleSheet.create({
689
+ var styles2 = import_react_native4.StyleSheet.create({
538
690
  overlay: {
539
691
  flex: 1,
540
692
  backgroundColor: "rgba(0,0,0,0.5)",