feat(block2): 'use my location' — set the sharing area without typing a code

Makes the coarse area human, per feedback that a typed code is jargon.
- CoarseLocationProvider interface + geolocator-backed impl (BSD-3): low
  accuracy only, returns null on any denial/error (degrades quietly). Behind an
  interface so it's fakeable and the plugin stays at the composition root.
- Config sheet: a 'Use my approximate location' button (shown only when a
  provider is wired) fills the area from the device position, reduced to a
  5-char geohash via commons_core's Geohash — a precise fix never leaves the
  device. 'Couldn't get your location' on failure.
- Threaded via TaneApp(location); main wires GeolocatorCoarseLocation.
- Platform: ACCESS_COARSE_LOCATION (Android) + NSLocationWhenInUseUsageDescription
  (iOS), both scoped to the coarse-area use.
- i18n en/es/pt. Widget test fills the area from a fake location. 8 tests green.
This commit is contained in:
vjrj 2026-07-10 10:25:44 +02:00
parent f8f73c4153
commit 492bc62025
15 changed files with 176 additions and 10 deletions

View file

@ -7,6 +7,7 @@ import 'package:go_router/go_router.dart';
import 'data/species_repository.dart';
import 'data/variety_repository.dart';
import 'i18n/strings.g.dart';
import 'services/coarse_location.dart';
import 'services/onboarding_store.dart';
import 'services/social_service.dart';
import 'services/social_settings.dart';
@ -31,6 +32,7 @@ class TaneApp extends StatelessWidget {
required this.onboarding,
this.social,
this.socialSettings,
this.location,
this.showIntro = false,
super.key,
}) : _router = _buildRouter(
@ -39,6 +41,7 @@ class TaneApp extends StatelessWidget {
showIntro,
social,
socialSettings,
location,
);
final VarietyRepository repository;
@ -49,6 +52,9 @@ class TaneApp extends StatelessWidget {
/// null the market stays a "coming soon" card and `/market` is not routed.
final SocialService? social;
final SocialSettings? socialSettings;
/// Optional device-location source for the market's "use my location".
final CoarseLocationProvider? location;
final bool showIntro;
final GoRouter _router;
@ -58,6 +64,7 @@ class TaneApp extends StatelessWidget {
bool showIntro,
SocialService? social,
SocialSettings? socialSettings,
CoarseLocationProvider? location,
) {
return GoRouter(
initialLocation: showIntro ? '/intro' : '/',
@ -70,8 +77,11 @@ class TaneApp extends StatelessWidget {
if (social != null && socialSettings != null)
GoRoute(
path: '/market',
builder: (context, state) =>
MarketScreen(social: social, settings: socialSettings),
builder: (context, state) => MarketScreen(
social: social,
settings: socialSettings,
location: location,
),
),
GoRoute(
path: '/intro',