Improve geolocation reliability: 3-stage fix with forgiving timeouts

Now that the native crash is fixed (Kotlin 2.1 alignment on the Android
side), the remaining issue was GPS "Location timed out" on a cold first
fix / indoors. Rework the native position acquisition into three
increasingly-forgiving stages that return the first success:

  1. INSTANT — accept a cached/coarse fix up to 5 min old (resolves
     immediately on a warm chip).
  2. PRECISE — high-accuracy GPS with a generous 30s window so a cold
     first-fix has room to complete.
  3. COARSE FALLBACK — network/wifi fix with the longest window to
     rescue the "GPS can't lock indoors" case.

Also bump the web-path timeout to 20s, and make the "Location timed out"
toast actionable on native (suggests stepping near a window / retrying).

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy 2026-07-24 22:47:48 +07:00
parent 71e5c5df1f
commit 9c7cd5ec4e
2 changed files with 41 additions and 17 deletions

View file

@ -188,31 +188,53 @@ async function getNativePosition(native: CapGeolocationPlugin): Promise<GeoCoord
throw new GeolocationError('denied', 'Location permission denied.'); throw new GeolocationError('denied', 'Location permission denied.');
} }
// First try a precise fix. High accuracy can fail/time out indoors or // Getting a location fix — especially a "cold start" the first time after
// with a weak GPS signal, which shows up as "unavailable" or "timeout". // granting permission, or indoors — can be slow. A GPS chip can take 30s+
// to acquire its first lock. So we try three increasingly-forgiving stages
// and return the first one that succeeds. Each is guarded so a failure just
// moves on to the next stage rather than surfacing as an error.
// Stage 1 — INSTANT: accept any recently cached fix (up to 5 min old).
// enableHighAccuracy:false + short timeout means "give me a cached/coarse
// fix immediately if you have one". This resolves instantly on a warm chip.
try {
const pos = await safeCall(() =>
native.getCurrentPosition({
enableHighAccuracy: false,
timeout: 8000,
maximumAge: 300000, // accept a fix up to 5 minutes old
}),
);
return { lat: pos.coords.latitude, lng: pos.coords.longitude };
} catch (cachedErr) {
console.warn('[geolocation] cached/coarse fix unavailable, trying GPS:', cachedErr);
}
// Stage 2 — PRECISE: full high-accuracy GPS with a generous window so a
// cold first-fix has room to complete.
try { try {
const pos = await safeCall(() => const pos = await safeCall(() =>
native.getCurrentPosition({ native.getCurrentPosition({
enableHighAccuracy: true, enableHighAccuracy: true,
timeout: 10000, timeout: 30000, // give a cold GPS lock plenty of time
maximumAge: 30000, maximumAge: 60000,
}), }),
); );
return { lat: pos.coords.latitude, lng: pos.coords.longitude }; return { lat: pos.coords.latitude, lng: pos.coords.longitude };
} catch (highAccErr) { } catch (highAccErr) {
// Fall back to a coarse (network/cell/wifi) fix with a longer window.
// This rescues the common "GPS can't lock" case that otherwise looks
// like a hard failure.
console.warn('[geolocation] high-accuracy failed, trying coarse:', highAccErr); console.warn('[geolocation] high-accuracy failed, trying coarse:', highAccErr);
const pos = await safeCall(() =>
native.getCurrentPosition({
enableHighAccuracy: false,
timeout: 20000,
maximumAge: 120000,
}),
);
return { lat: pos.coords.latitude, lng: pos.coords.longitude };
} }
// Stage 3 — COARSE FALLBACK: network/cell/wifi fix with the longest window.
// This rescues the common "GPS can't lock indoors" case.
const pos = await safeCall(() =>
native.getCurrentPosition({
enableHighAccuracy: false,
timeout: 30000,
maximumAge: 300000,
}),
);
return { lat: pos.coords.latitude, lng: pos.coords.longitude };
} catch (err) { } catch (err) {
if (err instanceof GeolocationError) throw err; if (err instanceof GeolocationError) throw err;
const msg = (err instanceof Error ? err.message : String(err)).toLowerCase(); const msg = (err instanceof Error ? err.message : String(err)).toLowerCase();
@ -255,7 +277,7 @@ function getWebPosition(): Promise<GeoCoords> {
reject(new GeolocationError('unavailable', 'Location unavailable.')); reject(new GeolocationError('unavailable', 'Location unavailable.'));
} }
}, },
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 30000 }, { enableHighAccuracy: true, timeout: 20000, maximumAge: 60000 },
); );
}); });
} }

View file

@ -656,7 +656,9 @@ export default function MapPage() {
setNearMeError('timeout'); setNearMeError('timeout');
toast({ toast({
title: 'Location timed out', title: 'Location timed out',
description: 'Could not get your location in time. Check your signal and try again.', description: isNativeGeolocation()
? "Couldn't get a GPS fix in time. This is common on the first try or indoors — step near a window or outside and tap Near Me again."
: 'Could not get your location in time. Check your signal and try again.',
variant: 'destructive', variant: 'destructive',
}); });
} else { } else {