Reputation: 18660
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
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
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