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 extends ConfigurableNavigator { /** * Expose the `isSupported` flag and a `query` method that returns the raw `PermissionStatus` * * @default false */ controls?: Controls; } export type UsePermissionReturn = Readonly>; export interface UsePermissionReturnWithControls { /** * Reactive permission state (`granted` | `denied` | `prompt`), or `undefined` while unsupported/unresolved */ state: UsePermissionReturn; /** * Whether the Permissions API is available */ isSupported: Readonly>; /** * Query (or re-query) the permission, resolving to the raw `PermissionStatus` */ query: () => Promise; } /** * @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, ): UsePermissionReturn; export function usePermission( permissionDesc: GeneralPermissionDescriptor | GeneralPermissionDescriptor['name'], options: UsePermissionOptions, ): UsePermissionReturnWithControls; export function usePermission( permissionDesc: GeneralPermissionDescriptor | GeneralPermissionDescriptor['name'], options: UsePermissionOptions = {}, ): 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 = shallowRef(); const state: ShallowRef = 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 | undefined; const query = (): Promise => { 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; }