@ankhorage/permissions 0.2.3 → 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 +6 -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 +10 -11
- 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 +4 -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/expo/client.test.ts
CHANGED
|
@@ -1,557 +1,132 @@
|
|
|
1
|
-
import { beforeEach,
|
|
1
|
+
import { beforeEach, expect, test } from 'bun:test';
|
|
2
2
|
|
|
3
3
|
import { Permission, PERMISSIONS } from '../registry/permissions';
|
|
4
4
|
import { createPermissionClient } from './client';
|
|
5
|
+
import {
|
|
6
|
+
audioMock,
|
|
7
|
+
cameraMock,
|
|
8
|
+
locationMock,
|
|
9
|
+
mediaLibraryMock,
|
|
10
|
+
notificationsMock,
|
|
11
|
+
permissionResponse,
|
|
12
|
+
resetExpoMocks,
|
|
13
|
+
} from './client.testSupport';
|
|
14
|
+
|
|
15
|
+
beforeEach(resetExpoMocks);
|
|
16
|
+
|
|
17
|
+
test('Expo client covers getStatus and request for every known permission', async () => {
|
|
18
|
+
const client = createPermissionClient();
|
|
19
|
+
for (const permission of PERMISSIONS) {
|
|
20
|
+
const statusState = await client.getStatus(permission);
|
|
21
|
+
const requestState = await client.request(permission);
|
|
22
|
+
expect(statusState).toMatchObject({ permission, status: 'granted', granted: true });
|
|
23
|
+
expect(requestState).toMatchObject({ permission, status: 'granted', granted: true });
|
|
24
|
+
}
|
|
25
|
+
expect(mediaLibraryMock.getCalls).toEqual([false, true]);
|
|
26
|
+
expect(mediaLibraryMock.requestCalls).toEqual([false, true]);
|
|
27
|
+
expect(locationMock.getCalls).toEqual(['foreground', 'background']);
|
|
28
|
+
expect(locationMock.requestCalls).toEqual(['foreground', 'background']);
|
|
29
|
+
});
|
|
5
30
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
type CameraMockMode = 'topLevel' | 'cameraNamespace' | 'missingExports';
|
|
16
|
-
type AudioMockMode = 'topLevel' | 'audioModule' | 'missingExports';
|
|
17
|
-
|
|
18
|
-
function permissionResponse(status: string, canAskAgain = true): MockPermissionResponse {
|
|
19
|
-
return { status, canAskAgain };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const cameraMock = {
|
|
23
|
-
mode: 'topLevel' as CameraMockMode,
|
|
24
|
-
getResponse: permissionResponse('granted'),
|
|
25
|
-
requestResponse: permissionResponse('granted'),
|
|
26
|
-
getError: undefined as string | undefined,
|
|
27
|
-
requestError: undefined as string | undefined,
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const audioMock = {
|
|
31
|
-
mode: 'topLevel' as AudioMockMode,
|
|
32
|
-
getResponse: permissionResponse('granted'),
|
|
33
|
-
requestResponse: permissionResponse('granted'),
|
|
34
|
-
getError: undefined as string | undefined,
|
|
35
|
-
requestError: undefined as string | undefined,
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const mediaLibraryMock = {
|
|
39
|
-
getResponse: permissionResponse('granted'),
|
|
40
|
-
requestResponse: permissionResponse('granted'),
|
|
41
|
-
getError: undefined as string | undefined,
|
|
42
|
-
requestError: undefined as string | undefined,
|
|
43
|
-
getCalls: [] as boolean[],
|
|
44
|
-
requestCalls: [] as boolean[],
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const locationMock = {
|
|
48
|
-
foregroundGetResponse: permissionResponse('granted'),
|
|
49
|
-
foregroundRequestResponse: permissionResponse('granted'),
|
|
50
|
-
backgroundGetResponse: permissionResponse('granted'),
|
|
51
|
-
backgroundRequestResponse: permissionResponse('granted'),
|
|
52
|
-
getCalls: [] as string[],
|
|
53
|
-
requestCalls: [] as string[],
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const notificationsMock = {
|
|
57
|
-
getResponse: permissionResponse('granted'),
|
|
58
|
-
requestResponse: permissionResponse('granted'),
|
|
59
|
-
getError: undefined as string | undefined,
|
|
60
|
-
requestError: undefined as string | undefined,
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
const linkingMock = {
|
|
64
|
-
calls: 0,
|
|
65
|
-
error: undefined as Error | undefined,
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
void mock.module('expo-camera', () => ({
|
|
69
|
-
get getCameraPermissionsAsync() {
|
|
70
|
-
if (cameraMock.mode !== 'topLevel') {
|
|
71
|
-
return undefined;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return () => {
|
|
75
|
-
if (cameraMock.getError !== undefined) {
|
|
76
|
-
throw new Error(cameraMock.getError);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return Promise.resolve(cameraMock.getResponse);
|
|
80
|
-
};
|
|
81
|
-
},
|
|
82
|
-
get requestCameraPermissionsAsync() {
|
|
83
|
-
if (cameraMock.mode !== 'topLevel') {
|
|
84
|
-
return undefined;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return () => {
|
|
88
|
-
if (cameraMock.requestError !== undefined) {
|
|
89
|
-
throw new Error(cameraMock.requestError);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return Promise.resolve(cameraMock.requestResponse);
|
|
93
|
-
};
|
|
94
|
-
},
|
|
95
|
-
get Camera() {
|
|
96
|
-
if (cameraMock.mode !== 'cameraNamespace') {
|
|
97
|
-
return undefined;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
getCameraPermissionsAsync: () => Promise.resolve(cameraMock.getResponse),
|
|
102
|
-
requestCameraPermissionsAsync: () => Promise.resolve(cameraMock.requestResponse),
|
|
103
|
-
};
|
|
104
|
-
},
|
|
105
|
-
}));
|
|
106
|
-
|
|
107
|
-
void mock.module('expo-audio', () => ({
|
|
108
|
-
get getRecordingPermissionsAsync() {
|
|
109
|
-
if (audioMock.mode !== 'topLevel') {
|
|
110
|
-
return undefined;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return () => {
|
|
114
|
-
if (audioMock.getError !== undefined) {
|
|
115
|
-
throw new Error(audioMock.getError);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return Promise.resolve(audioMock.getResponse);
|
|
119
|
-
};
|
|
120
|
-
},
|
|
121
|
-
get requestRecordingPermissionsAsync() {
|
|
122
|
-
if (audioMock.mode !== 'topLevel') {
|
|
123
|
-
return undefined;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return () => {
|
|
127
|
-
if (audioMock.requestError !== undefined) {
|
|
128
|
-
throw new Error(audioMock.requestError);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return Promise.resolve(audioMock.requestResponse);
|
|
132
|
-
};
|
|
133
|
-
},
|
|
134
|
-
get AudioModule() {
|
|
135
|
-
if (audioMock.mode !== 'audioModule') {
|
|
136
|
-
return undefined;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return {
|
|
140
|
-
getRecordingPermissionsAsync: () => Promise.resolve(audioMock.getResponse),
|
|
141
|
-
requestRecordingPermissionsAsync: () => Promise.resolve(audioMock.requestResponse),
|
|
142
|
-
};
|
|
143
|
-
},
|
|
144
|
-
}));
|
|
145
|
-
|
|
146
|
-
void mock.module('expo-media-library', () => ({
|
|
147
|
-
getPermissionsAsync: (writeOnly?: boolean) => {
|
|
148
|
-
mediaLibraryMock.getCalls.push(writeOnly === true);
|
|
149
|
-
|
|
150
|
-
if (mediaLibraryMock.getError !== undefined) {
|
|
151
|
-
throw new Error(mediaLibraryMock.getError);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return Promise.resolve(mediaLibraryMock.getResponse);
|
|
155
|
-
},
|
|
156
|
-
requestPermissionsAsync: (writeOnly?: boolean) => {
|
|
157
|
-
mediaLibraryMock.requestCalls.push(writeOnly === true);
|
|
158
|
-
|
|
159
|
-
if (mediaLibraryMock.requestError !== undefined) {
|
|
160
|
-
throw new Error(mediaLibraryMock.requestError);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return Promise.resolve(mediaLibraryMock.requestResponse);
|
|
164
|
-
},
|
|
165
|
-
}));
|
|
166
|
-
|
|
167
|
-
void mock.module('expo-location', () => ({
|
|
168
|
-
getForegroundPermissionsAsync: () => {
|
|
169
|
-
locationMock.getCalls.push('foreground');
|
|
170
|
-
|
|
171
|
-
return Promise.resolve(locationMock.foregroundGetResponse);
|
|
172
|
-
},
|
|
173
|
-
requestForegroundPermissionsAsync: () => {
|
|
174
|
-
locationMock.requestCalls.push('foreground');
|
|
175
|
-
|
|
176
|
-
return Promise.resolve(locationMock.foregroundRequestResponse);
|
|
177
|
-
},
|
|
178
|
-
getBackgroundPermissionsAsync: () => {
|
|
179
|
-
locationMock.getCalls.push('background');
|
|
180
|
-
|
|
181
|
-
return Promise.resolve(locationMock.backgroundGetResponse);
|
|
182
|
-
},
|
|
183
|
-
requestBackgroundPermissionsAsync: () => {
|
|
184
|
-
locationMock.requestCalls.push('background');
|
|
185
|
-
|
|
186
|
-
return Promise.resolve(locationMock.backgroundRequestResponse);
|
|
187
|
-
},
|
|
188
|
-
}));
|
|
189
|
-
|
|
190
|
-
void mock.module('expo-notifications', () => ({
|
|
191
|
-
getPermissionsAsync: () => {
|
|
192
|
-
if (notificationsMock.getError !== undefined) {
|
|
193
|
-
throw new Error(notificationsMock.getError);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return Promise.resolve(notificationsMock.getResponse);
|
|
197
|
-
},
|
|
198
|
-
requestPermissionsAsync: () => {
|
|
199
|
-
if (notificationsMock.requestError !== undefined) {
|
|
200
|
-
throw new Error(notificationsMock.requestError);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return Promise.resolve(notificationsMock.requestResponse);
|
|
204
|
-
},
|
|
205
|
-
}));
|
|
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
|
-
|
|
219
|
-
describe('createPermissionClient', () => {
|
|
220
|
-
beforeEach(() => {
|
|
221
|
-
resetExpoMocks();
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
test('covers getStatus and request for every known permission', async () => {
|
|
225
|
-
const client = createPermissionClient();
|
|
226
|
-
|
|
227
|
-
for (const permission of PERMISSIONS) {
|
|
228
|
-
const statusState = await client.getStatus(permission);
|
|
229
|
-
const requestState = await client.request(permission);
|
|
230
|
-
|
|
231
|
-
expect(statusState.permission).toBe(permission);
|
|
232
|
-
expect(statusState.status).toBe('granted');
|
|
233
|
-
expect(statusState.granted).toBe(true);
|
|
234
|
-
|
|
235
|
-
expect(requestState.permission).toBe(permission);
|
|
236
|
-
expect(requestState.status).toBe('granted');
|
|
237
|
-
expect(requestState.granted).toBe(true);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
expect(mediaLibraryMock.getCalls).toEqual([false, true]);
|
|
241
|
-
expect(mediaLibraryMock.requestCalls).toEqual([false, true]);
|
|
242
|
-
expect(locationMock.getCalls).toEqual(['foreground', 'background']);
|
|
243
|
-
expect(locationMock.requestCalls).toEqual(['foreground', 'background']);
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
test('treats clipboard as not requiring a native Expo permission module', async () => {
|
|
247
|
-
const client = createPermissionClient();
|
|
248
|
-
|
|
249
|
-
const statusState = await client.getStatus(Permission.Clipboard);
|
|
250
|
-
const requestState = await client.request(Permission.Clipboard);
|
|
251
|
-
|
|
252
|
-
expect(statusState).toMatchObject({
|
|
253
|
-
permission: Permission.Clipboard,
|
|
254
|
-
status: 'granted',
|
|
255
|
-
granted: true,
|
|
256
|
-
});
|
|
257
|
-
expect(requestState).toMatchObject({
|
|
258
|
-
permission: Permission.Clipboard,
|
|
259
|
-
status: 'granted',
|
|
260
|
-
granted: true,
|
|
261
|
-
});
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
test('falls back to the Camera namespace when camera permission functions are nested', async () => {
|
|
265
|
-
cameraMock.mode = 'cameraNamespace';
|
|
266
|
-
cameraMock.getResponse = permissionResponse('denied', false);
|
|
267
|
-
cameraMock.requestResponse = permissionResponse('granted');
|
|
268
|
-
|
|
269
|
-
const client = createPermissionClient();
|
|
270
|
-
|
|
271
|
-
expect(await client.getStatus(Permission.Camera)).toMatchObject({
|
|
272
|
-
permission: Permission.Camera,
|
|
273
|
-
status: 'blocked',
|
|
274
|
-
granted: false,
|
|
275
|
-
canAskAgain: false,
|
|
276
|
-
reason: 'Permission is blocked. Open system settings to change it.',
|
|
277
|
-
});
|
|
278
|
-
expect(await client.request(Permission.Camera)).toMatchObject({
|
|
279
|
-
permission: Permission.Camera,
|
|
280
|
-
status: 'granted',
|
|
281
|
-
granted: true,
|
|
282
|
-
});
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
test('falls back to AudioModule recording permission functions when top-level exports are absent', async () => {
|
|
286
|
-
audioMock.mode = 'audioModule';
|
|
287
|
-
audioMock.getResponse = permissionResponse('denied', false);
|
|
288
|
-
audioMock.requestResponse = permissionResponse('granted');
|
|
289
|
-
|
|
290
|
-
const client = createPermissionClient();
|
|
291
|
-
|
|
292
|
-
expect(await client.getStatus(Permission.Microphone)).toMatchObject({
|
|
293
|
-
permission: Permission.Microphone,
|
|
294
|
-
status: 'blocked',
|
|
295
|
-
granted: false,
|
|
296
|
-
canAskAgain: false,
|
|
297
|
-
reason: 'Permission is blocked. Open system settings to change it.',
|
|
298
|
-
});
|
|
299
|
-
expect(await client.request(Permission.Microphone)).toMatchObject({
|
|
300
|
-
permission: Permission.Microphone,
|
|
301
|
-
status: 'granted',
|
|
302
|
-
granted: true,
|
|
303
|
-
});
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
test('normalizes unknown adapter statuses to unknown', async () => {
|
|
307
|
-
notificationsMock.getResponse = permissionResponse('mystery-status', false);
|
|
308
|
-
notificationsMock.requestResponse = permissionResponse('denied', false);
|
|
309
|
-
|
|
310
|
-
const client = createPermissionClient();
|
|
311
|
-
const state = await client.getStatus(Permission.Notifications);
|
|
312
|
-
|
|
313
|
-
expect(state).toMatchObject({
|
|
314
|
-
permission: Permission.Notifications,
|
|
315
|
-
status: 'unknown',
|
|
316
|
-
granted: false,
|
|
317
|
-
canAskAgain: false,
|
|
318
|
-
});
|
|
31
|
+
test('Expo client treats clipboard as not requiring a native module', async () => {
|
|
32
|
+
const client = createPermissionClient();
|
|
33
|
+
expect(await client.getStatus(Permission.Clipboard)).toMatchObject({
|
|
34
|
+
permission: Permission.Clipboard,
|
|
35
|
+
status: 'granted',
|
|
36
|
+
granted: true,
|
|
319
37
|
});
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const client = createPermissionClient();
|
|
326
|
-
|
|
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
|
-
}
|
|
38
|
+
expect(await client.request(Permission.Clipboard)).toMatchObject({
|
|
39
|
+
permission: Permission.Clipboard,
|
|
40
|
+
status: 'granted',
|
|
41
|
+
granted: true,
|
|
339
42
|
});
|
|
43
|
+
});
|
|
340
44
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
}
|
|
45
|
+
test('Expo client falls back to the Camera namespace', async () => {
|
|
46
|
+
cameraMock.mode = 'cameraNamespace';
|
|
47
|
+
cameraMock.getResponse = permissionResponse('denied', false);
|
|
48
|
+
cameraMock.requestResponse = permissionResponse('granted');
|
|
49
|
+
const client = createPermissionClient();
|
|
50
|
+
expect(await client.getStatus(Permission.Camera)).toMatchObject({
|
|
51
|
+
permission: Permission.Camera,
|
|
52
|
+
status: 'blocked',
|
|
53
|
+
granted: false,
|
|
54
|
+
canAskAgain: false,
|
|
55
|
+
reason: 'Permission is blocked. Open system settings to change it.',
|
|
367
56
|
});
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
}
|
|
57
|
+
expect(await client.request(Permission.Camera)).toMatchObject({
|
|
58
|
+
permission: Permission.Camera,
|
|
59
|
+
status: 'granted',
|
|
60
|
+
granted: true,
|
|
439
61
|
});
|
|
62
|
+
});
|
|
440
63
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
64
|
+
test('Expo client falls back to AudioModule recording permissions', async () => {
|
|
65
|
+
audioMock.mode = 'audioModule';
|
|
66
|
+
audioMock.getResponse = permissionResponse('denied', false);
|
|
67
|
+
audioMock.requestResponse = permissionResponse('granted');
|
|
68
|
+
const client = createPermissionClient();
|
|
69
|
+
expect(await client.getStatus(Permission.Microphone)).toMatchObject({
|
|
70
|
+
permission: Permission.Microphone,
|
|
71
|
+
status: 'blocked',
|
|
72
|
+
granted: false,
|
|
73
|
+
canAskAgain: false,
|
|
74
|
+
reason: 'Permission is blocked. Open system settings to change it.',
|
|
451
75
|
});
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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);
|
|
76
|
+
expect(await client.request(Permission.Microphone)).toMatchObject({
|
|
77
|
+
permission: Permission.Microphone,
|
|
78
|
+
status: 'granted',
|
|
79
|
+
granted: true,
|
|
461
80
|
});
|
|
81
|
+
});
|
|
462
82
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
expect(error.cause).toBe(cause);
|
|
83
|
+
test('Expo client normalizes unknown adapter statuses', async () => {
|
|
84
|
+
notificationsMock.getResponse = permissionResponse('mystery-status', false);
|
|
85
|
+
const state = await createPermissionClient().getStatus(Permission.Notifications);
|
|
86
|
+
expect(state).toMatchObject({
|
|
87
|
+
permission: Permission.Notifications,
|
|
88
|
+
status: 'unknown',
|
|
89
|
+
granted: false,
|
|
90
|
+
canAskAgain: false,
|
|
472
91
|
});
|
|
92
|
+
});
|
|
473
93
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
94
|
+
test('Expo client normalizes non-requestable denials to blocked', async () => {
|
|
95
|
+
notificationsMock.getResponse = permissionResponse('denied', false);
|
|
96
|
+
notificationsMock.requestResponse = permissionResponse('denied', false);
|
|
97
|
+
const client = createPermissionClient();
|
|
98
|
+
for (const state of [
|
|
99
|
+
await client.getStatus(Permission.Notifications),
|
|
100
|
+
await client.request(Permission.Notifications),
|
|
101
|
+
]) {
|
|
480
102
|
expect(state).toMatchObject({
|
|
481
|
-
permission: Permission.
|
|
482
|
-
status: '
|
|
103
|
+
permission: Permission.Notifications,
|
|
104
|
+
status: 'blocked',
|
|
483
105
|
granted: false,
|
|
484
|
-
|
|
106
|
+
canAskAgain: false,
|
|
107
|
+
reason: 'Permission is blocked. Open system settings to change it.',
|
|
485
108
|
});
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
test('returns unavailable when a request call throws at runtime', async () => {
|
|
489
|
-
audioMock.requestError = 'microphone bridge unavailable';
|
|
490
|
-
|
|
491
|
-
const client = createPermissionClient();
|
|
492
|
-
const state = await client.request(Permission.Microphone);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
493
111
|
|
|
112
|
+
test('Expo client normalizes limited media-library access', async () => {
|
|
113
|
+
mediaLibraryMock.getResponse = {
|
|
114
|
+
status: 'granted',
|
|
115
|
+
canAskAgain: false,
|
|
116
|
+
accessPrivileges: 'limited',
|
|
117
|
+
};
|
|
118
|
+
mediaLibraryMock.requestResponse = mediaLibraryMock.getResponse;
|
|
119
|
+
const client = createPermissionClient();
|
|
120
|
+
for (const state of [
|
|
121
|
+
await client.getStatus(Permission.MediaLibrary),
|
|
122
|
+
await client.request(Permission.MediaLibrary),
|
|
123
|
+
]) {
|
|
494
124
|
expect(state).toMatchObject({
|
|
495
|
-
permission: Permission.
|
|
496
|
-
status: '
|
|
497
|
-
granted:
|
|
498
|
-
|
|
125
|
+
permission: Permission.MediaLibrary,
|
|
126
|
+
status: 'limited',
|
|
127
|
+
granted: true,
|
|
128
|
+
canAskAgain: false,
|
|
129
|
+
reason: 'Media library access is limited to user-selected assets.',
|
|
499
130
|
});
|
|
500
|
-
});
|
|
501
|
-
});
|
|
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
131
|
}
|
|
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
|
-
|
|
523
|
-
function resetExpoMocks(): void {
|
|
524
|
-
cameraMock.mode = 'topLevel';
|
|
525
|
-
cameraMock.getResponse = permissionResponse('granted');
|
|
526
|
-
cameraMock.requestResponse = permissionResponse('granted');
|
|
527
|
-
cameraMock.getError = undefined;
|
|
528
|
-
cameraMock.requestError = undefined;
|
|
529
|
-
|
|
530
|
-
audioMock.mode = 'topLevel';
|
|
531
|
-
audioMock.getResponse = permissionResponse('granted');
|
|
532
|
-
audioMock.requestResponse = permissionResponse('granted');
|
|
533
|
-
audioMock.getError = undefined;
|
|
534
|
-
audioMock.requestError = undefined;
|
|
535
|
-
|
|
536
|
-
mediaLibraryMock.getResponse = permissionResponse('granted');
|
|
537
|
-
mediaLibraryMock.requestResponse = permissionResponse('granted');
|
|
538
|
-
mediaLibraryMock.getError = undefined;
|
|
539
|
-
mediaLibraryMock.requestError = undefined;
|
|
540
|
-
mediaLibraryMock.getCalls.length = 0;
|
|
541
|
-
mediaLibraryMock.requestCalls.length = 0;
|
|
542
|
-
|
|
543
|
-
locationMock.foregroundGetResponse = permissionResponse('granted');
|
|
544
|
-
locationMock.foregroundRequestResponse = permissionResponse('granted');
|
|
545
|
-
locationMock.backgroundGetResponse = permissionResponse('granted');
|
|
546
|
-
locationMock.backgroundRequestResponse = permissionResponse('granted');
|
|
547
|
-
locationMock.getCalls.length = 0;
|
|
548
|
-
locationMock.requestCalls.length = 0;
|
|
549
|
-
|
|
550
|
-
notificationsMock.getResponse = permissionResponse('granted');
|
|
551
|
-
notificationsMock.requestResponse = permissionResponse('granted');
|
|
552
|
-
notificationsMock.getError = undefined;
|
|
553
|
-
notificationsMock.requestError = undefined;
|
|
554
|
-
|
|
555
|
-
linkingMock.calls = 0;
|
|
556
|
-
linkingMock.error = undefined;
|
|
557
|
-
}
|
|
132
|
+
});
|