338 lines
12 KiB
JavaScript
338 lines
12 KiB
JavaScript
![]() |
import { PermissionStatus, } from './Permissions.types';
|
||
|
/*
|
||
|
* TODO: Bacon: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#Permissions
|
||
|
* Add messages to manifest like we do with iOS info.plist
|
||
|
*/
|
||
|
// https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#Using_the_new_API_in_older_browsers
|
||
|
// Older browsers might not implement mediaDevices at all, so we set an empty object first
|
||
|
function _getUserMedia(constraints) {
|
||
|
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
||
|
return navigator.mediaDevices.getUserMedia(constraints);
|
||
|
}
|
||
|
// Some browsers partially implement mediaDevices. We can't just assign an object
|
||
|
// with getUserMedia as it would overwrite existing properties.
|
||
|
// Here, we will just add the getUserMedia property if it's missing.
|
||
|
// First get ahold of the legacy getUserMedia, if present
|
||
|
const getUserMedia = navigator.getUserMedia ||
|
||
|
navigator.webkitGetUserMedia ||
|
||
|
navigator.mozGetUserMedia ||
|
||
|
function () {
|
||
|
const error = new Error('Permission unimplemented');
|
||
|
error.code = 0;
|
||
|
error.name = 'NotAllowedError';
|
||
|
throw error;
|
||
|
};
|
||
|
return new Promise((resolve, reject) => {
|
||
|
getUserMedia.call(navigator, constraints, resolve, reject);
|
||
|
});
|
||
|
}
|
||
|
async function askForMediaPermissionAsync(options) {
|
||
|
try {
|
||
|
await _getUserMedia(options);
|
||
|
return {
|
||
|
status: PermissionStatus.GRANTED,
|
||
|
expires: 'never',
|
||
|
canAskAgain: true,
|
||
|
granted: true,
|
||
|
};
|
||
|
}
|
||
|
catch ({ message }) {
|
||
|
// name: NotAllowedError
|
||
|
// code: 0
|
||
|
if (message === 'Permission dismissed') {
|
||
|
// message: Permission dismissed
|
||
|
return {
|
||
|
status: PermissionStatus.UNDETERMINED,
|
||
|
expires: 'never',
|
||
|
canAskAgain: true,
|
||
|
granted: false,
|
||
|
};
|
||
|
}
|
||
|
else {
|
||
|
// TODO: Bacon: [OSX] The system could deny access to chrome.
|
||
|
// TODO: Bacon: add: { status: 'unimplemented' }
|
||
|
// message: Permission denied
|
||
|
return {
|
||
|
status: PermissionStatus.DENIED,
|
||
|
expires: 'never',
|
||
|
canAskAgain: true,
|
||
|
granted: false,
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
async function askForMicrophonePermissionAsync() {
|
||
|
return await askForMediaPermissionAsync({ audio: true });
|
||
|
}
|
||
|
async function askForCameraPermissionAsync() {
|
||
|
return await askForMediaPermissionAsync({ video: true });
|
||
|
}
|
||
|
async function askForLocationPermissionAsync() {
|
||
|
return new Promise(resolve => {
|
||
|
navigator.geolocation.getCurrentPosition(() => resolve({
|
||
|
status: PermissionStatus.GRANTED,
|
||
|
expires: 'never',
|
||
|
canAskAgain: true,
|
||
|
granted: true,
|
||
|
}), ({ code }) => {
|
||
|
// https://developer.mozilla.org/en-US/docs/Web/API/PositionError/code
|
||
|
if (code === 1) {
|
||
|
resolve({
|
||
|
status: PermissionStatus.DENIED,
|
||
|
expires: 'never',
|
||
|
canAskAgain: true,
|
||
|
granted: false,
|
||
|
});
|
||
|
}
|
||
|
else {
|
||
|
resolve({
|
||
|
status: PermissionStatus.UNDETERMINED,
|
||
|
expires: 'never',
|
||
|
canAskAgain: true,
|
||
|
granted: false,
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
async function getPermissionWithQueryAsync(name) {
|
||
|
if (!navigator || !navigator.permissions || !navigator.permissions.query)
|
||
|
return null;
|
||
|
const { state } = await navigator.permissions.query({ name });
|
||
|
if (state === 'prompt') {
|
||
|
return PermissionStatus.UNDETERMINED;
|
||
|
}
|
||
|
else if (state === 'granted') {
|
||
|
return PermissionStatus.GRANTED;
|
||
|
}
|
||
|
else if (state === 'denied') {
|
||
|
return PermissionStatus.DENIED;
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
async function enumerateDevices() {
|
||
|
if (navigator && navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {
|
||
|
return await navigator.mediaDevices.enumerateDevices();
|
||
|
}
|
||
|
// @ts-ignore: This is deprecated but we should still attempt to use it.
|
||
|
if (window.MediaStreamTrack && typeof window.MediaStreamTrack.getSources === 'function') {
|
||
|
// @ts-ignore
|
||
|
return await MediaStreamTrack.getSources();
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
async function askSensorPermissionAsync() {
|
||
|
const requestPermission = getRequestMotionPermission();
|
||
|
// Technically this is incorrect because it doesn't account for iOS 12.2 Safari.
|
||
|
// But unfortunately we can only abstract so much.
|
||
|
if (!requestPermission)
|
||
|
return PermissionStatus.GRANTED;
|
||
|
// If this isn't invoked in a touch-event then it never resolves.
|
||
|
// Safari probably should throw an error but because it doesn't we have no way of informing the developer.
|
||
|
const status = await requestPermission();
|
||
|
switch (status) {
|
||
|
case 'granted':
|
||
|
return PermissionStatus.GRANTED;
|
||
|
case 'denied':
|
||
|
return PermissionStatus.DENIED;
|
||
|
default:
|
||
|
return PermissionStatus.UNDETERMINED;
|
||
|
}
|
||
|
}
|
||
|
async function getMediaMaybeGrantedAsync(targetKind) {
|
||
|
const devices = await enumerateDevices();
|
||
|
if (!devices) {
|
||
|
return false;
|
||
|
}
|
||
|
const result = await devices
|
||
|
.filter(({ kind }) => kind === targetKind)
|
||
|
.some(({ label }) => label !== '');
|
||
|
// Granted or denied or undetermined or no devices
|
||
|
return result;
|
||
|
}
|
||
|
async function getPermissionAsync(permission, shouldAsk) {
|
||
|
switch (permission) {
|
||
|
case 'userFacingNotifications':
|
||
|
case 'notifications':
|
||
|
{
|
||
|
if (!shouldAsk) {
|
||
|
const status = await getPermissionWithQueryAsync('notifications');
|
||
|
if (status) {
|
||
|
return {
|
||
|
status,
|
||
|
expires: 'never',
|
||
|
granted: status === PermissionStatus.GRANTED,
|
||
|
canAskAgain: true,
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
const { Notification = {} } = window;
|
||
|
if (Notification.requestPermission) {
|
||
|
let status = Notification.permission;
|
||
|
if (shouldAsk) {
|
||
|
status = await Notification.requestPermission();
|
||
|
}
|
||
|
if (!status || status === 'default') {
|
||
|
return {
|
||
|
status: PermissionStatus.UNDETERMINED,
|
||
|
expires: 'never',
|
||
|
canAskAgain: true,
|
||
|
granted: false,
|
||
|
};
|
||
|
}
|
||
|
return {
|
||
|
status,
|
||
|
expires: 'never',
|
||
|
canAskAgain: true,
|
||
|
granted: status === PermissionStatus.GRANTED,
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
case 'motion': {
|
||
|
if (shouldAsk) {
|
||
|
const status = await askSensorPermissionAsync();
|
||
|
return {
|
||
|
status,
|
||
|
expires: 'never',
|
||
|
granted: status === PermissionStatus.GRANTED,
|
||
|
canAskAgain: false,
|
||
|
};
|
||
|
}
|
||
|
// We can infer from the requestor if this is an older browser.
|
||
|
const status = getRequestMotionPermission()
|
||
|
? PermissionStatus.UNDETERMINED
|
||
|
: isIOS()
|
||
|
? PermissionStatus.DENIED
|
||
|
: PermissionStatus.GRANTED;
|
||
|
return {
|
||
|
status,
|
||
|
expires: 'never',
|
||
|
canAskAgain: true,
|
||
|
granted: status === PermissionStatus.GRANTED,
|
||
|
};
|
||
|
}
|
||
|
case 'location':
|
||
|
{
|
||
|
const maybeStatus = await getPermissionWithQueryAsync('geolocation');
|
||
|
if (maybeStatus) {
|
||
|
if (maybeStatus === PermissionStatus.UNDETERMINED && shouldAsk) {
|
||
|
return await askForLocationPermissionAsync();
|
||
|
}
|
||
|
return {
|
||
|
status: maybeStatus,
|
||
|
expires: 'never',
|
||
|
canAskAgain: true,
|
||
|
granted: maybeStatus === PermissionStatus.GRANTED,
|
||
|
};
|
||
|
}
|
||
|
else if (shouldAsk) {
|
||
|
// TODO: Bacon: should this function as ask async when not in chrome?
|
||
|
return await askForLocationPermissionAsync();
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
case 'audioRecording':
|
||
|
{
|
||
|
const maybeStatus = await getPermissionWithQueryAsync('microphone');
|
||
|
if (maybeStatus) {
|
||
|
if (maybeStatus === PermissionStatus.UNDETERMINED && shouldAsk) {
|
||
|
return await askForMicrophonePermissionAsync();
|
||
|
}
|
||
|
return {
|
||
|
status: maybeStatus,
|
||
|
expires: 'never',
|
||
|
canAskAgain: true,
|
||
|
granted: maybeStatus === PermissionStatus.GRANTED,
|
||
|
};
|
||
|
}
|
||
|
else if (shouldAsk) {
|
||
|
return await askForMicrophonePermissionAsync();
|
||
|
}
|
||
|
else {
|
||
|
const maybeGranted = await getMediaMaybeGrantedAsync('audioinput');
|
||
|
if (maybeGranted) {
|
||
|
return {
|
||
|
status: PermissionStatus.GRANTED,
|
||
|
expires: 'never',
|
||
|
canAskAgain: true,
|
||
|
granted: true,
|
||
|
};
|
||
|
}
|
||
|
// TODO: Bacon: Get denied or undetermined...
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
case 'camera':
|
||
|
{
|
||
|
const maybeStatus = await getPermissionWithQueryAsync('camera');
|
||
|
if (maybeStatus) {
|
||
|
if (maybeStatus === PermissionStatus.UNDETERMINED && shouldAsk) {
|
||
|
return await askForCameraPermissionAsync();
|
||
|
}
|
||
|
return {
|
||
|
status: maybeStatus,
|
||
|
expires: 'never',
|
||
|
canAskAgain: true,
|
||
|
granted: maybeStatus === PermissionStatus.GRANTED,
|
||
|
};
|
||
|
}
|
||
|
else if (shouldAsk) {
|
||
|
return await askForCameraPermissionAsync();
|
||
|
}
|
||
|
else {
|
||
|
const maybeGranted = await getMediaMaybeGrantedAsync('videoinput');
|
||
|
if (maybeGranted) {
|
||
|
return {
|
||
|
status: PermissionStatus.GRANTED,
|
||
|
expires: 'never',
|
||
|
canAskAgain: true,
|
||
|
granted: true,
|
||
|
};
|
||
|
}
|
||
|
// TODO: Bacon: Get denied or undetermined...
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
default:
|
||
|
break;
|
||
|
}
|
||
|
return {
|
||
|
status: PermissionStatus.UNDETERMINED,
|
||
|
expires: 'never',
|
||
|
canAskAgain: true,
|
||
|
granted: false,
|
||
|
};
|
||
|
}
|
||
|
export default {
|
||
|
get name() {
|
||
|
return 'ExpoPermissions';
|
||
|
},
|
||
|
async getAsync(permissionTypes) {
|
||
|
const results = {};
|
||
|
for (const permissionType of new Set(permissionTypes)) {
|
||
|
results[permissionType] = await getPermissionAsync(permissionType, /* shouldAsk */ false);
|
||
|
}
|
||
|
return results;
|
||
|
},
|
||
|
async askAsync(permissionTypes) {
|
||
|
const results = {};
|
||
|
for (const permissionType of new Set(permissionTypes)) {
|
||
|
results[permissionType] = await getPermissionAsync(permissionType, /* shouldAsk */ true);
|
||
|
}
|
||
|
return results;
|
||
|
},
|
||
|
};
|
||
|
export function getRequestMotionPermission() {
|
||
|
if (typeof DeviceMotionEvent !== 'undefined' && !!DeviceMotionEvent?.requestPermission) {
|
||
|
return DeviceMotionEvent.requestPermission;
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
// https://stackoverflow.com/a/9039885/4047926
|
||
|
function isIOS() {
|
||
|
const isIOSUA = /(iPad|iPhone|iPod)/g.test(navigator.userAgent);
|
||
|
const isIE11 = !!window['MSStream'];
|
||
|
return isIOSUA && !isIE11;
|
||
|
}
|
||
|
//# sourceMappingURL=ExpoPermissions.web.js.map
|