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
LocationGateand 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:
| Status | How we detect it |
|---|---|
granted | navigator.permissions.query({name:'geolocation'}).state === 'granted' AND most-recent fix had accuracy โค 100m. |
granted_coarse | Permission state is granted, but the most-recent fix had accuracy > 100m. iOS "Approximate Location" and Android "Use approximate location" both surface here. |
prompt | navigator.permissions.query(...).state === 'prompt'. User has never been asked, or has cleared site data. |
denied | navigator.permissions.query(...).state === 'denied'. Per-site/per-app denial โ JS cannot re-prompt. |
services_off | Permission 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_disabled | localStorage.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:
- Detect โ what
detectPlatform()returns for this combination. - Recovery path โ the literal taps the user needs.
- App copy โ the string we render in
LocationGate. Keep these short; the screenshot does the heavy lifting. - 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+):
- Open Settings โ Apps โ Safari โ Location
- Choose Ask or Allow, and enable Precise Location
- 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 viawindow.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+):
- Open Settings โ Apps โ Lantern โ Location
- 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:
- Tap the lock icon in the address bar (or the tune/settings icon, depending on Chrome version)
- Tap Permissions โ Location
- Choose Allow
- 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 todeniedinnavigator.permissions; the system layer surfaces asservices_offfor our app even when system-wide Location Services are on. - Recovery path (system-level, the one users usually need):
- Open Settings โ Apps โ Lantern โ Permissions โ Location
- Choose Allow only while using the app
- 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:
- Click the lock icon (or "Not secure" warning) in the address bar
- Click Site settings
- Set Location to Allow
- 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:
- Safari โ Settings โ Websites โ Location
- Find lantern.app and choose Allow
- 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:
- Click the lock icon in the address bar
- Click the > next to "Connection secure" โ More information โ Permissions tab (older path: click the lock โ "Clear permissions and login")
- 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.
| Platform | Path |
|---|---|
| iOS (any) | Settings โ Privacy & Security โ Location Services โ toggle on. |
| Android (any) | Pull down quick settings โ tap Location, or Settings โ Location โ Use location. |
| macOS | System Settings โ Privacy & Security โ Location Services โ toggle on, then enable Safari/Chrome. |
| Windows | Settings โ 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 matchingdetectPlatform().os).
Unsupported browsers / privacy modes โ
- Detect:
{ ...anything, supported: false }โ surfaced as enumunsupported. - 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,TikTokin 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
oxipngor 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: lightonly 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
LocationGateand venue-list components, not here.