import { useState, useEffect, useCallback, useRef } from 'react'; import type { ParticipantLocation, LocationSource } from '@/types'; interface UseLocationSharingOptions { /** Called when location updates */ onLocationUpdate?: (location: ParticipantLocation) => void; /** Update interval in milliseconds (default: 5000) */ updateInterval?: number; /** Enable high accuracy mode (uses more battery) */ highAccuracy?: boolean; /** Maximum age of cached position in ms (default: 5000) - lower = fresher positions */ maxAge?: number; /** Timeout for position request in ms (default: 30000) */ timeout?: number; } interface UseLocationSharingReturn { /** Whether location sharing is currently active */ isSharing: boolean; /** Current location (if available) */ currentLocation: ParticipantLocation | null; /** Any error that occurred */ error: GeolocationPositionError | null; /** Start sharing location */ startSharing: () => void; /** Stop sharing location */ stopSharing: () => void; /** Request a single location update */ requestUpdate: () => void; /** Permission state */ permissionState: PermissionState | null; } export function useLocationSharing( options: UseLocationSharingOptions = {} ): UseLocationSharingReturn { const { onLocationUpdate, updateInterval = 5000, highAccuracy = true, maxAge = 30000, // Allow cached position up to 30 seconds old timeout = 15000, // 15 second timeout before fallback } = options; const [isSharing, setIsSharing] = useState(false); const [currentLocation, setCurrentLocation] = useState(null); const [error, setError] = useState(null); const [permissionState, setPermissionState] = useState(null); const watchIdRef = useRef(null); const onLocationUpdateRef = useRef(onLocationUpdate); const timeoutCountRef = useRef(0); const usingLowAccuracyRef = useRef(false); // Keep callback ref updated useEffect(() => { onLocationUpdateRef.current = onLocationUpdate; }, [onLocationUpdate]); // Check permission state useEffect(() => { if ('permissions' in navigator) { navigator.permissions .query({ name: 'geolocation' }) .then((result) => { setPermissionState(result.state); result.addEventListener('change', () => { setPermissionState(result.state); }); }) .catch(() => { // Permissions API not supported, will check on first request }); } }, []); const handlePosition = useCallback((position: GeolocationPosition) => { // Reset timeout counter on successful position timeoutCountRef.current = 0; const location: ParticipantLocation = { latitude: position.coords.latitude, longitude: position.coords.longitude, accuracy: position.coords.accuracy, altitude: position.coords.altitude ?? undefined, altitudeAccuracy: position.coords.altitudeAccuracy ?? undefined, heading: position.coords.heading ?? undefined, speed: position.coords.speed ?? undefined, timestamp: new Date(position.timestamp), source: 'gps' as LocationSource, }; setCurrentLocation(location); setError(null); onLocationUpdateRef.current?.(location); }, []); // Restart watcher with low accuracy mode const restartWithLowAccuracy = useCallback(() => { if (watchIdRef.current !== null) { navigator.geolocation.clearWatch(watchIdRef.current); } console.log('Switching to low accuracy mode for faster location'); usingLowAccuracyRef.current = true; const geoOptions: PositionOptions = { enableHighAccuracy: false, maximumAge: 60000, // Accept positions up to 1 minute old timeout: 60000, // Longer timeout for low accuracy }; watchIdRef.current = navigator.geolocation.watchPosition( handlePosition, (err) => { setError(err); console.warn('Geolocation warning (low accuracy):', err.message, '(code:', err.code, ')'); }, geoOptions ); }, [handlePosition]); const handleError = useCallback((err: GeolocationPositionError) => { setError(err); // Count consecutive timeouts if (err.code === 3) { // TIMEOUT timeoutCountRef.current++; console.warn('Geolocation timeout', timeoutCountRef.current, '/', 3); // After 2 timeouts, switch to low accuracy mode if (timeoutCountRef.current >= 2 && !usingLowAccuracyRef.current && highAccuracy) { restartWithLowAccuracy(); return; } } // Only log as warning, not error - timeouts are common indoors console.warn('Geolocation warning:', err.message, '(code:', err.code, ')'); // Don't stop sharing on errors - keep trying }, [highAccuracy, restartWithLowAccuracy]); const startSharing = useCallback(() => { if (!('geolocation' in navigator)) { console.error('Geolocation is not supported'); return; } if (watchIdRef.current !== null) { return; // Already watching } // Reset fallback flags timeoutCountRef.current = 0; usingLowAccuracyRef.current = false; const geoOptions: PositionOptions = { enableHighAccuracy: highAccuracy, maximumAge: maxAge, timeout, }; // Start watching position watchIdRef.current = navigator.geolocation.watchPosition( handlePosition, handleError, geoOptions ); setIsSharing(true); console.log('Started location sharing'); }, [highAccuracy, maxAge, timeout, handlePosition, handleError]); const stopSharing = useCallback(() => { if (watchIdRef.current !== null) { navigator.geolocation.clearWatch(watchIdRef.current); watchIdRef.current = null; } setIsSharing(false); console.log('Stopped location sharing'); }, []); const requestUpdate = useCallback(() => { if (!('geolocation' in navigator)) { return; } navigator.geolocation.getCurrentPosition( handlePosition, handleError, { enableHighAccuracy: highAccuracy, maximumAge: 0, timeout, } ); }, [highAccuracy, timeout, handlePosition, handleError]); // Cleanup on unmount useEffect(() => { return () => { if (watchIdRef.current !== null) { navigator.geolocation.clearWatch(watchIdRef.current); } }; }, []); return { isSharing, currentLocation, error, startSharing, stopSharing, requestUpdate, permissionState, }; }