@ankhorage/permissions 0.2.2 → 0.2.4
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 +13 -0
- package/README.md +27 -3
- 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 +42 -13
- package/dist/expo/client.js.map +1 -1
- package/dist/expo/client.testSupport.d.ts +54 -0
- package/dist/expo/client.testSupport.d.ts.map +1 -0
- package/dist/expo/client.testSupport.js +183 -0
- package/dist/expo/client.testSupport.js.map +1 -0
- 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 +16 -9
- package/dist/expo/permissionResponse.js.map +1 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +16 -11
- package/dist/react/index.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/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +27 -17
- package/dist/testing/index.js.map +1 -1
- package/dist/web/index.d.ts +1 -2
- package/dist/web/index.d.ts.map +1 -1
- package/dist/web/index.js +2 -76
- package/dist/web/index.js.map +1 -1
- package/dist/web/permissionStates.d.ts +10 -0
- package/dist/web/permissionStates.d.ts.map +1 -0
- package/dist/web/permissionStates.js +64 -0
- package/dist/web/permissionStates.js.map +1 -0
- package/package.json +22 -13
- 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.notifications.test.ts +106 -0
- package/src/expo/client.test.ts +109 -349
- package/src/expo/client.testSupport.ts +188 -0
- package/src/expo/client.ts +45 -13
- package/src/expo/manifest.test.ts +49 -47
- package/src/expo/manifest.ts +7 -5
- package/src/expo/modules.d.ts +18 -4
- package/src/expo/permissionResponse.ts +26 -9
- package/src/manager/createPermissionManager.test.ts +48 -50
- package/src/react/index.tsx +21 -13
- package/src/registry/permissions.test.ts +4 -3
- package/src/state/permissionState.test.ts +8 -1
- package/src/state/permissionState.ts +4 -7
- package/src/testing/index.ts +36 -25
- package/src/web/index.test.ts +149 -151
- package/src/web/index.ts +9 -102
- package/src/web/permissionStates.ts +83 -0
package/src/expo/client.ts
CHANGED
|
@@ -8,25 +8,41 @@ 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>;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
const adapters
|
|
17
|
-
[Permission.Camera
|
|
18
|
-
[Permission.MediaLibrary
|
|
19
|
-
[Permission.MediaLibraryWrite
|
|
20
|
-
[Permission.Microphone
|
|
21
|
-
[Permission.LocationForeground
|
|
22
|
-
[Permission.LocationBackground
|
|
23
|
-
[Permission.Notifications
|
|
24
|
-
|
|
18
|
+
const adapters = new Map<Permission, ExpoPermissionAdapter>([
|
|
19
|
+
[Permission.Camera, cameraAdapter],
|
|
20
|
+
[Permission.MediaLibrary, mediaLibraryAdapter],
|
|
21
|
+
[Permission.MediaLibraryWrite, mediaLibraryAdapter],
|
|
22
|
+
[Permission.Microphone, microphoneAdapter],
|
|
23
|
+
[Permission.LocationForeground, locationAdapter],
|
|
24
|
+
[Permission.LocationBackground, locationAdapter],
|
|
25
|
+
[Permission.Notifications, notificationsAdapter],
|
|
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> {
|
|
29
|
-
const metadata =
|
|
45
|
+
const metadata = getPermissionSupport(permission);
|
|
30
46
|
if (metadata.support === 'unsupported' || metadata.support === 'notImplemented') {
|
|
31
47
|
return createUnavailableSupportState(permission);
|
|
32
48
|
}
|
|
@@ -35,7 +51,7 @@ export function createPermissionClient(): PermissionClient {
|
|
|
35
51
|
return createNotRequiredState(permission);
|
|
36
52
|
}
|
|
37
53
|
|
|
38
|
-
const adapter = adapters
|
|
54
|
+
const adapter = adapters.get(permission);
|
|
39
55
|
if (!adapter) {
|
|
40
56
|
throw new Error(`No adapter registered for permission: ${permission}`);
|
|
41
57
|
}
|
|
@@ -44,7 +60,7 @@ export function createPermissionClient(): PermissionClient {
|
|
|
44
60
|
},
|
|
45
61
|
|
|
46
62
|
async request(permission: Permission): Promise<PermissionState> {
|
|
47
|
-
const metadata =
|
|
63
|
+
const metadata = getPermissionSupport(permission);
|
|
48
64
|
if (metadata.support === 'unsupported' || metadata.support === 'notImplemented') {
|
|
49
65
|
return createUnavailableSupportState(permission);
|
|
50
66
|
}
|
|
@@ -53,16 +69,32 @@ export function createPermissionClient(): PermissionClient {
|
|
|
53
69
|
return createNotRequiredState(permission);
|
|
54
70
|
}
|
|
55
71
|
|
|
56
|
-
const adapter = adapters
|
|
72
|
+
const adapter = adapters.get(permission);
|
|
57
73
|
if (!adapter) {
|
|
58
74
|
throw new Error(`No adapter registered for permission: ${permission}`);
|
|
59
75
|
}
|
|
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
|
|
|
94
|
+
function getPermissionSupport(permission: Permission) {
|
|
95
|
+
return Reflect.get(EXPO_PERMISSION_SUPPORT, permission);
|
|
96
|
+
}
|
|
97
|
+
|
|
66
98
|
function createUnavailableSupportState(permission: Permission): PermissionState {
|
|
67
99
|
return createPermissionState({
|
|
68
100
|
permission,
|
|
@@ -1,65 +1,67 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { 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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
});
|
|
8
|
+
test('defines metadata for every known permission', () => {
|
|
9
|
+
expect(Object.keys(EXPO_PERMISSION_SUPPORT).sort()).toEqual([...PERMISSIONS].sort());
|
|
10
|
+
});
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
test('stays metadata-only and does not import Expo runtime modules', () => {
|
|
13
|
+
const source = readFileSync(new URL('./manifest.ts', import.meta.url), 'utf8');
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
expect(source).not.toContain("from 'expo-");
|
|
16
|
+
expect(source).not.toContain('await import(');
|
|
17
|
+
});
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
requiredPackages: ['expo-camera'],
|
|
24
|
-
configHints: ['cameraPermission'],
|
|
25
|
-
});
|
|
19
|
+
test('projects client-level settings recovery dependencies', () => {
|
|
20
|
+
expect(EXPO_PERMISSION_CLIENT_REQUIRED_PACKAGES).toEqual(['expo-linking']);
|
|
21
|
+
});
|
|
26
22
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
test('includes package and config hints for camera, microphone, and location permissions', () => {
|
|
24
|
+
expect(EXPO_PERMISSION_SUPPORT[Permission.Camera]).toEqual({
|
|
25
|
+
support: 'supported',
|
|
26
|
+
requiredPackages: ['expo-camera'],
|
|
27
|
+
configHints: ['cameraPermission'],
|
|
28
|
+
});
|
|
32
29
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
expect(EXPO_PERMISSION_SUPPORT[Permission.Microphone]).toEqual({
|
|
31
|
+
support: 'supported',
|
|
32
|
+
requiredPackages: ['expo-audio'],
|
|
33
|
+
configHints: ['microphonePermission', 'recordAudioAndroid'],
|
|
34
|
+
});
|
|
38
35
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
});
|
|
36
|
+
expect(EXPO_PERMISSION_SUPPORT[Permission.LocationForeground]).toEqual({
|
|
37
|
+
support: 'supported',
|
|
38
|
+
requiredPackages: ['expo-location'],
|
|
39
|
+
configHints: ['locationWhenInUsePermission'],
|
|
44
40
|
});
|
|
45
41
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
configHints: [],
|
|
51
|
-
});
|
|
42
|
+
expect(EXPO_PERMISSION_SUPPORT[Permission.LocationBackground]).toEqual({
|
|
43
|
+
support: 'supported',
|
|
44
|
+
requiredPackages: ['expo-location'],
|
|
45
|
+
configHints: ['locationAlwaysAndWhenInUsePermission'],
|
|
52
46
|
});
|
|
47
|
+
});
|
|
53
48
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
49
|
+
test('treats clipboard as a not-required permission without runtime package requirements', () => {
|
|
50
|
+
expect(EXPO_PERMISSION_SUPPORT[Permission.Clipboard]).toEqual({
|
|
51
|
+
support: 'notRequired',
|
|
52
|
+
requiredPackages: [],
|
|
53
|
+
configHints: [],
|
|
54
|
+
});
|
|
55
|
+
});
|
|
60
56
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
test('keeps MediaLibraryWrite aligned with MediaLibrary metadata', () => {
|
|
58
|
+
expect(EXPO_PERMISSION_SUPPORT[Permission.MediaLibraryWrite]).toEqual({
|
|
59
|
+
support: 'supported',
|
|
60
|
+
requiredPackages: ['expo-media-library'],
|
|
61
|
+
configHints: ['mediaLibraryPermission'],
|
|
64
62
|
});
|
|
63
|
+
|
|
64
|
+
expect(EXPO_PERMISSION_SUPPORT[Permission.MediaLibraryWrite]).toEqual(
|
|
65
|
+
EXPO_PERMISSION_SUPPORT[Permission.MediaLibrary],
|
|
66
|
+
);
|
|
65
67
|
});
|
package/src/expo/manifest.ts
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import { Permission } from '../registry/permissions';
|
|
2
2
|
|
|
3
3
|
export type PermissionSupport =
|
|
4
|
-
| '
|
|
5
|
-
| 'notRequired'
|
|
6
|
-
| 'limited'
|
|
7
|
-
| 'notImplemented'
|
|
8
|
-
| 'unsupported';
|
|
4
|
+
'supported' | 'notRequired' | 'limited' | 'notImplemented' | 'unsupported';
|
|
9
5
|
|
|
10
6
|
export interface ExpoPermissionMetadata {
|
|
11
7
|
readonly support: PermissionSupport;
|
|
@@ -13,6 +9,12 @@ export interface ExpoPermissionMetadata {
|
|
|
13
9
|
readonly configHints: readonly string[];
|
|
14
10
|
}
|
|
15
11
|
|
|
12
|
+
/***
|
|
13
|
+
* Expo packages required by the permission client independently of a specific
|
|
14
|
+
* permission capability.
|
|
15
|
+
*/
|
|
16
|
+
export const EXPO_PERMISSION_CLIENT_REQUIRED_PACKAGES = ['expo-linking'] as const;
|
|
17
|
+
|
|
16
18
|
export const EXPO_PERMISSION_SUPPORT: Readonly<Record<Permission, ExpoPermissionMetadata>> = {
|
|
17
19
|
[Permission.Camera]: {
|
|
18
20
|
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(
|
|
@@ -42,7 +39,7 @@ export function findExpoPermissionMethod(
|
|
|
42
39
|
containerNames: readonly string[] = [],
|
|
43
40
|
): ExpoPermissionMethod | undefined {
|
|
44
41
|
if (hasExpoPermissionMethod(source, methodName)) {
|
|
45
|
-
return source
|
|
42
|
+
return Reflect.get(source, methodName);
|
|
46
43
|
}
|
|
47
44
|
|
|
48
45
|
if (!isRecord(source)) {
|
|
@@ -50,10 +47,10 @@ export function findExpoPermissionMethod(
|
|
|
50
47
|
}
|
|
51
48
|
|
|
52
49
|
for (const containerName of containerNames) {
|
|
53
|
-
const container = source
|
|
50
|
+
const container = Reflect.get(source, containerName);
|
|
54
51
|
|
|
55
52
|
if (hasExpoPermissionMethod(container, methodName)) {
|
|
56
|
-
return container
|
|
53
|
+
return Reflect.get(container, methodName);
|
|
57
54
|
}
|
|
58
55
|
}
|
|
59
56
|
|
|
@@ -72,11 +69,31 @@ 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,
|
|
78
95
|
): value is Record<string, ExpoPermissionMethod> {
|
|
79
|
-
return isRecord(value) && typeof value
|
|
96
|
+
return isRecord(value) && typeof Reflect.get(value, methodName) === 'function';
|
|
80
97
|
}
|
|
81
98
|
|
|
82
99
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -1,67 +1,65 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { expect, test } from 'bun:test';
|
|
2
2
|
|
|
3
3
|
import { Permission } from '../registry/permissions';
|
|
4
4
|
import { createFakePermissionClient } from '../testing/index';
|
|
5
5
|
import { createPermissionManager } from './createPermissionManager';
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
);
|
|
7
|
+
test('normalizes getStatus results', async () => {
|
|
8
|
+
const manager = createPermissionManager(
|
|
9
|
+
createFakePermissionClient({
|
|
10
|
+
initialStates: [
|
|
11
|
+
{
|
|
12
|
+
permission: Permission.Camera,
|
|
13
|
+
status: 'granted',
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
}),
|
|
17
|
+
);
|
|
19
18
|
|
|
20
|
-
|
|
19
|
+
const state = await manager.getStatus(Permission.Camera);
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
});
|
|
21
|
+
expect(state).toMatchObject({
|
|
22
|
+
permission: Permission.Camera,
|
|
23
|
+
status: 'granted',
|
|
24
|
+
granted: true,
|
|
27
25
|
});
|
|
26
|
+
});
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
28
|
+
test('does not throw for normal denial', async () => {
|
|
29
|
+
const manager = createPermissionManager(
|
|
30
|
+
createFakePermissionClient({
|
|
31
|
+
requestStates: [
|
|
32
|
+
{
|
|
33
|
+
permission: Permission.Notifications,
|
|
34
|
+
status: 'denied',
|
|
35
|
+
canAskAgain: false,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
41
40
|
|
|
42
|
-
|
|
41
|
+
const state = await manager.request(Permission.Notifications);
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
});
|
|
43
|
+
expect(state).toMatchObject({
|
|
44
|
+
status: 'denied',
|
|
45
|
+
granted: false,
|
|
46
|
+
canAskAgain: false,
|
|
49
47
|
});
|
|
48
|
+
});
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
test('exposes openSettings only when the client supports it', async () => {
|
|
51
|
+
let opened = false;
|
|
52
|
+
const manager = createPermissionManager(
|
|
53
|
+
createFakePermissionClient({
|
|
54
|
+
openSettings: () => {
|
|
55
|
+
opened = true;
|
|
57
56
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
return Promise.resolve();
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
62
61
|
|
|
63
|
-
|
|
62
|
+
await manager.openSettings?.();
|
|
64
63
|
|
|
65
|
-
|
|
66
|
-
});
|
|
64
|
+
expect(opened).toBe(true);
|
|
67
65
|
});
|
package/src/react/index.tsx
CHANGED
|
@@ -105,15 +105,8 @@ export function usePermission(
|
|
|
105
105
|
options: UsePermissionOptions = {},
|
|
106
106
|
): PermissionHookResult {
|
|
107
107
|
const manager = usePermissions();
|
|
108
|
-
const [state, setState] = useState<PermissionState>(
|
|
109
|
-
()
|
|
110
|
-
(options.initialState === undefined
|
|
111
|
-
? undefined
|
|
112
|
-
: normalizePermissionState(permission, options.initialState)) ??
|
|
113
|
-
createPermissionState({
|
|
114
|
-
permission,
|
|
115
|
-
status: 'unknown',
|
|
116
|
-
}),
|
|
108
|
+
const [state, setState] = useState<PermissionState>(() =>
|
|
109
|
+
createInitialPermissionState(permission, options.initialState),
|
|
117
110
|
);
|
|
118
111
|
|
|
119
112
|
const refresh = useCallback(async () => {
|
|
@@ -131,10 +124,15 @@ export function usePermission(
|
|
|
131
124
|
}, [manager, permission]);
|
|
132
125
|
|
|
133
126
|
useEffect(() => {
|
|
134
|
-
if (options.refreshOnMount
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
127
|
+
if (options.refreshOnMount !== true) return;
|
|
128
|
+
let active = true;
|
|
129
|
+
void manager.getStatus(permission).then((nextState) => {
|
|
130
|
+
if (active) setState(nextState);
|
|
131
|
+
});
|
|
132
|
+
return () => {
|
|
133
|
+
active = false;
|
|
134
|
+
};
|
|
135
|
+
}, [manager, options.refreshOnMount, permission]);
|
|
138
136
|
|
|
139
137
|
const openSettings =
|
|
140
138
|
manager.openSettings === undefined
|
|
@@ -154,3 +152,13 @@ export function usePermission(
|
|
|
154
152
|
openSettings,
|
|
155
153
|
};
|
|
156
154
|
}
|
|
155
|
+
|
|
156
|
+
function createInitialPermissionState(
|
|
157
|
+
permission: Permission,
|
|
158
|
+
initialState: PermissionState | undefined,
|
|
159
|
+
): PermissionState {
|
|
160
|
+
return (
|
|
161
|
+
(initialState === undefined ? undefined : normalizePermissionState(permission, initialState)) ??
|
|
162
|
+
createPermissionState({ permission, status: 'unknown' })
|
|
163
|
+
);
|
|
164
|
+
}
|
|
@@ -24,9 +24,10 @@ describe('permission registry', () => {
|
|
|
24
24
|
|
|
25
25
|
test('exposes metadata for every permission', () => {
|
|
26
26
|
for (const permission of PERMISSIONS) {
|
|
27
|
-
|
|
28
|
-
expect(
|
|
29
|
-
expect(
|
|
27
|
+
const definition = Reflect.get(PERMISSION_REGISTRY, permission);
|
|
28
|
+
expect(definition.permission).toBe(permission);
|
|
29
|
+
expect(definition.label.length).toBeGreaterThan(0);
|
|
30
|
+
expect(getPermissionDefinition(permission)).toEqual(definition);
|
|
30
31
|
}
|
|
31
32
|
});
|
|
32
33
|
|
|
@@ -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',
|
|
@@ -7,12 +7,7 @@ import type { Permission } from '../registry/permissions';
|
|
|
7
7
|
* but user denial should be represented as a permission state.
|
|
8
8
|
*/
|
|
9
9
|
export type PermissionStatus =
|
|
10
|
-
| '
|
|
11
|
-
| 'granted'
|
|
12
|
-
| 'denied'
|
|
13
|
-
| 'blocked'
|
|
14
|
-
| 'limited'
|
|
15
|
-
| 'unavailable';
|
|
10
|
+
'unknown' | 'granted' | 'denied' | 'blocked' | 'limited' | 'unavailable';
|
|
16
11
|
|
|
17
12
|
/***
|
|
18
13
|
* Stable ordered list of normalized statuses.
|
|
@@ -57,13 +52,15 @@ export function isPermissionStatus(value: string): value is PermissionStatus {
|
|
|
57
52
|
|
|
58
53
|
/***
|
|
59
54
|
* Creates a normalized state and derives the `granted` convenience flag.
|
|
55
|
+
* Limited access is usable partial authorization, so both `granted` and
|
|
56
|
+
* `limited` states set `granted` to true.
|
|
60
57
|
*/
|
|
61
58
|
export function createPermissionState(input: CreatePermissionStateInput): PermissionState {
|
|
62
59
|
return {
|
|
63
60
|
permission: input.permission,
|
|
64
61
|
status: input.status,
|
|
65
62
|
canAskAgain: input.canAskAgain,
|
|
66
|
-
granted: input.status === 'granted',
|
|
63
|
+
granted: input.status === 'granted' || input.status === 'limited',
|
|
67
64
|
requestedAt: input.requestedAt,
|
|
68
65
|
reason: input.reason,
|
|
69
66
|
};
|