import 'package:geolocator/geolocator.dart'; /// Supplies the device's *approximate* location (or null when unavailable or /// denied). Behind an interface so the UI is fakeable in tests and the concrete /// platform plugin stays at the composition root. The caller immediately reduces /// the result to a low-precision geohash — a precise fix never leaves the device. abstract interface class CoarseLocationProvider { Future<({double lat, double lon})?> currentCoarseLatLon(); } /// [geolocator]-backed implementation. Requests low accuracy only, and returns /// null on any denial/error rather than throwing, so the UI can degrade quietly. class GeolocatorCoarseLocation implements CoarseLocationProvider { const GeolocatorCoarseLocation(); @override Future<({double lat, double lon})?> currentCoarseLatLon() async { try { var permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); } // Give the user a way out of a permanent denial. if (permission == LocationPermission.deniedForever) { await Geolocator.openAppSettings(); return null; } if (permission == LocationPermission.denied) return null; if (!await Geolocator.isLocationServiceEnabled()) { await Geolocator.openLocationSettings(); return null; } // A fresh coarse fix can take a while (or never come indoors); fall back // to the last known position so the button stays responsive. Position? position; try { position = await Geolocator.getCurrentPosition( locationSettings: const LocationSettings( accuracy: LocationAccuracy.low, timeLimit: Duration(seconds: 12), ), ); } catch (_) { position = await Geolocator.getLastKnownPosition(); } if (position == null) return null; return (lat: position.latitude, lon: position.longitude); } catch (_) { return null; // plugin unsupported (desktop), etc. } } }