From 1a81db9bf0a054a9ba658c86b098c055083dfb95 Mon Sep 17 00:00:00 2001 From: vjrj Date: Fri, 10 Jul 2026 11:09:12 +0200 Subject: [PATCH] fix(block2): sharing sheet overflow + location button feedback/robustness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported: sheet overflowed 7.7px with the keyboard up, and 'use my location' seemed to do nothing. - Config sheet is now scrollable (SingleChildScrollView) — no keyboard overflow. - 'Use my location' shows an inline spinner + inline error (the old snackbar was hidden BEHIND the bottom sheet, so failures were invisible). Message is now actionable ('check location is on and the permission is granted'). - Provider hardening: request permission first; on permanent denial open app settings; if location services are off open location settings; time-limit the fix and fall back to last-known position so it doesn't hang indoors. Analyzer clean; market tests green. --- apps/app_seeds/lib/i18n/en.i18n.json | 2 +- apps/app_seeds/lib/i18n/es.i18n.json | 2 +- apps/app_seeds/lib/i18n/pt.i18n.json | 2 +- apps/app_seeds/lib/i18n/strings.g.dart | 2 +- apps/app_seeds/lib/i18n/strings_en.g.dart | 6 +-- apps/app_seeds/lib/i18n/strings_es.g.dart | 4 +- apps/app_seeds/lib/i18n/strings_pt.g.dart | 4 +- .../lib/services/coarse_location.dart | 33 ++++++++++---- apps/app_seeds/lib/ui/market_screen.dart | 45 ++++++++++++++----- 9 files changed, 69 insertions(+), 31 deletions(-) diff --git a/apps/app_seeds/lib/i18n/en.i18n.json b/apps/app_seeds/lib/i18n/en.i18n.json index 5feeae6..2b6496c 100644 --- a/apps/app_seeds/lib/i18n/en.i18n.json +++ b/apps/app_seeds/lib/i18n/en.i18n.json @@ -357,7 +357,7 @@ "sharedCount": "Shared {n} seeds nearby", "nothingToShare": "Mark some seeds to give, swap or sell first", "useLocation": "Use my approximate location", - "locationFailed": "Couldn't get your location", + "locationFailed": "Couldn't get your location — check that location is on and the permission is granted", "queued": "Saved — we'll share these when you're connected" }, "chat": { diff --git a/apps/app_seeds/lib/i18n/es.i18n.json b/apps/app_seeds/lib/i18n/es.i18n.json index 62e5aa9..71e3ea0 100644 --- a/apps/app_seeds/lib/i18n/es.i18n.json +++ b/apps/app_seeds/lib/i18n/es.i18n.json @@ -357,7 +357,7 @@ "sharedCount": "Compartidas {n} semillas", "nothingToShare": "Marca antes algunas semillas para regalar, cambiar o vender", "useLocation": "Usar mi ubicación aproximada", - "locationFailed": "No se pudo obtener tu ubicación", + "locationFailed": "No se pudo obtener tu ubicación — comprueba que la ubicación está activada y el permiso concedido", "queued": "Guardado — las compartiremos cuando tengas conexión" }, "chat": { diff --git a/apps/app_seeds/lib/i18n/pt.i18n.json b/apps/app_seeds/lib/i18n/pt.i18n.json index 5e5dbf8..294f85b 100644 --- a/apps/app_seeds/lib/i18n/pt.i18n.json +++ b/apps/app_seeds/lib/i18n/pt.i18n.json @@ -353,7 +353,7 @@ "sharedCount": "Partilhadas {n} sementes", "nothingToShare": "Marca primeiro algumas sementes para dar, trocar ou vender", "useLocation": "Usar a minha localização aproximada", - "locationFailed": "Não foi possível obter a tua localização", + "locationFailed": "Não foi possível obter a tua localização — verifica se a localização está ativa e a permissão concedida", "queued": "Guardado — vamos partilhá-las quando tiveres ligação" }, "chat": { diff --git a/apps/app_seeds/lib/i18n/strings.g.dart b/apps/app_seeds/lib/i18n/strings.g.dart index 77491c8..a36c314 100644 --- a/apps/app_seeds/lib/i18n/strings.g.dart +++ b/apps/app_seeds/lib/i18n/strings.g.dart @@ -6,7 +6,7 @@ /// Locales: 3 /// Strings: 986 (328 per locale) /// -/// Built on 2026-07-10 at 09:03 UTC +/// Built on 2026-07-10 at 09:08 UTC // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/apps/app_seeds/lib/i18n/strings_en.g.dart b/apps/app_seeds/lib/i18n/strings_en.g.dart index dc6c5e6..46f9cac 100644 --- a/apps/app_seeds/lib/i18n/strings_en.g.dart +++ b/apps/app_seeds/lib/i18n/strings_en.g.dart @@ -1132,8 +1132,8 @@ class Translations$market$en { /// en: 'Use my approximate location' String get useLocation => 'Use my approximate location'; - /// en: 'Couldn't get your location' - String get locationFailed => 'Couldn\'t get your location'; + /// en: 'Couldn't get your location — check that location is on and the permission is granted' + String get locationFailed => 'Couldn\'t get your location — check that location is on and the permission is granted'; /// en: 'Saved — we'll share these when you're connected' String get queued => 'Saved — we\'ll share these when you\'re connected'; @@ -1960,7 +1960,7 @@ extension on Translations { 'market.sharedCount' => ({required Object n}) => 'Shared ${n} seeds nearby', 'market.nothingToShare' => 'Mark some seeds to give, swap or sell first', 'market.useLocation' => 'Use my approximate location', - 'market.locationFailed' => 'Couldn\'t get your location', + 'market.locationFailed' => 'Couldn\'t get your location — check that location is on and the permission is granted', 'market.queued' => 'Saved — we\'ll share these when you\'re connected', 'chat.title' => 'Chat', 'chat.hint' => 'Write a message…', diff --git a/apps/app_seeds/lib/i18n/strings_es.g.dart b/apps/app_seeds/lib/i18n/strings_es.g.dart index a36d6b5..9ffe13d 100644 --- a/apps/app_seeds/lib/i18n/strings_es.g.dart +++ b/apps/app_seeds/lib/i18n/strings_es.g.dart @@ -629,7 +629,7 @@ class _Translations$market$es extends Translations$market$en { @override String sharedCount({required Object n}) => 'Compartidas ${n} semillas'; @override String get nothingToShare => 'Marca antes algunas semillas para regalar, cambiar o vender'; @override String get useLocation => 'Usar mi ubicación aproximada'; - @override String get locationFailed => 'No se pudo obtener tu ubicación'; + @override String get locationFailed => 'No se pudo obtener tu ubicación — comprueba que la ubicación está activada y el permiso concedido'; @override String get queued => 'Guardado — las compartiremos cuando tengas conexión'; } @@ -1320,7 +1320,7 @@ extension on TranslationsEs { 'market.sharedCount' => ({required Object n}) => 'Compartidas ${n} semillas', 'market.nothingToShare' => 'Marca antes algunas semillas para regalar, cambiar o vender', 'market.useLocation' => 'Usar mi ubicación aproximada', - 'market.locationFailed' => 'No se pudo obtener tu ubicación', + 'market.locationFailed' => 'No se pudo obtener tu ubicación — comprueba que la ubicación está activada y el permiso concedido', 'market.queued' => 'Guardado — las compartiremos cuando tengas conexión', 'chat.title' => 'Chat', 'chat.hint' => 'Escribe un mensaje…', diff --git a/apps/app_seeds/lib/i18n/strings_pt.g.dart b/apps/app_seeds/lib/i18n/strings_pt.g.dart index ae63640..bbfc317 100644 --- a/apps/app_seeds/lib/i18n/strings_pt.g.dart +++ b/apps/app_seeds/lib/i18n/strings_pt.g.dart @@ -625,7 +625,7 @@ class _Translations$market$pt extends Translations$market$en { @override String sharedCount({required Object n}) => 'Partilhadas ${n} sementes'; @override String get nothingToShare => 'Marca primeiro algumas sementes para dar, trocar ou vender'; @override String get useLocation => 'Usar a minha localização aproximada'; - @override String get locationFailed => 'Não foi possível obter a tua localização'; + @override String get locationFailed => 'Não foi possível obter a tua localização — verifica se a localização está ativa e a permissão concedida'; @override String get queued => 'Guardado — vamos partilhá-las quando tiveres ligação'; } @@ -1312,7 +1312,7 @@ extension on TranslationsPt { 'market.sharedCount' => ({required Object n}) => 'Partilhadas ${n} sementes', 'market.nothingToShare' => 'Marca primeiro algumas sementes para dar, trocar ou vender', 'market.useLocation' => 'Usar a minha localização aproximada', - 'market.locationFailed' => 'Não foi possível obter a tua localização', + 'market.locationFailed' => 'Não foi possível obter a tua localização — verifica se a localização está ativa e a permissão concedida', 'market.queued' => 'Guardado — vamos partilhá-las quando tiveres ligação', 'chat.title' => 'Conversa', 'chat.hint' => 'Escreve uma mensagem…', diff --git a/apps/app_seeds/lib/services/coarse_location.dart b/apps/app_seeds/lib/services/coarse_location.dart index cd7a50b..5dfef0b 100644 --- a/apps/app_seeds/lib/services/coarse_location.dart +++ b/apps/app_seeds/lib/services/coarse_location.dart @@ -16,23 +16,38 @@ class GeolocatorCoarseLocation implements CoarseLocationProvider { @override Future<({double lat, double lon})?> currentCoarseLatLon() async { try { - if (!await Geolocator.isLocationServiceEnabled()) return null; var permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); } - if (permission == LocationPermission.denied || - permission == LocationPermission.deniedForever) { + // Give the user a way out of a permanent denial. + if (permission == LocationPermission.deniedForever) { + await Geolocator.openAppSettings(); return null; } - final position = await Geolocator.getCurrentPosition( - locationSettings: const LocationSettings( - accuracy: LocationAccuracy.low, - ), - ); + 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; // service off, plugin unsupported (desktop), timeout, … + return null; // plugin unsupported (desktop), etc. } } } diff --git a/apps/app_seeds/lib/ui/market_screen.dart b/apps/app_seeds/lib/ui/market_screen.dart index 5daeff4..5529c28 100644 --- a/apps/app_seeds/lib/ui/market_screen.dart +++ b/apps/app_seeds/lib/ui/market_screen.dart @@ -410,6 +410,8 @@ class _ConfigSheetState extends State<_ConfigSheet> { final _area = TextEditingController(); final _servers = TextEditingController(); bool _loading = true; + bool _locationBusy = false; + String? _locationError; @override void initState() { @@ -433,16 +435,21 @@ class _ConfigSheetState extends State<_ConfigSheet> { /// geohash so no precise fix is stored. Future _useLocation() async { final provider = widget.location; - if (provider == null) return; - final messenger = ScaffoldMessenger.of(context); - final t = context.t; + if (provider == null || _locationBusy) return; + setState(() { + _locationBusy = true; + _locationError = null; + }); final loc = await provider.currentCoarseLatLon(); if (!mounted) return; - if (loc == null) { - messenger.showSnackBar(SnackBar(content: Text(t.market.locationFailed))); - return; - } - setState(() => _area.text = Geohash.encode(loc.lat, loc.lon, precision: 5)); + setState(() { + _locationBusy = false; + if (loc == null) { + _locationError = context.t.market.locationFailed; + } else { + _area.text = Geohash.encode(loc.lat, loc.lon, precision: 5); + } + }); } @override @@ -464,7 +471,8 @@ class _ConfigSheetState extends State<_ConfigSheet> { ), child: _loading ? const SizedBox(height: 120, child: Center(child: CircularProgressIndicator())) - : Column( + : SingleChildScrollView( + child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -496,9 +504,23 @@ class _ConfigSheetState extends State<_ConfigSheet> { alignment: AlignmentDirectional.centerStart, child: TextButton.icon( key: const Key('market.useLocation'), - icon: const Icon(Icons.my_location, size: 18), + icon: _locationBusy + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.my_location, size: 18), label: Text(t.market.useLocation), - onPressed: _useLocation, + onPressed: _locationBusy ? null : _useLocation, + ), + ), + if (_locationError != null) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + _locationError!, + style: const TextStyle(color: Color(0xFFB3261E), fontSize: 12), ), ), const SizedBox(height: 8), @@ -541,6 +563,7 @@ class _ConfigSheetState extends State<_ConfigSheet> { ), ], ), + ), ); } }