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