@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.
Files changed (55) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +143 -0
  4. package/dist/client/types.d.ts +19 -0
  5. package/dist/client/types.d.ts.map +1 -0
  6. package/dist/client/types.js +2 -0
  7. package/dist/client/types.js.map +1 -0
  8. package/dist/index.d.ts +12 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +6 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/manager/createPermissionManager.d.ts +31 -0
  13. package/dist/manager/createPermissionManager.d.ts.map +1 -0
  14. package/dist/manager/createPermissionManager.js +39 -0
  15. package/dist/manager/createPermissionManager.js.map +1 -0
  16. package/dist/metadata/index.d.ts +8 -0
  17. package/dist/metadata/index.d.ts.map +1 -0
  18. package/dist/metadata/index.js +8 -0
  19. package/dist/metadata/index.js.map +1 -0
  20. package/dist/react/index.d.ts +66 -0
  21. package/dist/react/index.d.ts.map +1 -0
  22. package/dist/react/index.js +91 -0
  23. package/dist/react/index.js.map +1 -0
  24. package/dist/registry/permissions.d.ts +52 -0
  25. package/dist/registry/permissions.d.ts.map +1 -0
  26. package/dist/registry/permissions.js +108 -0
  27. package/dist/registry/permissions.js.map +1 -0
  28. package/dist/state/permissionState.d.ts +43 -0
  29. package/dist/state/permissionState.d.ts.map +1 -0
  30. package/dist/state/permissionState.js +44 -0
  31. package/dist/state/permissionState.js.map +1 -0
  32. package/dist/testing/index.d.ts +49 -0
  33. package/dist/testing/index.d.ts.map +1 -0
  34. package/dist/testing/index.js +95 -0
  35. package/dist/testing/index.js.map +1 -0
  36. package/dist/web/index.d.ts +60 -0
  37. package/dist/web/index.d.ts.map +1 -0
  38. package/dist/web/index.js +234 -0
  39. package/dist/web/index.js.map +1 -0
  40. package/package.json +114 -0
  41. package/src/client/types.ts +19 -0
  42. package/src/index.ts +31 -0
  43. package/src/manager/createPermissionManager.test.ts +67 -0
  44. package/src/manager/createPermissionManager.ts +54 -0
  45. package/src/metadata/index.ts +7 -0
  46. package/src/react/index.test.ts +11 -0
  47. package/src/react/index.tsx +170 -0
  48. package/src/registry/permissions.test.ts +37 -0
  49. package/src/registry/permissions.ts +128 -0
  50. package/src/state/permissionState.test.ts +54 -0
  51. package/src/state/permissionState.ts +86 -0
  52. package/src/testing/index.test.ts +57 -0
  53. package/src/testing/index.ts +168 -0
  54. package/src/web/index.test.ts +205 -0
  55. package/src/web/index.ts +390 -0
