Skip to content

Location Permission Recovery โ€‹

Status: Phase 1 deliverable for #355. This doc is the source of truth for the platform-specific recovery copy and screenshots that ship inside the LocationGate and persistent banner UIs.

Browsers do not allow JavaScript to re-prompt for geolocation once a user has denied it. Recovery requires the user to change a setting outside of our app. The path to that setting depends on platform, browser, and whether the app is running in a browser tab or as an installed PWA.

This document covers:

  • How to detect which permission/services state the user is in
  • The recovery path for each (platform, browser, install context) combination
  • The copy we render inside the app to lead the user there
  • The screenshots that ship alongside that copy

Detection matrix โ€‹

apps/web/src/lib/locationPermission.js exposes a single status enum we render against. The mapping from browser signals to enum value:

StatusHow we detect it
grantednavigator.permissions.query({name:'geolocation'}).state === 'granted' AND most-recent fix had accuracy โ‰ค 100m.
granted_coarsePermission state is granted, but the most-recent fix had accuracy > 100m. iOS "Approximate Location" and Android "Use approximate location" both surface here.
promptnavigator.permissions.query(...).state === 'prompt'. User has never been asked, or has cleared site data.
deniednavigator.permissions.query(...).state === 'denied'. Per-site/per-app denial โ€” JS cannot re-prompt.
services_offPermission state is granted but getCurrentPosition returns POSITION_UNAVAILABLE. OS-level Location Services are off. (On iOS, this also happens when Airplane Mode is on without Wi-Fi.)
unsupported!('geolocation' in navigator) or !navigator.permissions. Privacy browsers, very old WebViews.
tracking_disabledlocalStorage.location_tracking_disabled === 'true'. App-level opt-out we expose in Settings โ€” orthogonal to the OS state.

A user can be in two states at once (e.g. tracking_disabled AND denied). The library returns the highest-priority state in the order above (tracking_disabled is checked first because we never want to surface a "fix your phone" CTA when the user is the one who turned it off).

subscribeToPermissionChanges(callback) listens to PermissionStatus.onchange so the persistent banner can react when the user fixes (or breaks) the state mid-session. There is no equivalent listener for services_off โ€” that only flips when we attempt a fix, so we re-probe on visibility change.


Per-platform recovery โ€‹

Each section below has the same shape:

  1. Detect โ€” what detectPlatform() returns for this combination.
  2. Recovery path โ€” the literal taps the user needs.
  3. App copy โ€” the string we render in LocationGate. Keep these short; the screenshot does the heavy lifting.
  4. Screenshot โ€” the asset ID we ship in apps/web/src/assets/location-recovery/. Filenames are stable so engineers can replace screenshots without code changes.

Screenshots themselves still need to be captured by a human on real hardware โ€” see "Screenshot capture checklist" at the bottom. Filenames below are the contract.

iOS Safari (browser tab) โ€‹

  • Detect: { os: 'ios', browser: 'safari', isPWA: false }
  • Recovery path (iOS 17+):
    1. Open Settings โ†’ Apps โ†’ Safari โ†’ Location
    2. Choose Ask or Allow, and enable Precise Location
    3. Return to Lantern and tap I've fixed it
  • App copy:

    Safari is blocking your location. Open Settings โ†’ Apps โ†’ Safari โ†’ Location, choose Ask, and turn on Precise Location. Then come back and tap below.

  • Screenshot: ios-safari-location.png

iOS 16 and earlier put this under Settings โ†’ Safari โ†’ Location (no "Apps" middle layer). We render the iOS 17+ path because iOS 16 share is now negligible in our analytics โ€” but the iOS 16 fallback is functionally the same final screen.

iOS PWA (added to Home Screen) โ€‹

  • Detect: { os: 'ios', browser: 'safari', isPWA: true } โ€” we detect via window.matchMedia('(display-mode: standalone)').matches || navigator.standalone === true.
  • Why this is its own case: iOS treats an installed PWA as a separate "app" with its own permissions. Granting Safari location access does not grant the PWA, and vice versa. This trips up users constantly.
  • Recovery path (iOS 17+):
    1. Open Settings โ†’ Apps โ†’ Lantern โ†’ Location
    2. Choose While Using the App and enable Precise Location
  • App copy:

    Lantern is installed as an app, which has its own location setting. Open Settings โ†’ Apps โ†’ Lantern โ†’ Location, pick While Using the App, and turn on Precise Location.

  • Screenshot: ios-pwa-location.png

Android Chrome (browser tab) โ€‹

  • Detect: { os: 'android', browser: 'chrome', isPWA: false }
  • Recovery path:
    1. Tap the lock icon in the address bar (or the tune/settings icon, depending on Chrome version)
    2. Tap Permissions โ†’ Location
    3. Choose Allow
    4. Reload the page
  • App copy:

    Tap the lock icon next to lantern.app in the address bar, then Permissions โ†’ Location โ†’ Allow, and reload.

  • Screenshot: android-chrome-site-permission.png

Android Chrome PWA (installed) โ€‹

  • Detect: { os: 'android', browser: 'chrome', isPWA: true }
  • Why this is its own case: Installed Android PWAs have two permission layers โ€” the Chrome site-permission layer and the Android system app-permission layer (Settings โ†’ Apps โ†’ Lantern โ†’ Permissions โ†’ Location). Either one being denied blocks us. The site-permission layer is the one that flips to denied in navigator.permissions; the system layer surfaces as services_off for our app even when system-wide Location Services are on.
  • Recovery path (system-level, the one users usually need):
    1. Open Settings โ†’ Apps โ†’ Lantern โ†’ Permissions โ†’ Location
    2. Choose Allow only while using the app
    3. Make sure Use precise location is on
  • App copy:

    Lantern is installed and has its own location permission. Open Settings โ†’ Apps โ†’ Lantern โ†’ Permissions โ†’ Location, choose Allow only while using the app, and turn on Use precise location.

  • Screenshot: android-pwa-app-permission.png

