import { useCallback, useState } from 'react'; import type { LatLng } from './map.types'; export type DeviceLocationStatus = 'idle' | 'locating' | 'granted' | 'denied' | 'unavailable' | 'timeout'; export interface DeviceLocationReading { position: LatLng; /** Radius of the reported accuracy circle, in metres. */ accuracyMeters: number; /** * The platform flagged the reading as coming from a mock provider. * * Only some environments report this at all, so `false` means "not flagged", * never "proven genuine". Treat it as a signal, not as verification. */ mocked: boolean; timestamp: number; } export interface UseDeviceLocationOptions { enableHighAccuracy?: boolean; /** @default 10000 */ timeoutMs?: number; maximumAgeMs?: number; } /** * Reads the device's position, with every failure mode named. * * @description * Wraps `navigator.geolocation` so callers get a status they can render instead * of an untyped error: permission denied, no provider, or timed out are three * different situations for the user and only one of them is worth retrying. * * `mocked` surfaces the platform's own mock-provider flag when it exists. It is * a signal, not proof — deployments that must reject simulated coordinates need * server-side corroboration as well. */ export function useDeviceLocation(options: UseDeviceLocationOptions = {}) { const { enableHighAccuracy = true, timeoutMs = 10000, maximumAgeMs = 0 } = options; const [status, setStatus] = useState('idle'); const [reading, setReading] = useState(null); const [error, setError] = useState(null); const locate = useCallback( () => new Promise(resolve => { if (typeof navigator === 'undefined' || !navigator.geolocation) { setStatus('unavailable'); setError('Geolocation is not available in this environment'); resolve(null); return; } setStatus('locating'); setError(null); navigator.geolocation.getCurrentPosition( position => { const next: DeviceLocationReading = { position: { lat: position.coords.latitude, lng: position.coords.longitude }, accuracyMeters: position.coords.accuracy, // Non-standard but widely present on Android WebViews. mocked: Boolean((position as unknown as { mocked?: boolean }).mocked), timestamp: position.timestamp, }; setReading(next); setStatus('granted'); resolve(next); }, positionError => { switch (positionError.code) { case positionError.PERMISSION_DENIED: setStatus('denied'); break; case positionError.TIMEOUT: setStatus('timeout'); break; default: setStatus('unavailable'); } setError(positionError.message); resolve(null); }, { enableHighAccuracy, timeout: timeoutMs, maximumAge: maximumAgeMs } ); }), [enableHighAccuracy, timeoutMs, maximumAgeMs] ); return { locate, status, reading, error }; }