@@ -0,0 +1,170 @@
1
+ import {
2
+ createContext,
3
+ createElement,
4
+ type ReactNode,
5
+ useCallback,
6
+ useContext,
7
+ useEffect,
8
+ useMemo,
9
+ useState,
10
+ } from 'react';
11
+
12
+ import type { PermissionClient } from '../client/types';
13
+ import {
14
+ createPermissionManager,
15
+ type PermissionManager,
16
+ } from '../manager/createPermissionManager';
17
+ import type { Permission } from '../registry/permissions';
18
+ import {
19
+ createPermissionState,
20
+ normalizePermissionState,
21
+ type PermissionState,
22
+ type PermissionStatus,
23
+ } from '../state/permissionState';
24
+
25
+ const PermissionsContext = createContext<PermissionManager | undefined>(undefined);
26
+
27
+ /***
28
+ * Props accepted by `PermissionsProvider`.
29
+ */
30
+ export interface PermissionsProviderProps {
31
+ readonly children: ReactNode;
32
+ readonly client?: PermissionClient;
33
+ readonly manager?: PermissionManager;
34
+ }
35
+
36
+ /***
37
+ * Options for `usePermission`.
38
+ */
39
+ export interface UsePermissionOptions {
40
+ readonly initialState?: PermissionState;
41
+ readonly refreshOnMount?: boolean;
42
+ }
43
+
44
+ /***
45
+ * Result returned by `usePermission`.
46
+ */
47
+ export interface PermissionHookResult {
48
+ readonly permission: Permission;
49
+ readonly status: PermissionStatus;
50
+ readonly canAskAgain?: boolean;
51
+ readonly granted: boolean;
52
+ readonly requestedAt?: Date;
53
+ readonly reason?: string;
54
+ readonly state: PermissionState;
55
+ readonly refresh: () => Promise<PermissionState>;
56
+ readonly request: () => Promise<PermissionState>;
57
+ readonly openSettings?: () => Promise<void>;
58
+ }
59
+
60
+ /***
61
+ * Provides a permission manager to React hooks.
62
+ *
63
+ * @readme
64
+ * React helpers are framework-neutral. They depend on React only and do not
65
+ * import browser, Expo, or React Native permission APIs.
66
+ */
67
+ export function PermissionsProvider({
68
+ children,
69
+ client,
70
+ manager,
71
+ }: PermissionsProviderProps): ReactNode {
72
+ const resolvedManager = useMemo(() => {
73
+ if (manager !== undefined) {
74
+ return manager;
75
+ }
76
+
77
+ if (client !== undefined) {
78
+ return createPermissionManager(client);
79
+ }
80
+
81
+ throw new Error('PermissionsProvider requires a client or manager.');
82
+ }, [client, manager]);
83
+
84
+ return createElement(PermissionsContext.Provider, { value: resolvedManager }, children);
85
+ }
86
+
87
+ /***
88
+ * Reads the current permission manager from context.
89
+ */
90
+ export function usePermissions(): PermissionManager {
91
+ const manager = useContext(PermissionsContext);
92
+
93
+ if (manager === undefined) {
94
+ throw new Error('usePermissions must be used within PermissionsProvider.');
95
+ }
96
+
97
+ return manager;
98
+ }
99
+
100
+ /***
101
+ * Tracks a single permission and exposes explicit refresh/request actions.
102
+ *
103
+ * @usage
104
+ * ```tsx
105
+ * const camera = usePermission(Permission.Camera, { refreshOnMount: true });
106
+ *
107
+ * return (
108
+ * <Button
109
+ * disabled={camera.granted}
110
+ * onPress={() => {
111
+ * void camera.request();
112
+ * }}
113
+ * />
114
+ * );
115
+ * ```
116
+ */
117
+ export function usePermission(
118
+ permission: Permission,
119
+ options: UsePermissionOptions = {},
120
+ ): PermissionHookResult {
121
+ const manager = usePermissions();
122
+ const [state, setState] = useState<PermissionState>(
123
+ () =>
124
+ (options.initialState === undefined
125
+ ? undefined
126
+ : normalizePermissionState(permission, options.initialState)) ??
127
+ createPermissionState({
128
+ permission,
129
+ status: 'unknown',
130
+ }),
131
+ );
132
+
133
+ const refresh = useCallback(async () => {
134
+ const nextState = await manager.getStatus(permission);
135
+ setState(nextState);
136
+
137
+ return nextState;
138
+ }, [manager, permission]);
139
+
140
+ const request = useCallback(async () => {
141
+ const nextState = await manager.request(permission);
142
+ setState(nextState);
143
+
144
+ return nextState;
145
+ }, [manager, permission]);
146
+
147
+ useEffect(() => {
148
+ if (options.refreshOnMount === true) {
149
+ void refresh();
150
+ }
151
+ }, [options.refreshOnMount, refresh]);
152
+
153
+ const openSettings =
154
+ manager.openSettings === undefined
155
+ ? undefined
156
+ : () => manager.openSettings?.() ?? Promise.resolve();
157
+
158
+ return {
159
+ permission: state.permission,
160
+ status: state.status,
161
+ canAskAgain: state.canAskAgain,
162
+ granted: state.granted,
163
+ requestedAt: state.requestedAt,
164
+ reason: state.reason,
165
+ state,
166
+ refresh,
167
+ request,
168
+ openSettings,
169
+ };
170
+ }
@@ -0,0 +1,37 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import {
4
+ getPermissionDefinition,
5
+ isPermission,
6
+ Permission,
7
+ PERMISSION_REGISTRY,
8
+ PERMISSIONS,
9
+ } from './permissions';
10
+
11
+ describe('permission registry', () => {
12
+ test('contains the initial stable permissions', () => {
13
+ expect(PERMISSIONS).toEqual([
14
+ Permission.Camera,
15
+ Permission.Microphone,
16
+ Permission.MediaLibrary,
17
+ Permission.MediaLibraryWrite,
18
+ Permission.LocationForeground,
19
+ Permission.LocationBackground,
20
+ Permission.Notifications,
21
+ Permission.Clipboard,
22
+ ]);
23
+ });
24
+
25
+ test('exposes metadata for every permission', () => {
26
+ for (const permission of PERMISSIONS) {
27
+ expect(PERMISSION_REGISTRY[permission].permission).toBe(permission);
28
+ expect(PERMISSION_REGISTRY[permission].label.length).toBeGreaterThan(0);
29
+ expect(getPermissionDefinition(permission)).toEqual(PERMISSION_REGISTRY[permission]);
30
+ }
31
+ });
32
+
33
+ test('validates permission strings', () => {
34
+ expect(isPermission('camera')).toBe(true);
35
+ expect(isPermission('not-a-permission')).toBe(false);
36
+ });
37
+ });
@@ -0,0 +1,128 @@
1
+ /***
2
+ * Common runtime permissions supported by the registry.
3
+ *
4
+ * @readme
5
+ * The registry is intentionally platform-neutral. Adapters translate each
6
+ * permission into whatever a browser, React Native app, Expo app, or test
7
+ * environment can actually check or request.
8
+ */
9
+ export enum Permission {
10
+ Camera = 'camera',
11
+ Microphone = 'microphone',
12
+ MediaLibrary = 'mediaLibrary',
13
+ MediaLibraryWrite = 'mediaLibraryWrite',
14
+ LocationForeground = 'locationForeground',
15
+ LocationBackground = 'locationBackground',
16
+ Notifications = 'notifications',
17
+ Clipboard = 'clipboard',
18
+ }
19
+
20
+ /***
21
+ * Broad environment categories where a permission can be meaningful.
22
+ */
23
+ export type PermissionEnvironment = 'web' | 'native' | 'test';
24
+
25
+ /***
26
+ * Describes a known permission without coupling the registry to a platform SDK.
27
+ */
28
+ export interface PermissionDefinition {
29
+ readonly permission: Permission;
30
+ readonly label: string;
31
+ readonly description: string;
32
+ readonly environments: readonly PermissionEnvironment[];
33
+ }
34
+
35
+ /***
36
+ * Stable ordered list of known permissions.
37
+ */
38
+ export const PERMISSIONS = [
39
+ Permission.Camera,
40
+ Permission.Microphone,
41
+ Permission.MediaLibrary,
42
+ Permission.MediaLibraryWrite,
43
+ Permission.LocationForeground,
44
+ Permission.LocationBackground,
45
+ Permission.Notifications,
46
+ Permission.Clipboard,
47
+ ] as const;
48
+
49
+ const PERMISSION_SET = new Set<string>(PERMISSIONS);
50
+
51
+ /***
52
+ * Metadata for known permissions.
53
+ */
54
+ export const PERMISSION_REGISTRY: Readonly<Record<Permission, PermissionDefinition>> = {
55
+ [Permission.Camera]: {
56
+ permission: Permission.Camera,
57
+ label: 'Camera',
58
+ description: 'Access to a device camera for capture or scanning workflows.',
59
+ environments: ['web', 'native', 'test'],
60
+ },
61
+ [Permission.Microphone]: {
62
+ permission: Permission.Microphone,
63
+ label: 'Microphone',
64
+ description: 'Access to a device microphone for audio recording or calls.',
65
+ environments: ['web', 'native', 'test'],
66
+ },
67
+ [Permission.MediaLibrary]: {
68
+ permission: Permission.MediaLibrary,
69
+ label: 'Media library',
70
+ description: 'Read access to photos, videos, or other local media assets.',
71
+ environments: ['native', 'test'],
72
+ },
73
+ [Permission.MediaLibraryWrite]: {
74
+ permission: Permission.MediaLibraryWrite,
75
+ label: 'Media library write',
76
+ description: 'Write access for saving media to a local library.',
77
+ environments: ['native', 'test'],
78
+ },
79
+ [Permission.LocationForeground]: {
80
+ permission: Permission.LocationForeground,
81
+ label: 'Foreground location',
82
+ description: 'Location access while the app or page is actively in use.',
83
+ environments: ['web', 'native', 'test'],
84
+ },
85
+ [Permission.LocationBackground]: {
86
+ permission: Permission.LocationBackground,
87
+ label: 'Background location',
88
+ description: 'Location access while the app is not actively in use.',
89
+ environments: ['native', 'test'],
90
+ },
91
+ [Permission.Notifications]: {
92
+ permission: Permission.Notifications,
93
+ label: 'Notifications',
94
+ description: 'Permission to show user-visible notifications.',
95
+ environments: ['web', 'native', 'test'],
96
+ },
97
+ [Permission.Clipboard]: {
98
+ permission: Permission.Clipboard,
99
+ label: 'Clipboard',
100
+ description: 'Read or write access to clipboard contents.',
101
+ environments: ['web', 'native', 'test'],
102
+ },
103
+ };
104
+
105
+ /***
106
+ * Returns true when a string is a registered permission identifier.
107
+ */
108
+ export function isPermission(value: string): value is Permission {
109
+ return PERMISSION_SET.has(value);
110
+ }
111
+
112
+ /***
113
+ * Throws for programmer errors where an invalid permission reaches runtime.
114
+ */
115
+ export function assertKnownPermission(permission: string): Permission {
116
+ if (!isPermission(permission)) {
117
+ throw new TypeError(`Unknown permission: ${permission}`);
118
+ }
119
+
120
+ return permission;
121
+ }
122
+
123
+ /***
124
+ * Reads registry metadata for a known permission.
125
+ */
126
+ export function getPermissionDefinition(permission: Permission): PermissionDefinition {
127
+ return PERMISSION_REGISTRY[assertKnownPermission(permission)];
128
+ }
@@ -0,0 +1,54 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { Permission } from '../registry/permissions';
4
+ import {
5
+ createPermissionState,
6
+ isPermissionStatus,
7
+ normalizePermissionState,
8
+ PERMISSION_STATUSES,
9
+ } from './permissionState';
10
+
11
+ describe('permission state', () => {
12
+ test('recognizes normalized statuses', () => {
13
+ for (const status of PERMISSION_STATUSES) {
14
+ expect(isPermissionStatus(status)).toBe(true);
15
+ }
16
+
17
+ expect(isPermissionStatus('prompt')).toBe(false);
18
+ });
19
+
20
+ test('derives granted from status', () => {
21
+ expect(
22
+ createPermissionState({
23
+ permission: Permission.Camera,
24
+ status: 'granted',
25
+ }).granted,
26
+ ).toBe(true);
27
+
28
+ expect(
29
+ createPermissionState({
30
+ permission: Permission.Camera,
31
+ status: 'denied',
32
+ }).granted,
33
+ ).toBe(false);
34
+ });
35
+
36
+ test('normalizes adapter results to the requested permission', () => {
37
+ const state = normalizePermissionState(Permission.Camera, {
38
+ permission: Permission.Microphone,
39
+ status: 'limited',
40
+ granted: false,
41
+ canAskAgain: true,
42
+ reason: 'library-limited',
43
+ });
44
+
45
+ expect(state).toEqual({
46
+ permission: Permission.Camera,
47
+ status: 'limited',
48
+ granted: false,
49
+ canAskAgain: true,
50
+ requestedAt: undefined,
51
+ reason: 'library-limited',
52
+ });
53
+ });
54
+ });
@@ -0,0 +1,86 @@
1
+ import type { Permission } from '../registry/permissions';
2
+
3
+ /***
4
+ * Normalized permission statuses shared by adapters and UI code.
5
+ *
6
+ * Denial is normal control flow. Unexpected adapter failures may still throw,
7
+ * but user denial should be represented as a permission state.
8
+ */
9
+ export type PermissionStatus =
10
+ | 'unknown'
11
+ | 'granted'
12
+ | 'denied'
13
+ | 'blocked'
14
+ | 'limited'
15
+ | 'unavailable';
16
+
17
+ /***
18
+ * Stable ordered list of normalized statuses.
19
+ */
20
+ export const PERMISSION_STATUSES = [
21
+ 'unknown',
22
+ 'granted',
23
+ 'denied',
24
+ 'blocked',
25
+ 'limited',
26
+ 'unavailable',
27
+ ] as const satisfies readonly PermissionStatus[];
28
+
29
+ const PERMISSION_STATUS_SET = new Set<string>(PERMISSION_STATUSES);
30
+
31
+ /***
32
+ * Normalized result returned by permission clients and managers.
33
+ */
34
+ export interface PermissionState {
35
+ readonly permission: Permission;
36
+ readonly status: PermissionStatus;
37
+ readonly canAskAgain?: boolean;
38
+ readonly granted: boolean;
39
+ readonly requestedAt?: Date;
40
+ readonly reason?: string;
41
+ }
42
+
43
+ export interface CreatePermissionStateInput {
44
+ readonly permission: Permission;
45
+ readonly status: PermissionStatus;
46
+ readonly canAskAgain?: boolean;
47
+ readonly requestedAt?: Date;
48
+ readonly reason?: string;
49
+ }
50
+
51
+ /***
52
+ * Returns true when a string is one of the normalized statuses.
53
+ */
54
+ export function isPermissionStatus(value: string): value is PermissionStatus {
55
+ return PERMISSION_STATUS_SET.has(value);
56
+ }
57
+
58
+ /***
59
+ * Creates a normalized state and derives the `granted` convenience flag.
60
+ */
61
+ export function createPermissionState(input: CreatePermissionStateInput): PermissionState {
62
+ return {
63
+ permission: input.permission,
64
+ status: input.status,
65
+ canAskAgain: input.canAskAgain,
66
+ granted: input.status === 'granted',
67
+ requestedAt: input.requestedAt,
68
+ reason: input.reason,
69
+ };
70
+ }
71
+
72
+ /***
73
+ * Normalizes an adapter result to the requested permission and status shape.
74
+ */
75
+ export function normalizePermissionState(
76
+ permission: Permission,
77
+ state: PermissionState,
78
+ ): PermissionState {
79
+ return createPermissionState({
80
+ permission,
81
+ status: state.status,
82
+ canAskAgain: state.canAskAgain,
83
+ requestedAt: state.requestedAt,
84
+ reason: state.reason,
85
+ });
86
+ }
@@ -0,0 +1,57 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { Permission, PERMISSIONS } from '../registry/permissions';
4
+ import { createFakePermissionClient } from './index';
5
+
6
+ describe('createFakePermissionClient', () => {
7
+ test('defaults every permission to unknown', async () => {
8
+ const client = createFakePermissionClient();
9
+
10
+ expect(client.getSnapshot()).toHaveLength(PERMISSIONS.length);
11
+ const state = await client.getStatus(Permission.Camera);
12
+
13
+ expect(state).toMatchObject({
14
+ status: 'unknown',
15
+ granted: false,
16
+ canAskAgain: true,
17
+ });
18
+ });
19
+
20
+ test('stores status updates', async () => {
21
+ const client = createFakePermissionClient();
22
+
23
+ client.setStatus(Permission.Clipboard, 'blocked', {
24
+ canAskAgain: false,
25
+ reason: 'policy',
26
+ });
27
+
28
+ const state = await client.getStatus(Permission.Clipboard);
29
+
30
+ expect(state).toMatchObject({
31
+ status: 'blocked',
32
+ canAskAgain: false,
33
+ reason: 'policy',
34
+ });
35
+ });
36
+
37
+ test('uses deterministic request results and clock', async () => {
38
+ const requestedAt = new Date('2026-06-08T12:00:00.000Z');
39
+ const client = createFakePermissionClient({
40
+ now: () => requestedAt,
41
+ requestStates: [
42
+ {
43
+ permission: Permission.Camera,
44
+ status: 'granted',
45
+ },
46
+ ],
47
+ });
48
+
49
+ const result = await client.request(Permission.Camera);
50
+
51
+ expect(result).toMatchObject({
52
+ status: 'granted',
53
+ granted: true,
54
+ requestedAt,
55
+ });
56
+ });
57
+ });
@@ -0,0 +1,168 @@
1
+ import type { PermissionClient } from '../client/types';
2
+ import type { Permission } from '../registry/permissions';
3
+ import { assertKnownPermission, PERMISSIONS } from '../registry/permissions';
4
+ import {
5
+ createPermissionState,
6
+ type PermissionState,
7
+ type PermissionStatus,
8
+ } from '../state/permissionState';
9
+
10
+ /***
11
+ * Seed format accepted by the fake client.
12
+ */
13
+ export type FakePermissionStateSeed =
14
+ | PermissionState
15
+ | {
16
+ readonly permission: Permission;
17
+ readonly status: PermissionStatus;
18
+ readonly canAskAgain?: boolean;
19
+ readonly reason?: string;
20
+ };
21
+
22
+ /***
23
+ * Options for deterministic fake clients.
24
+ */
25
+ export interface FakePermissionClientOptions {
26
+ readonly initialStates?: readonly FakePermissionStateSeed[];
27
+ readonly requestStates?: readonly FakePermissionStateSeed[];
28
+ readonly now?: () => Date;
29
+ readonly openSettings?: () => Promise<void>;
30
+ }
31
+
32
+ /***
33
+ * Test client that stores permission states in memory.
34
+ */
35
+ export interface FakePermissionClient extends PermissionClient {
36
+ setState(state: FakePermissionStateSeed): void;
37
+ setStatus(
38
+ permission: Permission,
39
+ status: PermissionStatus,
40
+ options?: {
41
+ readonly canAskAgain?: boolean;
42
+ readonly reason?: string;
43
+ },
44
+ ): void;
45
+ getSnapshot(): readonly PermissionState[];
46
+ }
47
+
48
+ /***
49
+ * Creates a deterministic in-memory client for tests and examples.
50
+ *
51
+ * @readme
52
+ * Fake clients make permission flows testable without native devices, browser
53
+ * prompts, simulators, or network access.
54
+ *
55
+ * @usage
56
+ * ```ts
57
+ * const client = createFakePermissionClient({
58
+ * initialStates: [{ permission: Permission.Camera, status: 'denied' }],
59
+ * });
60
+ * await client.request(Permission.Camera);
61
+ * ```
62
+ */
63
+ export function createFakePermissionClient(
64
+ options: FakePermissionClientOptions = {},
65
+ ): FakePermissionClient {
66
+ const now = options.now ?? (() => new Date());
67
+ const states = new Map<Permission, PermissionState>();
68
+ const requestStates = new Map<Permission, PermissionState>();
69
+
70
+ for (const permission of PERMISSIONS) {
71
+ states.set(
72
+ permission,
73
+ createPermissionState({
74
+ permission,
75
+ status: 'unknown',
76
+ canAskAgain: true,
77
+ }),
78
+ );
79
+ }
80
+
81
+ for (const state of options.initialStates ?? []) {
82
+ states.set(state.permission, normalizeSeed(state));
83
+ }
84
+
85
+ for (const state of options.requestStates ?? []) {
86
+ requestStates.set(state.permission, normalizeSeed(state));
87
+ }
88
+
89
+ const client: FakePermissionClient = {
90
+ getStatus(permission) {
91
+ return Promise.resolve(getStoredState(states, assertKnownPermission(permission)));
92
+ },
93
+
94
+ request(permission) {
95
+ const knownPermission = assertKnownPermission(permission);
96
+ const requestState =
97
+ requestStates.get(knownPermission) ?? getStoredState(states, knownPermission);
98
+ const nextState = createPermissionState({
99
+ permission: knownPermission,
100
+ status: requestState.status,
101
+ canAskAgain: requestState.canAskAgain,
102
+ requestedAt: now(),
103
+ reason: requestState.reason,
104
+ });
105
+
106
+ states.set(knownPermission, nextState);
107
+
108
+ return Promise.resolve(nextState);
109
+ },
110
+
111
+ openSettings: options.openSettings,
112
+
113
+ setState(state) {
114
+ const normalizedState = normalizeSeed(state);
115
+ states.set(normalizedState.permission, normalizedState);
116
+ },
117
+
118
+ setStatus(permission, status, statusOptions = {}) {
119
+ const knownPermission = assertKnownPermission(permission);
120
+ states.set(
121
+ knownPermission,
122
+ createPermissionState({
123
+ permission: knownPermission,
124
+ status,
125
+ canAskAgain: statusOptions.canAskAgain,
126
+ reason: statusOptions.reason,
127
+ }),
128
+ );
129
+ },
130
+
131
+ getSnapshot() {
132
+ return PERMISSIONS.map((permission) => getStoredState(states, permission));
133
+ },
134
+ };
135
+
136
+ if (options.openSettings === undefined) {
137
+ delete client.openSettings;
138
+ }
139
+
140
+ return client;
141
+ }
142
+
143
+ function normalizeSeed(seed: FakePermissionStateSeed): PermissionState {
144
+ return createPermissionState({
145
+ permission: assertKnownPermission(seed.permission),
146
+ status: seed.status,
147
+ canAskAgain: seed.canAskAgain,
148
+ requestedAt: 'requestedAt' in seed ? seed.requestedAt : undefined,
149
+ reason: seed.reason,
150
+ });
151
+ }
152
+
153
+ function getStoredState(
154
+ states: ReadonlyMap<Permission, PermissionState>,
155
+ permission: Permission,
156
+ ): PermissionState {
157
+ const state = states.get(permission);
158
+
159
+ if (state === undefined) {
160
+ return createPermissionState({
161
+ permission,
162
+ status: 'unknown',
163
+ canAskAgain: true,
164
+ });
165
+ }
166
+
167
+ return state;
168
+ }