Search code examples
javascriptiostypescriptprogressive-web-appsw3c-geolocation

`navigator.geolocation.getCurrentPosition()` hangs on iOS PWA


I have this snippet:

const getCurrentPosition = () =>
  new Promise<GeolocationPosition>((resolve, reject) => {
    if (navigator.geolocation) {
      navigator.geolocation.getCurrentPosition(resolve, reject, {
        timeout: 5000,
        maximumAge: 2000,
        enableHighAccuracy: true,
      });
    } else {
      reject(new Error('Browser does not support geolocation!'));
    }
  });

Which I call like so:

try {
  const { coords } = await getCurrentPosition();
  // do stuff with coords
}
catch (e){
  // handle the error
}

When executing this code in an iOS PWA (navigator.standalone) if I have my location settings for Safari (Settings > Privacy > Location Services > Safari Websites) set to: Ask Next Time, the getCurrentPosition() promise hangs and does not timeout, resolve or reject. It does not prompt me for my location like it does in the iOS Safari browser

If I change the settings to Never or While Using the App, then it prompts me and works fine.

I want to be able to handle the scenario where a user has set their settings to Ask Next Time whilst using a PWA.


Solution

  • The solution to this was fairly trivial once some colleagues and I figured it out:

    export function getUserPosition(): Promise<GeolocationPosition> {
      const promiseArray = [];
    
      if (navigator.standalone) {
        promiseArray.push(
          new Promise((resolve, reject) => {
            const wait = setTimeout(() => {
              clearTimeout(wait);
              reject('Location has timed out');
            }, 4000);
          })
        );
      }
    
      const getCurrentPositionPromise = new Promise((resolve, reject) => {
        if (navigator.geolocation) {
          navigator.geolocation.getCurrentPosition(resolve, reject, {
            timeout: 5000,
            maximumAge: 2000,
            enableHighAccuracy: true,
          });
        } else {
          reject(new Error('Browser does not support geolocation!'));
        }
      });
    
      promiseArray.push(getCurrentPositionPromise);
    
      return Promise.race(promiseArray) as Promise<GeolocationPosition>;
    }
    

    What's going on here?

    1. We're creating an empty promise array
    2. If the navigator is identified as standalone, we're creating a promise that rejects after 4 seconds (this value is arbitrary, the maximum time you want your users to wait) and pushing it to the promiseArray.
    3. We're creating another promise which gets the user's position and pushing that into the promiseArray as well.
    4. We then return a promise that is returned from the execution of Promise.race(). The key here is that there will only be one promise in the promiseArray if the navigator is not standalone, and will therefore work as intended. If the navigator is standalone, then there will be two promises in the promiseArray and the Promise.race() will reject from the timeout, thus rejecting from getUserPosition() function.