@ankhorage/permissions 0.2.2 → 0.2.3

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 (39) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +25 -1
  3. package/dist/cli/index.d.ts +2 -0
  4. package/dist/cli/index.d.ts.map +1 -0
  5. package/dist/cli/index.js +2 -0
  6. package/dist/cli/index.js.map +1 -0
  7. package/dist/expo/adapters/mediaLibrary.d.ts.map +1 -1
  8. package/dist/expo/adapters/mediaLibrary.js +15 -3
  9. package/dist/expo/adapters/mediaLibrary.js.map +1 -1
  10. package/dist/expo/adapters/notifications.d.ts.map +1 -1
  11. package/dist/expo/adapters/notifications.js +32 -3
  12. package/dist/expo/adapters/notifications.js.map +1 -1
  13. package/dist/expo/client.d.ts +14 -0
  14. package/dist/expo/client.d.ts.map +1 -1
  15. package/dist/expo/client.js +26 -0
  16. package/dist/expo/client.js.map +1 -1
  17. package/dist/expo/manifest.d.ts +5 -0
  18. package/dist/expo/manifest.d.ts.map +1 -1
  19. package/dist/expo/manifest.js +5 -0
  20. package/dist/expo/manifest.js.map +1 -1
  21. package/dist/expo/permissionResponse.d.ts.map +1 -1
  22. package/dist/expo/permissionResponse.js +12 -5
  23. package/dist/expo/permissionResponse.js.map +1 -1
  24. package/dist/state/permissionState.d.ts +2 -0
  25. package/dist/state/permissionState.d.ts.map +1 -1
  26. package/dist/state/permissionState.js +3 -1
  27. package/dist/state/permissionState.js.map +1 -1
  28. package/package.json +13 -3
  29. package/src/cli/index.ts +1 -0
  30. package/src/expo/adapters/mediaLibrary.ts +28 -4
  31. package/src/expo/adapters/notifications.ts +73 -4
  32. package/src/expo/client.test.ts +194 -9
  33. package/src/expo/client.ts +28 -0
  34. package/src/expo/manifest.test.ts +5 -1
  35. package/src/expo/manifest.ts +6 -0
  36. package/src/expo/modules.d.ts +18 -4
  37. package/src/expo/permissionResponse.ts +22 -5
  38. package/src/state/permissionState.test.ts +8 -1
  39. package/src/state/permissionState.ts +3 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # @ankhorage/permissions
2
2
 
3
+ ## 0.2.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 8ee977d: Normalize blocked and partial Expo authorization states, project the Expo client dependency,
8
+ and add settings recovery with a stable failure contract.
9
+
3
10
  ## 0.2.2
4
11
 
5
12
  ### Patch Changes
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  # PERMISSIONS
5
5
 
6
- ![license: MIT](././paradox/badges/license.svg) ![npm: v0.2.0](././paradox/badges/npm.svg) ![runtime: bun](././paradox/badges/runtime.svg) ![typescript: strict](././paradox/badges/typescript.svg) ![eslint: checked](././paradox/badges/eslint.svg) ![prettier: checked](././paradox/badges/prettier.svg) ![build: checked](././paradox/badges/build.svg) ![tests: checked](././paradox/badges/tests.svg) ![docs: paradox](././paradox/badges/docs.svg)
6
+ ![license: MIT](././paradox/badges/license.svg) ![npm: v0.2.2](././paradox/badges/npm.svg) ![runtime: bun](././paradox/badges/runtime.svg) ![typescript: strict](././paradox/badges/typescript.svg) ![eslint: checked](././paradox/badges/eslint.svg) ![prettier: checked](././paradox/badges/prettier.svg) ![build: checked](././paradox/badges/build.svg) ![tests: checked](././paradox/badges/tests.svg) ![docs: paradox](././paradox/badges/docs.svg)
7
7
 
8
8
  Cross-platform permission registry and runtime helpers for Expo, React Native, and React Native Web apps, with unified request/check APIs for camera, media, location, notifications, microphone, and other web/native capabilities.
9
9
 
@@ -137,6 +137,30 @@ Related symbols: `FakePermissionClient`, `FakePermissionClientOptions`
137
137
 
138
138
  </details>
139
139
 
140
+ <details>
141
+ <summary>createPermissionClient</summary>
142
+
143
+ ```ts
144
+ createPermissionClient() => PermissionClient
145
+ ```
146
+
147
+ Creates a permission client backed by Expo SDK modules.
148
+
149
+ Denials that Expo cannot request again are normalized to `blocked`. Limited
150
+ media-library access and provisional or ephemeral iOS notification access
151
+ are normalized to usable `limited` states, where `granted` remains true.
152
+ The client requires `expo-linking` for settings recovery and rejects with a
153
+ stable error when the operating-system settings page cannot be opened.
154
+
155
+ Expo modules are loaded only when a client operation uses them. The package
156
+ root and Expo manifest metadata remain free of native module imports.
157
+
158
+ Module: `src/expo/client.ts`
159
+ Source: `src/expo/client.ts:42:1`
160
+ Related symbols: `PermissionClient`
161
+
162
+ </details>
163
+
140
164
  <details>
141
165
  <summary>createPermissionManager</summary>
142
166
 
