diff --git a/src/lib/geolocation.ts b/src/lib/geolocation.ts index 2dd65e4..9cd1e25 100644 --- a/src/lib/geolocation.ts +++ b/src/lib/geolocation.ts @@ -184,24 +184,37 @@ function raceForFirstFix( ): Promise { return new Promise((resolve, reject) => { let settled = false; - let watchId: string | null = null; + const watchIds: string[] = []; let deadline: ReturnType | null = null; + let sawTimeoutish = false; // remember if the failure looked like a timeout - const cleanup = () => { - if (deadline) clearTimeout(deadline); - if (watchId !== null) { - const id = watchId; - watchId = null; - // clearWatch is fire-and-forget; guard so it can't throw into us. + const clearAllWatches = () => { + while (watchIds.length) { + const id = watchIds.pop()!; safeCall(() => native.clearWatch({ id })).catch(() => {}); } }; + const cleanup = () => { + if (deadline) clearTimeout(deadline); + clearAllWatches(); + }; + const succeed = (pos: CapPosition) => { if (settled) return; + // Guard against garbage readings (0,0 / null island). + const { latitude, longitude } = pos?.coords ?? {}; + if ( + typeof latitude !== 'number' || + typeof longitude !== 'number' || + (latitude === 0 && longitude === 0) + ) { + console.warn('[geolocation] ignoring invalid reading', pos); + return; + } settled = true; cleanup(); - resolve({ lat: pos.coords.latitude, lng: pos.coords.longitude }); + resolve({ lat: latitude, lng: longitude }); }; const fail = (err: unknown) => { @@ -211,46 +224,67 @@ function raceForFirstFix( reject(err instanceof Error ? err : new Error(String(err))); }; - // Overall deadline — if nothing arrives, let the caller try high-accuracy. - deadline = setTimeout(() => fail(new Error('fast-fix timeout')), 25000); + // GrapheneOS (no Play Services) has no fast fused provider — a cold GPS lock + // can genuinely take 30–60s. Give it a real chance before giving up. + deadline = setTimeout( + () => fail(new Error(sawTimeoutish ? 'timeout' : 'fast-fix timeout')), + 60000, + ); - // 1) watchPosition — first update wins (fast network/fused fix). - if (typeof native.watchPosition === 'function') { + // Register a watch with the given accuracy; first valid reading wins. + const startWatch = (highAccuracy: boolean) => { + if (typeof native.watchPosition !== 'function') return; safeCall(() => native.watchPosition( - { enableHighAccuracy: false, timeout: 20000, maximumAge: 60000 }, + { enableHighAccuracy: highAccuracy, timeout: 60000, maximumAge: 0 }, (position, err) => { - if (position) succeed(position); - else if (err) console.warn('[geolocation] watch update error:', err); + if (position) { + succeed(position); + } else if (err) { + const m = String( + (err as { message?: string })?.message ?? err, + ).toLowerCase(); + if (m.includes('timeout') || m.includes('timed out')) sawTimeoutish = true; + console.warn('[geolocation] watch update error:', err); + } }, ), ) .then((id) => { if (settled) { - // Already resolved by getCurrentPosition; tidy up this watch. safeCall(() => native.clearWatch({ id })).catch(() => {}); } else { - watchId = id; + watchIds.push(id); } }) .catch((watchErr) => { - console.warn('[geolocation] watchPosition unavailable:', watchErr); + console.warn('[geolocation] watchPosition failed:', watchErr); }); - } + }; - // 2) getCurrentPosition — instant if the OS has a recent fix. + // Run BOTH a high-accuracy (GPS) and low-accuracy (network, if any) watch in + // parallel. On GrapheneOS the GPS one is usually what actually delivers, so + // high accuracy MUST be engaged — the old low-accuracy-only watch never + // fired without a network provider. Whichever returns first wins. + startWatch(true); + startWatch(false); + + // Also fire a one-shot high-accuracy getCurrentPosition (some plugin + // versions deliver here even when the watch is quiet). Cached ok. safeCall(() => native.getCurrentPosition({ - enableHighAccuracy: false, - timeout: 15000, - maximumAge: 120000, + enableHighAccuracy: true, + timeout: 55000, + maximumAge: 60000, }), ) .then((pos) => succeed(pos)) .catch((gcpErr) => { - // Don't fail the whole race — watchPosition may still deliver. Only the - // deadline (or watch) decides the outcome. Log for visibility. - console.warn('[geolocation] getCurrentPosition (fast) failed:', gcpErr); + const m = String( + (gcpErr as { message?: string })?.message ?? gcpErr, + ).toLowerCase(); + if (m.includes('timeout') || m.includes('timed out')) sawTimeoutish = true; + console.warn('[geolocation] getCurrentPosition failed:', gcpErr); }); }); } @@ -298,21 +332,10 @@ async function getNativePosition(native: CapGeolocationPlugin): Promise - native.getCurrentPosition({ - enableHighAccuracy: true, - timeout: 30000, - maximumAge: 300000, - }), - ); - return { lat: pos.coords.latitude, lng: pos.coords.longitude }; - } + // raceForFirstFix already runs parallel high+low accuracy watches AND a + // one-shot getCurrentPosition for up to 60s, so there's nothing more to try + // if it fails — let the error propagate to the classifier below. + return await raceForFirstFix(native, safeCall); } catch (err) { if (err instanceof GeolocationError) throw err; const msg = (err instanceof Error ? err.message : String(err)).toLowerCase(); diff --git a/src/pages/MapPage.tsx b/src/pages/MapPage.tsx index a78caf6..bec10aa 100644 --- a/src/pages/MapPage.tsx +++ b/src/pages/MapPage.tsx @@ -629,13 +629,28 @@ export default function MapPage() { setNearMeError(null); setNearMeLoading(true); + // Getting a GPS lock (especially first-time / on ROMs without Google's + // fused provider) can take a while — let the user know it's working. + const locatingToast = toast({ + title: 'Locating you…', + description: 'Getting a GPS fix. This can take up to a minute the first time.', + duration: 60000, + }); + try { const { lat, lng } = await getCurrentPosition(); + locatingToast.dismiss(); setFlyTo({ lat, lng, zoom: 14 }); setUserLocation({ lat, lon: lng }); setNearMeLoading(false); setNearMeError(null); + toast({ + title: 'Found you', + description: 'Centered the map on your location.', + duration: 3000, + }); } catch (err) { + locatingToast.dismiss(); setNearMeLoading(false); const kind = err instanceof GeolocationError ? err.kind : 'unavailable';