115 lines
3.5 KiB
TypeScript
115 lines
3.5 KiB
TypeScript
import React, { createContext, useCallback, useEffect, useRef, useState, ReactNode } from "react";
|
|
import { Location } from "../types/location";
|
|
|
|
type LocationError = "permission_denied" | "location_error" | null;
|
|
|
|
type LocationContextValue = {
|
|
location: Location | null;
|
|
loading: boolean;
|
|
error: LocationError;
|
|
requestLocation: () => void;
|
|
};
|
|
|
|
export const LocationContext = createContext<LocationContextValue | undefined>(undefined);
|
|
|
|
export const LocationProvider = ({ children }: { children: ReactNode }) => {
|
|
const [location, setLocation] = useState<Location | null>(null);
|
|
const [loading, setLoading] = useState<boolean>(true);
|
|
const [error, setError] = useState<LocationError>(null);
|
|
const currentRequest = useRef(0);
|
|
|
|
const handleSuccess = (pos: GeolocationPosition) => {
|
|
const loc: Location = {
|
|
latitude: pos.coords.latitude,
|
|
longitude: pos.coords.longitude,
|
|
accuracy: pos.coords.accuracy,
|
|
};
|
|
setLocation(loc);
|
|
setError(null);
|
|
setLoading(false);
|
|
// Log once received
|
|
// eslint-disable-next-line no-console
|
|
console.info("User location obtained", loc);
|
|
};
|
|
|
|
const handleError = (err: GeolocationPositionError) => {
|
|
setLocation(null);
|
|
setLoading(false);
|
|
if (err.code === err.PERMISSION_DENIED) {
|
|
setError("permission_denied");
|
|
// eslint-disable-next-line no-console
|
|
console.warn("Geolocation permission denied", err.message);
|
|
} else {
|
|
setError("location_error");
|
|
// eslint-disable-next-line no-console
|
|
console.warn("Geolocation error", err.message);
|
|
}
|
|
};
|
|
|
|
const performGetPosition = useCallback(() => {
|
|
if (!("geolocation" in navigator)) {
|
|
setLocation(null);
|
|
setError("location_error");
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
const requestId = ++currentRequest.current;
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
navigator.geolocation.getCurrentPosition(
|
|
(pos) => {
|
|
// ignore if a newer request started
|
|
if (requestId !== currentRequest.current) return;
|
|
handleSuccess(pos);
|
|
},
|
|
(err) => {
|
|
if (requestId !== currentRequest.current) return;
|
|
handleError(err);
|
|
},
|
|
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 0 },
|
|
);
|
|
}, []);
|
|
|
|
const requestLocation = useCallback(() => {
|
|
// If Permissions API exists, read the state first to provide immediate feedback
|
|
if ((navigator as any).permissions && typeof (navigator as any).permissions.query === 'function') {
|
|
try {
|
|
;(navigator as any).permissions.query({ name: 'geolocation' }).then((status: PermissionStatus) => {
|
|
if (status.state === 'granted') {
|
|
// permission already granted
|
|
performGetPosition();
|
|
} else if (status.state === 'denied') {
|
|
// permission denied
|
|
setLoading(false);
|
|
setError('permission_denied');
|
|
setLocation(null);
|
|
} else {
|
|
// prompt
|
|
performGetPosition();
|
|
}
|
|
}).catch(() => {
|
|
// fallback to direct request
|
|
performGetPosition();
|
|
});
|
|
} catch (e) {
|
|
performGetPosition();
|
|
}
|
|
} else {
|
|
performGetPosition();
|
|
}
|
|
}, [performGetPosition]);
|
|
|
|
// Automatically request location on provider mount
|
|
useEffect(() => {
|
|
requestLocation();
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
return (
|
|
<LocationContext.Provider value={{ location, loading, error, requestLocation }}>
|
|
{children}
|
|
</LocationContext.Provider>
|
|
);
|
|
};
|