import 'package:geolocator/geolocator.dart'; /// Why a location fix is not available. Each value maps to a distinct thing we /// tell the customer, so none of them is a generic failure. enum LocationDenial { /// Device location is switched off entirely. serviceOff, /// Permission refused this time; asking again is allowed. denied, /// Refused permanently — only Settings can undo it. deniedForever, /// Permission granted, but the fix timed out or the platform failed. unavailable, } /// A resolved device position, or the reason there isn't one. class LocationFix { const LocationFix.ok(this.lat, this.lng, {this.accuracy}) : denial = null; const LocationFix.denied(this.denial) : lat = null, lng = null, accuracy = null; final double? lat; final double? lng; /// Horizontal accuracy in metres, when the platform reports it. Drawn as the /// halo around the blue dot, so the customer can see how sure we are. final double? accuracy; final LocationDenial? denial; bool get hasFix => lat != null && lng != null; /// What to show the customer when there is no fix. Never a raw error. String get message => switch (denial) { LocationDenial.serviceOff => 'Turn on location to drop the pin for you. You can still search for the address.', LocationDenial.denied => 'Allow location access and we will drop the pin for you.', LocationDenial.deniedForever => 'Location is blocked for Doormile. Enable it in Settings, or search for the address.', _ => 'We could not get your location. Search for the address instead.', }; } /// The only place the app touches platform GPS. /// /// Every failure — permission refused, location off, no plugin (widget tests) — /// comes back as a [LocationFix] with a [LocationDenial]. Nothing throws, so a /// caller never has to guard the booking flow behind a try/catch. class LocationService { LocationService(); /// Swappable so tests can answer without a platform. static LocationService instance = LocationService(); /// Coimbatore city centre. Where the map opens before we know better, so the /// customer never stares at a grey ocean tile. static const fallbackLat = 11.0168; static const fallbackLng = 76.9558; /// How long to wait on a permission or service answer before giving up. static const _probe = Duration(seconds: 4); /// Asks for permission if we do not have it yet, then returns a fix. /// /// Every platform call is bounded: a wedged location plugin must never leave /// the customer watching "Getting address…" with no way forward. Future current({ Duration timeout = const Duration(seconds: 12), }) async { final enabled = await _guard(Geolocator.isLocationServiceEnabled, _probe); if (enabled == null) { return const LocationFix.denied(LocationDenial.unavailable); } if (!enabled) return const LocationFix.denied(LocationDenial.serviceOff); var permission = await _guard(Geolocator.checkPermission, _probe); if (permission == LocationPermission.denied) { permission = await _guard(Geolocator.requestPermission, timeout); } if (permission == null) { return const LocationFix.denied(LocationDenial.unavailable); } if (permission == LocationPermission.deniedForever) { return const LocationFix.denied(LocationDenial.deniedForever); } if (permission == LocationPermission.denied) { return const LocationFix.denied(LocationDenial.denied); } final fix = await _guard( () => Geolocator.getCurrentPosition( locationSettings: LocationSettings( accuracy: LocationAccuracy.high, timeLimit: timeout, ), ), timeout, // A cached fix beats no fix at all when the precise one times out. ) ?? await _guard(() => Geolocator.getLastKnownPosition(), _probe); return fix == null ? const LocationFix.denied(LocationDenial.unavailable) : LocationFix.ok( fix.latitude, fix.longitude, accuracy: fix.accuracy > 0 ? fix.accuracy : null, ); } /// Runs a platform call, returning null instead of throwing or hanging. /// Covers MissingPluginException, a refused channel, and a plugin that /// simply never answers. static Future _guard(Future Function() call, Duration limit) async { try { return await call().timeout(limit); } on Object { return null; } } /// Opens the OS settings page so a permanently denied customer can recover. Future openSettings() async { try { await Geolocator.openAppSettings(); } on Object { // Nothing useful to do if the platform refuses. } } } /// Answers instantly with a known position, without touching the platform. /// /// **A test seam, and only that.** Widget tests run on a fake clock that a real /// platform-channel reply never reaches, so every test that renders the pickup /// screen installs one of these as [LocationService.instance]. Nothing in the /// app does, and nothing should: a build that invents where the customer is /// sends a Miler to the wrong door. class FixedLocationService extends LocationService { FixedLocationService({ this.lat = LocationService.fallbackLat, this.lng = LocationService.fallbackLng, this.accuracy = 18, this.denial, }); /// A service that always refuses, for exercising the denied states. FixedLocationService.denied(LocationDenial reason) : lat = LocationService.fallbackLat, lng = LocationService.fallbackLng, accuracy = null, denial = reason; final double lat; final double lng; final double? accuracy; final LocationDenial? denial; @override Future current({ Duration timeout = const Duration(seconds: 12), }) async => denial != null ? LocationFix.denied(denial!) : LocationFix.ok(lat, lng, accuracy: accuracy); @override Future openSettings() async {} }