1/**
2 * Effect service for the browser Permissions API.
3 *
4 * This module defines a `Permissions` service backed by
5 * `navigator.permissions`. The service exposes `query`, which returns the
6 * browser `PermissionStatus` for a permission name. Failed browser operations
7 * are represented as `PermissionsError` values with `InvalidStateError` or
8 * `TypeError` reasons, and `layer` provides the browser-backed service.
9 *
10 * @since 4.0.0
11 */
12import * as Context from "effect/Context"
13import * as Data from "effect/Data"
14import * as Effect from "effect/Effect"
15import * as Layer from "effect/Layer"
16
17const TypeId = "~@effect/platform-browser/Permissions"
18const ErrorTypeId = "~@effect/platform-browser/Permissions/PermissionsError"
19
20/**
21 * Wrapper on the Permission API (`navigator.permissions`) with methods for
22 * querying status of permissions.
23 *
24 * @category services
25 * @since 4.0.0
26 */
27export interface Permissions {
28 readonly [TypeId]: typeof TypeId
29
30 /**
31 * Returns the state of a user permission on the global scope.
32 */
33 readonly query: <Name extends PermissionName>(
34 name: Name
35 ) => Effect.Effect<
36 // `name` is identical to the name passed to Permissions.query
37 // https://developer.mozilla.org/en-US/docs/Web/API/PermissionStatus
38 Omit<PermissionStatus, "name"> & { name: Name },
39 PermissionsError
40 >
41}
42
43/**
44 * Error reason for an `InvalidStateError` raised by the browser Permissions API.
45 *
46 * @category errors
47 * @since 4.0.0
48 */
49export class PermissionsInvalidStateError extends Data.TaggedError("InvalidStateError")<{
50 readonly cause: unknown
51}> {
52 override get message(): string {
53 return this._tag
54 }
55}
56
57/**
58 * Error reason for a `TypeError` raised by the browser Permissions API.
59 *
60 * @category errors
61 * @since 4.0.0
62 */
63export class PermissionsTypeError extends Data.TaggedError("TypeError")<{
64 readonly cause: unknown
65}> {
66 override get message(): string {
67 return this._tag
68 }
69}
70
71/**
72 * Union of browser Permissions API error reasons represented by the service.
73 *
74 * @category errors
75 * @since 4.0.0
76 */
77export type PermissionsErrorReason = PermissionsInvalidStateError | PermissionsTypeError
78
79/**
80 * Tagged error wrapping a browser Permissions API failure reason.
81 *
82 * @category errors
83 * @since 4.0.0
84 */
85export class PermissionsError extends Data.TaggedError("PermissionsError")<{
86 readonly reason: PermissionsErrorReason
87}> {
88 constructor(props: { readonly reason: PermissionsErrorReason }) {
89 super({
90 ...props,
91 cause: props.reason.cause
92 } as any)
93 }
94
95 readonly [ErrorTypeId] = ErrorTypeId
96
97 override get message(): string {
98 return this.reason.message
99 }
100}
101
102/**
103 * Service tag for browser permission querying.
104 *
105 * **When to use**
106 *
107 * Use when you need to require or provide browser permission querying through
108 * Effect's context.
109 *
110 * @category services
111 * @since 4.0.0
112 */
113export const Permissions: Context.Service<Permissions, Permissions> = Context.Service<Permissions>(TypeId)
114
115/**
116 * Provides the `Permissions` service using the browser `navigator.permissions` API.
117 *
118 * **When to use**
119 *
120 * Use when you need a live browser `Permissions` service backed by the ambient
121 * `navigator.permissions` implementation.
122 *
123 * **Details**
124 *
125 * `query` delegates to `navigator.permissions.query({ name })` and wraps
126 * rejected browser operations in `PermissionsError`.
127 *
128 * @category layers
129 * @since 4.0.0
130 */
131export const layer: Layer.Layer<Permissions> = Layer.succeed(
132 Permissions,
133 Permissions.of({
134 [TypeId]: TypeId,
135 query: (name) =>
136 Effect.tryPromise({
137 try: () => navigator.permissions.query({ name }) as Promise<any>,
138 catch: (cause) =>
139 new PermissionsError({
140 reason: cause instanceof DOMException
141 ? new PermissionsInvalidStateError({ cause })
142 : new PermissionsTypeError({ cause })
143 })
144 })
145 })
146)