160 lines
4.9 KiB
TypeScript
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;
|
|
}
|