@ankhorage/permissions 0.2.3 → 0.2.5
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 +12 -0
- package/README.md +3 -3
- package/dist/expo/client.js +16 -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.map +1 -1
- package/dist/expo/manifest.js.map +1 -1
- package/dist/expo/permissionResponse.js +4 -4
- 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.map +1 -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 +29 -22
- package/src/expo/client.notifications.test.ts +106 -0
- package/src/expo/client.test.ts +110 -535
- package/src/expo/client.testSupport.ts +188 -0
- package/src/expo/client.ts +17 -13
- package/src/expo/manifest.test.ts +48 -50
- package/src/expo/manifest.ts +1 -5
- package/src/expo/permissionResponse.ts +4 -4
- package/src/manager/createPermissionManager.test.ts +48 -50
- package/src/react/index.tsx +21 -13
- package/src/registry/permissions.test.ts +15 -3
- package/src/state/permissionState.ts +1 -6
- 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/testing/index.ts
CHANGED
|
@@ -45,6 +45,12 @@ export interface FakePermissionClient extends PermissionClient {
|
|
|
45
45
|
getSnapshot(): readonly PermissionState[];
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
interface FakePermissionContext {
|
|
49
|
+
readonly now: () => Date;
|
|
50
|
+
readonly requestStates: Map<Permission, PermissionState>;
|
|
51
|
+
readonly states: Map<Permission, PermissionState>;
|
|
52
|
+
}
|
|
53
|
+
|
|
48
54
|
/***
|
|
49
55
|
* Creates a deterministic in-memory client for tests and examples.
|
|
50
56
|
*
|
|
@@ -55,12 +61,21 @@ export interface FakePermissionClient extends PermissionClient {
|
|
|
55
61
|
export function createFakePermissionClient(
|
|
56
62
|
options: FakePermissionClientOptions = {},
|
|
57
63
|
): FakePermissionClient {
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
|
|
64
|
+
const context = createFakePermissionContext(options);
|
|
65
|
+
const client = createFakePermissionOperations(context, options.openSettings);
|
|
66
|
+
if (options.openSettings === undefined) delete client.openSettings;
|
|
67
|
+
return client;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function createFakePermissionContext(options: FakePermissionClientOptions): FakePermissionContext {
|
|
71
|
+
const context: FakePermissionContext = {
|
|
72
|
+
now: options.now ?? (() => new Date()),
|
|
73
|
+
states: new Map<Permission, PermissionState>(),
|
|
74
|
+
requestStates: new Map<Permission, PermissionState>(),
|
|
75
|
+
};
|
|
61
76
|
|
|
62
77
|
for (const permission of PERMISSIONS) {
|
|
63
|
-
states.set(
|
|
78
|
+
context.states.set(
|
|
64
79
|
permission,
|
|
65
80
|
createPermissionState({
|
|
66
81
|
permission,
|
|
@@ -71,45 +86,47 @@ export function createFakePermissionClient(
|
|
|
71
86
|
}
|
|
72
87
|
|
|
73
88
|
for (const state of options.initialStates ?? []) {
|
|
74
|
-
states.set(state.permission, normalizeSeed(state));
|
|
89
|
+
context.states.set(state.permission, normalizeSeed(state));
|
|
75
90
|
}
|
|
76
91
|
|
|
77
92
|
for (const state of options.requestStates ?? []) {
|
|
78
|
-
requestStates.set(state.permission, normalizeSeed(state));
|
|
93
|
+
context.requestStates.set(state.permission, normalizeSeed(state));
|
|
79
94
|
}
|
|
95
|
+
return context;
|
|
96
|
+
}
|
|
80
97
|
|
|
98
|
+
function createFakePermissionOperations(
|
|
99
|
+
context: FakePermissionContext,
|
|
100
|
+
openSettings: (() => Promise<void>) | undefined,
|
|
101
|
+
): FakePermissionClient {
|
|
81
102
|
const client: FakePermissionClient = {
|
|
82
103
|
getStatus(permission) {
|
|
83
|
-
return Promise.resolve(getStoredState(states, assertKnownPermission(permission)));
|
|
104
|
+
return Promise.resolve(getStoredState(context.states, assertKnownPermission(permission)));
|
|
84
105
|
},
|
|
85
|
-
|
|
86
106
|
request(permission) {
|
|
87
107
|
const knownPermission = assertKnownPermission(permission);
|
|
88
108
|
const requestState =
|
|
89
|
-
requestStates.get(knownPermission) ??
|
|
109
|
+
context.requestStates.get(knownPermission) ??
|
|
110
|
+
getStoredState(context.states, knownPermission);
|
|
90
111
|
const nextState = createPermissionState({
|
|
91
112
|
permission: knownPermission,
|
|
92
113
|
status: requestState.status,
|
|
93
114
|
canAskAgain: requestState.canAskAgain,
|
|
94
|
-
requestedAt: now(),
|
|
115
|
+
requestedAt: context.now(),
|
|
95
116
|
reason: requestState.reason,
|
|
96
117
|
});
|
|
97
118
|
|
|
98
|
-
states.set(knownPermission, nextState);
|
|
99
|
-
|
|
119
|
+
context.states.set(knownPermission, nextState);
|
|
100
120
|
return Promise.resolve(nextState);
|
|
101
121
|
},
|
|
102
|
-
|
|
103
|
-
openSettings: options.openSettings,
|
|
104
|
-
|
|
122
|
+
openSettings,
|
|
105
123
|
setState(state) {
|
|
106
124
|
const normalizedState = normalizeSeed(state);
|
|
107
|
-
states.set(normalizedState.permission, normalizedState);
|
|
125
|
+
context.states.set(normalizedState.permission, normalizedState);
|
|
108
126
|
},
|
|
109
|
-
|
|
110
127
|
setStatus(permission, status, statusOptions = {}) {
|
|
111
128
|
const knownPermission = assertKnownPermission(permission);
|
|
112
|
-
states.set(
|
|
129
|
+
context.states.set(
|
|
113
130
|
knownPermission,
|
|
114
131
|
createPermissionState({
|
|
115
132
|
permission: knownPermission,
|
|
@@ -119,16 +136,10 @@ export function createFakePermissionClient(
|
|
|
119
136
|
}),
|
|
120
137
|
);
|
|
121
138
|
},
|
|
122
|
-
|
|
123
139
|
getSnapshot() {
|
|
124
|
-
return PERMISSIONS.map((permission) => getStoredState(states, permission));
|
|
140
|
+
return PERMISSIONS.map((permission) => getStoredState(context.states, permission));
|
|
125
141
|
},
|
|
126
142
|
};
|
|
127
|
-
|
|
128
|
-
if (options.openSettings === undefined) {
|
|
129
|
-
delete client.openSettings;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
143
|
return client;
|
|
133
144
|
}
|
|
134
145
|
|
package/src/web/index.test.ts
CHANGED
|
@@ -1,205 +1,203 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { expect, test } from 'bun:test';
|
|
2
2
|
|
|
3
3
|
import { Permission } from '../registry/permissions';
|
|
4
4
|
import { createWebPermissionClient, type WebPermissionClientOptions } from './index';
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const client = createWebPermissionClient({ global: {} });
|
|
6
|
+
test('returns unavailable for unsupported web permissions', async () => {
|
|
7
|
+
const client = createWebPermissionClient({ global: {} });
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
const state = await client.getStatus(Permission.MediaLibrary);
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
});
|
|
11
|
+
expect(state).toMatchObject({
|
|
12
|
+
status: 'unavailable',
|
|
13
|
+
canAskAgain: false,
|
|
16
14
|
});
|
|
15
|
+
});
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
},
|
|
17
|
+
test('reads notification permission structurally', async () => {
|
|
18
|
+
const client = createWebPermissionClient({
|
|
19
|
+
global: {
|
|
20
|
+
Notification: {
|
|
21
|
+
permission: 'granted',
|
|
22
|
+
requestPermission: () => 'granted',
|
|
25
23
|
},
|
|
26
|
-
}
|
|
24
|
+
},
|
|
25
|
+
});
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
const state = await client.getStatus(Permission.Notifications);
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
});
|
|
29
|
+
expect(state).toMatchObject({
|
|
30
|
+
status: 'granted',
|
|
31
|
+
granted: true,
|
|
34
32
|
});
|
|
33
|
+
});
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
},
|
|
35
|
+
test('requests notifications without throwing on denial', async () => {
|
|
36
|
+
const client = createWebPermissionClient({
|
|
37
|
+
global: {
|
|
38
|
+
Notification: {
|
|
39
|
+
permission: 'default',
|
|
40
|
+
requestPermission: () => Promise.resolve('denied'),
|
|
43
41
|
},
|
|
44
|
-
}
|
|
42
|
+
},
|
|
43
|
+
});
|
|
45
44
|
|
|
46
|
-
|
|
45
|
+
const state = await client.request(Permission.Notifications);
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
});
|
|
47
|
+
expect(state).toMatchObject({
|
|
48
|
+
status: 'denied',
|
|
49
|
+
granted: false,
|
|
52
50
|
});
|
|
51
|
+
});
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
},
|
|
53
|
+
test('returns unavailable when notification request throws', async () => {
|
|
54
|
+
const client = createWebPermissionClient({
|
|
55
|
+
global: {
|
|
56
|
+
Notification: {
|
|
57
|
+
permission: 'default',
|
|
58
|
+
requestPermission: () => {
|
|
59
|
+
throw Object.assign(new Error('blocked'), { name: 'SecurityError' });
|
|
62
60
|
},
|
|
63
61
|
},
|
|
64
|
-
}
|
|
62
|
+
},
|
|
63
|
+
});
|
|
65
64
|
|
|
66
|
-
|
|
65
|
+
const state = await client.request(Permission.Notifications);
|
|
67
66
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
});
|
|
67
|
+
expect(state).toMatchObject({
|
|
68
|
+
status: 'unavailable',
|
|
69
|
+
granted: false,
|
|
70
|
+
reason: 'Notification permission request failed: SecurityError.',
|
|
73
71
|
});
|
|
72
|
+
});
|
|
74
73
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
},
|
|
74
|
+
test('uses Permissions API for location status when available', async () => {
|
|
75
|
+
const client = createWebPermissionClient({
|
|
76
|
+
global: {
|
|
77
|
+
navigator: {
|
|
78
|
+
permissions: {
|
|
79
|
+
query: () => Promise.resolve({ state: 'prompt' }),
|
|
82
80
|
},
|
|
83
81
|
},
|
|
84
|
-
}
|
|
82
|
+
},
|
|
83
|
+
});
|
|
85
84
|
|
|
86
|
-
|
|
85
|
+
const state = await client.getStatus(Permission.LocationForeground);
|
|
87
86
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
});
|
|
87
|
+
expect(state).toMatchObject({
|
|
88
|
+
status: 'unknown',
|
|
89
|
+
canAskAgain: true,
|
|
92
90
|
});
|
|
91
|
+
});
|
|
93
92
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
},
|
|
93
|
+
test('returns blocked for media in insecure contexts', async () => {
|
|
94
|
+
const client = createWebPermissionClient({
|
|
95
|
+
global: {
|
|
96
|
+
isSecureContext: false,
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const state = await client.getStatus(Permission.Camera);
|
|
101
|
+
|
|
102
|
+
expect(state).toMatchObject({
|
|
103
|
+
status: 'blocked',
|
|
104
|
+
canAskAgain: false,
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('requests geolocation without a 1ms timeout', async () => {
|
|
109
|
+
let requestedOptions:
|
|
110
|
+
| {
|
|
111
|
+
readonly maximumAge?: number;
|
|
112
|
+
readonly timeout?: number;
|
|
113
|
+
}
|
|
114
|
+
| undefined;
|
|
115
|
+
const client = createWebPermissionClient({
|
|
116
|
+
global: {
|
|
117
|
+
navigator: {
|
|
118
|
+
geolocation: {
|
|
119
|
+
getCurrentPosition: (success, _error, options) => {
|
|
120
|
+
requestedOptions = options;
|
|
121
|
+
success();
|
|
124
122
|
},
|
|
125
123
|
},
|
|
126
124
|
},
|
|
127
|
-
}
|
|
125
|
+
},
|
|
126
|
+
});
|
|
128
127
|
|
|
129
|
-
|
|
128
|
+
const state = await client.request(Permission.LocationForeground);
|
|
130
129
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
expect(state.status).toBe('granted');
|
|
131
|
+
expect(requestedOptions).toEqual({ maximumAge: 0 });
|
|
132
|
+
});
|
|
134
133
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
},
|
|
134
|
+
test('requests media with getUserMedia and stops tracks', async () => {
|
|
135
|
+
let stopped = false;
|
|
136
|
+
const global: WebPermissionClientOptions['global'] = {
|
|
137
|
+
navigator: {
|
|
138
|
+
mediaDevices: {
|
|
139
|
+
getUserMedia: () =>
|
|
140
|
+
Promise.resolve({
|
|
141
|
+
getTracks: () => [
|
|
142
|
+
{
|
|
143
|
+
stop: () => {
|
|
144
|
+
stopped = true;
|
|
147
145
|
},
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
}),
|
|
151
149
|
},
|
|
152
|
-
}
|
|
153
|
-
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
const client = createWebPermissionClient({ global });
|
|
154
153
|
|
|
155
|
-
|
|
154
|
+
const state = await client.request(Permission.Microphone);
|
|
156
155
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
});
|
|
161
|
-
expect(stopped).toBe(true);
|
|
156
|
+
expect(state).toMatchObject({
|
|
157
|
+
status: 'granted',
|
|
158
|
+
granted: true,
|
|
162
159
|
});
|
|
160
|
+
expect(stopped).toBe(true);
|
|
161
|
+
});
|
|
163
162
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
},
|
|
163
|
+
test('maps missing media devices to unavailable', async () => {
|
|
164
|
+
const client = createWebPermissionClient({
|
|
165
|
+
global: {
|
|
166
|
+
navigator: {
|
|
167
|
+
mediaDevices: {
|
|
168
|
+
getUserMedia: () =>
|
|
169
|
+
Promise.reject(Object.assign(new Error('missing'), { name: 'NotFoundError' })),
|
|
172
170
|
},
|
|
173
171
|
},
|
|
174
|
-
}
|
|
172
|
+
},
|
|
173
|
+
});
|
|
175
174
|
|
|
176
|
-
|
|
175
|
+
const state = await client.request(Permission.Camera);
|
|
177
176
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
});
|
|
177
|
+
expect(state).toMatchObject({
|
|
178
|
+
status: 'unavailable',
|
|
179
|
+
granted: false,
|
|
180
|
+
reason: 'Media permission request failed: NotFoundError.',
|
|
183
181
|
});
|
|
182
|
+
});
|
|
184
183
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
},
|
|
184
|
+
test('maps user media denial to denied', async () => {
|
|
185
|
+
const client = createWebPermissionClient({
|
|
186
|
+
global: {
|
|
187
|
+
navigator: {
|
|
188
|
+
mediaDevices: {
|
|
189
|
+
getUserMedia: () =>
|
|
190
|
+
Promise.reject(Object.assign(new Error('denied'), { name: 'NotAllowedError' })),
|
|
193
191
|
},
|
|
194
192
|
},
|
|
195
|
-
}
|
|
193
|
+
},
|
|
194
|
+
});
|
|
196
195
|
|
|
197
|
-
|
|
196
|
+
const state = await client.request(Permission.Camera);
|
|
198
197
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
});
|
|
198
|
+
expect(state).toMatchObject({
|
|
199
|
+
status: 'denied',
|
|
200
|
+
granted: false,
|
|
201
|
+
reason: 'Media permission request was denied: NotAllowedError.',
|
|
204
202
|
});
|
|
205
203
|
});
|
package/src/web/index.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import type { PermissionClient } from '../client/types';
|
|
2
2
|
import { assertKnownPermission, Permission } from '../registry/permissions';
|
|
3
|
+
import { createPermissionState, type PermissionState } from '../state/permissionState';
|
|
3
4
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
type
|
|
7
|
-
|
|
5
|
+
type BrowserPermissionState,
|
|
6
|
+
formatErrorName,
|
|
7
|
+
type NotificationPermissionState,
|
|
8
|
+
stateFromBrowserPermission,
|
|
9
|
+
stateFromMediaError,
|
|
10
|
+
stateFromNotificationPermission,
|
|
11
|
+
unavailable,
|
|
12
|
+
} from './permissionStates';
|
|
8
13
|
|
|
9
14
|
type BrowserPermissionName = 'camera' | 'microphone' | 'geolocation' | 'notifications';
|
|
10
|
-
type BrowserPermissionState = 'granted' | 'denied' | 'prompt';
|
|
11
|
-
type NotificationPermissionState = 'default' | 'denied' | 'granted';
|
|
12
|
-
|
|
13
15
|
interface BrowserPermissionStatus {
|
|
14
16
|
readonly state: BrowserPermissionState;
|
|
15
17
|
}
|
|
@@ -293,98 +295,3 @@ async function requestMedia(
|
|
|
293
295
|
return stateFromMediaError(permission, error);
|
|
294
296
|
}
|
|
295
297
|
}
|
|
296
|
-
|
|
297
|
-
function stateFromMediaError(permission: Permission, error: unknown): PermissionState {
|
|
298
|
-
const errorName = getErrorName(error);
|
|
299
|
-
const unavailableErrorNames = new Set([
|
|
300
|
-
'AbortError',
|
|
301
|
-
'NotFoundError',
|
|
302
|
-
'NotReadableError',
|
|
303
|
-
'OverconstrainedError',
|
|
304
|
-
'SecurityError',
|
|
305
|
-
]);
|
|
306
|
-
|
|
307
|
-
if (errorName !== undefined && unavailableErrorNames.has(errorName)) {
|
|
308
|
-
return createPermissionState({
|
|
309
|
-
permission,
|
|
310
|
-
status: 'unavailable',
|
|
311
|
-
canAskAgain: false,
|
|
312
|
-
requestedAt: new Date(),
|
|
313
|
-
reason: `Media permission request failed: ${errorName}.`,
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
return createPermissionState({
|
|
318
|
-
permission,
|
|
319
|
-
status: 'denied',
|
|
320
|
-
canAskAgain: false,
|
|
321
|
-
requestedAt: new Date(),
|
|
322
|
-
reason:
|
|
323
|
-
errorName === undefined
|
|
324
|
-
? 'Media permission request was denied.'
|
|
325
|
-
: `Media permission request was denied: ${errorName}.`,
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function stateFromNotificationPermission(
|
|
330
|
-
permission: Permission,
|
|
331
|
-
status: NotificationPermissionState,
|
|
332
|
-
): PermissionState {
|
|
333
|
-
if (status === 'granted') {
|
|
334
|
-
return createPermissionState({
|
|
335
|
-
permission,
|
|
336
|
-
status: 'granted',
|
|
337
|
-
canAskAgain: true,
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
if (status === 'denied') {
|
|
342
|
-
return createPermissionState({
|
|
343
|
-
permission,
|
|
344
|
-
status: 'denied',
|
|
345
|
-
canAskAgain: false,
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
return createPermissionState({
|
|
350
|
-
permission,
|
|
351
|
-
status: 'unknown',
|
|
352
|
-
canAskAgain: true,
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function stateFromBrowserPermission(
|
|
357
|
-
permission: Permission,
|
|
358
|
-
status: BrowserPermissionState,
|
|
359
|
-
): PermissionState {
|
|
360
|
-
const normalizedStatus: PermissionStatus = status === 'prompt' ? 'unknown' : status;
|
|
361
|
-
|
|
362
|
-
return createPermissionState({
|
|
363
|
-
permission,
|
|
364
|
-
status: normalizedStatus,
|
|
365
|
-
canAskAgain: status === 'prompt' || status === 'granted',
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function unavailable(permission: Permission, reason: string): PermissionState {
|
|
370
|
-
return createPermissionState({
|
|
371
|
-
permission,
|
|
372
|
-
status: 'unavailable',
|
|
373
|
-
canAskAgain: false,
|
|
374
|
-
reason,
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function formatErrorName(error: unknown): string {
|
|
379
|
-
const errorName = getErrorName(error);
|
|
380
|
-
|
|
381
|
-
return errorName === undefined ? '' : `: ${errorName}`;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function getErrorName(error: unknown): string | undefined {
|
|
385
|
-
if (typeof error !== 'object' || error === null || !('name' in error)) {
|
|
386
|
-
return undefined;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
return typeof error.name === 'string' && error.name.length > 0 ? error.name : undefined;
|
|
390
|
-
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { Permission } from '../registry/permissions';
|
|
2
|
+
import {
|
|
3
|
+
createPermissionState,
|
|
4
|
+
type PermissionState,
|
|
5
|
+
type PermissionStatus,
|
|
6
|
+
} from '../state/permissionState';
|
|
7
|
+
|
|
8
|
+
export type BrowserPermissionState = 'granted' | 'denied' | 'prompt';
|
|
9
|
+
export type NotificationPermissionState = 'default' | 'denied' | 'granted';
|
|
10
|
+
|
|
11
|
+
export function stateFromMediaError(permission: Permission, error: unknown): PermissionState {
|
|
12
|
+
const errorName = getErrorName(error);
|
|
13
|
+
const unavailableErrorNames = new Set([
|
|
14
|
+
'AbortError',
|
|
15
|
+
'NotFoundError',
|
|
16
|
+
'NotReadableError',
|
|
17
|
+
'OverconstrainedError',
|
|
18
|
+
'SecurityError',
|
|
19
|
+
]);
|
|
20
|
+
if (errorName !== undefined && unavailableErrorNames.has(errorName)) {
|
|
21
|
+
return createPermissionState({
|
|
22
|
+
permission,
|
|
23
|
+
status: 'unavailable',
|
|
24
|
+
canAskAgain: false,
|
|
25
|
+
requestedAt: new Date(),
|
|
26
|
+
reason: `Media permission request failed: ${errorName}.`,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return createPermissionState({
|
|
30
|
+
permission,
|
|
31
|
+
status: 'denied',
|
|
32
|
+
canAskAgain: false,
|
|
33
|
+
requestedAt: new Date(),
|
|
34
|
+
reason:
|
|
35
|
+
errorName === undefined
|
|
36
|
+
? 'Media permission request was denied.'
|
|
37
|
+
: `Media permission request was denied: ${errorName}.`,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function stateFromNotificationPermission(
|
|
42
|
+
permission: Permission,
|
|
43
|
+
status: NotificationPermissionState,
|
|
44
|
+
): PermissionState {
|
|
45
|
+
if (status === 'granted') {
|
|
46
|
+
return createPermissionState({ permission, status: 'granted', canAskAgain: true });
|
|
47
|
+
}
|
|
48
|
+
if (status === 'denied') {
|
|
49
|
+
return createPermissionState({ permission, status: 'denied', canAskAgain: false });
|
|
50
|
+
}
|
|
51
|
+
return createPermissionState({ permission, status: 'unknown', canAskAgain: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function stateFromBrowserPermission(
|
|
55
|
+
permission: Permission,
|
|
56
|
+
status: BrowserPermissionState,
|
|
57
|
+
): PermissionState {
|
|
58
|
+
const normalizedStatus: PermissionStatus = status === 'prompt' ? 'unknown' : status;
|
|
59
|
+
return createPermissionState({
|
|
60
|
+
permission,
|
|
61
|
+
status: normalizedStatus,
|
|
62
|
+
canAskAgain: status === 'prompt' || status === 'granted',
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function unavailable(permission: Permission, reason: string): PermissionState {
|
|
67
|
+
return createPermissionState({
|
|
68
|
+
permission,
|
|
69
|
+
status: 'unavailable',
|
|
70
|
+
canAskAgain: false,
|
|
71
|
+
reason,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function formatErrorName(error: unknown): string {
|
|
76
|
+
const errorName = getErrorName(error);
|
|
77
|
+
return errorName === undefined ? '' : `: ${errorName}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getErrorName(error: unknown): string | undefined {
|
|
81
|
+
if (typeof error !== 'object' || error === null || !('name' in error)) return undefined;
|
|
82
|
+
return typeof error.name === 'string' && error.name.length > 0 ? error.name : undefined;
|
|
83
|
+
}
|