ReynierPM
ReynierPM

Reputation: 18660

It is possible to know when the user clicked on "Allow" or "Deny" while using the Geolocation API and browser permissions?

I need to know whether the user clicked on "Allow" or "Deny" while accessing a webpage built using React+Typescript but so far I have been unable to get it working properly. See the following piece of code:

import { useEffect, useState } from "react";
import useGeolocation from "./useGeolocation";

export default function App() {
  const [locationAccess, setLocationAccess] = useState<boolean>(false);

  useEffect(() => {
    navigator.permissions
      .query({ name: "geolocation" })
      .then((permissionStatus) => {
        setLocationAccess(permissionStatus.state === "granted");
      });
  }, [locationAccess]);

  const geoLocation = useGeolocation({ locationAccess });

  console.log("geoLocation", geoLocation);

  return <></>;
}

When I access the page for the first time and you are requested to give permission to access your geolocation I am able to see the following object being return:

{
    "loaded": false,
    "coordinates": {
        "lat": "",
        "lng": ""
    },
    "locale": "",
    "countryCode": "",
    "error": {}
}

which is fine because useGeolocation does return such an object if no access has been allowed| or denied. Now if I click on "Allow" I would expect right away to see the following object being returned:

{
    "loaded": true,
    "coordinates": {
        "lat": 28.504016,
        "lng": -82.5510363
    },
    "locale": "en-us",
    "countryCode": "US",
    "error": {}
}

but instead, I have to reload the page to get the values back. Is it possible to achieve this once the user clicks the "Allow" button?

I have set up a CodeSanbox if you need access to the code and to play with it here

Upvotes: 0

Views: 540

Answers (2)

Kaiido
Kaiido

Reputation: 136598

When the PermissionStatus's state is "prompt", you're supposed to wait for its onchange event to actually know if it has been "granted" or "denied".

So you need to modify your code to look like

useEffect(() => {
  navigator.permissions
    .query({ name: "geolocation" })
    .then((permissionStatus) => {
      if (permissionStatus.state === "prompt") {
        permissionStatus.onchange = (evt) => {
          setLocationAccess(permissionStatus.state === "granted");
        };
      }
      else { // User has already saved a setting
        setLocationAccess(permissionStatus.state === "granted");
      }
    });
}, [locationAccess]);

PS: It seems that contrarily to what MDN's browser-compat data states, a few browsers do not support the change event here. One workaround for the geolocation one can be found in this answer by user Kuday, which does ab-use the fact that a call to getCurrentPosition() will wait fort the user's response before either resolving or erroring.

We can rewrite their solution in a more practical way which will return a Promise resolving with a Boolean stating whether the permission has been granted or denied:

async function requestLocationPermission() {
  const queryResult = await navigator.permissions.query({
    name: "geolocation"
  });
  if (queryResult.state === "denied") {
    return false;
  }
  if (queryResult.state === "granted") {
    return true;
  }
  // Whatever comes first between the actual event (Chrome)
  // or the request to getCurrentPosition() (others).
  return Promise.race([
    new Promise((resolve) => {
      navigator.geolocation
        .getCurrentPosition(() => resolve(true), (err) => resolve(false));
    }),
    new Promise((resolve) => {
      queryResult.onchange = () => resolve(queryResult.state === "granted");
    }),
  ]);
}

// Usage:
const hasAccess = await requestLocationPermission();
if (hasAccess) {
  // do your stuff
}

Upvotes: 2

Vivekajee
Vivekajee

Reputation: 61

One way to address this is to move the useGeolocation hook's logic inside the useEffect in the main component, so that it can directly listen to the changes in the locationAccess state. Here's an updated version of your code:

import { useEffect, useState } from "react";
import useGeolocation from "./useGeolocation";

export default function App() {
  const [locationAccess, setLocationAccess] = useState<boolean>(false);
  const [geoLocation, setGeoLocation] = useState({
    loaded: false,
    coordinates: {
      lat: "",
      lng: ""
    },
    locale: "",
    countryCode: "",
    error: {}
  });

  useEffect(() => {
    navigator.permissions
      .query({ name: "geolocation" })
      .then((permissionStatus) => {
        setLocationAccess(permissionStatus.state === "granted");
      });
  }, [locationAccess]);

  useEffect(() => {
    if (locationAccess) {
      // Call the logic of useGeolocation directly
      navigator.geolocation.getCurrentPosition(
        (position) => {
          setGeoLocation({
            loaded: true,
            coordinates: {
              lat: position.coords.latitude,
              lng: position.coords.longitude
            },
            locale: navigator.language,
            countryCode: navigator.language.slice(3).toUpperCase(),
            error: {}
          });
        },
        (error) => {
          setGeoLocation({
            loaded: true,
            coordinates: {
              lat: "",
              lng: ""
            },
            locale: "",
            countryCode: "",
            error: error.message
          });
        }
      );
    }
  }, [locationAccess]);

  console.log("geoLocation", geoLocation);

  return <></>;
}

In this modification, the logic to fetch the geolocation is moved into the main useEffect, which listens to changes in the locationAccess state. This should result in an immediate update of the geoLocation state when the user grants permission.

Upvotes: 1

Related Questions