@@ -0,0 +1,2 @@
1
+ export { default } from '../ankh.provider.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { default } from '../ankh.provider.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC","sourcesContent":["export { default } from '../ankh.provider.js';\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"mediaLibrary.d.ts","sourceRoot":"","sources":["../../../src/expo/adapters/mediaLibrary.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAGvD,eAAO,MAAM,mBAAmB,EAAE,qBAsBjC,CAAC"}
1
+ {"version":3,"file":"mediaLibrary.d.ts","sourceRoot":"","sources":["../../../src/expo/adapters/mediaLibrary.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAWvD,eAAO,MAAM,mBAAmB,EAAE,qBAsBjC,CAAC"}
@@ -1,11 +1,12 @@
1
1
  import { Permission as KnownPermission } from '../../registry/permissions';
2
- import { createExpoPermissionState, createExpoUnavailableState } from '../permissionResponse';
2
+ import { createPermissionState } from '../../state/permissionState';
3
+ import { createExpoPermissionState, createExpoUnavailableState, } from '../permissionResponse';
3
4
  export const mediaLibraryAdapter = {
4
5
  async getStatus(permission) {
5
6
  try {
6
7
  const mod = await import('expo-media-library');
7
8
  const result = await mod.getPermissionsAsync(isWriteOnlyPermission(permission));
8
- return createExpoPermissionState(permission, result);
9
+ return createMediaLibraryPermissionState(permission, result);
9
10
  }
10
11
  catch (error) {
11
12
  return createExpoUnavailableState(permission, error);
@@ -15,13 +16,24 @@ export const mediaLibraryAdapter = {
15
16
  try {
16
17
  const mod = await import('expo-media-library');
17
18
  const result = await mod.requestPermissionsAsync(isWriteOnlyPermission(permission));
18
- return createExpoPermissionState(permission, result);
19
+ return createMediaLibraryPermissionState(permission, result);
19
20
  }
20
21
  catch (error) {
21
22
  return createExpoUnavailableState(permission, error);
22
23
  }
23
24
  },
24
25
  };
26
+ function createMediaLibraryPermissionState(permission, response) {
27
+ if (response.accessPrivileges !== 'limited') {
28
+ return createExpoPermissionState(permission, response);
29
+ }
30
+ return createPermissionState({
31
+ permission,
32
+ status: 'limited',
33
+ canAskAgain: response.canAskAgain,
34
+ reason: 'Media library access is limited to user-selected assets.',
35
+ });
36
+ }
25
37
  function isWriteOnlyPermission(permission) {
26
38
  return permission === KnownPermission.MediaLibraryWrite;
27
39
  }
@@ -1 +1 @@
1
- {"version":3,"file":"mediaLibrary.js","sourceRoot":"","sources":["../../../src/expo/adapters/mediaLibrary.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,IAAI,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAG3E,OAAO,EAAE,yBAAyB,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAC;AAE9F,MAAM,CAAC,MAAM,mBAAmB,GAA0B;IACxD,KAAK,CAAC,SAAS,CAAC,UAAsB;QACpC,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,oBAAoB,CAAC,CAAC;YAC/C,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,mBAAmB,CAAC,qBAAqB,CAAC,UAAU,CAAC,CAAC,CAAC;YAEhF,OAAO,yBAAyB,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QACvD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,0BAA0B,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QACvD,CAAC;IACH,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,UAAsB;QAClC,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,oBAAoB,CAAC,CAAC;YAC/C,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,uBAAuB,CAAC,qBAAqB,CAAC,UAAU,CAAC,CAAC,CAAC;YAEpF,OAAO,yBAAyB,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QACvD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,0BAA0B,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QACvD,CAAC;IACH,CAAC;CACF,CAAC;AAEF,SAAS,qBAAqB,CAAC,UAAsB;IACnD,OAAO,UAAU,KAAK,eAAe,CAAC,iBAAiB,CAAC;AAC1D,CAAC","sourcesContent":["import type { Permission } from '../../registry/permissions';\nimport { Permission as KnownPermission } from '../../registry/permissions';\nimport type { PermissionState } from '../../state/permissionState';\nimport type { ExpoPermissionAdapter } from '../client';\nimport { createExpoPermissionState, createExpoUnavailableState } from '../permissionResponse';\n\nexport const mediaLibraryAdapter: ExpoPermissionAdapter = {\n async getStatus(permission: Permission): Promise<PermissionState> {\n try {\n const mod = await import('expo-media-library');\n const result = await mod.getPermissionsAsync(isWriteOnlyPermission(permission));\n\n return createExpoPermissionState(permission, result);\n } catch (error) {\n return createExpoUnavailableState(permission, error);\n }\n },\n\n async request(permission: Permission): Promise<PermissionState> {\n try {\n const mod = await import('expo-media-library');\n const result = await mod.requestPermissionsAsync(isWriteOnlyPermission(permission));\n\n return createExpoPermissionState(permission, result);\n } catch (error) {\n return createExpoUnavailableState(permission, error);\n }\n },\n};\n\nfunction isWriteOnlyPermission(permission: Permission): boolean {\n return permission === KnownPermission.MediaLibraryWrite;\n}\n"]}
1
+ {"version":3,"file":"mediaLibrary.js","sourceRoot":"","sources":["../../../src/expo/adapters/mediaLibrary.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,IAAI,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC3E,OAAO,EAAE,qBAAqB,EAAwB,MAAM,6BAA6B,CAAC;AAE1F,OAAO,EACL,yBAAyB,EACzB,0BAA0B,GAE3B,MAAM,uBAAuB,CAAC;AAM/B,MAAM,CAAC,MAAM,mBAAmB,GAA0B;IACxD,KAAK,CAAC,SAAS,CAAC,UAAsB;QACpC,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,oBAAoB,CAAC,CAAC;YAC/C,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,mBAAmB,CAAC,qBAAqB,CAAC,UAAU,CAAC,CAAC,CAAC;YAEhF,OAAO,iCAAiC,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QAC/D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,0BAA0B,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QACvD,CAAC;IACH,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,UAAsB;QAClC,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,oBAAoB,CAAC,CAAC;YAC/C,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,uBAAuB,CAAC,qBAAqB,CAAC,UAAU,CAAC,CAAC,CAAC;YAEpF,OAAO,iCAAiC,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QAC/D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,0BAA0B,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QACvD,CAAC;IACH,CAAC;CACF,CAAC;AAEF,SAAS,iCAAiC,CACxC,UAAsB,EACtB,QAA4C;IAE5C,IAAI,QAAQ,CAAC,gBAAgB,KAAK,SAAS,EAAE,CAAC;QAC5C,OAAO,yBAAyB,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;IACzD,CAAC;IAED,OAAO,qBAAqB,CAAC;QAC3B,UAAU;QACV,MAAM,EAAE,SAAS;QACjB,WAAW,EAAE,QAAQ,CAAC,WAAW;QACjC,MAAM,EAAE,0DAA0D;KACnE,CAAC,CAAC;AACL,CAAC;AAED,SAAS,qBAAqB,CAAC,UAAsB;IACnD,OAAO,UAAU,KAAK,eAAe,CAAC,iBAAiB,CAAC;AAC1D,CAAC","sourcesContent":["import type { Permission } from '../../registry/permissions';\nimport { Permission as KnownPermission } from '../../registry/permissions';\nimport { createPermissionState, type PermissionState } from '../../state/permissionState';\nimport type { ExpoPermissionAdapter } from '../client';\nimport {\n createExpoPermissionState,\n createExpoUnavailableState,\n type ExpoPermissionResponse,\n} from '../permissionResponse';\n\ninterface ExpoMediaLibraryPermissionResponse extends ExpoPermissionResponse {\n readonly accessPrivileges?: 'all' | 'limited' | 'none';\n}\n\nexport const mediaLibraryAdapter: ExpoPermissionAdapter = {\n async getStatus(permission: Permission): Promise<PermissionState> {\n try {\n const mod = await import('expo-media-library');\n const result = await mod.getPermissionsAsync(isWriteOnlyPermission(permission));\n\n return createMediaLibraryPermissionState(permission, result);\n } catch (error) {\n return createExpoUnavailableState(permission, error);\n }\n },\n\n async request(permission: Permission): Promise<PermissionState> {\n try {\n const mod = await import('expo-media-library');\n const result = await mod.requestPermissionsAsync(isWriteOnlyPermission(permission));\n\n return createMediaLibraryPermissionState(permission, result);\n } catch (error) {\n return createExpoUnavailableState(permission, error);\n }\n },\n};\n\nfunction createMediaLibraryPermissionState(\n permission: Permission,\n response: ExpoMediaLibraryPermissionResponse,\n): PermissionState {\n if (response.accessPrivileges !== 'limited') {\n return createExpoPermissionState(permission, response);\n }\n\n return createPermissionState({\n permission,\n status: 'limited',\n canAskAgain: response.canAskAgain,\n reason: 'Media library access is limited to user-selected assets.',\n });\n}\n\nfunction isWriteOnlyPermission(permission: Permission): boolean {\n return permission === KnownPermission.MediaLibraryWrite;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"notifications.d.ts","sourceRoot":"","sources":["../../../src/expo/adapters/notifications.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAGvD,eAAO,MAAM,oBAAoB,EAAE,qBAsBlC,CAAC"}
1
+ {"version":3,"file":"notifications.d.ts","sourceRoot":"","sources":["../../../src/expo/adapters/notifications.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAavD,eAAO,MAAM,oBAAoB,EAAE,qBAsBlC,CAAC"}
@@ -1,10 +1,11 @@
1
- import { createExpoPermissionState, createExpoUnavailableState } from '../permissionResponse';
1
+ import { createPermissionState } from '../../state/permissionState';
2
+ import { createExpoPermissionState, createExpoUnavailableState, } from '../permissionResponse';
2
3
  export const notificationsAdapter = {
3
4
  async getStatus(permission) {
4
5
  try {
5
6
  const mod = await import('expo-notifications');
6
7
  const result = await mod.getPermissionsAsync();
7
- return createExpoPermissionState(permission, result);
8
+ return createNotificationPermissionState(permission, result);
8
9
  }
9
10
  catch (error) {
10
11
  return createExpoUnavailableState(permission, error);
@@ -14,11 +15,39 @@ export const notificationsAdapter = {
14
15
  try {
15
16
  const mod = await import('expo-notifications');
16
17
  const result = await mod.requestPermissionsAsync();
17
- return createExpoPermissionState(permission, result);
18
+ return createNotificationPermissionState(permission, result);
18
19
  }
19
20
  catch (error) {
20
21
  return createExpoUnavailableState(permission, error);
21
22
  }
22
23
  },
23
24
  };
25
+ function createNotificationPermissionState(permission, response) {
26
+ switch (response.ios?.status) {
27
+ case 0:
28
+ return createNotificationState(permission, response, 'unknown', 'iOS notification authorization has not been determined.');
29
+ case 1:
30
+ return createNotificationState(permission, response, 'denied', 'iOS notification authorization is denied.');
31
+ case 2:
32
+ return createNotificationState(permission, response, 'granted');
33
+ case 3:
34
+ return createNotificationState(permission, response, 'limited', 'iOS notification authorization is provisional.');
35
+ case 4:
36
+ return createNotificationState(permission, response, 'limited', 'iOS notification authorization is ephemeral.');
37
+ default:
38
+ return createExpoPermissionState(permission, response);
39
+ }
40
+ }
41
+ function createNotificationState(permission, response, status, reason) {
42
+ const state = createExpoPermissionState(permission, {
43
+ status,
44
+ canAskAgain: response.canAskAgain,
45
+ });
46
+ return createPermissionState({
47
+ permission,
48
+ status: state.status,
49
+ canAskAgain: state.canAskAgain,
50
+ reason: state.reason ?? reason,
51
+ });
52
+ }
24
53
  //# sourceMappingURL=notifications.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"notifications.js","sourceRoot":"","sources":["../../../src/expo/adapters/notifications.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,yBAAyB,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAC;AAE9F,MAAM,CAAC,MAAM,oBAAoB,GAA0B;IACzD,KAAK,CAAC,SAAS,CAAC,UAAsB;QACpC,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,oBAAoB,CAAC,CAAC;YAC/C,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,mBAAmB,EAAE,CAAC;YAE/C,OAAO,yBAAyB,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QACvD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,0BAA0B,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QACvD,CAAC;IACH,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,UAAsB;QAClC,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,oBAAoB,CAAC,CAAC;YAC/C,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,uBAAuB,EAAE,CAAC;YAEnD,OAAO,yBAAyB,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QACvD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,0BAA0B,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QACvD,CAAC;IACH,CAAC;CACF,CAAC","sourcesContent":["import type { Permission } from '../../registry/permissions';\nimport type { PermissionState } from '../../state/permissionState';\nimport type { ExpoPermissionAdapter } from '../client';\nimport { createExpoPermissionState, createExpoUnavailableState } from '../permissionResponse';\n\nexport const notificationsAdapter: ExpoPermissionAdapter = {\n async getStatus(permission: Permission): Promise<PermissionState> {\n try {\n const mod = await import('expo-notifications');\n const result = await mod.getPermissionsAsync();\n\n return createExpoPermissionState(permission, result);\n } catch (error) {\n return createExpoUnavailableState(permission, error);\n }\n },\n\n async request(permission: Permission): Promise<PermissionState> {\n try {\n const mod = await import('expo-notifications');\n const result = await mod.requestPermissionsAsync();\n\n return createExpoPermissionState(permission, result);\n } catch (error) {\n return createExpoUnavailableState(permission, error);\n }\n },\n};\n"]}
1
+ {"version":3,"file":"notifications.js","sourceRoot":"","sources":["../../../src/expo/adapters/notifications.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,qBAAqB,EAAwB,MAAM,6BAA6B,CAAC;AAE1F,OAAO,EACL,yBAAyB,EACzB,0BAA0B,GAE3B,MAAM,uBAAuB,CAAC;AAQ/B,MAAM,CAAC,MAAM,oBAAoB,GAA0B;IACzD,KAAK,CAAC,SAAS,CAAC,UAAsB;QACpC,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,oBAAoB,CAAC,CAAC;YAC/C,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,mBAAmB,EAAE,CAAC;YAE/C,OAAO,iCAAiC,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QAC/D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,0BAA0B,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QACvD,CAAC;IACH,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,UAAsB;QAClC,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,oBAAoB,CAAC,CAAC;YAC/C,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,uBAAuB,EAAE,CAAC;YAEnD,OAAO,iCAAiC,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QAC/D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,0BAA0B,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QACvD,CAAC;IACH,CAAC;CACF,CAAC;AAEF,SAAS,iCAAiC,CACxC,UAAsB,EACtB,QAA4C;IAE5C,QAAQ,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,CAAC;QAC7B,KAAK,CAAC;YACJ,OAAO,uBAAuB,CAC5B,UAAU,EACV,QAAQ,EACR,SAAS,EACT,yDAAyD,CAC1D,CAAC;QACJ,KAAK,CAAC;YACJ,OAAO,uBAAuB,CAC5B,UAAU,EACV,QAAQ,EACR,QAAQ,EACR,2CAA2C,CAC5C,CAAC;QACJ,KAAK,CAAC;YACJ,OAAO,uBAAuB,CAAC,UAAU,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;QAClE,KAAK,CAAC;YACJ,OAAO,uBAAuB,CAC5B,UAAU,EACV,QAAQ,EACR,SAAS,EACT,gDAAgD,CACjD,CAAC;QACJ,KAAK,CAAC;YACJ,OAAO,uBAAuB,CAC5B,UAAU,EACV,QAAQ,EACR,SAAS,EACT,8CAA8C,CAC/C,CAAC;QACJ;YACE,OAAO,yBAAyB,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;IAC3D,CAAC;AACH,CAAC;AAED,SAAS,uBAAuB,CAC9B,UAAsB,EACtB,QAA4C,EAC5C,MAAiC,EACjC,MAAe;IAEf,MAAM,KAAK,GAAG,yBAAyB,CAAC,UAAU,EAAE;QAClD,MAAM;QACN,WAAW,EAAE,QAAQ,CAAC,WAAW;KAClC,CAAC,CAAC;IAEH,OAAO,qBAAqB,CAAC;QAC3B,UAAU;QACV,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,MAAM,EAAE,KAAK,CAAC,MAAM,IAAI,MAAM;KAC/B,CAAC,CAAC;AACL,CAAC","sourcesContent":["import type { Permission } from '../../registry/permissions';\nimport { createPermissionState, type PermissionState } from '../../state/permissionState';\nimport type { ExpoPermissionAdapter } from '../client';\nimport {\n createExpoPermissionState,\n createExpoUnavailableState,\n type ExpoPermissionResponse,\n} from '../permissionResponse';\n\ninterface ExpoNotificationPermissionResponse extends ExpoPermissionResponse {\n readonly ios?: {\n readonly status?: number;\n };\n}\n\nexport const notificationsAdapter: ExpoPermissionAdapter = {\n async getStatus(permission: Permission): Promise<PermissionState> {\n try {\n const mod = await import('expo-notifications');\n const result = await mod.getPermissionsAsync();\n\n return createNotificationPermissionState(permission, result);\n } catch (error) {\n return createExpoUnavailableState(permission, error);\n }\n },\n\n async request(permission: Permission): Promise<PermissionState> {\n try {\n const mod = await import('expo-notifications');\n const result = await mod.requestPermissionsAsync();\n\n return createNotificationPermissionState(permission, result);\n } catch (error) {\n return createExpoUnavailableState(permission, error);\n }\n },\n};\n\nfunction createNotificationPermissionState(\n permission: Permission,\n response: ExpoNotificationPermissionResponse,\n): PermissionState {\n switch (response.ios?.status) {\n case 0:\n return createNotificationState(\n permission,\n response,\n 'unknown',\n 'iOS notification authorization has not been determined.',\n );\n case 1:\n return createNotificationState(\n permission,\n response,\n 'denied',\n 'iOS notification authorization is denied.',\n );\n case 2:\n return createNotificationState(permission, response, 'granted');\n case 3:\n return createNotificationState(\n permission,\n response,\n 'limited',\n 'iOS notification authorization is provisional.',\n );\n case 4:\n return createNotificationState(\n permission,\n response,\n 'limited',\n 'iOS notification authorization is ephemeral.',\n );\n default:\n return createExpoPermissionState(permission, response);\n }\n}\n\nfunction createNotificationState(\n permission: Permission,\n response: ExpoNotificationPermissionResponse,\n status: PermissionState['status'],\n reason?: string,\n): PermissionState {\n const state = createExpoPermissionState(permission, {\n status,\n canAskAgain: response.canAskAgain,\n });\n\n return createPermissionState({\n permission,\n status: state.status,\n canAskAgain: state.canAskAgain,\n reason: state.reason ?? reason,\n });\n}\n"]}
@@ -5,5 +5,19 @@ export interface ExpoPermissionAdapter {
5
5
  getStatus(permission: Permission): Promise<PermissionState>;
6
6
  request(permission: Permission): Promise<PermissionState>;
7
7
  }
8
+ /***
9
+ * Creates a permission client backed by Expo SDK modules.
10
+ *
11
+ * Denials that Expo cannot request again are normalized to `blocked`. Limited
12
+ * media-library access and provisional or ephemeral iOS notification access
13
+ * are normalized to usable `limited` states, where `granted` remains true.
14
+ * The client requires `expo-linking` for settings recovery and rejects with a
15
+ * stable error when the operating-system settings page cannot be opened.
16
+ *
17
+ * Expo modules are loaded only when a client operation uses them. The package
18
+ * root and Expo manifest metadata remain free of native module imports.
19
+ *
20
+ * @readme
21
+ */
8
22
  export declare function createPermissionClient(): PermissionClient;
9
23
  //# sourceMappingURL=client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/expo/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAQvF,MAAM,WAAW,qBAAqB;IACpC,SAAS,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IAC5D,OAAO,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;CAC3D;AAYD,wBAAgB,sBAAsB,IAAI,gBAAgB,CAsCzD"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/expo/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAUvF,MAAM,WAAW,qBAAqB;IACpC,SAAS,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IAC5D,OAAO,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;CAC3D;AAYD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,IAAI,gBAAgB,CAkDzD"}
@@ -6,6 +6,7 @@ import { mediaLibraryAdapter } from './adapters/mediaLibrary';
6
6
  import { microphoneAdapter } from './adapters/microphone';
7
7
  import { notificationsAdapter } from './adapters/notifications';
8
8
  import { EXPO_PERMISSION_SUPPORT } from './manifest';
9
+ const OPEN_SETTINGS_ERROR_MESSAGE = 'Unable to open application settings.';
9
10
  const adapters = {
10
11
  [Permission.Camera]: cameraAdapter,
11
12
  [Permission.MediaLibrary]: mediaLibraryAdapter,
@@ -15,6 +16,20 @@ const adapters = {
15
16
  [Permission.LocationBackground]: locationAdapter,
16
17
  [Permission.Notifications]: notificationsAdapter,
17
18
  };
19
+ /***
20
+ * Creates a permission client backed by Expo SDK modules.
21
+ *
22
+ * Denials that Expo cannot request again are normalized to `blocked`. Limited
23
+ * media-library access and provisional or ephemeral iOS notification access
24
+ * are normalized to usable `limited` states, where `granted` remains true.
25
+ * The client requires `expo-linking` for settings recovery and rejects with a
26
+ * stable error when the operating-system settings page cannot be opened.
27
+ *
28
+ * Expo modules are loaded only when a client operation uses them. The package
29
+ * root and Expo manifest metadata remain free of native module imports.
30
+ *
31
+ * @readme
32
+ */
18
33
  export function createPermissionClient() {
19
34
  return {
20
35
  async getStatus(permission) {
@@ -45,6 +60,17 @@ export function createPermissionClient() {
45
60
  }
46
61
  return adapter.request(permission);
47
62
  },
63
+ async openSettings() {
64
+ try {
65
+ const linking = await import('expo-linking');
66
+ await linking.openSettings();
67
+ }
68
+ catch (error) {
69
+ throw new Error(OPEN_SETTINGS_ERROR_MESSAGE, {
70
+ cause: error,
71
+ });
72
+ }
73
+ },
48
74
  };
49
75
  }
50
76
  function createUnavailableSupportState(permission) {
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/expo/client.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,qBAAqB,EAAwB,MAAM,0BAA0B,CAAC;AACvF,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAOrD,MAAM,QAAQ,GAAuD;IACnE,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,aAAa;IAClC,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,mBAAmB;IAC9C,CAAC,UAAU,CAAC,iBAAiB,CAAC,EAAE,mBAAmB;IACnD,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,iBAAiB;IAC1C,CAAC,UAAU,CAAC,kBAAkB,CAAC,EAAE,eAAe;IAChD,CAAC,UAAU,CAAC,kBAAkB,CAAC,EAAE,eAAe;IAChD,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,oBAAoB;CACjD,CAAC;AAEF,MAAM,UAAU,sBAAsB;IACpC,OAAO;QACL,KAAK,CAAC,SAAS,CAAC,UAAsB;YACpC,MAAM,QAAQ,GAAG,uBAAuB,CAAC,UAAU,CAAC,CAAC;YACrD,IAAI,QAAQ,CAAC,OAAO,KAAK,aAAa,IAAI,QAAQ,CAAC,OAAO,KAAK,gBAAgB,EAAE,CAAC;gBAChF,OAAO,6BAA6B,CAAC,UAAU,CAAC,CAAC;YACnD,CAAC;YAED,IAAI,QAAQ,CAAC,OAAO,KAAK,aAAa,EAAE,CAAC;gBACvC,OAAO,sBAAsB,CAAC,UAAU,CAAC,CAAC;YAC5C,CAAC;YAED,MAAM,OAAO,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC;YACrC,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,MAAM,IAAI,KAAK,CAAC,yCAAyC,UAAU,EAAE,CAAC,CAAC;YACzE,CAAC;YAED,OAAO,OAAO,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;QACvC,CAAC;QAED,KAAK,CAAC,OAAO,CAAC,UAAsB;YAClC,MAAM,QAAQ,GAAG,uBAAuB,CAAC,UAAU,CAAC,CAAC;YACrD,IAAI,QAAQ,CAAC,OAAO,KAAK,aAAa,IAAI,QAAQ,CAAC,OAAO,KAAK,gBAAgB,EAAE,CAAC;gBAChF,OAAO,6BAA6B,CAAC,UAAU,CAAC,CAAC;YACnD,CAAC;YAED,IAAI,QAAQ,CAAC,OAAO,KAAK,aAAa,EAAE,CAAC;gBACvC,OAAO,sBAAsB,CAAC,UAAU,CAAC,CAAC;YAC5C,CAAC;YAED,MAAM,OAAO,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC;YACrC,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,MAAM,IAAI,KAAK,CAAC,yCAAyC,UAAU,EAAE,CAAC,CAAC;YACzE,CAAC;YAED,OAAO,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QACrC,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,6BAA6B,CAAC,UAAsB;IAC3D,OAAO,qBAAqB,CAAC;QAC3B,UAAU;QACV,MAAM,EAAE,aAAa;QACrB,MAAM,EAAE,cAAc,UAAU,2CAA2C;KAC5E,CAAC,CAAC;AACL,CAAC;AAED,SAAS,sBAAsB,CAAC,UAAsB;IACpD,OAAO,qBAAqB,CAAC;QAC3B,UAAU;QACV,MAAM,EAAE,SAAS;KAClB,CAAC,CAAC;AACL,CAAC","sourcesContent":["import type { PermissionClient } from '../client/types';\nimport { Permission } from '../registry/permissions';\nimport { createPermissionState, type PermissionState } from '../state/permissionState';\nimport { cameraAdapter } from './adapters/camera';\nimport { locationAdapter } from './adapters/location';\nimport { mediaLibraryAdapter } from './adapters/mediaLibrary';\nimport { microphoneAdapter } from './adapters/microphone';\nimport { notificationsAdapter } from './adapters/notifications';\nimport { EXPO_PERMISSION_SUPPORT } from './manifest';\n\nexport interface ExpoPermissionAdapter {\n getStatus(permission: Permission): Promise<PermissionState>;\n request(permission: Permission): Promise<PermissionState>;\n}\n\nconst adapters: Partial<Record<Permission, ExpoPermissionAdapter>> = {\n [Permission.Camera]: cameraAdapter,\n [Permission.MediaLibrary]: mediaLibraryAdapter,\n [Permission.MediaLibraryWrite]: mediaLibraryAdapter,\n [Permission.Microphone]: microphoneAdapter,\n [Permission.LocationForeground]: locationAdapter,\n [Permission.LocationBackground]: locationAdapter,\n [Permission.Notifications]: notificationsAdapter,\n};\n\nexport function createPermissionClient(): PermissionClient {\n return {\n async getStatus(permission: Permission): Promise<PermissionState> {\n const metadata = EXPO_PERMISSION_SUPPORT[permission];\n if (metadata.support === 'unsupported' || metadata.support === 'notImplemented') {\n return createUnavailableSupportState(permission);\n }\n\n if (metadata.support === 'notRequired') {\n return createNotRequiredState(permission);\n }\n\n const adapter = adapters[permission];\n if (!adapter) {\n throw new Error(`No adapter registered for permission: ${permission}`);\n }\n\n return adapter.getStatus(permission);\n },\n\n async request(permission: Permission): Promise<PermissionState> {\n const metadata = EXPO_PERMISSION_SUPPORT[permission];\n if (metadata.support === 'unsupported' || metadata.support === 'notImplemented') {\n return createUnavailableSupportState(permission);\n }\n\n if (metadata.support === 'notRequired') {\n return createNotRequiredState(permission);\n }\n\n const adapter = adapters[permission];\n if (!adapter) {\n throw new Error(`No adapter registered for permission: ${permission}`);\n }\n\n return adapter.request(permission);\n },\n };\n}\n\nfunction createUnavailableSupportState(permission: Permission): PermissionState {\n return createPermissionState({\n permission,\n status: 'unavailable',\n reason: `Permission ${permission} is not implemented or supported on Expo.`,\n });\n}\n\nfunction createNotRequiredState(permission: Permission): PermissionState {\n return createPermissionState({\n permission,\n status: 'granted',\n });\n}\n"]}
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/expo/client.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,qBAAqB,EAAwB,MAAM,0BAA0B,CAAC;AACvF,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAErD,MAAM,2BAA2B,GAAG,sCAAsC,CAAC;AAO3E,MAAM,QAAQ,GAAuD;IACnE,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,aAAa;IAClC,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,mBAAmB;IAC9C,CAAC,UAAU,CAAC,iBAAiB,CAAC,EAAE,mBAAmB;IACnD,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,iBAAiB;IAC1C,CAAC,UAAU,CAAC,kBAAkB,CAAC,EAAE,eAAe;IAChD,CAAC,UAAU,CAAC,kBAAkB,CAAC,EAAE,eAAe;IAChD,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,oBAAoB;CACjD,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,sBAAsB;IACpC,OAAO;QACL,KAAK,CAAC,SAAS,CAAC,UAAsB;YACpC,MAAM,QAAQ,GAAG,uBAAuB,CAAC,UAAU,CAAC,CAAC;YACrD,IAAI,QAAQ,CAAC,OAAO,KAAK,aAAa,IAAI,QAAQ,CAAC,OAAO,KAAK,gBAAgB,EAAE,CAAC;gBAChF,OAAO,6BAA6B,CAAC,UAAU,CAAC,CAAC;YACnD,CAAC;YAED,IAAI,QAAQ,CAAC,OAAO,KAAK,aAAa,EAAE,CAAC;gBACvC,OAAO,sBAAsB,CAAC,UAAU,CAAC,CAAC;YAC5C,CAAC;YAED,MAAM,OAAO,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC;YACrC,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,MAAM,IAAI,KAAK,CAAC,yCAAyC,UAAU,EAAE,CAAC,CAAC;YACzE,CAAC;YAED,OAAO,OAAO,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;QACvC,CAAC;QAED,KAAK,CAAC,OAAO,CAAC,UAAsB;YAClC,MAAM,QAAQ,GAAG,uBAAuB,CAAC,UAAU,CAAC,CAAC;YACrD,IAAI,QAAQ,CAAC,OAAO,KAAK,aAAa,IAAI,QAAQ,CAAC,OAAO,KAAK,gBAAgB,EAAE,CAAC;gBAChF,OAAO,6BAA6B,CAAC,UAAU,CAAC,CAAC;YACnD,CAAC;YAED,IAAI,QAAQ,CAAC,OAAO,KAAK,aAAa,EAAE,CAAC;gBACvC,OAAO,sBAAsB,CAAC,UAAU,CAAC,CAAC;YAC5C,CAAC;YAED,MAAM,OAAO,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC;YACrC,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,MAAM,IAAI,KAAK,CAAC,yCAAyC,UAAU,EAAE,CAAC,CAAC;YACzE,CAAC;YAED,OAAO,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QACrC,CAAC;QAED,KAAK,CAAC,YAAY;YAChB,IAAI,CAAC;gBACH,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAC;gBAE7C,MAAM,OAAO,CAAC,YAAY,EAAE,CAAC;YAC/B,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,IAAI,KAAK,CAAC,2BAA2B,EAAE;oBAC3C,KAAK,EAAE,KAAK;iBACb,CAAC,CAAC;YACL,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,6BAA6B,CAAC,UAAsB;IAC3D,OAAO,qBAAqB,CAAC;QAC3B,UAAU;QACV,MAAM,EAAE,aAAa;QACrB,MAAM,EAAE,cAAc,UAAU,2CAA2C;KAC5E,CAAC,CAAC;AACL,CAAC;AAED,SAAS,sBAAsB,CAAC,UAAsB;IACpD,OAAO,qBAAqB,CAAC;QAC3B,UAAU;QACV,MAAM,EAAE,SAAS;KAClB,CAAC,CAAC;AACL,CAAC","sourcesContent":["import type { PermissionClient } from '../client/types';\nimport { Permission } from '../registry/permissions';\nimport { createPermissionState, type PermissionState } from '../state/permissionState';\nimport { cameraAdapter } from './adapters/camera';\nimport { locationAdapter } from './adapters/location';\nimport { mediaLibraryAdapter } from './adapters/mediaLibrary';\nimport { microphoneAdapter } from './adapters/microphone';\nimport { notificationsAdapter } from './adapters/notifications';\nimport { EXPO_PERMISSION_SUPPORT } from './manifest';\n\nconst OPEN_SETTINGS_ERROR_MESSAGE = 'Unable to open application settings.';\n\nexport interface ExpoPermissionAdapter {\n getStatus(permission: Permission): Promise<PermissionState>;\n request(permission: Permission): Promise<PermissionState>;\n}\n\nconst adapters: Partial<Record<Permission, ExpoPermissionAdapter>> = {\n [Permission.Camera]: cameraAdapter,\n [Permission.MediaLibrary]: mediaLibraryAdapter,\n [Permission.MediaLibraryWrite]: mediaLibraryAdapter,\n [Permission.Microphone]: microphoneAdapter,\n [Permission.LocationForeground]: locationAdapter,\n [Permission.LocationBackground]: locationAdapter,\n [Permission.Notifications]: notificationsAdapter,\n};\n\n/***\n * Creates a permission client backed by Expo SDK modules.\n *\n * Denials that Expo cannot request again are normalized to `blocked`. Limited\n * media-library access and provisional or ephemeral iOS notification access\n * are normalized to usable `limited` states, where `granted` remains true.\n * The client requires `expo-linking` for settings recovery and rejects with a\n * stable error when the operating-system settings page cannot be opened.\n *\n * Expo modules are loaded only when a client operation uses them. The package\n * root and Expo manifest metadata remain free of native module imports.\n *\n * @readme\n */\nexport function createPermissionClient(): PermissionClient {\n return {\n async getStatus(permission: Permission): Promise<PermissionState> {\n const metadata = EXPO_PERMISSION_SUPPORT[permission];\n if (metadata.support === 'unsupported' || metadata.support === 'notImplemented') {\n return createUnavailableSupportState(permission);\n }\n\n if (metadata.support === 'notRequired') {\n return createNotRequiredState(permission);\n }\n\n const adapter = adapters[permission];\n if (!adapter) {\n throw new Error(`No adapter registered for permission: ${permission}`);\n }\n\n return adapter.getStatus(permission);\n },\n\n async request(permission: Permission): Promise<PermissionState> {\n const metadata = EXPO_PERMISSION_SUPPORT[permission];\n if (metadata.support === 'unsupported' || metadata.support === 'notImplemented') {\n return createUnavailableSupportState(permission);\n }\n\n if (metadata.support === 'notRequired') {\n return createNotRequiredState(permission);\n }\n\n const adapter = adapters[permission];\n if (!adapter) {\n throw new Error(`No adapter registered for permission: ${permission}`);\n }\n\n return adapter.request(permission);\n },\n\n async openSettings(): Promise<void> {\n try {\n const linking = await import('expo-linking');\n\n await linking.openSettings();\n } catch (error) {\n throw new Error(OPEN_SETTINGS_ERROR_MESSAGE, {\n cause: error,\n });\n }\n },\n };\n}\n\nfunction createUnavailableSupportState(permission: Permission): PermissionState {\n return createPermissionState({\n permission,\n status: 'unavailable',\n reason: `Permission ${permission} is not implemented or supported on Expo.`,\n });\n}\n\nfunction createNotRequiredState(permission: Permission): PermissionState {\n return createPermissionState({\n permission,\n status: 'granted',\n });\n}\n"]}
@@ -5,5 +5,10 @@ export interface ExpoPermissionMetadata {
5
5
  readonly requiredPackages: readonly string[];
6
6
  readonly configHints: readonly string[];
7
7
  }
8
+ /***
9
+ * Expo packages required by the permission client independently of a specific
10
+ * permission capability.
11
+ */
12
+ export declare const EXPO_PERMISSION_CLIENT_REQUIRED_PACKAGES: readonly ["expo-linking"];
8
13
  export declare const EXPO_PERMISSION_SUPPORT: Readonly<Record<Permission, ExpoPermissionMetadata>>;
9
14
  //# sourceMappingURL=manifest.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/expo/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAErD,MAAM,MAAM,iBAAiB,GACzB,WAAW,GACX,aAAa,GACb,SAAS,GACT,gBAAgB,GAChB,aAAa,CAAC;AAElB,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACpC,QAAQ,CAAC,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7C,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CACzC;AAED,eAAO,MAAM,uBAAuB,EAAE,QAAQ,CAAC,MAAM,CAAC,UAAU,EAAE,sBAAsB,CAAC,CAyCxF,CAAC"}
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/expo/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAErD,MAAM,MAAM,iBAAiB,GACzB,WAAW,GACX,aAAa,GACb,SAAS,GACT,gBAAgB,GAChB,aAAa,CAAC;AAElB,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACpC,QAAQ,CAAC,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7C,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CACzC;AAED;;;GAGG;AACH,eAAO,MAAM,wCAAwC,2BAA4B,CAAC;AAElF,eAAO,MAAM,uBAAuB,EAAE,QAAQ,CAAC,MAAM,CAAC,UAAU,EAAE,sBAAsB,CAAC,CAyCxF,CAAC"}
@@ -1,4 +1,9 @@
1
1
  import { Permission } from '../registry/permissions';
2
+ /***
3
+ * Expo packages required by the permission client independently of a specific
4
+ * permission capability.
5
+ */
6
+ export const EXPO_PERMISSION_CLIENT_REQUIRED_PACKAGES = ['expo-linking'];
2
7
  export const EXPO_PERMISSION_SUPPORT = {
3
8
  [Permission.Camera]: {
4
9
  support: 'supported',
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../src/expo/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAerD,MAAM,CAAC,MAAM,uBAAuB,GAAyD;IAC3F,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE;QACnB,OAAO,EAAE,WAAW;QACpB,gBAAgB,EAAE,CAAC,aAAa,CAAC;QACjC,WAAW,EAAE,CAAC,kBAAkB,CAAC;KAClC;IACD,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE;QACvB,OAAO,EAAE,WAAW;QACpB,gBAAgB,EAAE,CAAC,YAAY,CAAC;QAChC,WAAW,EAAE,CAAC,sBAAsB,EAAE,oBAAoB,CAAC;KAC5D;IACD,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE;QACzB,OAAO,EAAE,WAAW;QACpB,gBAAgB,EAAE,CAAC,oBAAoB,CAAC;QACxC,WAAW,EAAE,CAAC,wBAAwB,CAAC;KACxC;IACD,CAAC,UAAU,CAAC,iBAAiB,CAAC,EAAE;QAC9B,OAAO,EAAE,WAAW;QACpB,gBAAgB,EAAE,CAAC,oBAAoB,CAAC;QACxC,WAAW,EAAE,CAAC,wBAAwB,CAAC;KACxC;IACD,CAAC,UAAU,CAAC,kBAAkB,CAAC,EAAE;QAC/B,OAAO,EAAE,WAAW;QACpB,gBAAgB,EAAE,CAAC,eAAe,CAAC;QACnC,WAAW,EAAE,CAAC,6BAA6B,CAAC;KAC7C;IACD,CAAC,UAAU,CAAC,kBAAkB,CAAC,EAAE;QAC/B,OAAO,EAAE,WAAW;QACpB,gBAAgB,EAAE,CAAC,eAAe,CAAC;QACnC,WAAW,EAAE,CAAC,sCAAsC,CAAC;KACtD;IACD,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE;QAC1B,OAAO,EAAE,WAAW;QACpB,gBAAgB,EAAE,CAAC,oBAAoB,CAAC;QACxC,WAAW,EAAE,CAAC,yBAAyB,CAAC;KACzC;IACD,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE;QACtB,OAAO,EAAE,aAAa;QACtB,gBAAgB,EAAE,EAAE;QACpB,WAAW,EAAE,EAAE;KAChB;CACF,CAAC","sourcesContent":["import { Permission } from '../registry/permissions';\n\nexport type PermissionSupport =\n | 'supported'\n | 'notRequired'\n | 'limited'\n | 'notImplemented'\n | 'unsupported';\n\nexport interface ExpoPermissionMetadata {\n readonly support: PermissionSupport;\n readonly requiredPackages: readonly string[];\n readonly configHints: readonly string[];\n}\n\nexport const EXPO_PERMISSION_SUPPORT: Readonly<Record<Permission, ExpoPermissionMetadata>> = {\n [Permission.Camera]: {\n support: 'supported',\n requiredPackages: ['expo-camera'],\n configHints: ['cameraPermission'],\n },\n [Permission.Microphone]: {\n support: 'supported',\n requiredPackages: ['expo-audio'],\n configHints: ['microphonePermission', 'recordAudioAndroid'],\n },\n [Permission.MediaLibrary]: {\n support: 'supported',\n requiredPackages: ['expo-media-library'],\n configHints: ['mediaLibraryPermission'],\n },\n [Permission.MediaLibraryWrite]: {\n support: 'supported',\n requiredPackages: ['expo-media-library'],\n configHints: ['mediaLibraryPermission'],\n },\n [Permission.LocationForeground]: {\n support: 'supported',\n requiredPackages: ['expo-location'],\n configHints: ['locationWhenInUsePermission'],\n },\n [Permission.LocationBackground]: {\n support: 'supported',\n requiredPackages: ['expo-location'],\n configHints: ['locationAlwaysAndWhenInUsePermission'],\n },\n [Permission.Notifications]: {\n support: 'supported',\n requiredPackages: ['expo-notifications'],\n configHints: ['notificationsPermission'],\n },\n [Permission.Clipboard]: {\n support: 'notRequired',\n requiredPackages: [],\n configHints: [],\n },\n};\n"]}
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../src/expo/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAerD;;;GAGG;AACH,MAAM,CAAC,MAAM,wCAAwC,GAAG,CAAC,cAAc,CAAU,CAAC;AAElF,MAAM,CAAC,MAAM,uBAAuB,GAAyD;IAC3F,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE;QACnB,OAAO,EAAE,WAAW;QACpB,gBAAgB,EAAE,CAAC,aAAa,CAAC;QACjC,WAAW,EAAE,CAAC,kBAAkB,CAAC;KAClC;IACD,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE;QACvB,OAAO,EAAE,WAAW;QACpB,gBAAgB,EAAE,CAAC,YAAY,CAAC;QAChC,WAAW,EAAE,CAAC,sBAAsB,EAAE,oBAAoB,CAAC;KAC5D;IACD,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE;QACzB,OAAO,EAAE,WAAW;QACpB,gBAAgB,EAAE,CAAC,oBAAoB,CAAC;QACxC,WAAW,EAAE,CAAC,wBAAwB,CAAC;KACxC;IACD,CAAC,UAAU,CAAC,iBAAiB,CAAC,EAAE;QAC9B,OAAO,EAAE,WAAW;QACpB,gBAAgB,EAAE,CAAC,oBAAoB,CAAC;QACxC,WAAW,EAAE,CAAC,wBAAwB,CAAC;KACxC;IACD,CAAC,UAAU,CAAC,kBAAkB,CAAC,EAAE;QAC/B,OAAO,EAAE,WAAW;QACpB,gBAAgB,EAAE,CAAC,eAAe,CAAC;QACnC,WAAW,EAAE,CAAC,6BAA6B,CAAC;KAC7C;IACD,CAAC,UAAU,CAAC,kBAAkB,CAAC,EAAE;QAC/B,OAAO,EAAE,WAAW;QACpB,gBAAgB,EAAE,CAAC,eAAe,CAAC;QACnC,WAAW,EAAE,CAAC,sCAAsC,CAAC;KACtD;IACD,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE;QAC1B,OAAO,EAAE,WAAW;QACpB,gBAAgB,EAAE,CAAC,oBAAoB,CAAC;QACxC,WAAW,EAAE,CAAC,yBAAyB,CAAC;KACzC;IACD,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE;QACtB,OAAO,EAAE,aAAa;QACtB,gBAAgB,EAAE,EAAE;QACpB,WAAW,EAAE,EAAE;KAChB;CACF,CAAC","sourcesContent":["import { Permission } from '../registry/permissions';\n\nexport type PermissionSupport =\n | 'supported'\n | 'notRequired'\n | 'limited'\n | 'notImplemented'\n | 'unsupported';\n\nexport interface ExpoPermissionMetadata {\n readonly support: PermissionSupport;\n readonly requiredPackages: readonly string[];\n readonly configHints: readonly string[];\n}\n\n/***\n * Expo packages required by the permission client independently of a specific\n * permission capability.\n */\nexport const EXPO_PERMISSION_CLIENT_REQUIRED_PACKAGES = ['expo-linking'] as const;\n\nexport const EXPO_PERMISSION_SUPPORT: Readonly<Record<Permission, ExpoPermissionMetadata>> = {\n [Permission.Camera]: {\n support: 'supported',\n requiredPackages: ['expo-camera'],\n configHints: ['cameraPermission'],\n },\n [Permission.Microphone]: {\n support: 'supported',\n requiredPackages: ['expo-audio'],\n configHints: ['microphonePermission', 'recordAudioAndroid'],\n },\n [Permission.MediaLibrary]: {\n support: 'supported',\n requiredPackages: ['expo-media-library'],\n configHints: ['mediaLibraryPermission'],\n },\n [Permission.MediaLibraryWrite]: {\n support: 'supported',\n requiredPackages: ['expo-media-library'],\n configHints: ['mediaLibraryPermission'],\n },\n [Permission.LocationForeground]: {\n support: 'supported',\n requiredPackages: ['expo-location'],\n configHints: ['locationWhenInUsePermission'],\n },\n [Permission.LocationBackground]: {\n support: 'supported',\n requiredPackages: ['expo-location'],\n configHints: ['locationAlwaysAndWhenInUsePermission'],\n },\n [Permission.Notifications]: {\n support: 'supported',\n requiredPackages: ['expo-notifications'],\n configHints: ['notificationsPermission'],\n },\n [Permission.Clipboard]: {\n support: 'notRequired',\n requiredPackages: [],\n configHints: [],\n },\n};\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"permissionResponse.d.ts","sourceRoot":"","sources":["../../src/expo/permissionResponse.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAGL,KAAK,eAAe,EACrB,MAAM,0BAA0B,CAAC;AAElC,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;CAChC;AAED,MAAM,MAAM,oBAAoB,GAAG,MAAM,OAAO,CAAC,sBAAsB,CAAC,CAAC;AAEzE,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,sBAAsB,GAC/B,eAAe,CAQjB;AAED,wBAAgB,0BAA0B,CACxC,UAAU,EAAE,UAAU,EACtB,KAAK,EAAE,OAAO,GACb,eAAe,CAMjB;AAED,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,OAAO,EACf,UAAU,EAAE,MAAM,EAClB,cAAc,GAAE,SAAS,MAAM,EAAO,GACrC,oBAAoB,GAAG,SAAS,CAkBlC"}
1
+ {"version":3,"file":"permissionResponse.d.ts","sourceRoot":"","sources":["../../src/expo/permissionResponse.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAGL,KAAK,eAAe,EAErB,MAAM,0BAA0B,CAAC;AAElC,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;CAChC;AAED,MAAM,MAAM,oBAAoB,GAAG,MAAM,OAAO,CAAC,sBAAsB,CAAC,CAAC;AAEzE,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,sBAAsB,GAC/B,eAAe,CAIjB;AAED,wBAAgB,0BAA0B,CACxC,UAAU,EAAE,UAAU,EACtB,KAAK,EAAE,OAAO,GACb,eAAe,CAMjB;AAED,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,OAAO,EACf,UAAU,EAAE,MAAM,EAClB,cAAc,GAAE,SAAS,MAAM,EAAO,GACrC,oBAAoB,GAAG,SAAS,CAkBlC"}
@@ -1,11 +1,7 @@
1
1
  import { createPermissionState, isPermissionStatus, } from '../state/permissionState';
2
2
  export function createExpoPermissionState(permission, response) {
3
3
  const status = isPermissionStatus(response.status) ? response.status : 'unknown';
4
- return createPermissionState({
5
- permission,
6
- status,
7
- canAskAgain: response.canAskAgain,
8
- });
4
+ return createNormalizedExpoPermissionState(permission, response, status);
9
5
  }
10
6
  export function createExpoUnavailableState(permission, error) {
11
7
  return createPermissionState({
@@ -38,6 +34,17 @@ function getExpoPermissionErrorMessage(error) {
38
34
  }
39
35
  return 'Expo permission operation failed.';
40
36
  }
37
+ function createNormalizedExpoPermissionState(permission, response, status, reason) {
38
+ const normalizedStatus = status === 'denied' && response.canAskAgain === false ? 'blocked' : status;
39
+ return createPermissionState({
40
+ permission,
41
+ status: normalizedStatus,
42
+ canAskAgain: response.canAskAgain,
43
+ reason: normalizedStatus === 'blocked'
44
+ ? 'Permission is blocked. Open system settings to change it.'
45
+ : reason,
46
+ });
47
+ }
41
48
  function hasExpoPermissionMethod(value, methodName) {
42
49
  return isRecord(value) && typeof value[methodName] === 'function';
43
50
  }
@@ -1 +1 @@
1
- {"version":3,"file":"permissionResponse.js","sourceRoot":"","sources":["../../src/expo/permissionResponse.ts"],"names":[],"mappings":"AACA,OAAO,EACL,qBAAqB,EACrB,kBAAkB,GAEnB,MAAM,0BAA0B,CAAC;AASlC,MAAM,UAAU,yBAAyB,CACvC,UAAsB,EACtB,QAAgC;IAEhC,MAAM,MAAM,GAAG,kBAAkB,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;IAEjF,OAAO,qBAAqB,CAAC;QAC3B,UAAU;QACV,MAAM;QACN,WAAW,EAAE,QAAQ,CAAC,WAAW;KAClC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,0BAA0B,CACxC,UAAsB,EACtB,KAAc;IAEd,OAAO,qBAAqB,CAAC;QAC3B,UAAU;QACV,MAAM,EAAE,aAAa;QACrB,MAAM,EAAE,6BAA6B,CAAC,KAAK,CAAC;KAC7C,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,wBAAwB,CACtC,MAAe,EACf,UAAkB,EAClB,iBAAoC,EAAE;IAEtC,IAAI,uBAAuB,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE,CAAC;QAChD,OAAO,MAAM,CAAC,UAAU,CAAC,CAAC;IAC5B,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QACtB,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,KAAK,MAAM,aAAa,IAAI,cAAc,EAAE,CAAC;QAC3C,MAAM,SAAS,GAAG,MAAM,CAAC,aAAa,CAAC,CAAC;QAExC,IAAI,uBAAuB,CAAC,SAAS,EAAE,UAAU,CAAC,EAAE,CAAC;YACnD,OAAO,SAAS,CAAC,UAAU,CAAC,CAAC;QAC/B,CAAC;IACH,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,6BAA6B,CAAC,KAAc;IACnD,IAAI,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvD,OAAO,KAAK,CAAC,OAAO,CAAC;IACvB,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAClD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,OAAO,mCAAmC,CAAC;AAC7C,CAAC;AAED,SAAS,uBAAuB,CAC9B,KAAc,EACd,UAAkB;IAElB,OAAO,QAAQ,CAAC,KAAK,CAAC,IAAI,OAAO,KAAK,CAAC,UAAU,CAAC,KAAK,UAAU,CAAC;AACpE,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAC;AACrD,CAAC","sourcesContent":["import type { Permission } from '../registry/permissions';\nimport {\n createPermissionState,\n isPermissionStatus,\n type PermissionState,\n} from '../state/permissionState';\n\nexport interface ExpoPermissionResponse {\n readonly status: string;\n readonly canAskAgain?: boolean;\n}\n\nexport type ExpoPermissionMethod = () => Promise<ExpoPermissionResponse>;\n\nexport function createExpoPermissionState(\n permission: Permission,\n response: ExpoPermissionResponse,\n): PermissionState {\n const status = isPermissionStatus(response.status) ? response.status : 'unknown';\n\n return createPermissionState({\n permission,\n status,\n canAskAgain: response.canAskAgain,\n });\n}\n\nexport function createExpoUnavailableState(\n permission: Permission,\n error: unknown,\n): PermissionState {\n return createPermissionState({\n permission,\n status: 'unavailable',\n reason: getExpoPermissionErrorMessage(error),\n });\n}\n\nexport function findExpoPermissionMethod(\n source: unknown,\n methodName: string,\n containerNames: readonly string[] = [],\n): ExpoPermissionMethod | undefined {\n if (hasExpoPermissionMethod(source, methodName)) {\n return source[methodName];\n }\n\n if (!isRecord(source)) {\n return undefined;\n }\n\n for (const containerName of containerNames) {\n const container = source[containerName];\n\n if (hasExpoPermissionMethod(container, methodName)) {\n return container[methodName];\n }\n }\n\n return undefined;\n}\n\nfunction getExpoPermissionErrorMessage(error: unknown): string {\n if (error instanceof Error && error.message.length > 0) {\n return error.message;\n }\n\n if (typeof error === 'string' && error.length > 0) {\n return error;\n }\n\n return 'Expo permission operation failed.';\n}\n\nfunction hasExpoPermissionMethod(\n value: unknown,\n methodName: string,\n): value is Record<string, ExpoPermissionMethod> {\n return isRecord(value) && typeof value[methodName] === 'function';\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n"]}
1
+ {"version":3,"file":"permissionResponse.js","sourceRoot":"","sources":["../../src/expo/permissionResponse.ts"],"names":[],"mappings":"AACA,OAAO,EACL,qBAAqB,EACrB,kBAAkB,GAGnB,MAAM,0BAA0B,CAAC;AASlC,MAAM,UAAU,yBAAyB,CACvC,UAAsB,EACtB,QAAgC;IAEhC,MAAM,MAAM,GAAG,kBAAkB,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;IAEjF,OAAO,mCAAmC,CAAC,UAAU,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;AAC3E,CAAC;AAED,MAAM,UAAU,0BAA0B,CACxC,UAAsB,EACtB,KAAc;IAEd,OAAO,qBAAqB,CAAC;QAC3B,UAAU;QACV,MAAM,EAAE,aAAa;QACrB,MAAM,EAAE,6BAA6B,CAAC,KAAK,CAAC;KAC7C,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,wBAAwB,CACtC,MAAe,EACf,UAAkB,EAClB,iBAAoC,EAAE;IAEtC,IAAI,uBAAuB,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE,CAAC;QAChD,OAAO,MAAM,CAAC,UAAU,CAAC,CAAC;IAC5B,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QACtB,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,KAAK,MAAM,aAAa,IAAI,cAAc,EAAE,CAAC;QAC3C,MAAM,SAAS,GAAG,MAAM,CAAC,aAAa,CAAC,CAAC;QAExC,IAAI,uBAAuB,CAAC,SAAS,EAAE,UAAU,CAAC,EAAE,CAAC;YACnD,OAAO,SAAS,CAAC,UAAU,CAAC,CAAC;QAC/B,CAAC;IACH,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,6BAA6B,CAAC,KAAc;IACnD,IAAI,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvD,OAAO,KAAK,CAAC,OAAO,CAAC;IACvB,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAClD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,OAAO,mCAAmC,CAAC;AAC7C,CAAC;AAED,SAAS,mCAAmC,CAC1C,UAAsB,EACtB,QAAgC,EAChC,MAAwB,EACxB,MAAe;IAEf,MAAM,gBAAgB,GACpB,MAAM,KAAK,QAAQ,IAAI,QAAQ,CAAC,WAAW,KAAK,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC;IAE7E,OAAO,qBAAqB,CAAC;QAC3B,UAAU;QACV,MAAM,EAAE,gBAAgB;QACxB,WAAW,EAAE,QAAQ,CAAC,WAAW;QACjC,MAAM,EACJ,gBAAgB,KAAK,SAAS;YAC5B,CAAC,CAAC,2DAA2D;YAC7D,CAAC,CAAC,MAAM;KACb,CAAC,CAAC;AACL,CAAC;AAED,SAAS,uBAAuB,CAC9B,KAAc,EACd,UAAkB;IAElB,OAAO,QAAQ,CAAC,KAAK,CAAC,IAAI,OAAO,KAAK,CAAC,UAAU,CAAC,KAAK,UAAU,CAAC;AACpE,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAC;AACrD,CAAC","sourcesContent":["import type { Permission } from '../registry/permissions';\nimport {\n createPermissionState,\n isPermissionStatus,\n type PermissionState,\n type PermissionStatus,\n} from '../state/permissionState';\n\nexport interface ExpoPermissionResponse {\n readonly status: string;\n readonly canAskAgain?: boolean;\n}\n\nexport type ExpoPermissionMethod = () => Promise<ExpoPermissionResponse>;\n\nexport function createExpoPermissionState(\n permission: Permission,\n response: ExpoPermissionResponse,\n): PermissionState {\n const status = isPermissionStatus(response.status) ? response.status : 'unknown';\n\n return createNormalizedExpoPermissionState(permission, response, status);\n}\n\nexport function createExpoUnavailableState(\n permission: Permission,\n error: unknown,\n): PermissionState {\n return createPermissionState({\n permission,\n status: 'unavailable',\n reason: getExpoPermissionErrorMessage(error),\n });\n}\n\nexport function findExpoPermissionMethod(\n source: unknown,\n methodName: string,\n containerNames: readonly string[] = [],\n): ExpoPermissionMethod | undefined {\n if (hasExpoPermissionMethod(source, methodName)) {\n return source[methodName];\n }\n\n if (!isRecord(source)) {\n return undefined;\n }\n\n for (const containerName of containerNames) {\n const container = source[containerName];\n\n if (hasExpoPermissionMethod(container, methodName)) {\n return container[methodName];\n }\n }\n\n return undefined;\n}\n\nfunction getExpoPermissionErrorMessage(error: unknown): string {\n if (error instanceof Error && error.message.length > 0) {\n return error.message;\n }\n\n if (typeof error === 'string' && error.length > 0) {\n return error;\n }\n\n return 'Expo permission operation failed.';\n}\n\nfunction createNormalizedExpoPermissionState(\n permission: Permission,\n response: ExpoPermissionResponse,\n status: PermissionStatus,\n reason?: string,\n): PermissionState {\n const normalizedStatus =\n status === 'denied' && response.canAskAgain === false ? 'blocked' : status;\n\n return createPermissionState({\n permission,\n status: normalizedStatus,\n canAskAgain: response.canAskAgain,\n reason:\n normalizedStatus === 'blocked'\n ? 'Permission is blocked. Open system settings to change it.'\n : reason,\n });\n}\n\nfunction hasExpoPermissionMethod(\n value: unknown,\n methodName: string,\n): value is Record<string, ExpoPermissionMethod> {\n return isRecord(value) && typeof value[methodName] === 'function';\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n"]}
@@ -34,6 +34,8 @@ export interface CreatePermissionStateInput {
34
34
  export declare function isPermissionStatus(value: string): value is PermissionStatus;
35
35
  /***
36
36
  * Creates a normalized state and derives the `granted` convenience flag.
37
+ * Limited access is usable partial authorization, so both `granted` and
38
+ * `limited` states set `granted` to true.
37
39
  */
38
40
  export declare function createPermissionState(input: CreatePermissionStateInput): PermissionState;
39
41
  /***
@@ -1 +1 @@
1
- {"version":3,"file":"permissionState.d.ts","sourceRoot":"","sources":["../../src/state/permissionState.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAE1D;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GACxB,SAAS,GACT,SAAS,GACT,QAAQ,GACR,SAAS,GACT,SAAS,GACT,aAAa,CAAC;AAElB;;GAEG;AACH,eAAO,MAAM,mBAAmB,gFAOgB,CAAC;AAIjD;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,WAAW,CAAC,EAAE,IAAI,CAAC;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,WAAW,CAAC,EAAE,IAAI,CAAC;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,gBAAgB,CAE3E;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,0BAA0B,GAAG,eAAe,CASxF;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,UAAU,EACtB,KAAK,EAAE,eAAe,GACrB,eAAe,CAQjB"}
1
+ {"version":3,"file":"permissionState.d.ts","sourceRoot":"","sources":["../../src/state/permissionState.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAE1D;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GACxB,SAAS,GACT,SAAS,GACT,QAAQ,GACR,SAAS,GACT,SAAS,GACT,aAAa,CAAC;AAElB;;GAEG;AACH,eAAO,MAAM,mBAAmB,gFAOgB,CAAC;AAIjD;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,WAAW,CAAC,EAAE,IAAI,CAAC;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,WAAW,CAAC,EAAE,IAAI,CAAC;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,gBAAgB,CAE3E;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,0BAA0B,GAAG,eAAe,CASxF;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,UAAU,EACtB,KAAK,EAAE,eAAe,GACrB,eAAe,CAQjB"}
@@ -18,13 +18,15 @@ export function isPermissionStatus(value) {
18
18
  }
19
19
  /***
20
20
  * Creates a normalized state and derives the `granted` convenience flag.
21
+ * Limited access is usable partial authorization, so both `granted` and
22
+ * `limited` states set `granted` to true.
21
23
  */
22
24
  export function createPermissionState(input) {
23
25
  return {
24
26
  permission: input.permission,
25
27
  status: input.status,
26
28
  canAskAgain: input.canAskAgain,
27
- granted: input.status === 'granted',
29
+ granted: input.status === 'granted' || input.status === 'limited',
28
30
  requestedAt: input.requestedAt,
29
31
  reason: input.reason,
30
32
  };
@@ -1 +1 @@
1
- {"version":3,"file":"permissionState.js","sourceRoot":"","sources":["../../src/state/permissionState.ts"],"names":[],"mappings":"AAgBA;;GAEG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,SAAS;IACT,SAAS;IACT,QAAQ;IACR,SAAS;IACT,SAAS;IACT,aAAa;CACiC,CAAC;AAEjD,MAAM,qBAAqB,GAAG,IAAI,GAAG,CAAS,mBAAmB,CAAC,CAAC;AAsBnE;;GAEG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAa;IAC9C,OAAO,qBAAqB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AAC1C,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAiC;IACrE,OAAO;QACL,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,OAAO,EAAE,KAAK,CAAC,MAAM,KAAK,SAAS;QACnC,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,MAAM,EAAE,KAAK,CAAC,MAAM;KACrB,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,wBAAwB,CACtC,UAAsB,EACtB,KAAsB;IAEtB,OAAO,qBAAqB,CAAC;QAC3B,UAAU;QACV,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,MAAM,EAAE,KAAK,CAAC,MAAM;KACrB,CAAC,CAAC;AACL,CAAC","sourcesContent":["import type { Permission } from '../registry/permissions';\n\n/***\n * Normalized permission statuses shared by adapters and UI code.\n *\n * Denial is normal control flow. Unexpected adapter failures may still throw,\n * but user denial should be represented as a permission state.\n */\nexport type PermissionStatus =\n | 'unknown'\n | 'granted'\n | 'denied'\n | 'blocked'\n | 'limited'\n | 'unavailable';\n\n/***\n * Stable ordered list of normalized statuses.\n */\nexport const PERMISSION_STATUSES = [\n 'unknown',\n 'granted',\n 'denied',\n 'blocked',\n 'limited',\n 'unavailable',\n] as const satisfies readonly PermissionStatus[];\n\nconst PERMISSION_STATUS_SET = new Set<string>(PERMISSION_STATUSES);\n\n/***\n * Normalized result returned by permission clients and managers.\n */\nexport interface PermissionState {\n readonly permission: Permission;\n readonly status: PermissionStatus;\n readonly canAskAgain?: boolean;\n readonly granted: boolean;\n readonly requestedAt?: Date;\n readonly reason?: string;\n}\n\nexport interface CreatePermissionStateInput {\n readonly permission: Permission;\n readonly status: PermissionStatus;\n readonly canAskAgain?: boolean;\n readonly requestedAt?: Date;\n readonly reason?: string;\n}\n\n/***\n * Returns true when a string is one of the normalized statuses.\n */\nexport function isPermissionStatus(value: string): value is PermissionStatus {\n return PERMISSION_STATUS_SET.has(value);\n}\n\n/***\n * Creates a normalized state and derives the `granted` convenience flag.\n */\nexport function createPermissionState(input: CreatePermissionStateInput): PermissionState {\n return {\n permission: input.permission,\n status: input.status,\n canAskAgain: input.canAskAgain,\n granted: input.status === 'granted',\n requestedAt: input.requestedAt,\n reason: input.reason,\n };\n}\n\n/***\n * Normalizes an adapter result to the requested permission and status shape.\n */\nexport function normalizePermissionState(\n permission: Permission,\n state: PermissionState,\n): PermissionState {\n return createPermissionState({\n permission,\n status: state.status,\n canAskAgain: state.canAskAgain,\n requestedAt: state.requestedAt,\n reason: state.reason,\n });\n}\n"]}
1
+ {"version":3,"file":"permissionState.js","sourceRoot":"","sources":["../../src/state/permissionState.ts"],"names":[],"mappings":"AAgBA;;GAEG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,SAAS;IACT,SAAS;IACT,QAAQ;IACR,SAAS;IACT,SAAS;IACT,aAAa;CACiC,CAAC;AAEjD,MAAM,qBAAqB,GAAG,IAAI,GAAG,CAAS,mBAAmB,CAAC,CAAC;AAsBnE;;GAEG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAa;IAC9C,OAAO,qBAAqB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AAC1C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAiC;IACrE,OAAO;QACL,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,OAAO,EAAE,KAAK,CAAC,MAAM,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS;QACjE,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,MAAM,EAAE,KAAK,CAAC,MAAM;KACrB,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,wBAAwB,CACtC,UAAsB,EACtB,KAAsB;IAEtB,OAAO,qBAAqB,CAAC;QAC3B,UAAU;QACV,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,MAAM,EAAE,KAAK,CAAC,MAAM;KACrB,CAAC,CAAC;AACL,CAAC","sourcesContent":["import type { Permission } from '../registry/permissions';\n\n/***\n * Normalized permission statuses shared by adapters and UI code.\n *\n * Denial is normal control flow. Unexpected adapter failures may still throw,\n * but user denial should be represented as a permission state.\n */\nexport type PermissionStatus =\n | 'unknown'\n | 'granted'\n | 'denied'\n | 'blocked'\n | 'limited'\n | 'unavailable';\n\n/***\n * Stable ordered list of normalized statuses.\n */\nexport const PERMISSION_STATUSES = [\n 'unknown',\n 'granted',\n 'denied',\n 'blocked',\n 'limited',\n 'unavailable',\n] as const satisfies readonly PermissionStatus[];\n\nconst PERMISSION_STATUS_SET = new Set<string>(PERMISSION_STATUSES);\n\n/***\n * Normalized result returned by permission clients and managers.\n */\nexport interface PermissionState {\n readonly permission: Permission;\n readonly status: PermissionStatus;\n readonly canAskAgain?: boolean;\n readonly granted: boolean;\n readonly requestedAt?: Date;\n readonly reason?: string;\n}\n\nexport interface CreatePermissionStateInput {\n readonly permission: Permission;\n readonly status: PermissionStatus;\n readonly canAskAgain?: boolean;\n readonly requestedAt?: Date;\n readonly reason?: string;\n}\n\n/***\n * Returns true when a string is one of the normalized statuses.\n */\nexport function isPermissionStatus(value: string): value is PermissionStatus {\n return PERMISSION_STATUS_SET.has(value);\n}\n\n/***\n * Creates a normalized state and derives the `granted` convenience flag.\n * Limited access is usable partial authorization, so both `granted` and\n * `limited` states set `granted` to true.\n */\nexport function createPermissionState(input: CreatePermissionStateInput): PermissionState {\n return {\n permission: input.permission,\n status: input.status,\n canAskAgain: input.canAskAgain,\n granted: input.status === 'granted' || input.status === 'limited',\n requestedAt: input.requestedAt,\n reason: input.reason,\n };\n}\n\n/***\n * Normalizes an adapter result to the requested permission and status shape.\n */\nexport function normalizePermissionState(\n permission: Permission,\n state: PermissionState,\n): PermissionState {\n return createPermissionState({\n permission,\n status: state.status,\n canAskAgain: state.canAskAgain,\n requestedAt: state.requestedAt,\n reason: state.reason,\n });\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ankhorage/permissions",
3
3
  "type": "module",
4
- "version": "0.2.2",
4
+ "version": "0.2.3",
5
5
  "description": "Cross-platform permission registry and runtime helpers for Expo, React Native, and React Native Web apps, with unified request/check APIs for camera, media, location, notifications, microphone, and other web/native capabilities.",
6
6
  "homepage": "https://github.com/ankhorage/permissions#readme",
7
7
  "bugs": {
@@ -39,8 +39,13 @@
39
39
  },
40
40
  "ankh": {
41
41
  "category": "permissions",
42
- "provider": null,
43
- "capabilities": []
42
+ "provider": "./dist/cli/index.js",
43
+ "capabilities": [
44
+ "permissions.inspect",
45
+ "permissions.check",
46
+ "permissions.request",
47
+ "permissions.manifest"
48
+ ]
44
49
  },
45
50
  "files": [
46
51
  "dist",
@@ -58,6 +63,11 @@
58
63
  "import": "./dist/index.js",
59
64
  "default": "./dist/index.js"
60
65
  },
66
+ "./cli": {
67
+ "types": "./dist/cli/index.d.ts",
68
+ "import": "./dist/cli/index.js",
69
+ "default": "./dist/cli/index.js"
70
+ },
61
71
  "./react": {
62
72
  "react-native": "./dist/react/index.js",
63
73
  "browser": "./dist/react/index.js",
@@ -0,0 +1 @@
1
+ export { default } from '../ankh.provider.js';
@@ -1,8 +1,16 @@
1
1
  import type { Permission } from '../../registry/permissions';
2
2
  import { Permission as KnownPermission } from '../../registry/permissions';
3
- import type { PermissionState } from '../../state/permissionState';
3
+ import { createPermissionState, type PermissionState } from '../../state/permissionState';
4
4
  import type { ExpoPermissionAdapter } from '../client';
5
- import { createExpoPermissionState, createExpoUnavailableState } from '../permissionResponse';
5
+ import {
6
+ createExpoPermissionState,
7
+ createExpoUnavailableState,
8
+ type ExpoPermissionResponse,
9
+ } from '../permissionResponse';
10
+
11
+ interface ExpoMediaLibraryPermissionResponse extends ExpoPermissionResponse {
12
+ readonly accessPrivileges?: 'all' | 'limited' | 'none';
13
+ }
6
14
 
7
15
  export const mediaLibraryAdapter: ExpoPermissionAdapter = {
8
16
  async getStatus(permission: Permission): Promise<PermissionState> {
@@ -10,7 +18,7 @@ export const mediaLibraryAdapter: ExpoPermissionAdapter = {
10
18
  const mod = await import('expo-media-library');
11
19
  const result = await mod.getPermissionsAsync(isWriteOnlyPermission(permission));
12
20
 
13
- return createExpoPermissionState(permission, result);
21
+ return createMediaLibraryPermissionState(permission, result);
14
22
  } catch (error) {
15
23
  return createExpoUnavailableState(permission, error);
16
24
  }
@@ -21,13 +29,29 @@ export const mediaLibraryAdapter: ExpoPermissionAdapter = {
21
29
  const mod = await import('expo-media-library');
22
30
  const result = await mod.requestPermissionsAsync(isWriteOnlyPermission(permission));
23
31
 
24
- return createExpoPermissionState(permission, result);
32
+ return createMediaLibraryPermissionState(permission, result);
25
33
  } catch (error) {
26
34
  return createExpoUnavailableState(permission, error);
27
35
  }
28
36
  },
29
37
  };
30
38
 
39
+ function createMediaLibraryPermissionState(
40
+ permission: Permission,
41
+ response: ExpoMediaLibraryPermissionResponse,
42
+ ): PermissionState {
43
+ if (response.accessPrivileges !== 'limited') {
44
+ return createExpoPermissionState(permission, response);
45
+ }
46
+
47
+ return createPermissionState({
48
+ permission,
49
+ status: 'limited',
50
+ canAskAgain: response.canAskAgain,
51
+ reason: 'Media library access is limited to user-selected assets.',
52
+ });
53
+ }
54
+
31
55
  function isWriteOnlyPermission(permission: Permission): boolean {
32
56
  return permission === KnownPermission.MediaLibraryWrite;
33
57
  }
@@ -1,7 +1,17 @@
1
1
  import type { Permission } from '../../registry/permissions';
2
- import type { PermissionState } from '../../state/permissionState';
2
+ import { createPermissionState, type PermissionState } from '../../state/permissionState';
3
3
  import type { ExpoPermissionAdapter } from '../client';
4
- import { createExpoPermissionState, createExpoUnavailableState } from '../permissionResponse';
4
+ import {
5
+ createExpoPermissionState,
6
+ createExpoUnavailableState,
7
+ type ExpoPermissionResponse,
8
+ } from '../permissionResponse';
9
+
10
+ interface ExpoNotificationPermissionResponse extends ExpoPermissionResponse {
11
+ readonly ios?: {
12
+ readonly status?: number;
13
+ };
14
+ }
5
15
 
6
16
  export const notificationsAdapter: ExpoPermissionAdapter = {
7
17
  async getStatus(permission: Permission): Promise<PermissionState> {
@@ -9,7 +19,7 @@ export const notificationsAdapter: ExpoPermissionAdapter = {
9
19
  const mod = await import('expo-notifications');
10
20
  const result = await mod.getPermissionsAsync();
11
21
 
12
- return createExpoPermissionState(permission, result);
22
+ return createNotificationPermissionState(permission, result);
13
23
  } catch (error) {
14
24
  return createExpoUnavailableState(permission, error);
15
25
  }
@@ -20,9 +30,68 @@ export const notificationsAdapter: ExpoPermissionAdapter = {
20
30
  const mod = await import('expo-notifications');
21
31
  const result = await mod.requestPermissionsAsync();
22
32
 
23
- return createExpoPermissionState(permission, result);
33
+ return createNotificationPermissionState(permission, result);
24
34
  } catch (error) {
25
35
  return createExpoUnavailableState(permission, error);
26
36
  }
27
37
  },
28
38
  };
39
+
40
+ function createNotificationPermissionState(
41
+ permission: Permission,
42
+ response: ExpoNotificationPermissionResponse,
43
+ ): PermissionState {
44
+ switch (response.ios?.status) {
45
+ case 0:
46
+ return createNotificationState(
47
+ permission,
48
+ response,
49
+ 'unknown',
50
+ 'iOS notification authorization has not been determined.',
51
+ );
52
+ case 1:
53
+ return createNotificationState(
54
+ permission,
55
+ response,
56
+ 'denied',
57
+ 'iOS notification authorization is denied.',
58
+ );
59
+ case 2:
60
+ return createNotificationState(permission, response, 'granted');
61
+ case 3:
62
+ return createNotificationState(
63
+ permission,
64
+ response,
65
+ 'limited',
66
+ 'iOS notification authorization is provisional.',
67
+ );
68
+ case 4:
69
+ return createNotificationState(
70
+ permission,
71
+ response,
72
+ 'limited',
73
+ 'iOS notification authorization is ephemeral.',
74
+ );
75
+ default:
76
+ return createExpoPermissionState(permission, response);
77
+ }
78
+ }
79
+
80
+ function createNotificationState(
81
+ permission: Permission,
82
+ response: ExpoNotificationPermissionResponse,
83
+ status: PermissionState['status'],
84
+ reason?: string,
85
+ ): PermissionState {
86
+ const state = createExpoPermissionState(permission, {
87
+ status,
88
+ canAskAgain: response.canAskAgain,
89
+ });
90
+
91
+ return createPermissionState({
92
+ permission,
93
+ status: state.status,
94
+ canAskAgain: state.canAskAgain,
95
+ reason: state.reason ?? reason,
96
+ });
97
+ }
@@ -6,6 +6,10 @@ import { createPermissionClient } from './client';
6
6
  interface MockPermissionResponse {
7
7
  readonly status: string;
8
8
  readonly canAskAgain?: boolean;
9
+ readonly accessPrivileges?: 'all' | 'limited' | 'none';
10
+ readonly ios?: {
11
+ readonly status?: number;
12
+ };
9
13
  }
10
14
 
11
15
  type CameraMockMode = 'topLevel' | 'cameraNamespace' | 'missingExports';
@@ -56,6 +60,11 @@ const notificationsMock = {
56
60
  requestError: undefined as string | undefined,
57
61
  };
58
62
 
63
+ const linkingMock = {
64
+ calls: 0,
65
+ error: undefined as Error | undefined,
66
+ };
67
+
59
68
  void mock.module('expo-camera', () => ({
60
69
  get getCameraPermissionsAsync() {
61
70
  if (cameraMock.mode !== 'topLevel') {
@@ -195,6 +204,18 @@ void mock.module('expo-notifications', () => ({
195
204
  },
196
205
  }));
197
206
 
207
+ void mock.module('expo-linking', () => ({
208
+ openSettings: () => {
209
+ linkingMock.calls += 1;
210
+
211
+ if (linkingMock.error !== undefined) {
212
+ throw linkingMock.error;
213
+ }
214
+
215
+ return Promise.resolve();
216
+ },
217
+ }));
218
+
198
219
  describe('createPermissionClient', () => {
199
220
  beforeEach(() => {
200
221
  resetExpoMocks();
@@ -249,9 +270,10 @@ describe('createPermissionClient', () => {
249
270
 
250
271
  expect(await client.getStatus(Permission.Camera)).toMatchObject({
251
272
  permission: Permission.Camera,
252
- status: 'denied',
273
+ status: 'blocked',
253
274
  granted: false,
254
275
  canAskAgain: false,
276
+ reason: 'Permission is blocked. Open system settings to change it.',
255
277
  });
256
278
  expect(await client.request(Permission.Camera)).toMatchObject({
257
279
  permission: Permission.Camera,
@@ -269,9 +291,10 @@ describe('createPermissionClient', () => {
269
291
 
270
292
  expect(await client.getStatus(Permission.Microphone)).toMatchObject({
271
293
  permission: Permission.Microphone,
272
- status: 'denied',
294
+ status: 'blocked',
273
295
  granted: false,
274
296
  canAskAgain: false,
297
+ reason: 'Permission is blocked. Open system settings to change it.',
275
298
  });
276
299
  expect(await client.request(Permission.Microphone)).toMatchObject({
277
300
  permission: Permission.Microphone,
@@ -295,18 +318,157 @@ describe('createPermissionClient', () => {
295
318
  });
296
319
  });
297
320
 
298
- test('preserves denied request responses', async () => {
321
+ test('normalizes non-requestable denials to blocked for getStatus and request', async () => {
322
+ notificationsMock.getResponse = permissionResponse('denied', false);
299
323
  notificationsMock.requestResponse = permissionResponse('denied', false);
300
324
 
301
325
  const client = createPermissionClient();
302
- const state = await client.request(Permission.Notifications);
303
326
 
304
- expect(state).toMatchObject({
305
- permission: Permission.Notifications,
306
- status: 'denied',
307
- granted: false,
327
+ for (const state of [
328
+ await client.getStatus(Permission.Notifications),
329
+ await client.request(Permission.Notifications),
330
+ ]) {
331
+ expect(state).toMatchObject({
332
+ permission: Permission.Notifications,
333
+ status: 'blocked',
334
+ granted: false,
335
+ canAskAgain: false,
336
+ reason: 'Permission is blocked. Open system settings to change it.',
337
+ });
338
+ }
339
+ });
340
+
341
+ test('normalizes limited media-library access as usable partial authorization', async () => {
342
+ mediaLibraryMock.getResponse = {
343
+ status: 'granted',
308
344
  canAskAgain: false,
309
- });
345
+ accessPrivileges: 'limited',
346
+ };
347
+ mediaLibraryMock.requestResponse = {
348
+ status: 'granted',
349
+ canAskAgain: false,
350
+ accessPrivileges: 'limited',
351
+ };
352
+
353
+ const client = createPermissionClient();
354
+
355
+ for (const state of [
356
+ await client.getStatus(Permission.MediaLibrary),
357
+ await client.request(Permission.MediaLibrary),
358
+ ]) {
359
+ expect(state).toMatchObject({
360
+ permission: Permission.MediaLibrary,
361
+ status: 'limited',
362
+ granted: true,
363
+ canAskAgain: false,
364
+ reason: 'Media library access is limited to user-selected assets.',
365
+ });
366
+ }
367
+ });
368
+
369
+ test('uses granular iOS notification authorization for getStatus and request', async () => {
370
+ const cases = [
371
+ {
372
+ iosStatus: 0,
373
+ canAskAgain: true,
374
+ expectedStatus: 'unknown',
375
+ expectedGranted: false,
376
+ expectedReason: 'iOS notification authorization has not been determined.',
377
+ },
378
+ {
379
+ iosStatus: 1,
380
+ canAskAgain: true,
381
+ expectedStatus: 'denied',
382
+ expectedGranted: false,
383
+ expectedReason: 'iOS notification authorization is denied.',
384
+ },
385
+ {
386
+ iosStatus: 1,
387
+ canAskAgain: false,
388
+ expectedStatus: 'blocked',
389
+ expectedGranted: false,
390
+ expectedReason: 'Permission is blocked. Open system settings to change it.',
391
+ },
392
+ {
393
+ iosStatus: 2,
394
+ canAskAgain: false,
395
+ expectedStatus: 'granted',
396
+ expectedGranted: true,
397
+ expectedReason: undefined,
398
+ },
399
+ {
400
+ iosStatus: 3,
401
+ canAskAgain: false,
402
+ expectedStatus: 'limited',
403
+ expectedGranted: true,
404
+ expectedReason: 'iOS notification authorization is provisional.',
405
+ },
406
+ {
407
+ iosStatus: 4,
408
+ canAskAgain: false,
409
+ expectedStatus: 'limited',
410
+ expectedGranted: true,
411
+ expectedReason: 'iOS notification authorization is ephemeral.',
412
+ },
413
+ ] as const;
414
+
415
+ const client = createPermissionClient();
416
+
417
+ for (const testCase of cases) {
418
+ const response: MockPermissionResponse = {
419
+ status: testCase.iosStatus === 2 ? 'denied' : 'granted',
420
+ canAskAgain: testCase.canAskAgain,
421
+ ios: { status: testCase.iosStatus },
422
+ };
423
+ notificationsMock.getResponse = response;
424
+ notificationsMock.requestResponse = response;
425
+
426
+ for (const state of [
427
+ await client.getStatus(Permission.Notifications),
428
+ await client.request(Permission.Notifications),
429
+ ]) {
430
+ expect(state).toMatchObject({
431
+ permission: Permission.Notifications,
432
+ status: testCase.expectedStatus,
433
+ granted: testCase.expectedGranted,
434
+ canAskAgain: testCase.canAskAgain,
435
+ reason: testCase.expectedReason,
436
+ });
437
+ }
438
+ }
439
+ });
440
+
441
+ test('opens native application settings through Expo Linking', async () => {
442
+ const client = createPermissionClient();
443
+
444
+ if (client.openSettings === undefined) {
445
+ throw new Error('Expo permission client must expose openSettings.');
446
+ }
447
+
448
+ await client.openSettings();
449
+
450
+ expect(linkingMock.calls).toBe(1);
451
+ });
452
+
453
+ test('rejects with a stable error and preserves the native failure cause', async () => {
454
+ const cause = new Error('native settings unavailable');
455
+ linkingMock.error = cause;
456
+ const client = createPermissionClient();
457
+ const error = await captureOpenSettingsErrorAsync(client);
458
+
459
+ expect(error.message).toBe('Unable to open application settings.');
460
+ expect(error.cause).toBe(cause);
461
+ });
462
+
463
+ test('uses the same stable error for Expo Linking on Web', async () => {
464
+ const cause = new Error('openSettings is unavailable on web');
465
+ cause.name = 'UnavailabilityError';
466
+ linkingMock.error = cause;
467
+ const client = createPermissionClient();
468
+ const error = await captureOpenSettingsErrorAsync(client);
469
+
470
+ expect(error.message).toBe('Unable to open application settings.');
471
+ expect(error.cause).toBe(cause);
310
472
  });
311
473
 
312
474
  test('returns unavailable when a camera permission call fails at runtime', async () => {
@@ -338,6 +500,26 @@ describe('createPermissionClient', () => {
338
500
  });
339
501
  });
340
502
 
503
+ async function captureOpenSettingsErrorAsync(
504
+ client: ReturnType<typeof createPermissionClient>,
505
+ ): Promise<Error> {
506
+ if (client.openSettings === undefined) {
507
+ throw new Error('Expo permission client must expose openSettings.');
508
+ }
509
+
510
+ try {
511
+ await client.openSettings();
512
+ } catch (error) {
513
+ if (error instanceof Error) {
514
+ return error;
515
+ }
516
+
517
+ throw new Error('Opening settings must reject with an Error instance.', { cause: error });
518
+ }
519
+
520
+ throw new Error('Opening settings must reject.');
521
+ }
522
+
341
523
  function resetExpoMocks(): void {
342
524
  cameraMock.mode = 'topLevel';
343
525
  cameraMock.getResponse = permissionResponse('granted');
@@ -369,4 +551,7 @@ function resetExpoMocks(): void {
369
551
  notificationsMock.requestResponse = permissionResponse('granted');
370
552
  notificationsMock.getError = undefined;
371
553
  notificationsMock.requestError = undefined;
554
+
555
+ linkingMock.calls = 0;
556
+ linkingMock.error = undefined;
372
557
  }
@@ -8,6 +8,8 @@ import { microphoneAdapter } from './adapters/microphone';
8
8
  import { notificationsAdapter } from './adapters/notifications';
9
9
  import { EXPO_PERMISSION_SUPPORT } from './manifest';
10
10
 
11
+ const OPEN_SETTINGS_ERROR_MESSAGE = 'Unable to open application settings.';
12
+
11
13
  export interface ExpoPermissionAdapter {
12
14
  getStatus(permission: Permission): Promise<PermissionState>;
13
15
  request(permission: Permission): Promise<PermissionState>;
@@ -23,6 +25,20 @@ const adapters: Partial<Record<Permission, ExpoPermissionAdapter>> = {
23
25
  [Permission.Notifications]: notificationsAdapter,
24
26
  };
25
27
 
28
+ /***
29
+ * Creates a permission client backed by Expo SDK modules.
30
+ *
31
+ * Denials that Expo cannot request again are normalized to `blocked`. Limited
32
+ * media-library access and provisional or ephemeral iOS notification access
33
+ * are normalized to usable `limited` states, where `granted` remains true.
34
+ * The client requires `expo-linking` for settings recovery and rejects with a
35
+ * stable error when the operating-system settings page cannot be opened.
36
+ *
37
+ * Expo modules are loaded only when a client operation uses them. The package
38
+ * root and Expo manifest metadata remain free of native module imports.
39
+ *
40
+ * @readme
41
+ */
26
42
  export function createPermissionClient(): PermissionClient {
27
43
  return {
28
44
  async getStatus(permission: Permission): Promise<PermissionState> {
@@ -60,6 +76,18 @@ export function createPermissionClient(): PermissionClient {
60
76
 
61
77
  return adapter.request(permission);
62
78
  },
79
+
80
+ async openSettings(): Promise<void> {
81
+ try {
82
+ const linking = await import('expo-linking');
83
+
84
+ await linking.openSettings();
85
+ } catch (error) {
86
+ throw new Error(OPEN_SETTINGS_ERROR_MESSAGE, {
87
+ cause: error,
88
+ });
89
+ }
90
+ },
63
91
  };
64
92
  }
65
93
 
@@ -3,7 +3,7 @@ import { readFileSync } from 'node:fs';
3
3
  import { describe, expect, test } from 'bun:test';
4
4
 
5
5
  import { Permission, PERMISSIONS } from '../registry/permissions';
6
- import { EXPO_PERMISSION_SUPPORT } from './manifest';
6
+ import { EXPO_PERMISSION_CLIENT_REQUIRED_PACKAGES, EXPO_PERMISSION_SUPPORT } from './manifest';
7
7
 
8
8
  describe('EXPO_PERMISSION_SUPPORT', () => {
9
9
  test('defines metadata for every known permission', () => {
@@ -17,6 +17,10 @@ describe('EXPO_PERMISSION_SUPPORT', () => {
17
17
  expect(source).not.toContain('await import(');
18
18
  });
19
19
 
20
+ test('projects client-level settings recovery dependencies', () => {
21
+ expect(EXPO_PERMISSION_CLIENT_REQUIRED_PACKAGES).toEqual(['expo-linking']);
22
+ });
23
+
20
24
  test('includes package and config hints for camera, microphone, and location permissions', () => {
21
25
  expect(EXPO_PERMISSION_SUPPORT[Permission.Camera]).toEqual({
22
26
  support: 'supported',
@@ -13,6 +13,12 @@ export interface ExpoPermissionMetadata {
13
13
  readonly configHints: readonly string[];
14
14
  }
15
15
 
16
+ /***
17
+ * Expo packages required by the permission client independently of a specific
18
+ * permission capability.
19
+ */
20
+ export const EXPO_PERMISSION_CLIENT_REQUIRED_PACKAGES = ['expo-linking'] as const;
21
+
16
22
  export const EXPO_PERMISSION_SUPPORT: Readonly<Record<Permission, ExpoPermissionMetadata>> = {
17
23
  [Permission.Camera]: {
18
24
  support: 'supported',
@@ -3,6 +3,20 @@ interface ExpoPermissionResult {
3
3
  canAskAgain?: boolean;
4
4
  }
5
5
 
6
+ interface ExpoMediaLibraryPermissionResult extends ExpoPermissionResult {
7
+ accessPrivileges?: 'all' | 'limited' | 'none';
8
+ }
9
+
10
+ interface ExpoNotificationPermissionResult extends ExpoPermissionResult {
11
+ ios?: {
12
+ status?: number;
13
+ };
14
+ }
15
+
16
+ declare module 'expo-linking' {
17
+ export function openSettings(): Promise<void>;
18
+ }
19
+
6
20
  declare module 'expo-camera' {
7
21
  export function getCameraPermissionsAsync(): Promise<ExpoPermissionResult>;
8
22
  export function requestCameraPermissionsAsync(): Promise<ExpoPermissionResult>;
@@ -27,11 +41,11 @@ declare module 'expo-media-library' {
27
41
  export function getPermissionsAsync(
28
42
  writeOnly?: boolean,
29
43
  granularPermissions?: readonly string[],
30
- ): Promise<ExpoPermissionResult>;
44
+ ): Promise<ExpoMediaLibraryPermissionResult>;
31
45
  export function requestPermissionsAsync(
32
46
  writeOnly?: boolean,
33
47
  granularPermissions?: readonly string[],
34
- ): Promise<ExpoPermissionResult>;
48
+ ): Promise<ExpoMediaLibraryPermissionResult>;
35
49
  }
36
50
 
37
51
  declare module 'expo-audio' {
@@ -48,6 +62,6 @@ declare module 'expo-audio' {
48
62
  }
49
63
 
50
64
  declare module 'expo-notifications' {
51
- export function getPermissionsAsync(): Promise<ExpoPermissionResult>;
52
- export function requestPermissionsAsync(): Promise<ExpoPermissionResult>;
65
+ export function getPermissionsAsync(): Promise<ExpoNotificationPermissionResult>;
66
+ export function requestPermissionsAsync(): Promise<ExpoNotificationPermissionResult>;
53
67
  }
@@ -3,6 +3,7 @@ import {
3
3
  createPermissionState,
4
4
  isPermissionStatus,
5
5
  type PermissionState,
6
+ type PermissionStatus,
6
7
  } from '../state/permissionState';
7
8
 
8
9
  export interface ExpoPermissionResponse {
@@ -18,11 +19,7 @@ export function createExpoPermissionState(
18
19
  ): PermissionState {
19
20
  const status = isPermissionStatus(response.status) ? response.status : 'unknown';
20
21
 
21
- return createPermissionState({
22
- permission,
23
- status,
24
- canAskAgain: response.canAskAgain,
25
- });
22
+ return createNormalizedExpoPermissionState(permission, response, status);
26
23
  }
27
24
 
28
25
  export function createExpoUnavailableState(
@@ -72,6 +69,26 @@ function getExpoPermissionErrorMessage(error: unknown): string {
72
69
  return 'Expo permission operation failed.';
73
70
  }
74
71
 
72
+ function createNormalizedExpoPermissionState(
73
+ permission: Permission,
74
+ response: ExpoPermissionResponse,
75
+ status: PermissionStatus,
76
+ reason?: string,
77
+ ): PermissionState {
78
+ const normalizedStatus =
79
+ status === 'denied' && response.canAskAgain === false ? 'blocked' : status;
80
+
81
+ return createPermissionState({
82
+ permission,
83
+ status: normalizedStatus,
84
+ canAskAgain: response.canAskAgain,
85
+ reason:
86
+ normalizedStatus === 'blocked'
87
+ ? 'Permission is blocked. Open system settings to change it.'
88
+ : reason,
89
+ });
90
+ }
91
+
75
92
  function hasExpoPermissionMethod(
76
93
  value: unknown,
77
94
  methodName: string,
@@ -25,6 +25,13 @@ describe('permission state', () => {
25
25
  }).granted,
26
26
  ).toBe(true);
27
27
 
28
+ expect(
29
+ createPermissionState({
30
+ permission: Permission.MediaLibrary,
31
+ status: 'limited',
32
+ }).granted,
33
+ ).toBe(true);
34
+
28
35
  expect(
29
36
  createPermissionState({
30
37
  permission: Permission.Camera,
@@ -45,7 +52,7 @@ describe('permission state', () => {
45
52
  expect(state).toEqual({
46
53
  permission: Permission.Camera,
47
54
  status: 'limited',
48
- granted: false,
55
+ granted: true,
49
56
  canAskAgain: true,
50
57
  requestedAt: undefined,
51
58
  reason: 'library-limited',
@@ -57,13 +57,15 @@ export function isPermissionStatus(value: string): value is PermissionStatus {
57
57
 
58
58
  /***
59
59
  * Creates a normalized state and derives the `granted` convenience flag.
60
+ * Limited access is usable partial authorization, so both `granted` and
61
+ * `limited` states set `granted` to true.
60
62
  */
61
63
  export function createPermissionState(input: CreatePermissionStateInput): PermissionState {
62
64
  return {
63
65
  permission: input.permission,
64
66
  status: input.status,
65
67
  canAskAgain: input.canAskAgain,
66
- granted: input.status === 'granted',
68
+ granted: input.status === 'granted' || input.status === 'limited',
67
69
  requestedAt: input.requestedAt,
68
70
  reason: input.reason,
69
71
  };