@ankhorage/permissions 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +143 -0
- package/dist/client/types.d.ts +19 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +2 -0
- package/dist/client/types.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/manager/createPermissionManager.d.ts +31 -0
- package/dist/manager/createPermissionManager.d.ts.map +1 -0
- package/dist/manager/createPermissionManager.js +39 -0
- package/dist/manager/createPermissionManager.js.map +1 -0
- package/dist/metadata/index.d.ts +8 -0
- package/dist/metadata/index.d.ts.map +1 -0
- package/dist/metadata/index.js +8 -0
- package/dist/metadata/index.js.map +1 -0
- package/dist/react/index.d.ts +66 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +91 -0
- package/dist/react/index.js.map +1 -0
- package/dist/registry/permissions.d.ts +52 -0
- package/dist/registry/permissions.d.ts.map +1 -0
- package/dist/registry/permissions.js +108 -0
- package/dist/registry/permissions.js.map +1 -0
- package/dist/state/permissionState.d.ts +43 -0
- package/dist/state/permissionState.d.ts.map +1 -0
- package/dist/state/permissionState.js +44 -0
- package/dist/state/permissionState.js.map +1 -0
- package/dist/testing/index.d.ts +49 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +95 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/web/index.d.ts +60 -0
- package/dist/web/index.d.ts.map +1 -0
- package/dist/web/index.js +234 -0
- package/dist/web/index.js.map +1 -0
- package/package.json +114 -0
- package/src/client/types.ts +19 -0
- package/src/index.ts +31 -0
- package/src/manager/createPermissionManager.test.ts +67 -0
- package/src/manager/createPermissionManager.ts +54 -0
- package/src/metadata/index.ts +7 -0
- package/src/react/index.test.ts +11 -0
- package/src/react/index.tsx +170 -0
- package/src/registry/permissions.test.ts +37 -0
- package/src/registry/permissions.ts +128 -0
- package/src/state/permissionState.test.ts +54 -0
- package/src/state/permissionState.ts +86 -0
- package/src/testing/index.test.ts +57 -0
- package/src/testing/index.ts +168 -0
- package/src/web/index.test.ts +205 -0
- package/src/web/index.ts +390 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { Permission } from '../registry/permissions';
|
|
4
|
+
import { createWebPermissionClient, type WebPermissionClientOptions } from './index';
|
|
5
|
+
|
|
6
|
+
describe('createWebPermissionClient', () => {
|
|
7
|
+
test('returns unavailable for unsupported web permissions', async () => {
|
|
8
|
+
const client = createWebPermissionClient({ global: {} });
|
|
9
|
+
|
|
10
|
+
const state = await client.getStatus(Permission.MediaLibrary);
|
|
11
|
+
|
|
12
|
+
expect(state).toMatchObject({
|
|
13
|
+
status: 'unavailable',
|
|
14
|
+
canAskAgain: false,
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('reads notification permission structurally', async () => {
|
|
19
|
+
const client = createWebPermissionClient({
|
|
20
|
+
global: {
|
|
21
|
+
Notification: {
|
|
22
|
+
permission: 'granted',
|
|
23
|
+
requestPermission: () => 'granted',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const state = await client.getStatus(Permission.Notifications);
|
|
29
|
+
|
|
30
|
+
expect(state).toMatchObject({
|
|
31
|
+
status: 'granted',
|
|
32
|
+
granted: true,
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('requests notifications without throwing on denial', async () => {
|
|
37
|
+
const client = createWebPermissionClient({
|
|
38
|
+
global: {
|
|
39
|
+
Notification: {
|
|
40
|
+
permission: 'default',
|
|
41
|
+
requestPermission: () => Promise.resolve('denied'),
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const state = await client.request(Permission.Notifications);
|
|
47
|
+
|
|
48
|
+
expect(state).toMatchObject({
|
|
49
|
+
status: 'denied',
|
|
50
|
+
granted: false,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('returns unavailable when notification request throws', async () => {
|
|
55
|
+
const client = createWebPermissionClient({
|
|
56
|
+
global: {
|
|
57
|
+
Notification: {
|
|
58
|
+
permission: 'default',
|
|
59
|
+
requestPermission: () => {
|
|
60
|
+
throw Object.assign(new Error('blocked'), { name: 'SecurityError' });
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const state = await client.request(Permission.Notifications);
|
|
67
|
+
|
|
68
|
+
expect(state).toMatchObject({
|
|
69
|
+
status: 'unavailable',
|
|
70
|
+
granted: false,
|
|
71
|
+
reason: 'Notification permission request failed: SecurityError.',
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('uses Permissions API for location status when available', async () => {
|
|
76
|
+
const client = createWebPermissionClient({
|
|
77
|
+
global: {
|
|
78
|
+
navigator: {
|
|
79
|
+
permissions: {
|
|
80
|
+
query: () => Promise.resolve({ state: 'prompt' }),
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const state = await client.getStatus(Permission.LocationForeground);
|
|
87
|
+
|
|
88
|
+
expect(state).toMatchObject({
|
|
89
|
+
status: 'unknown',
|
|
90
|
+
canAskAgain: true,
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('returns blocked for media in insecure contexts', async () => {
|
|
95
|
+
const client = createWebPermissionClient({
|
|
96
|
+
global: {
|
|
97
|
+
isSecureContext: false,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const state = await client.getStatus(Permission.Camera);
|
|
102
|
+
|
|
103
|
+
expect(state).toMatchObject({
|
|
104
|
+
status: 'blocked',
|
|
105
|
+
canAskAgain: false,
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('requests geolocation without a 1ms timeout', async () => {
|
|
110
|
+
let requestedOptions:
|
|
111
|
+
| {
|
|
112
|
+
readonly maximumAge?: number;
|
|
113
|
+
readonly timeout?: number;
|
|
114
|
+
}
|
|
115
|
+
| undefined;
|
|
116
|
+
const client = createWebPermissionClient({
|
|
117
|
+
global: {
|
|
118
|
+
navigator: {
|
|
119
|
+
geolocation: {
|
|
120
|
+
getCurrentPosition: (success, _error, options) => {
|
|
121
|
+
requestedOptions = options;
|
|
122
|
+
success();
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const state = await client.request(Permission.LocationForeground);
|
|
130
|
+
|
|
131
|
+
expect(state.status).toBe('granted');
|
|
132
|
+
expect(requestedOptions).toEqual({ maximumAge: 0 });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('requests media with getUserMedia and stops tracks', async () => {
|
|
136
|
+
let stopped = false;
|
|
137
|
+
const global: WebPermissionClientOptions['global'] = {
|
|
138
|
+
navigator: {
|
|
139
|
+
mediaDevices: {
|
|
140
|
+
getUserMedia: () =>
|
|
141
|
+
Promise.resolve({
|
|
142
|
+
getTracks: () => [
|
|
143
|
+
{
|
|
144
|
+
stop: () => {
|
|
145
|
+
stopped = true;
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
}),
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
const client = createWebPermissionClient({ global });
|
|
154
|
+
|
|
155
|
+
const state = await client.request(Permission.Microphone);
|
|
156
|
+
|
|
157
|
+
expect(state).toMatchObject({
|
|
158
|
+
status: 'granted',
|
|
159
|
+
granted: true,
|
|
160
|
+
});
|
|
161
|
+
expect(stopped).toBe(true);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('maps missing media devices to unavailable', async () => {
|
|
165
|
+
const client = createWebPermissionClient({
|
|
166
|
+
global: {
|
|
167
|
+
navigator: {
|
|
168
|
+
mediaDevices: {
|
|
169
|
+
getUserMedia: () =>
|
|
170
|
+
Promise.reject(Object.assign(new Error('missing'), { name: 'NotFoundError' })),
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const state = await client.request(Permission.Camera);
|
|
177
|
+
|
|
178
|
+
expect(state).toMatchObject({
|
|
179
|
+
status: 'unavailable',
|
|
180
|
+
granted: false,
|
|
181
|
+
reason: 'Media permission request failed: NotFoundError.',
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('maps user media denial to denied', async () => {
|
|
186
|
+
const client = createWebPermissionClient({
|
|
187
|
+
global: {
|
|
188
|
+
navigator: {
|
|
189
|
+
mediaDevices: {
|
|
190
|
+
getUserMedia: () =>
|
|
191
|
+
Promise.reject(Object.assign(new Error('denied'), { name: 'NotAllowedError' })),
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const state = await client.request(Permission.Camera);
|
|
198
|
+
|
|
199
|
+
expect(state).toMatchObject({
|
|
200
|
+
status: 'denied',
|
|
201
|
+
granted: false,
|
|
202
|
+
reason: 'Media permission request was denied: NotAllowedError.',
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
});
|
package/src/web/index.ts
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import type { PermissionClient } from '../client/types';
|
|
2
|
+
import { assertKnownPermission, Permission } from '../registry/permissions';
|
|
3
|
+
import {
|
|
4
|
+
createPermissionState,
|
|
5
|
+
type PermissionState,
|
|
6
|
+
type PermissionStatus,
|
|
7
|
+
} from '../state/permissionState';
|
|
8
|
+
|
|
9
|
+
type BrowserPermissionName = 'camera' | 'microphone' | 'geolocation' | 'notifications';
|
|
10
|
+
type BrowserPermissionState = 'granted' | 'denied' | 'prompt';
|
|
11
|
+
type NotificationPermissionState = 'default' | 'denied' | 'granted';
|
|
12
|
+
|
|
13
|
+
interface BrowserPermissionStatus {
|
|
14
|
+
readonly state: BrowserPermissionState;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface BrowserPermissions {
|
|
18
|
+
query(descriptor: { readonly name: BrowserPermissionName }): Promise<BrowserPermissionStatus>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface MediaStreamTrackLike {
|
|
22
|
+
stop(): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface MediaStreamLike {
|
|
26
|
+
getTracks(): readonly MediaStreamTrackLike[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface MediaDevicesLike {
|
|
30
|
+
getUserMedia(constraints: {
|
|
31
|
+
readonly audio?: boolean;
|
|
32
|
+
readonly video?: boolean;
|
|
33
|
+
}): Promise<MediaStreamLike>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface GeolocationLike {
|
|
37
|
+
getCurrentPosition(
|
|
38
|
+
success: () => void,
|
|
39
|
+
error: () => void,
|
|
40
|
+
options?: {
|
|
41
|
+
readonly maximumAge?: number;
|
|
42
|
+
readonly timeout?: number;
|
|
43
|
+
},
|
|
44
|
+
): void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface NavigatorLike {
|
|
48
|
+
readonly permissions?: BrowserPermissions;
|
|
49
|
+
readonly mediaDevices?: MediaDevicesLike;
|
|
50
|
+
readonly geolocation?: GeolocationLike;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface NotificationLike {
|
|
54
|
+
readonly permission: NotificationPermissionState;
|
|
55
|
+
requestPermission(): Promise<NotificationPermissionState> | NotificationPermissionState;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface WebGlobalLike {
|
|
59
|
+
readonly isSecureContext?: boolean;
|
|
60
|
+
readonly navigator?: NavigatorLike;
|
|
61
|
+
readonly Notification?: NotificationLike;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/***
|
|
65
|
+
* Options for the browser permission adapter.
|
|
66
|
+
*/
|
|
67
|
+
export interface WebPermissionClientOptions {
|
|
68
|
+
readonly global?: WebGlobalLike;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/***
|
|
72
|
+
* Creates a browser permission client using guarded structural globals.
|
|
73
|
+
*
|
|
74
|
+
* @readme
|
|
75
|
+
* Unsupported web APIs resolve to `status: 'unavailable'`. The adapter does not
|
|
76
|
+
* import DOM types and does not assume it is running in a browser.
|
|
77
|
+
*/
|
|
78
|
+
export function createWebPermissionClient(
|
|
79
|
+
options: WebPermissionClientOptions = {},
|
|
80
|
+
): PermissionClient {
|
|
81
|
+
const global = options.global ?? getDefaultGlobal();
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
async getStatus(permission) {
|
|
85
|
+
const knownPermission = assertKnownPermission(permission);
|
|
86
|
+
|
|
87
|
+
switch (knownPermission) {
|
|
88
|
+
case Permission.Notifications:
|
|
89
|
+
return getNotificationStatus(knownPermission, global);
|
|
90
|
+
case Permission.LocationForeground:
|
|
91
|
+
return getPermissionApiStatus(
|
|
92
|
+
knownPermission,
|
|
93
|
+
global,
|
|
94
|
+
'geolocation',
|
|
95
|
+
'Geolocation is unavailable in this environment.',
|
|
96
|
+
);
|
|
97
|
+
case Permission.Camera:
|
|
98
|
+
return getMediaStatus(knownPermission, global, 'camera');
|
|
99
|
+
case Permission.Microphone:
|
|
100
|
+
return getMediaStatus(knownPermission, global, 'microphone');
|
|
101
|
+
default:
|
|
102
|
+
return unavailable(
|
|
103
|
+
knownPermission,
|
|
104
|
+
'This permission is not supported by the web adapter.',
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
async request(permission) {
|
|
110
|
+
const knownPermission = assertKnownPermission(permission);
|
|
111
|
+
|
|
112
|
+
switch (knownPermission) {
|
|
113
|
+
case Permission.Notifications:
|
|
114
|
+
return requestNotifications(knownPermission, global);
|
|
115
|
+
case Permission.LocationForeground:
|
|
116
|
+
return requestGeolocation(knownPermission, global);
|
|
117
|
+
case Permission.Camera:
|
|
118
|
+
return requestMedia(knownPermission, global, { video: true });
|
|
119
|
+
case Permission.Microphone:
|
|
120
|
+
return requestMedia(knownPermission, global, { audio: true });
|
|
121
|
+
default:
|
|
122
|
+
return unavailable(
|
|
123
|
+
knownPermission,
|
|
124
|
+
'This permission is not supported by the web adapter.',
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getDefaultGlobal(): WebGlobalLike {
|
|
132
|
+
return globalThis as WebGlobalLike;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function getNotificationStatus(permission: Permission, global: WebGlobalLike): PermissionState {
|
|
136
|
+
if (global.Notification === undefined) {
|
|
137
|
+
return unavailable(permission, 'Notification API is unavailable.');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return stateFromNotificationPermission(permission, global.Notification.permission);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function requestNotifications(
|
|
144
|
+
permission: Permission,
|
|
145
|
+
global: WebGlobalLike,
|
|
146
|
+
): Promise<PermissionState> {
|
|
147
|
+
if (global.Notification === undefined) {
|
|
148
|
+
return unavailable(permission, 'Notification API is unavailable.');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const result = await global.Notification.requestPermission();
|
|
153
|
+
|
|
154
|
+
return stateFromNotificationPermission(permission, result);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
return unavailable(
|
|
157
|
+
permission,
|
|
158
|
+
`Notification permission request failed${formatErrorName(error)}.`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function getPermissionApiStatus(
|
|
164
|
+
permission: Permission,
|
|
165
|
+
global: WebGlobalLike,
|
|
166
|
+
name: BrowserPermissionName,
|
|
167
|
+
unavailableReason: string,
|
|
168
|
+
): Promise<PermissionState> {
|
|
169
|
+
if (global.navigator?.permissions === undefined) {
|
|
170
|
+
return unavailable(permission, unavailableReason);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const status = await global.navigator.permissions.query({ name });
|
|
175
|
+
|
|
176
|
+
return stateFromBrowserPermission(permission, status.state);
|
|
177
|
+
} catch {
|
|
178
|
+
return unavailable(permission, unavailableReason);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function getMediaStatus(
|
|
183
|
+
permission: Permission,
|
|
184
|
+
global: WebGlobalLike,
|
|
185
|
+
name: BrowserPermissionName,
|
|
186
|
+
): Promise<PermissionState> {
|
|
187
|
+
if (global.navigator?.permissions !== undefined) {
|
|
188
|
+
try {
|
|
189
|
+
const status = await global.navigator.permissions.query({ name });
|
|
190
|
+
|
|
191
|
+
return stateFromBrowserPermission(permission, status.state);
|
|
192
|
+
} catch {
|
|
193
|
+
return getMediaAvailability(permission, global);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return getMediaAvailability(permission, global);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function getMediaAvailability(permission: Permission, global: WebGlobalLike): PermissionState {
|
|
201
|
+
if (global.isSecureContext === false) {
|
|
202
|
+
return createPermissionState({
|
|
203
|
+
permission,
|
|
204
|
+
status: 'blocked',
|
|
205
|
+
canAskAgain: false,
|
|
206
|
+
reason: 'Media permissions require a secure context.',
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (global.navigator?.mediaDevices?.getUserMedia === undefined) {
|
|
211
|
+
return unavailable(permission, 'Media devices API is unavailable.');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return createPermissionState({
|
|
215
|
+
permission,
|
|
216
|
+
status: 'unknown',
|
|
217
|
+
canAskAgain: true,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function requestGeolocation(
|
|
222
|
+
permission: Permission,
|
|
223
|
+
global: WebGlobalLike,
|
|
224
|
+
): Promise<PermissionState> {
|
|
225
|
+
if (global.isSecureContext === false) {
|
|
226
|
+
return createPermissionState({
|
|
227
|
+
permission,
|
|
228
|
+
status: 'blocked',
|
|
229
|
+
canAskAgain: false,
|
|
230
|
+
reason: 'Geolocation requires a secure context.',
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (global.navigator?.geolocation === undefined) {
|
|
235
|
+
return unavailable(permission, 'Geolocation API is unavailable.');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return new Promise((resolve) => {
|
|
239
|
+
global.navigator?.geolocation?.getCurrentPosition(
|
|
240
|
+
() => {
|
|
241
|
+
resolve(
|
|
242
|
+
createPermissionState({
|
|
243
|
+
permission,
|
|
244
|
+
status: 'granted',
|
|
245
|
+
canAskAgain: true,
|
|
246
|
+
requestedAt: new Date(),
|
|
247
|
+
}),
|
|
248
|
+
);
|
|
249
|
+
},
|
|
250
|
+
() => {
|
|
251
|
+
resolve(
|
|
252
|
+
createPermissionState({
|
|
253
|
+
permission,
|
|
254
|
+
status: 'denied',
|
|
255
|
+
canAskAgain: false,
|
|
256
|
+
requestedAt: new Date(),
|
|
257
|
+
}),
|
|
258
|
+
);
|
|
259
|
+
},
|
|
260
|
+
{ maximumAge: 0 },
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function requestMedia(
|
|
266
|
+
permission: Permission,
|
|
267
|
+
global: WebGlobalLike,
|
|
268
|
+
constraints: {
|
|
269
|
+
readonly audio?: boolean;
|
|
270
|
+
readonly video?: boolean;
|
|
271
|
+
},
|
|
272
|
+
): Promise<PermissionState> {
|
|
273
|
+
const availability = getMediaAvailability(permission, global);
|
|
274
|
+
|
|
275
|
+
if (availability.status !== 'unknown') {
|
|
276
|
+
return availability;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
const stream = await global.navigator?.mediaDevices?.getUserMedia(constraints);
|
|
281
|
+
|
|
282
|
+
for (const track of stream?.getTracks() ?? []) {
|
|
283
|
+
track.stop();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return createPermissionState({
|
|
287
|
+
permission,
|
|
288
|
+
status: 'granted',
|
|
289
|
+
canAskAgain: true,
|
|
290
|
+
requestedAt: new Date(),
|
|
291
|
+
});
|
|
292
|
+
} catch (error) {
|
|
293
|
+
return stateFromMediaError(permission, error);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
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
|
+
}
|