@usefy/use-permission 0.20.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/README.md +124 -0
- package/dist/index.d.mts +145 -0
- package/dist/index.d.ts +145 -0
- package/dist/index.js +103 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +74 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/mirunamu00/usefy/master/assets/logo.png" alt="usefy logo" width="120" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">@usefy/use-permission</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>Permissions API status with live updates — SSR-safe, typed, and race-safe.</strong>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://www.npmjs.com/package/@usefy/use-permission"><img src="https://img.shields.io/npm/v/@usefy/use-permission.svg?style=flat-square&color=007acc" alt="npm version" /></a>
|
|
13
|
+
<a href="https://www.npmjs.com/package/@usefy/use-permission"><img src="https://img.shields.io/npm/dm/@usefy/use-permission.svg?style=flat-square&color=007acc" alt="npm downloads" /></a>
|
|
14
|
+
<a href="https://bundlephobia.com/package/@usefy/use-permission"><img src="https://img.shields.io/bundlephobia/minzip/@usefy/use-permission?style=flat-square&color=007acc" alt="bundle size" /></a>
|
|
15
|
+
<a href="https://github.com/mirunamu00/usefy/blob/master/LICENSE"><img src="https://img.shields.io/npm/l/@usefy/use-permission.svg?style=flat-square&color=007acc" alt="license" /></a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p align="center">
|
|
19
|
+
<a href="#installation">Installation</a> •
|
|
20
|
+
<a href="#quick-start">Quick Start</a> •
|
|
21
|
+
<a href="#api">API</a> •
|
|
22
|
+
<a href="#testing">Testing</a> •
|
|
23
|
+
<a href="#license">License</a>
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
<p align="center">
|
|
27
|
+
<a href="https://mirunamu00.github.io/usefy/?path=/docs/hooks-usepermission--docs" target="_blank" rel="noopener noreferrer">
|
|
28
|
+
<strong>📚 View Storybook Demo</strong>
|
|
29
|
+
</a>
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Overview
|
|
35
|
+
|
|
36
|
+
`usePermission` is part of the [@usefy](https://www.npmjs.com/org/usefy) ecosystem — a collection of production-ready, TypeScript-first, SSR-safe React hooks. It reads the [Permissions API](https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API) status of a permission and keeps it up to date live, so your UI reacts when the user grants or revokes access.
|
|
37
|
+
|
|
38
|
+
## Features
|
|
39
|
+
|
|
40
|
+
- **Live updates** — subscribes to the `PermissionStatus` `change` event, so `state` reflects grant/revoke without polling or a re-mount.
|
|
41
|
+
- **Rich, honest return** — `{ state, status, isSupported, error }` makes the async, unsupported, and error paths explicit instead of hiding them behind a bare `PermissionState`.
|
|
42
|
+
- **Any permission name** — accepts a superset of `PermissionDescriptor`, so `camera`, `microphone`, `push`, `midi`, and future names all typecheck.
|
|
43
|
+
- **Stable descriptor identity** — keyed on the descriptor's serialized contents, so an inline `{ name: 'camera' }` literal does not re-query every render (no caller-side `useMemo`).
|
|
44
|
+
- **SSR-safe & StrictMode-safe** — reports `unsupported` on the server; the async query is race-guarded and the change listener is cleaned up on unmount.
|
|
45
|
+
- **TypeScript-first** — full type inference and exported types.
|
|
46
|
+
- **Tiny & tree-shakeable** — zero dependencies, published as its own package.
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# npm
|
|
52
|
+
npm install @usefy/use-permission
|
|
53
|
+
|
|
54
|
+
# yarn
|
|
55
|
+
yarn add @usefy/use-permission
|
|
56
|
+
|
|
57
|
+
# pnpm
|
|
58
|
+
pnpm add @usefy/use-permission
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Requires React 18 or 19 (`peerDependencies: "react": "^18.0.0 || ^19.0.0"`).
|
|
62
|
+
|
|
63
|
+
## Quick Start
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
import { usePermission } from "@usefy/use-permission";
|
|
67
|
+
|
|
68
|
+
function CameraStatus() {
|
|
69
|
+
const { state, status } = usePermission({ name: "camera" });
|
|
70
|
+
|
|
71
|
+
// Branch on `status` (deterministic "idle" on the first render, both on the
|
|
72
|
+
// server and client) so the UI is hydration-safe.
|
|
73
|
+
if (status === "idle" || status === "pending") return <span>Checking…</span>;
|
|
74
|
+
if (status === "unsupported") return <span>Permissions API unavailable</span>;
|
|
75
|
+
if (status === "error") return <span>Could not read camera permission</span>;
|
|
76
|
+
|
|
77
|
+
return <span>Camera: {state}</span>; // 'granted' | 'denied' | 'prompt'
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## API
|
|
82
|
+
|
|
83
|
+
### `usePermission(descriptor)`
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
function usePermission(descriptor: UsePermissionDescriptor): UsePermissionReturn;
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
#### Parameters
|
|
90
|
+
|
|
91
|
+
| Param | Type | Description |
|
|
92
|
+
| ----- | ---- | ----------- |
|
|
93
|
+
| `descriptor` | `UsePermissionDescriptor` | The permission to query, e.g. `{ name: 'camera' }`, `{ name: 'geolocation' }`, `{ name: 'push', userVisibleOnly: true }`, `{ name: 'midi', sysex: true }`. `name` accepts the standard `PermissionName` values (with autocomplete) plus any browser-specific string. Pass an inline literal freely — the hook keys re-queries on the descriptor's serialized contents, not its object identity. |
|
|
94
|
+
|
|
95
|
+
#### Returns — `UsePermissionReturn`
|
|
96
|
+
|
|
97
|
+
| Field | Type | Description |
|
|
98
|
+
| ----- | ---- | ----------- |
|
|
99
|
+
| `state` | `PermissionState \| null` | The raw permission state (`'granted' \| 'denied' \| 'prompt'`), or `null` until the first query resolves and whenever the API is unsupported or the query errored. |
|
|
100
|
+
| `status` | `UsePermissionStatus` | Coarse lifecycle: `'idle' \| 'pending' \| 'granted' \| 'denied' \| 'prompt' \| 'unsupported' \| 'error'`. The three permission values mirror `state`, so you can branch on either. |
|
|
101
|
+
| `isSupported` | `boolean` | Whether `navigator.permissions.query` is available. Starts `false` (on the server and the first client render, for hydration safety) and becomes `true` after mount in a supporting browser. Branch your first render on `status` (`'idle'`), not `isSupported`. |
|
|
102
|
+
| `error` | `Error \| null` | The error thrown by `navigator.permissions.query()`, if the query rejected (e.g. an unknown permission name in a browser that throws). |
|
|
103
|
+
|
|
104
|
+
#### Helpers
|
|
105
|
+
|
|
106
|
+
- `isPermissionsSupported(): boolean` — capability check used internally; exported for feature-detection.
|
|
107
|
+
- `serializeDescriptor(descriptor): string` — the stable-key function the hook uses to decide when to re-query (exported for advanced use; not part of the umbrella surface).
|
|
108
|
+
|
|
109
|
+
### Notes on behavior
|
|
110
|
+
|
|
111
|
+
- **Descriptor identity.** `usePermission({ name: 'camera' })` receives a fresh object literal every render. Rather than require callers to `useMemo` it, the hook derives a stable string key from the descriptor's own fields (sorted, JSON-serialized) and keys its effect on that. It re-queries only when the meaningful contents change, e.g. `name` `'camera'` → `'microphone'` or `userVisibleOnly` `true` → `false`.
|
|
112
|
+
- **Unsupported.** If `navigator.permissions` is missing (SSR or an unsupporting browser), `status` is `'unsupported'`, `isSupported` is `false`, and `state` is `null`.
|
|
113
|
+
- **Errors.** Some browsers reject `query()` for permission names they don't recognize (e.g. `camera` in Firefox) instead of returning `'denied'`. That surfaces as `status: 'error'` with the thrown `error`.
|
|
114
|
+
- **Race safety.** If the component unmounts while a query is in flight, the stale resolution/rejection is ignored — no state update after unmount — and the `change` listener is always removed on cleanup.
|
|
115
|
+
|
|
116
|
+
## Testing
|
|
117
|
+
|
|
118
|
+
📊 <a href="https://mirunamu00.github.io/usefy/coverage/use-permission/src/index.html" target="_blank" rel="noopener noreferrer"><strong>View Detailed Coverage Report</strong></a> (GitHub Pages) — **18 tests**, 100% statement coverage.
|
|
119
|
+
|
|
120
|
+
## License
|
|
121
|
+
|
|
122
|
+
MIT © [mirunamu](https://github.com/mirunamu00)
|
|
123
|
+
|
|
124
|
+
This package is part of the [usefy](https://github.com/mirunamu00/usefy) monorepo.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coarse lifecycle status for a permission query.
|
|
3
|
+
*
|
|
4
|
+
* - `idle` — no query has resolved yet (initial render, before the effect runs).
|
|
5
|
+
* - `pending` — a `navigator.permissions.query()` call is in flight.
|
|
6
|
+
* - `granted` / `denied` / `prompt` — the resolved {@link PermissionState}. These
|
|
7
|
+
* mirror the raw `state` field, so you can branch on either.
|
|
8
|
+
* - `unsupported` — the Permissions API is unavailable in this environment
|
|
9
|
+
* (SSR, or a browser without `navigator.permissions`).
|
|
10
|
+
* - `error` — the query rejected (e.g. an unknown permission name in a browser
|
|
11
|
+
* that throws instead of returning `denied`). See {@link UsePermissionReturn.error}.
|
|
12
|
+
*/
|
|
13
|
+
type UsePermissionStatus = "idle" | "pending" | "granted" | "denied" | "prompt" | "unsupported" | "error";
|
|
14
|
+
/**
|
|
15
|
+
* Permission descriptor accepted by {@link usePermission}.
|
|
16
|
+
*
|
|
17
|
+
* A superset of the DOM `PermissionDescriptor` so that *all* permission names
|
|
18
|
+
* typecheck — including the ones missing from the standard `PermissionName`
|
|
19
|
+
* union in `lib.dom` (`'camera'`, `'microphone'`, `'clipboard-read'`, …) and the
|
|
20
|
+
* extra fields required by specific permissions (`userVisibleOnly` for `'push'`,
|
|
21
|
+
* `sysex` for `'midi'`, device fields for `'camera'`). The index signature keeps
|
|
22
|
+
* it forward-compatible with descriptor fields not yet in the lib types.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const camera: UsePermissionDescriptor = { name: "camera" };
|
|
27
|
+
* const push: UsePermissionDescriptor = { name: "push", userVisibleOnly: true };
|
|
28
|
+
* const midi: UsePermissionDescriptor = { name: "midi", sysex: true };
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
interface UsePermissionDescriptor {
|
|
32
|
+
/**
|
|
33
|
+
* The permission name. Accepts the standard `PermissionName` values with
|
|
34
|
+
* autocomplete, plus any other string (e.g. `'camera'`, `'microphone'`) that a
|
|
35
|
+
* given browser supports.
|
|
36
|
+
*/
|
|
37
|
+
name: PermissionName | (string & {});
|
|
38
|
+
/** Required for the `'push'` permission — must be `true`, browsers reject `false`. */
|
|
39
|
+
userVisibleOnly?: boolean;
|
|
40
|
+
/** Requests SysEx access for the `'midi'` permission. */
|
|
41
|
+
sysex?: boolean;
|
|
42
|
+
/** Forward-compatible with descriptor fields not yet in the lib types. */
|
|
43
|
+
[key: string]: unknown;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Return value of {@link usePermission}.
|
|
47
|
+
*
|
|
48
|
+
* Destructure the field you need — the roadmap's `const { state } = usePermission(...)`
|
|
49
|
+
* reads naturally, while `status`, `isSupported`, and `error` cover the async and
|
|
50
|
+
* unsupported/error edges that a bare `PermissionState` cannot express.
|
|
51
|
+
*/
|
|
52
|
+
interface UsePermissionReturn {
|
|
53
|
+
/**
|
|
54
|
+
* The raw permission state (`'granted' | 'denied' | 'prompt'`), or `null` until
|
|
55
|
+
* the first query resolves and whenever the API is unsupported or the query
|
|
56
|
+
* errored.
|
|
57
|
+
*/
|
|
58
|
+
state: PermissionState | null;
|
|
59
|
+
/** Coarse lifecycle status. See {@link UsePermissionStatus}. */
|
|
60
|
+
status: UsePermissionStatus;
|
|
61
|
+
/** Whether the Permissions API is available in the current environment. */
|
|
62
|
+
isSupported: boolean;
|
|
63
|
+
/** The error thrown by `navigator.permissions.query()`, if the query rejected. */
|
|
64
|
+
error: Error | null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Track the status of a Permissions API permission, with live updates.
|
|
69
|
+
*
|
|
70
|
+
* Calls `navigator.permissions.query(descriptor)` and subscribes to the returned
|
|
71
|
+
* `PermissionStatus`'s `change` event, so the returned `state` reflects the user
|
|
72
|
+
* granting or revoking the permission without a re-mount. The hook is SSR-safe
|
|
73
|
+
* (reports `unsupported` on the server) and StrictMode/concurrent-safe (the async
|
|
74
|
+
* query is race-guarded and the listener is cleaned up on unmount).
|
|
75
|
+
*
|
|
76
|
+
* **Descriptor identity:** the effect is keyed on a *serialized* copy of the
|
|
77
|
+
* descriptor's fields (`name`, `userVisibleOnly`, `sysex`, …), not on the object
|
|
78
|
+
* reference. So passing an inline literal — `usePermission({ name: 'camera' })` —
|
|
79
|
+
* does not re-query on every render; it re-queries only when the descriptor's
|
|
80
|
+
* contents actually change. No `useMemo` on the caller's side is required.
|
|
81
|
+
*
|
|
82
|
+
* @param descriptor - The permission to query, e.g. `{ name: 'camera' }`,
|
|
83
|
+
* `{ name: 'geolocation' }`, `{ name: 'push', userVisibleOnly: true }`,
|
|
84
|
+
* `{ name: 'midi', sysex: true }`. Accepts any permission name (the standard
|
|
85
|
+
* ones plus browser-specific ones like `'camera'`/`'microphone'`).
|
|
86
|
+
* @returns `{ state, status, isSupported, error }` — see {@link UsePermissionReturn}.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```tsx
|
|
90
|
+
* function CameraBadge() {
|
|
91
|
+
* const { state, status } = usePermission({ name: "camera" });
|
|
92
|
+
*
|
|
93
|
+
* // Branch on `status` (deterministic "idle" on the first render, both on the
|
|
94
|
+
* // server and client) so the UI is hydration-safe.
|
|
95
|
+
* if (status === "idle" || status === "pending") return <span>Checking…</span>;
|
|
96
|
+
* if (status === "unsupported") return <span>Permissions API unavailable</span>;
|
|
97
|
+
* if (status === "error") return <span>Could not read camera permission</span>;
|
|
98
|
+
*
|
|
99
|
+
* return <span>Camera: {state}</span>; // 'granted' | 'denied' | 'prompt'
|
|
100
|
+
* }
|
|
101
|
+
* ```
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```tsx
|
|
105
|
+
* // Live updates: the badge re-renders when the user changes the permission in
|
|
106
|
+
* // the browser UI — no polling, no re-mount.
|
|
107
|
+
* function MicIndicator() {
|
|
108
|
+
* const { state } = usePermission({ name: "microphone" });
|
|
109
|
+
* return <div data-granted={state === "granted"}>Mic: {state ?? "unknown"}</div>;
|
|
110
|
+
* }
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
declare function usePermission(descriptor: UsePermissionDescriptor): UsePermissionReturn;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Whether the Permissions API (`navigator.permissions.query`) is available in the
|
|
117
|
+
* current environment. Returns `false` during SSR (no `navigator`) and in browsers
|
|
118
|
+
* that do not implement the Permissions API.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* if (isPermissionsSupported()) {
|
|
123
|
+
* // safe to call navigator.permissions.query(...)
|
|
124
|
+
* }
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
declare function isPermissionsSupported(): boolean;
|
|
128
|
+
/**
|
|
129
|
+
* Produce a stable string key from a permission descriptor's meaningful fields.
|
|
130
|
+
*
|
|
131
|
+
* `usePermission` keys its effect on this string, not on the descriptor object's
|
|
132
|
+
* reference — so an inline `usePermission({ name: 'camera' })` (a fresh object
|
|
133
|
+
* literal every render) does **not** re-query on every render. It re-queries only
|
|
134
|
+
* when the descriptor's actual contents change. Keys are sorted so field order in
|
|
135
|
+
* the literal does not affect the result.
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```ts
|
|
139
|
+
* serializeDescriptor({ name: "midi", sysex: true }); // '{"name":"midi","sysex":true}'
|
|
140
|
+
* serializeDescriptor({ sysex: true, name: "midi" }); // same string
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
declare function serializeDescriptor(descriptor: UsePermissionDescriptor): string;
|
|
144
|
+
|
|
145
|
+
export { type UsePermissionDescriptor, type UsePermissionReturn, type UsePermissionStatus, isPermissionsSupported, serializeDescriptor, usePermission };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coarse lifecycle status for a permission query.
|
|
3
|
+
*
|
|
4
|
+
* - `idle` — no query has resolved yet (initial render, before the effect runs).
|
|
5
|
+
* - `pending` — a `navigator.permissions.query()` call is in flight.
|
|
6
|
+
* - `granted` / `denied` / `prompt` — the resolved {@link PermissionState}. These
|
|
7
|
+
* mirror the raw `state` field, so you can branch on either.
|
|
8
|
+
* - `unsupported` — the Permissions API is unavailable in this environment
|
|
9
|
+
* (SSR, or a browser without `navigator.permissions`).
|
|
10
|
+
* - `error` — the query rejected (e.g. an unknown permission name in a browser
|
|
11
|
+
* that throws instead of returning `denied`). See {@link UsePermissionReturn.error}.
|
|
12
|
+
*/
|
|
13
|
+
type UsePermissionStatus = "idle" | "pending" | "granted" | "denied" | "prompt" | "unsupported" | "error";
|
|
14
|
+
/**
|
|
15
|
+
* Permission descriptor accepted by {@link usePermission}.
|
|
16
|
+
*
|
|
17
|
+
* A superset of the DOM `PermissionDescriptor` so that *all* permission names
|
|
18
|
+
* typecheck — including the ones missing from the standard `PermissionName`
|
|
19
|
+
* union in `lib.dom` (`'camera'`, `'microphone'`, `'clipboard-read'`, …) and the
|
|
20
|
+
* extra fields required by specific permissions (`userVisibleOnly` for `'push'`,
|
|
21
|
+
* `sysex` for `'midi'`, device fields for `'camera'`). The index signature keeps
|
|
22
|
+
* it forward-compatible with descriptor fields not yet in the lib types.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const camera: UsePermissionDescriptor = { name: "camera" };
|
|
27
|
+
* const push: UsePermissionDescriptor = { name: "push", userVisibleOnly: true };
|
|
28
|
+
* const midi: UsePermissionDescriptor = { name: "midi", sysex: true };
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
interface UsePermissionDescriptor {
|
|
32
|
+
/**
|
|
33
|
+
* The permission name. Accepts the standard `PermissionName` values with
|
|
34
|
+
* autocomplete, plus any other string (e.g. `'camera'`, `'microphone'`) that a
|
|
35
|
+
* given browser supports.
|
|
36
|
+
*/
|
|
37
|
+
name: PermissionName | (string & {});
|
|
38
|
+
/** Required for the `'push'` permission — must be `true`, browsers reject `false`. */
|
|
39
|
+
userVisibleOnly?: boolean;
|
|
40
|
+
/** Requests SysEx access for the `'midi'` permission. */
|
|
41
|
+
sysex?: boolean;
|
|
42
|
+
/** Forward-compatible with descriptor fields not yet in the lib types. */
|
|
43
|
+
[key: string]: unknown;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Return value of {@link usePermission}.
|
|
47
|
+
*
|
|
48
|
+
* Destructure the field you need — the roadmap's `const { state } = usePermission(...)`
|
|
49
|
+
* reads naturally, while `status`, `isSupported`, and `error` cover the async and
|
|
50
|
+
* unsupported/error edges that a bare `PermissionState` cannot express.
|
|
51
|
+
*/
|
|
52
|
+
interface UsePermissionReturn {
|
|
53
|
+
/**
|
|
54
|
+
* The raw permission state (`'granted' | 'denied' | 'prompt'`), or `null` until
|
|
55
|
+
* the first query resolves and whenever the API is unsupported or the query
|
|
56
|
+
* errored.
|
|
57
|
+
*/
|
|
58
|
+
state: PermissionState | null;
|
|
59
|
+
/** Coarse lifecycle status. See {@link UsePermissionStatus}. */
|
|
60
|
+
status: UsePermissionStatus;
|
|
61
|
+
/** Whether the Permissions API is available in the current environment. */
|
|
62
|
+
isSupported: boolean;
|
|
63
|
+
/** The error thrown by `navigator.permissions.query()`, if the query rejected. */
|
|
64
|
+
error: Error | null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Track the status of a Permissions API permission, with live updates.
|
|
69
|
+
*
|
|
70
|
+
* Calls `navigator.permissions.query(descriptor)` and subscribes to the returned
|
|
71
|
+
* `PermissionStatus`'s `change` event, so the returned `state` reflects the user
|
|
72
|
+
* granting or revoking the permission without a re-mount. The hook is SSR-safe
|
|
73
|
+
* (reports `unsupported` on the server) and StrictMode/concurrent-safe (the async
|
|
74
|
+
* query is race-guarded and the listener is cleaned up on unmount).
|
|
75
|
+
*
|
|
76
|
+
* **Descriptor identity:** the effect is keyed on a *serialized* copy of the
|
|
77
|
+
* descriptor's fields (`name`, `userVisibleOnly`, `sysex`, …), not on the object
|
|
78
|
+
* reference. So passing an inline literal — `usePermission({ name: 'camera' })` —
|
|
79
|
+
* does not re-query on every render; it re-queries only when the descriptor's
|
|
80
|
+
* contents actually change. No `useMemo` on the caller's side is required.
|
|
81
|
+
*
|
|
82
|
+
* @param descriptor - The permission to query, e.g. `{ name: 'camera' }`,
|
|
83
|
+
* `{ name: 'geolocation' }`, `{ name: 'push', userVisibleOnly: true }`,
|
|
84
|
+
* `{ name: 'midi', sysex: true }`. Accepts any permission name (the standard
|
|
85
|
+
* ones plus browser-specific ones like `'camera'`/`'microphone'`).
|
|
86
|
+
* @returns `{ state, status, isSupported, error }` — see {@link UsePermissionReturn}.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```tsx
|
|
90
|
+
* function CameraBadge() {
|
|
91
|
+
* const { state, status } = usePermission({ name: "camera" });
|
|
92
|
+
*
|
|
93
|
+
* // Branch on `status` (deterministic "idle" on the first render, both on the
|
|
94
|
+
* // server and client) so the UI is hydration-safe.
|
|
95
|
+
* if (status === "idle" || status === "pending") return <span>Checking…</span>;
|
|
96
|
+
* if (status === "unsupported") return <span>Permissions API unavailable</span>;
|
|
97
|
+
* if (status === "error") return <span>Could not read camera permission</span>;
|
|
98
|
+
*
|
|
99
|
+
* return <span>Camera: {state}</span>; // 'granted' | 'denied' | 'prompt'
|
|
100
|
+
* }
|
|
101
|
+
* ```
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```tsx
|
|
105
|
+
* // Live updates: the badge re-renders when the user changes the permission in
|
|
106
|
+
* // the browser UI — no polling, no re-mount.
|
|
107
|
+
* function MicIndicator() {
|
|
108
|
+
* const { state } = usePermission({ name: "microphone" });
|
|
109
|
+
* return <div data-granted={state === "granted"}>Mic: {state ?? "unknown"}</div>;
|
|
110
|
+
* }
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
declare function usePermission(descriptor: UsePermissionDescriptor): UsePermissionReturn;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Whether the Permissions API (`navigator.permissions.query`) is available in the
|
|
117
|
+
* current environment. Returns `false` during SSR (no `navigator`) and in browsers
|
|
118
|
+
* that do not implement the Permissions API.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* if (isPermissionsSupported()) {
|
|
123
|
+
* // safe to call navigator.permissions.query(...)
|
|
124
|
+
* }
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
declare function isPermissionsSupported(): boolean;
|
|
128
|
+
/**
|
|
129
|
+
* Produce a stable string key from a permission descriptor's meaningful fields.
|
|
130
|
+
*
|
|
131
|
+
* `usePermission` keys its effect on this string, not on the descriptor object's
|
|
132
|
+
* reference — so an inline `usePermission({ name: 'camera' })` (a fresh object
|
|
133
|
+
* literal every render) does **not** re-query on every render. It re-queries only
|
|
134
|
+
* when the descriptor's actual contents change. Keys are sorted so field order in
|
|
135
|
+
* the literal does not affect the result.
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```ts
|
|
139
|
+
* serializeDescriptor({ name: "midi", sysex: true }); // '{"name":"midi","sysex":true}'
|
|
140
|
+
* serializeDescriptor({ sysex: true, name: "midi" }); // same string
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
declare function serializeDescriptor(descriptor: UsePermissionDescriptor): string;
|
|
144
|
+
|
|
145
|
+
export { type UsePermissionDescriptor, type UsePermissionReturn, type UsePermissionStatus, isPermissionsSupported, serializeDescriptor, usePermission };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
isPermissionsSupported: () => isPermissionsSupported,
|
|
24
|
+
serializeDescriptor: () => serializeDescriptor,
|
|
25
|
+
usePermission: () => usePermission
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
|
|
29
|
+
// src/usePermission.ts
|
|
30
|
+
var import_react = require("react");
|
|
31
|
+
|
|
32
|
+
// src/utils.ts
|
|
33
|
+
function isPermissionsSupported() {
|
|
34
|
+
return typeof navigator !== "undefined" && typeof navigator.permissions !== "undefined" && typeof navigator.permissions.query === "function";
|
|
35
|
+
}
|
|
36
|
+
function serializeDescriptor(descriptor) {
|
|
37
|
+
const keys = Object.keys(descriptor).sort();
|
|
38
|
+
const normalized = {};
|
|
39
|
+
for (const key of keys) {
|
|
40
|
+
normalized[key] = descriptor[key];
|
|
41
|
+
}
|
|
42
|
+
return JSON.stringify(normalized);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/usePermission.ts
|
|
46
|
+
function usePermission(descriptor) {
|
|
47
|
+
const [state, setState] = (0, import_react.useState)(null);
|
|
48
|
+
const [status, setStatus] = (0, import_react.useState)("idle");
|
|
49
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
50
|
+
const [isSupported, setIsSupported] = (0, import_react.useState)(false);
|
|
51
|
+
const descriptorRef = (0, import_react.useRef)(descriptor);
|
|
52
|
+
descriptorRef.current = descriptor;
|
|
53
|
+
const serialized = serializeDescriptor(descriptor);
|
|
54
|
+
(0, import_react.useEffect)(() => {
|
|
55
|
+
const supported = isPermissionsSupported();
|
|
56
|
+
setIsSupported(supported);
|
|
57
|
+
if (!supported) {
|
|
58
|
+
setState(null);
|
|
59
|
+
setStatus("unsupported");
|
|
60
|
+
setError(null);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
let cancelled = false;
|
|
64
|
+
let permissionStatus = null;
|
|
65
|
+
let changeHandler = null;
|
|
66
|
+
setState(null);
|
|
67
|
+
setStatus("pending");
|
|
68
|
+
setError(null);
|
|
69
|
+
navigator.permissions.query(descriptorRef.current).then((result) => {
|
|
70
|
+
if (cancelled) return;
|
|
71
|
+
permissionStatus = result;
|
|
72
|
+
setState(result.state);
|
|
73
|
+
setStatus(result.state);
|
|
74
|
+
changeHandler = () => {
|
|
75
|
+
setState(result.state);
|
|
76
|
+
setStatus(result.state);
|
|
77
|
+
};
|
|
78
|
+
result.addEventListener("change", changeHandler);
|
|
79
|
+
}).catch((err) => {
|
|
80
|
+
if (cancelled) return;
|
|
81
|
+
setState(null);
|
|
82
|
+
setStatus("error");
|
|
83
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
84
|
+
});
|
|
85
|
+
return () => {
|
|
86
|
+
cancelled = true;
|
|
87
|
+
if (permissionStatus && changeHandler) {
|
|
88
|
+
permissionStatus.removeEventListener("change", changeHandler);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}, [serialized]);
|
|
92
|
+
return (0, import_react.useMemo)(
|
|
93
|
+
() => ({ state, status, isSupported, error }),
|
|
94
|
+
[state, status, isSupported, error]
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
98
|
+
0 && (module.exports = {
|
|
99
|
+
isPermissionsSupported,
|
|
100
|
+
serializeDescriptor,
|
|
101
|
+
usePermission
|
|
102
|
+
});
|
|
103
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/usePermission.ts","../src/utils.ts"],"sourcesContent":["export { usePermission } from \"./usePermission\";\nexport { isPermissionsSupported, serializeDescriptor } from \"./utils\";\nexport type {\n UsePermissionStatus,\n UsePermissionDescriptor,\n UsePermissionReturn,\n} from \"./types\";\n","import { useEffect, useMemo, useRef, useState } from \"react\";\nimport type {\n UsePermissionDescriptor,\n UsePermissionReturn,\n UsePermissionStatus,\n} from \"./types\";\nimport { isPermissionsSupported, serializeDescriptor } from \"./utils\";\n\n/**\n * Track the status of a Permissions API permission, with live updates.\n *\n * Calls `navigator.permissions.query(descriptor)` and subscribes to the returned\n * `PermissionStatus`'s `change` event, so the returned `state` reflects the user\n * granting or revoking the permission without a re-mount. The hook is SSR-safe\n * (reports `unsupported` on the server) and StrictMode/concurrent-safe (the async\n * query is race-guarded and the listener is cleaned up on unmount).\n *\n * **Descriptor identity:** the effect is keyed on a *serialized* copy of the\n * descriptor's fields (`name`, `userVisibleOnly`, `sysex`, …), not on the object\n * reference. So passing an inline literal — `usePermission({ name: 'camera' })` —\n * does not re-query on every render; it re-queries only when the descriptor's\n * contents actually change. No `useMemo` on the caller's side is required.\n *\n * @param descriptor - The permission to query, e.g. `{ name: 'camera' }`,\n * `{ name: 'geolocation' }`, `{ name: 'push', userVisibleOnly: true }`,\n * `{ name: 'midi', sysex: true }`. Accepts any permission name (the standard\n * ones plus browser-specific ones like `'camera'`/`'microphone'`).\n * @returns `{ state, status, isSupported, error }` — see {@link UsePermissionReturn}.\n *\n * @example\n * ```tsx\n * function CameraBadge() {\n * const { state, status } = usePermission({ name: \"camera\" });\n *\n * // Branch on `status` (deterministic \"idle\" on the first render, both on the\n * // server and client) so the UI is hydration-safe.\n * if (status === \"idle\" || status === \"pending\") return <span>Checking…</span>;\n * if (status === \"unsupported\") return <span>Permissions API unavailable</span>;\n * if (status === \"error\") return <span>Could not read camera permission</span>;\n *\n * return <span>Camera: {state}</span>; // 'granted' | 'denied' | 'prompt'\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Live updates: the badge re-renders when the user changes the permission in\n * // the browser UI — no polling, no re-mount.\n * function MicIndicator() {\n * const { state } = usePermission({ name: \"microphone\" });\n * return <div data-granted={state === \"granted\"}>Mic: {state ?? \"unknown\"}</div>;\n * }\n * ```\n */\nexport function usePermission(\n descriptor: UsePermissionDescriptor\n): UsePermissionReturn {\n // Every returned field starts deterministic (identical on server and the\n // first client render) so the hook never causes a hydration mismatch; the\n // effect corrects them after mount. In particular `isSupported` is NOT\n // computed in render — that would be `false` on the server and `true` on the\n // client's first paint, mismatching any UI that branches on it.\n const [state, setState] = useState<PermissionState | null>(null);\n const [status, setStatus] = useState<UsePermissionStatus>(\"idle\");\n const [error, setError] = useState<Error | null>(null);\n const [isSupported, setIsSupported] = useState(false);\n\n // Read the latest descriptor from a ref inside the effect. The effect is keyed\n // on the serialized descriptor (a primitive), so it re-runs on content changes\n // while `descriptorRef` guarantees the query uses the current fields.\n const descriptorRef = useRef(descriptor);\n descriptorRef.current = descriptor;\n\n const serialized = serializeDescriptor(descriptor);\n\n useEffect(() => {\n const supported = isPermissionsSupported();\n setIsSupported(supported);\n if (!supported) {\n setState(null);\n setStatus(\"unsupported\");\n setError(null);\n return;\n }\n\n let cancelled = false;\n let permissionStatus: PermissionStatus | null = null;\n let changeHandler: (() => void) | null = null;\n\n // Clear any prior permission's value so consumers don't read stale `state`\n // while the new query for a changed descriptor is in flight.\n setState(null);\n setStatus(\"pending\");\n setError(null);\n\n navigator.permissions\n .query(descriptorRef.current as PermissionDescriptor)\n .then((result) => {\n if (cancelled) return;\n permissionStatus = result;\n setState(result.state);\n setStatus(result.state);\n\n changeHandler = () => {\n // Read the fresh value off the live PermissionStatus object.\n setState(result.state);\n setStatus(result.state);\n };\n result.addEventListener(\"change\", changeHandler);\n })\n .catch((err: unknown) => {\n if (cancelled) return;\n setState(null);\n setStatus(\"error\");\n setError(err instanceof Error ? err : new Error(String(err)));\n });\n\n return () => {\n cancelled = true;\n if (permissionStatus && changeHandler) {\n permissionStatus.removeEventListener(\"change\", changeHandler);\n }\n };\n // `serialized` is the stable primitive key; `descriptorRef`/setters are stable.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [serialized]);\n\n return useMemo<UsePermissionReturn>(\n () => ({ state, status, isSupported, error }),\n [state, status, isSupported, error]\n );\n}\n","import type { UsePermissionDescriptor } from \"./types\";\n\n/**\n * Whether the Permissions API (`navigator.permissions.query`) is available in the\n * current environment. Returns `false` during SSR (no `navigator`) and in browsers\n * that do not implement the Permissions API.\n *\n * @example\n * ```ts\n * if (isPermissionsSupported()) {\n * // safe to call navigator.permissions.query(...)\n * }\n * ```\n */\nexport function isPermissionsSupported(): boolean {\n return (\n typeof navigator !== \"undefined\" &&\n typeof navigator.permissions !== \"undefined\" &&\n typeof navigator.permissions.query === \"function\"\n );\n}\n\n/**\n * Produce a stable string key from a permission descriptor's meaningful fields.\n *\n * `usePermission` keys its effect on this string, not on the descriptor object's\n * reference — so an inline `usePermission({ name: 'camera' })` (a fresh object\n * literal every render) does **not** re-query on every render. It re-queries only\n * when the descriptor's actual contents change. Keys are sorted so field order in\n * the literal does not affect the result.\n *\n * @example\n * ```ts\n * serializeDescriptor({ name: \"midi\", sysex: true }); // '{\"name\":\"midi\",\"sysex\":true}'\n * serializeDescriptor({ sysex: true, name: \"midi\" }); // same string\n * ```\n */\nexport function serializeDescriptor(descriptor: UsePermissionDescriptor): string {\n const keys = Object.keys(descriptor).sort();\n const normalized: Record<string, unknown> = {};\n for (const key of keys) {\n normalized[key] = descriptor[key];\n }\n return JSON.stringify(normalized);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,mBAAqD;;;ACc9C,SAAS,yBAAkC;AAChD,SACE,OAAO,cAAc,eACrB,OAAO,UAAU,gBAAgB,eACjC,OAAO,UAAU,YAAY,UAAU;AAE3C;AAiBO,SAAS,oBAAoB,YAA6C;AAC/E,QAAM,OAAO,OAAO,KAAK,UAAU,EAAE,KAAK;AAC1C,QAAM,aAAsC,CAAC;AAC7C,aAAW,OAAO,MAAM;AACtB,eAAW,GAAG,IAAI,WAAW,GAAG;AAAA,EAClC;AACA,SAAO,KAAK,UAAU,UAAU;AAClC;;;ADUO,SAAS,cACd,YACqB;AAMrB,QAAM,CAAC,OAAO,QAAQ,QAAI,uBAAiC,IAAI;AAC/D,QAAM,CAAC,QAAQ,SAAS,QAAI,uBAA8B,MAAM;AAChE,QAAM,CAAC,OAAO,QAAQ,QAAI,uBAAuB,IAAI;AACrD,QAAM,CAAC,aAAa,cAAc,QAAI,uBAAS,KAAK;AAKpD,QAAM,oBAAgB,qBAAO,UAAU;AACvC,gBAAc,UAAU;AAExB,QAAM,aAAa,oBAAoB,UAAU;AAEjD,8BAAU,MAAM;AACd,UAAM,YAAY,uBAAuB;AACzC,mBAAe,SAAS;AACxB,QAAI,CAAC,WAAW;AACd,eAAS,IAAI;AACb,gBAAU,aAAa;AACvB,eAAS,IAAI;AACb;AAAA,IACF;AAEA,QAAI,YAAY;AAChB,QAAI,mBAA4C;AAChD,QAAI,gBAAqC;AAIzC,aAAS,IAAI;AACb,cAAU,SAAS;AACnB,aAAS,IAAI;AAEb,cAAU,YACP,MAAM,cAAc,OAA+B,EACnD,KAAK,CAAC,WAAW;AAChB,UAAI,UAAW;AACf,yBAAmB;AACnB,eAAS,OAAO,KAAK;AACrB,gBAAU,OAAO,KAAK;AAEtB,sBAAgB,MAAM;AAEpB,iBAAS,OAAO,KAAK;AACrB,kBAAU,OAAO,KAAK;AAAA,MACxB;AACA,aAAO,iBAAiB,UAAU,aAAa;AAAA,IACjD,CAAC,EACA,MAAM,CAAC,QAAiB;AACvB,UAAI,UAAW;AACf,eAAS,IAAI;AACb,gBAAU,OAAO;AACjB,eAAS,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;AAAA,IAC9D,CAAC;AAEH,WAAO,MAAM;AACX,kBAAY;AACZ,UAAI,oBAAoB,eAAe;AACrC,yBAAiB,oBAAoB,UAAU,aAAa;AAAA,MAC9D;AAAA,IACF;AAAA,EAGF,GAAG,CAAC,UAAU,CAAC;AAEf,aAAO;AAAA,IACL,OAAO,EAAE,OAAO,QAAQ,aAAa,MAAM;AAAA,IAC3C,CAAC,OAAO,QAAQ,aAAa,KAAK;AAAA,EACpC;AACF;","names":[]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// src/usePermission.ts
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
|
|
4
|
+
// src/utils.ts
|
|
5
|
+
function isPermissionsSupported() {
|
|
6
|
+
return typeof navigator !== "undefined" && typeof navigator.permissions !== "undefined" && typeof navigator.permissions.query === "function";
|
|
7
|
+
}
|
|
8
|
+
function serializeDescriptor(descriptor) {
|
|
9
|
+
const keys = Object.keys(descriptor).sort();
|
|
10
|
+
const normalized = {};
|
|
11
|
+
for (const key of keys) {
|
|
12
|
+
normalized[key] = descriptor[key];
|
|
13
|
+
}
|
|
14
|
+
return JSON.stringify(normalized);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/usePermission.ts
|
|
18
|
+
function usePermission(descriptor) {
|
|
19
|
+
const [state, setState] = useState(null);
|
|
20
|
+
const [status, setStatus] = useState("idle");
|
|
21
|
+
const [error, setError] = useState(null);
|
|
22
|
+
const [isSupported, setIsSupported] = useState(false);
|
|
23
|
+
const descriptorRef = useRef(descriptor);
|
|
24
|
+
descriptorRef.current = descriptor;
|
|
25
|
+
const serialized = serializeDescriptor(descriptor);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const supported = isPermissionsSupported();
|
|
28
|
+
setIsSupported(supported);
|
|
29
|
+
if (!supported) {
|
|
30
|
+
setState(null);
|
|
31
|
+
setStatus("unsupported");
|
|
32
|
+
setError(null);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
let cancelled = false;
|
|
36
|
+
let permissionStatus = null;
|
|
37
|
+
let changeHandler = null;
|
|
38
|
+
setState(null);
|
|
39
|
+
setStatus("pending");
|
|
40
|
+
setError(null);
|
|
41
|
+
navigator.permissions.query(descriptorRef.current).then((result) => {
|
|
42
|
+
if (cancelled) return;
|
|
43
|
+
permissionStatus = result;
|
|
44
|
+
setState(result.state);
|
|
45
|
+
setStatus(result.state);
|
|
46
|
+
changeHandler = () => {
|
|
47
|
+
setState(result.state);
|
|
48
|
+
setStatus(result.state);
|
|
49
|
+
};
|
|
50
|
+
result.addEventListener("change", changeHandler);
|
|
51
|
+
}).catch((err) => {
|
|
52
|
+
if (cancelled) return;
|
|
53
|
+
setState(null);
|
|
54
|
+
setStatus("error");
|
|
55
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
56
|
+
});
|
|
57
|
+
return () => {
|
|
58
|
+
cancelled = true;
|
|
59
|
+
if (permissionStatus && changeHandler) {
|
|
60
|
+
permissionStatus.removeEventListener("change", changeHandler);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}, [serialized]);
|
|
64
|
+
return useMemo(
|
|
65
|
+
() => ({ state, status, isSupported, error }),
|
|
66
|
+
[state, status, isSupported, error]
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
export {
|
|
70
|
+
isPermissionsSupported,
|
|
71
|
+
serializeDescriptor,
|
|
72
|
+
usePermission
|
|
73
|
+
};
|
|
74
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/usePermission.ts","../src/utils.ts"],"sourcesContent":["import { useEffect, useMemo, useRef, useState } from \"react\";\nimport type {\n UsePermissionDescriptor,\n UsePermissionReturn,\n UsePermissionStatus,\n} from \"./types\";\nimport { isPermissionsSupported, serializeDescriptor } from \"./utils\";\n\n/**\n * Track the status of a Permissions API permission, with live updates.\n *\n * Calls `navigator.permissions.query(descriptor)` and subscribes to the returned\n * `PermissionStatus`'s `change` event, so the returned `state` reflects the user\n * granting or revoking the permission without a re-mount. The hook is SSR-safe\n * (reports `unsupported` on the server) and StrictMode/concurrent-safe (the async\n * query is race-guarded and the listener is cleaned up on unmount).\n *\n * **Descriptor identity:** the effect is keyed on a *serialized* copy of the\n * descriptor's fields (`name`, `userVisibleOnly`, `sysex`, …), not on the object\n * reference. So passing an inline literal — `usePermission({ name: 'camera' })` —\n * does not re-query on every render; it re-queries only when the descriptor's\n * contents actually change. No `useMemo` on the caller's side is required.\n *\n * @param descriptor - The permission to query, e.g. `{ name: 'camera' }`,\n * `{ name: 'geolocation' }`, `{ name: 'push', userVisibleOnly: true }`,\n * `{ name: 'midi', sysex: true }`. Accepts any permission name (the standard\n * ones plus browser-specific ones like `'camera'`/`'microphone'`).\n * @returns `{ state, status, isSupported, error }` — see {@link UsePermissionReturn}.\n *\n * @example\n * ```tsx\n * function CameraBadge() {\n * const { state, status } = usePermission({ name: \"camera\" });\n *\n * // Branch on `status` (deterministic \"idle\" on the first render, both on the\n * // server and client) so the UI is hydration-safe.\n * if (status === \"idle\" || status === \"pending\") return <span>Checking…</span>;\n * if (status === \"unsupported\") return <span>Permissions API unavailable</span>;\n * if (status === \"error\") return <span>Could not read camera permission</span>;\n *\n * return <span>Camera: {state}</span>; // 'granted' | 'denied' | 'prompt'\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Live updates: the badge re-renders when the user changes the permission in\n * // the browser UI — no polling, no re-mount.\n * function MicIndicator() {\n * const { state } = usePermission({ name: \"microphone\" });\n * return <div data-granted={state === \"granted\"}>Mic: {state ?? \"unknown\"}</div>;\n * }\n * ```\n */\nexport function usePermission(\n descriptor: UsePermissionDescriptor\n): UsePermissionReturn {\n // Every returned field starts deterministic (identical on server and the\n // first client render) so the hook never causes a hydration mismatch; the\n // effect corrects them after mount. In particular `isSupported` is NOT\n // computed in render — that would be `false` on the server and `true` on the\n // client's first paint, mismatching any UI that branches on it.\n const [state, setState] = useState<PermissionState | null>(null);\n const [status, setStatus] = useState<UsePermissionStatus>(\"idle\");\n const [error, setError] = useState<Error | null>(null);\n const [isSupported, setIsSupported] = useState(false);\n\n // Read the latest descriptor from a ref inside the effect. The effect is keyed\n // on the serialized descriptor (a primitive), so it re-runs on content changes\n // while `descriptorRef` guarantees the query uses the current fields.\n const descriptorRef = useRef(descriptor);\n descriptorRef.current = descriptor;\n\n const serialized = serializeDescriptor(descriptor);\n\n useEffect(() => {\n const supported = isPermissionsSupported();\n setIsSupported(supported);\n if (!supported) {\n setState(null);\n setStatus(\"unsupported\");\n setError(null);\n return;\n }\n\n let cancelled = false;\n let permissionStatus: PermissionStatus | null = null;\n let changeHandler: (() => void) | null = null;\n\n // Clear any prior permission's value so consumers don't read stale `state`\n // while the new query for a changed descriptor is in flight.\n setState(null);\n setStatus(\"pending\");\n setError(null);\n\n navigator.permissions\n .query(descriptorRef.current as PermissionDescriptor)\n .then((result) => {\n if (cancelled) return;\n permissionStatus = result;\n setState(result.state);\n setStatus(result.state);\n\n changeHandler = () => {\n // Read the fresh value off the live PermissionStatus object.\n setState(result.state);\n setStatus(result.state);\n };\n result.addEventListener(\"change\", changeHandler);\n })\n .catch((err: unknown) => {\n if (cancelled) return;\n setState(null);\n setStatus(\"error\");\n setError(err instanceof Error ? err : new Error(String(err)));\n });\n\n return () => {\n cancelled = true;\n if (permissionStatus && changeHandler) {\n permissionStatus.removeEventListener(\"change\", changeHandler);\n }\n };\n // `serialized` is the stable primitive key; `descriptorRef`/setters are stable.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [serialized]);\n\n return useMemo<UsePermissionReturn>(\n () => ({ state, status, isSupported, error }),\n [state, status, isSupported, error]\n );\n}\n","import type { UsePermissionDescriptor } from \"./types\";\n\n/**\n * Whether the Permissions API (`navigator.permissions.query`) is available in the\n * current environment. Returns `false` during SSR (no `navigator`) and in browsers\n * that do not implement the Permissions API.\n *\n * @example\n * ```ts\n * if (isPermissionsSupported()) {\n * // safe to call navigator.permissions.query(...)\n * }\n * ```\n */\nexport function isPermissionsSupported(): boolean {\n return (\n typeof navigator !== \"undefined\" &&\n typeof navigator.permissions !== \"undefined\" &&\n typeof navigator.permissions.query === \"function\"\n );\n}\n\n/**\n * Produce a stable string key from a permission descriptor's meaningful fields.\n *\n * `usePermission` keys its effect on this string, not on the descriptor object's\n * reference — so an inline `usePermission({ name: 'camera' })` (a fresh object\n * literal every render) does **not** re-query on every render. It re-queries only\n * when the descriptor's actual contents change. Keys are sorted so field order in\n * the literal does not affect the result.\n *\n * @example\n * ```ts\n * serializeDescriptor({ name: \"midi\", sysex: true }); // '{\"name\":\"midi\",\"sysex\":true}'\n * serializeDescriptor({ sysex: true, name: \"midi\" }); // same string\n * ```\n */\nexport function serializeDescriptor(descriptor: UsePermissionDescriptor): string {\n const keys = Object.keys(descriptor).sort();\n const normalized: Record<string, unknown> = {};\n for (const key of keys) {\n normalized[key] = descriptor[key];\n }\n return JSON.stringify(normalized);\n}\n"],"mappings":";AAAA,SAAS,WAAW,SAAS,QAAQ,gBAAgB;;;ACc9C,SAAS,yBAAkC;AAChD,SACE,OAAO,cAAc,eACrB,OAAO,UAAU,gBAAgB,eACjC,OAAO,UAAU,YAAY,UAAU;AAE3C;AAiBO,SAAS,oBAAoB,YAA6C;AAC/E,QAAM,OAAO,OAAO,KAAK,UAAU,EAAE,KAAK;AAC1C,QAAM,aAAsC,CAAC;AAC7C,aAAW,OAAO,MAAM;AACtB,eAAW,GAAG,IAAI,WAAW,GAAG;AAAA,EAClC;AACA,SAAO,KAAK,UAAU,UAAU;AAClC;;;ADUO,SAAS,cACd,YACqB;AAMrB,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAiC,IAAI;AAC/D,QAAM,CAAC,QAAQ,SAAS,IAAI,SAA8B,MAAM;AAChE,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAuB,IAAI;AACrD,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,KAAK;AAKpD,QAAM,gBAAgB,OAAO,UAAU;AACvC,gBAAc,UAAU;AAExB,QAAM,aAAa,oBAAoB,UAAU;AAEjD,YAAU,MAAM;AACd,UAAM,YAAY,uBAAuB;AACzC,mBAAe,SAAS;AACxB,QAAI,CAAC,WAAW;AACd,eAAS,IAAI;AACb,gBAAU,aAAa;AACvB,eAAS,IAAI;AACb;AAAA,IACF;AAEA,QAAI,YAAY;AAChB,QAAI,mBAA4C;AAChD,QAAI,gBAAqC;AAIzC,aAAS,IAAI;AACb,cAAU,SAAS;AACnB,aAAS,IAAI;AAEb,cAAU,YACP,MAAM,cAAc,OAA+B,EACnD,KAAK,CAAC,WAAW;AAChB,UAAI,UAAW;AACf,yBAAmB;AACnB,eAAS,OAAO,KAAK;AACrB,gBAAU,OAAO,KAAK;AAEtB,sBAAgB,MAAM;AAEpB,iBAAS,OAAO,KAAK;AACrB,kBAAU,OAAO,KAAK;AAAA,MACxB;AACA,aAAO,iBAAiB,UAAU,aAAa;AAAA,IACjD,CAAC,EACA,MAAM,CAAC,QAAiB;AACvB,UAAI,UAAW;AACf,eAAS,IAAI;AACb,gBAAU,OAAO;AACjB,eAAS,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;AAAA,IAC9D,CAAC;AAEH,WAAO,MAAM;AACX,kBAAY;AACZ,UAAI,oBAAoB,eAAe;AACrC,yBAAiB,oBAAoB,UAAU,aAAa;AAAA,MAC9D;AAAA,IACF;AAAA,EAGF,GAAG,CAAC,UAAU,CAAC;AAEf,SAAO;AAAA,IACL,OAAO,EAAE,OAAO,QAAQ,aAAa,MAAM;AAAA,IAC3C,CAAC,OAAO,QAAQ,aAAa,KAAK;AAAA,EACpC;AACF;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@usefy/use-permission",
|
|
3
|
+
"version": "0.20.0",
|
|
4
|
+
"description": "A React hook for reading Permissions API status with live updates, SSR-safe and typed",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"sideEffects": false,
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
24
|
+
"@testing-library/react": "^16.3.1",
|
|
25
|
+
"@types/react": "^19.0.0",
|
|
26
|
+
"@types/react-dom": "^19.0.0",
|
|
27
|
+
"jsdom": "^27.3.0",
|
|
28
|
+
"react": "^19.0.0",
|
|
29
|
+
"react-dom": "^19.0.0",
|
|
30
|
+
"rimraf": "^6.0.1",
|
|
31
|
+
"tsup": "^8.0.0",
|
|
32
|
+
"typescript": "^5.0.0",
|
|
33
|
+
"vitest": "^4.0.16"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/mirunamu00/usefy.git",
|
|
41
|
+
"directory": "packages/hooks/use-permission"
|
|
42
|
+
},
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"keywords": [
|
|
45
|
+
"react",
|
|
46
|
+
"hooks",
|
|
47
|
+
"permissions",
|
|
48
|
+
"permission",
|
|
49
|
+
"permissions-api",
|
|
50
|
+
"navigator",
|
|
51
|
+
"usePermission"
|
|
52
|
+
],
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "tsup",
|
|
55
|
+
"dev": "tsup --watch",
|
|
56
|
+
"test": "vitest run",
|
|
57
|
+
"test:watch": "vitest",
|
|
58
|
+
"typecheck": "tsc --noEmit",
|
|
59
|
+
"clean": "rimraf dist"
|
|
60
|
+
}
|
|
61
|
+
}
|