1import { PermissionResponse, PermissionStatus, Platform } from 'expo-modules-core'; 2 3type SensorEventName = 'deviceorientation' | 'devicemotion'; 4 5export async function getPermissionsAsync(): Promise<PermissionResponse> { 6 // We can infer from the requestor if this is an older browser. 7 const status = getRequestPermission() 8 ? PermissionStatus.UNDETERMINED 9 : isIOS() 10 ? PermissionStatus.DENIED 11 : PermissionStatus.GRANTED; 12 return { 13 status, 14 expires: 'never', 15 canAskAgain: true, 16 granted: status === PermissionStatus.GRANTED, 17 }; 18} 19 20export async function requestPermissionsAsync(): Promise<PermissionResponse> { 21 const status = await askSensorPermissionAsync(); 22 return { 23 status, 24 expires: 'never', 25 granted: status === PermissionStatus.GRANTED, 26 canAskAgain: false, 27 }; 28} 29 30async function askSensorPermissionAsync(): Promise<PermissionStatus> { 31 const requestPermission = getRequestPermission(); 32 // Technically this is incorrect because it doesn't account for iOS 12.2 Safari. 33 // But unfortunately we can only abstract so much. 34 if (!requestPermission) return PermissionStatus.GRANTED; 35 36 // If this isn't invoked in a touch-event then it never resolves. 37 // Safari probably should throw an error but because it doesn't we have no way of informing the developer. 38 const status = await requestPermission(); 39 switch (status) { 40 case 'granted': 41 return PermissionStatus.GRANTED; 42 case 'denied': 43 return PermissionStatus.DENIED; 44 default: 45 return PermissionStatus.UNDETERMINED; 46 } 47} 48 49/** 50 * Temporary solution until `tslib.d.ts` is updated to include the new DeviceOrientation/DeviceMotion API (as of 2021.10.26 it's not implemented completely, it's in experimental state). 51 * 52 * `[email protected]` is missing `requestPermission` described in 53 * - https://w3c.github.io/deviceorientation/#deviceorientation 54 * - https://w3c.github.io/deviceorientation/#devicemotion 55 * 56 * MDN docs does not describe this property as well: 57 * - https://developer.mozilla.org/en-US/docs/Web/API/DeviceOrientationEvent 58 * - https://developer.mozilla.org/en-US/docs/Web/API/DeviceMotionEvent 59 * 60 * Below are just redefinitions of the existing typing available in the: 61 * - https://github.com/microsoft/TypeScript/blob/01de6ff2ecdc6175727f7f999b887519d40ca115/lib/lib.dom.d.ts#L4216 62 * - https://github.com/microsoft/TypeScript/blob/01de6ff2ecdc6175727f7f999b887519d40ca115/lib/lib.dom.d.ts#L4241. 63 */ 64declare let DeviceMotionEvent: { 65 prototype: DeviceMotionEvent; 66 new (type: string, eventInitDict?: DeviceMotionEventInit): DeviceMotionEvent; 67 requestPermission?: () => Promise<PermissionState>; 68}; 69/** 70 * See `DeviceMotionEvent` description a few lines above. 71 */ 72declare let DeviceOrientationEvent: { 73 prototype: DeviceOrientationEvent; 74 new (type: string, eventInitDict?: DeviceOrientationEventInit): DeviceOrientationEvent; 75 requestPermission?: () => Promise<PermissionState>; 76}; 77 78export function getRequestPermission(): (() => Promise<PermissionState>) | null { 79 if (!Platform.isDOMAvailable) { 80 return null; 81 } 82 83 if (typeof DeviceMotionEvent !== 'undefined' && !!DeviceMotionEvent?.requestPermission) { 84 return DeviceMotionEvent.requestPermission; 85 } else if ( 86 typeof DeviceOrientationEvent !== 'undefined' && 87 !!DeviceOrientationEvent?.requestPermission 88 ) { 89 return DeviceOrientationEvent.requestPermission; 90 } 91 92 return null; 93} 94 95class PermissionError extends Error { 96 constructor(eventName: string) { 97 let errorMessage = `Cannot observe event: ${eventName}. How to fix:`; 98 99 errorMessage += `\n- Ensure you've requested the \`MOTION\` permission via expo-permissions (this must be done in a touch event).`; 100 101 if (location.protocol !== 'https:') { 102 errorMessage += 103 '\n- Ensure that you are hosting with `https` as DeviceMotion and DeviceOrientation are now secure APIs.'; 104 } 105 // is iOS and doesn't support requesting permissions, must be 12.2 106 if (isIOS() && !getRequestPermission()) { 107 errorMessage += 108 '\n- On iOS 12.2, you must manually enable device orientation in `Settings > Safari > Motion & Orientation Access`.'; 109 } 110 111 super(errorMessage); 112 } 113} 114 115// iOS 12.2 disables DeviceMotion by default now 116// https://github.com/w3c/deviceorientation/issues/57 117export async function assertSensorEventEnabledAsync( 118 eventName: SensorEventName, 119 timeout?: number 120): Promise<boolean> { 121 if (!Platform.isDOMAvailable) { 122 return false; 123 } 124 125 if (getRequestPermission()) { 126 if (await isSensorEnabledAsync(eventName, timeout)) { 127 return true; 128 } 129 throw new PermissionError(eventName); 130 } 131 return true; 132} 133 134// throw error if the sensor is disabled. 135export async function isSensorEnabledAsync( 136 eventName: SensorEventName, 137 // Initial interval tests found results on a median of 138 // devicemotion: 139 // - iPhone 7 Plus: 166.6666753590107ms 140 // - iPhone X: 166.6666753590107ms 141 // deviceorientation: 142 // - 143 // 144 // The initial launch of iOS Safari onto a page calling this API seems to take a little longer than a regular call. 145 // devicemotion: 146 // - ~35ms 147 // deviceorientation: 148 // - ~45ms 149 // 150 timeout: number = 250 151): Promise<boolean> { 152 if (!Platform.isDOMAvailable) { 153 return false; 154 } 155 156 // If there is no method to request permission then the device has access to device motion. 157 // Older versions of iOS have no method but still disable motion so we should always check on iOS. 158 if (!isIOS && !getRequestPermission()) { 159 return true; 160 } 161 162 return new Promise((resolve) => { 163 const id = setTimeout(() => { 164 window.removeEventListener(eventName, listener); 165 resolve(false); 166 }, timeout); 167 168 const listener = (): void => { 169 clearTimeout(id); 170 window.removeEventListener(eventName, listener); 171 resolve(true); 172 }; 173 174 window.addEventListener(eventName, listener); 175 }); 176} 177 178// https://stackoverflow.com/a/9039885/4047926 179function isIOS(): boolean { 180 const isIOSUA = /(iPad|iPhone|iPod)/g.test(navigator.userAgent); 181 const isIE11 = !!window['MSStream']; 182 return isIOSUA && !isIE11; 183} 184