Desktop Chrome (Windows/macOS/Linux) โ€‹

  • Detect: { os: 'windows' | 'macos' | 'linux', browser: 'chrome', isPWA: false }
  • Recovery path:
    1. Click the lock icon (or "Not secure" warning) in the address bar
    2. Click Site settings
    3. Set Location to Allow
    4. Reload the page
  • App copy:

    Click the lock icon in the address bar โ†’ Site settings โ†’ set Location to Allow, then reload.

  • Screenshot: desktop-chrome-site-settings.png

Desktop Safari (macOS) โ€‹

  • Detect: { os: 'macos', browser: 'safari', isPWA: false }
  • Recovery path:
    1. Safari โ†’ Settings โ†’ Websites โ†’ Location
    2. Find lantern.app and choose Allow
    3. Also confirm System Settings โ†’ Privacy & Security โ†’ Location Services โ†’ Safari is enabled
  • App copy:

    In Safari, choose Safari โ†’ Settings โ†’ Websites โ†’ Location, find lantern.app, and pick Allow.

  • Screenshot: desktop-safari-website-settings.png

Desktop Firefox โ€‹

  • Detect: { os: <any desktop>, browser: 'firefox', isPWA: false }
  • Recovery path:
    1. Click the lock icon in the address bar
    2. Click the > next to "Connection secure" โ†’ More information โ†’ Permissions tab (older path: click the lock โ†’ "Clear permissions and login")
    3. Find Access your location and uncheck Use Default, then choose Allow
  • App copy:

    Click the lock icon in the address bar, then under Permissions, set Access your location to Allow.

  • Screenshot: desktop-firefox-permissions.png

OS-level Location Services off โ€‹

This is platform-level โ€” independent of browser/PWA โ€” and surfaces to us as services_off. The user has location permission for our app, but the OS isn't giving anyone a fix.

PlatformPath
iOS (any)Settings โ†’ Privacy & Security โ†’ Location Services โ†’ toggle on.
Android (any)Pull down quick settings โ†’ tap Location, or Settings โ†’ Location โ†’ Use location.
macOSSystem Settings โ†’ Privacy & Security โ†’ Location Services โ†’ toggle on, then enable Safari/Chrome.
WindowsSettings โ†’ Privacy & security โ†’ Location โ†’ turn on Location services.
  • App copy (universal):

    Your phone's Location Services are turned off, so no app can find your location right now. Open Settings โ†’ Privacy โ†’ Location Services and turn it on.

  • Screenshot: os-location-services-off-ios.png, os-location-services-off-android.png (we render the one matching detectPlatform().os).

Unsupported browsers / privacy modes โ€‹

  • Detect: { ...anything, supported: false } โ€” surfaced as enum unsupported.
  • Common causes: old WebView, privacy browsers (Brave's "Block Geolocation" preset, Firefox Focus, DuckDuckGo Privacy Browser default), some embedded in-app browsers (Instagram, TikTok in-app browser).
  • App copy:

    Your browser is blocking location data. Try opening lantern.app in Safari (iPhone) or Chrome (Android).

  • No platform screenshot. Render a simple "open in your default browser" CTA. If we detect an in-app webview (FBAN, Instagram, TikTok in UA), we additionally render the universal "Open in browser" hint.

Coarse-vs-precise detection โ€‹

Both iOS ("Precise Location" off) and Android ("Use precise location" off) hand us a permission state of granted while feeding us a fuzzed coordinate. The fuzz is typically in the kilometers โ€” useless for venue identification.

We classify a fix as granted_coarse when position.coords.accuracy > 100. The 100m threshold is the constant ACCURACY_THRESHOLD_M exported from locationPermission.js. It was chosen because:

  • Typical urban GPS fix is 5โ€“20m.
  • iOS "Approximate Location" reports ~1โ€“5km accuracy.
  • Android "Approximate location" reports several hundred meters to several km.
  • 100m is comfortably above noise but well below any approximate-mode return.

Recovery for granted_coarse is the same screen as granted recovery on each platform โ€” just toggle on Precise Location (iOS) or Use precise location (Android) at the per-app permission screen.


Screenshot capture checklist โ€‹

Screenshots aren't part of this PR โ€” they need a human on real hardware. The contract is:

  • Filenames as listed above, all under apps/web/src/assets/location-recovery/.
  • PNG, โ‰ค 200KB each (compressed via oxipng or similar before commit).
  • Crop to the relevant Settings panel only โ€” don't ship full-screen status bars.
  • One screenshot per platform combination listed; do not ship light/dark variants (we render under prefers-color-scheme: light only for now to keep asset count down).
  • iOS shots from iOS 17+ (current required min). Android shots from Android 13+.

When a screenshot is missing the component falls back to text-only copy โ€” broken images do not render.


Out of scope for this doc โ€‹

  • Server-side geofence enforcement for write actions (light, wave, check-in). That stays on the API and is unaffected by this work โ€” see the issue's "Non-Goals" section.
  • Permission prompts inside the signup flow. All gating moved to post-signup dashboard so signup never blocks on GPS. Signup-flow copy lives elsewhere.
  • Read-only fallback UX (Phase 5) โ€” covered in LocationGate and venue-list components, not here.

Built with VitePress