Files
tools/vue/toolkit/src/composables/browser/usePermission/index.ts
T
robonen ab6d8f6ce0
Publish to NPM / Check version changes and publish (push) Failing after 10m34s
build: bump new versions
2026-06-18 02:57:03 +07:00

160 lines
4.9 KiB
TypeScript

import { shallowRef, toRaw } from 'vue';
import type { Ref, ShallowRef } from 'vue';
import { isString } from '@robonen/stdlib';
import { defaultNavigator } from '@/types';
import type { ConfigurableNavigator } from '@/types';
import { useSupported } from '@/composables/utilities/useSupported';
import { useEventListener } from '@/composables/browser/useEventListener';
/**
* Permission names not yet present in the lib DOM `PermissionName` union but
* supported by browsers behind the Permissions API.
*/
export type PermissionDescriptorNamePolyfill
= | 'accelerometer'
| 'accessibility-events'
| 'ambient-light-sensor'
| 'background-sync'
| 'camera'
| 'clipboard-read'
| 'clipboard-write'
| 'geolocation'
| 'gyroscope'
| 'local-fonts'
| 'magnetometer'
| 'microphone'
| 'midi'
| 'notifications'
| 'payment-handler'
| 'persistent-storage'
| 'push'
| 'screen-wake-lock'
| 'speaker'
| 'speaker-selection'
| 'storage-access'
| 'window-management';
export type GeneralPermissionDescriptor
= | PermissionDescriptor
| { name: PermissionDescriptorNamePolyfill };
export interface UsePermissionOptions<Controls extends boolean> extends ConfigurableNavigator {
/**
* Expose the `isSupported` flag and a `query` method that returns the raw `PermissionStatus`
*
* @default false
*/
controls?: Controls;
}
export type UsePermissionReturn = Readonly<Ref<PermissionState | undefined>>;
export interface UsePermissionReturnWithControls {
/**
* Reactive permission state (`granted` | `denied` | `prompt`), or `undefined` while unsupported/unresolved
*/
state: UsePermissionReturn;
/**
* Whether the Permissions API is available
*/
isSupported: Readonly<Ref<boolean>>;
/**
* Query (or re-query) the permission, resolving to the raw `PermissionStatus`
*/
query: () => Promise<PermissionStatus | undefined>;
}
/**
* @name usePermission
* @category Browser
* @description Reactive Permissions API state.
*
* @param {GeneralPermissionDescriptor | string} permissionDesc The permission to query
* @param {UsePermissionOptions} [options={}] Options
* @returns {UsePermissionReturn | UsePermissionReturnWithControls} The permission state, or controls when `controls: true`
*
* @example
* const microphone = usePermission('microphone');
*
* @example
* const { state, isSupported, query } = usePermission('camera', { controls: true });
*
* @since 0.0.14
*/
export function usePermission(
permissionDesc: GeneralPermissionDescriptor | GeneralPermissionDescriptor['name'],
options?: UsePermissionOptions<false>,
): UsePermissionReturn;
export function usePermission(
permissionDesc: GeneralPermissionDescriptor | GeneralPermissionDescriptor['name'],
options: UsePermissionOptions<true>,
): UsePermissionReturnWithControls;
export function usePermission(
permissionDesc: GeneralPermissionDescriptor | GeneralPermissionDescriptor['name'],
options: UsePermissionOptions<boolean> = {},
): UsePermissionReturn | UsePermissionReturnWithControls {
const { controls = false, navigator = defaultNavigator } = options;
const isSupported = useSupported(() => !!navigator && 'permissions' in navigator);
const desc = (isString(permissionDesc)
? { name: permissionDesc }
: permissionDesc) as PermissionDescriptor;
// Shallow refs: `PermissionStatus` is a host object, deep reactivity is wasteful.
const permissionStatus: ShallowRef<PermissionStatus | undefined> = shallowRef();
const state: ShallowRef<PermissionState | undefined> = shallowRef();
const update = (): void => {
state.value = permissionStatus.value?.state ?? 'prompt';
};
// Register the `change` listener synchronously against the reactive ref so it
// auto-rebinds when the status resolves and auto-cleans on scope dispose.
useEventListener(permissionStatus, 'change', update, { passive: true });
// Dedupe concurrent/repeat calls: once a query is in flight we reuse it.
let queryPromise: Promise<PermissionStatus | undefined> | undefined;
const query = (): Promise<PermissionStatus | undefined> => {
if (!isSupported.value)
return Promise.resolve(undefined);
if (permissionStatus.value)
return Promise.resolve(permissionStatus.value);
if (queryPromise)
return queryPromise;
queryPromise = navigator!.permissions
.query(desc)
.then((status) => {
permissionStatus.value = status;
return status;
})
.catch(() => {
permissionStatus.value = undefined;
return undefined;
})
.finally(() => {
update();
queryPromise = undefined;
});
return queryPromise;
};
query();
if (controls) {
return {
state: state as UsePermissionReturn,
isSupported,
// `toRaw` so callers get the underlying `PermissionStatus`, not a reactive proxy.
query: () => query().then(status => (status ? toRaw(status) : undefined)),
};
}
return state as UsePermissionReturn;
}