@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.
- package/CHANGELOG.md +7 -0
- package/README.md +25 -1
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/expo/adapters/mediaLibrary.d.ts.map +1 -1
- package/dist/expo/adapters/mediaLibrary.js +15 -3
- package/dist/expo/adapters/mediaLibrary.js.map +1 -1
- package/dist/expo/adapters/notifications.d.ts.map +1 -1
- package/dist/expo/adapters/notifications.js +32 -3
- package/dist/expo/adapters/notifications.js.map +1 -1
- package/dist/expo/client.d.ts +14 -0
- package/dist/expo/client.d.ts.map +1 -1
- package/dist/expo/client.js +26 -0
- package/dist/expo/client.js.map +1 -1
- package/dist/expo/manifest.d.ts +5 -0
- package/dist/expo/manifest.d.ts.map +1 -1
- package/dist/expo/manifest.js +5 -0
- package/dist/expo/manifest.js.map +1 -1
- package/dist/expo/permissionResponse.d.ts.map +1 -1
- package/dist/expo/permissionResponse.js +12 -5
- package/dist/expo/permissionResponse.js.map +1 -1
- package/dist/state/permissionState.d.ts +2 -0
- package/dist/state/permissionState.d.ts.map +1 -1
- package/dist/state/permissionState.js +3 -1
- package/dist/state/permissionState.js.map +1 -1
- package/package.json +13 -3
- package/src/cli/index.ts +1 -0
- package/src/expo/adapters/mediaLibrary.ts +28 -4
- package/src/expo/adapters/notifications.ts +73 -4
- package/src/expo/client.test.ts +194 -9
- package/src/expo/client.ts +28 -0
- package/src/expo/manifest.test.ts +5 -1
- package/src/expo/manifest.ts +6 -0
- package/src/expo/modules.d.ts +18 -4
- package/src/expo/permissionResponse.ts +22 -5
- package/src/state/permissionState.test.ts +8 -1
- 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
|
-
         
|
|
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 @@
|
|
|
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 @@
|
|
|
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;
|
|
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 {
|
|
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
|
|
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
|
|
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;
|
|
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;
|
|
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 {
|
|
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
|
|
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
|
|
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":"
|
|
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"]}
|
package/dist/expo/client.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/expo/client.js
CHANGED
|
@@ -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) {
|
package/dist/expo/client.js.map
CHANGED
|
@@ -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;
|
|
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"]}
|
package/dist/expo/manifest.d.ts
CHANGED
|
@@ -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"}
|
package/dist/expo/manifest.js
CHANGED
|
@@ -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,
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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.
|
|
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":
|
|
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",
|
package/src/cli/index.ts
ADDED
|
@@ -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
|
|
3
|
+
import { createPermissionState, type PermissionState } from '../../state/permissionState';
|
|
4
4
|
import type { ExpoPermissionAdapter } from '../client';
|
|
5
|
-
import {
|
|
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
|
|
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
|
|
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
|
|
2
|
+
import { createPermissionState, type PermissionState } from '../../state/permissionState';
|
|
3
3
|
import type { ExpoPermissionAdapter } from '../client';
|
|
4
|
-
import {
|
|
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
|
|
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
|
|
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
|
+
}
|
package/src/expo/client.test.ts
CHANGED
|
@@ -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: '
|
|
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: '
|
|
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('
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
}
|
package/src/expo/client.ts
CHANGED
|
@@ -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',
|
package/src/expo/manifest.ts
CHANGED
|
@@ -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',
|
package/src/expo/modules.d.ts
CHANGED
|
@@ -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<
|
|
44
|
+
): Promise<ExpoMediaLibraryPermissionResult>;
|
|
31
45
|
export function requestPermissionsAsync(
|
|
32
46
|
writeOnly?: boolean,
|
|
33
47
|
granularPermissions?: readonly string[],
|
|
34
|
-
): Promise<
|
|
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<
|
|
52
|
-
export function requestPermissionsAsync(): Promise<
|
|
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
|
|
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:
|
|
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
|
};
|