diff --git a/src/lib/geolocation.ts b/src/lib/geolocation.ts index 9cd1e25..83f594c 100644 --- a/src/lib/geolocation.ts +++ b/src/lib/geolocation.ts @@ -59,21 +59,17 @@ export class GeolocationError extends Error { // ─── Native plugin proxy ────────────────────────────────────────────────────── -interface CapPosition { - coords: { latitude: number; longitude: number }; -} interface CapPermissionStatus { location: 'granted' | 'denied' | 'prompt' | 'prompt-with-rationale'; coarseLocation?: 'granted' | 'denied' | 'prompt' | 'prompt-with-rationale'; } -type CapWatchCallback = (position: CapPosition | null, err?: unknown) => void; +/** + * We deliberately only type the PERMISSION methods. The position methods + * (getCurrentPosition/watchPosition) are NOT used on native because they route + * through Google Play Services and crash de-Googled devices — see + * getNativePosition below, which fetches the position via navigator.geolocation. + */ interface CapGeolocationPlugin { - getCurrentPosition(options?: { enableHighAccuracy?: boolean; timeout?: number; maximumAge?: number }): Promise; - watchPosition( - options: { enableHighAccuracy?: boolean; timeout?: number; maximumAge?: number }, - callback: CapWatchCallback, - ): Promise; - clearWatch(options: { id: string }): Promise; checkPermissions(): Promise; requestPermissions(options?: { permissions?: string[] }): Promise; } @@ -164,136 +160,23 @@ export async function getCurrentPosition(): Promise { } /** - * Get the FIRST location fix from any provider as fast as possible — the trick - * that makes Google Maps feel instant indoors. + * Native path. * - * Runs two things in parallel and returns whichever wins: - * 1. watchPosition (low accuracy): fires a callback on the first update from - * the network/wifi/cell (fused) provider — usually within ~1s, works - * indoors. We resolve on that first callback, then immediately clearWatch. - * 2. getCurrentPosition (low accuracy, accept cached): resolves instantly if - * the OS already has a recent fix. + * CRITICAL — GrapheneOS / de-Googled devices + * ------------------------------------------ + * Capacitor Geolocation v8 fetches the position through Google Play Services + * (`play-services-location` / Ionic's `iongeolocation-android`). On a phone + * WITHOUT Play Services (GrapheneOS, /e/OS, most de-Googled ROMs) the plugin's + * `getCurrentPosition`/`watchPosition` touch Google classes that don't exist on + * the device → `NoClassDefFoundError` on the Android MAIN thread → the whole + * app is killed ("Near Me closes the app"). No JS try/catch can stop that. * - * A 25s overall deadline rejects so the caller's last-ditch high-accuracy - * attempt can run. watchPosition is guarded — if the plugin build doesn't - * expose it, we quietly rely on getCurrentPosition alone. - */ -function raceForFirstFix( - native: CapGeolocationPlugin, - safeCall: (fn: () => Promise) => Promise, -): Promise { - return new Promise((resolve, reject) => { - let settled = false; - const watchIds: string[] = []; - let deadline: ReturnType | null = null; - let sawTimeoutish = false; // remember if the failure looked like a timeout - - 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: latitude, lng: longitude }); - }; - - const fail = (err: unknown) => { - if (settled) return; - settled = true; - cleanup(); - reject(err instanceof Error ? err : new Error(String(err))); - }; - - // 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, - ); - - // 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: highAccuracy, timeout: 60000, maximumAge: 0 }, - (position, 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) { - safeCall(() => native.clearWatch({ id })).catch(() => {}); - } else { - watchIds.push(id); - } - }) - .catch((watchErr) => { - console.warn('[geolocation] watchPosition failed:', watchErr); - }); - }; - - // 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: true, - timeout: 55000, - maximumAge: 60000, - }), - ) - .then((pos) => succeed(pos)) - .catch((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); - }); - }); -} - -/** - * Native path. The plugin has already been verified as genuinely registered by - * `getNativeGeo`, so calling it is safe. Every bridge call is still funneled - * through `safeCall` so a synchronous throw becomes a catchable rejection - * rather than an uncaught error. + * THE FIX: we use the native plugin ONLY to request the OS location permission + * (that path is pure Android, no Play Services). For the actual position we use + * the WebView's `navigator.geolocation`, which talks to Android's own + * LocationManager (GPS/network) directly and works fine without Google. This + * keeps a proper native permission prompt while never invoking the + * Play-Services position code that crashes de-Googled devices. */ async function getNativePosition(native: CapGeolocationPlugin): Promise { // Wrap EVERY bridge call in Promise.resolve().then(...) so that even a @@ -304,9 +187,8 @@ async function getNativePosition(native: CapGeolocationPlugin): Promise native.checkPermissions()).catch(() => null); if (!status || (status.location !== 'granted' && status.coarseLocation !== 'granted')) { status = await safeCall(() => @@ -320,22 +202,12 @@ async function getNativePosition(native: CapGeolocationPlugin): Promise