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

@ -6,6 +6,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tane/i18n/strings.g.dart';
import 'package:tane/services/coarse_location.dart';
import 'package:tane/services/social_service.dart';
import 'package:tane/services/social_settings.dart';
import 'package:tane/state/offers_cubit.dart';
@ -13,6 +14,16 @@ import 'package:tane/ui/market_screen.dart';
import '../support/test_support.dart';
/// A location provider returning a fixed coarse position.
class FakeLocation implements CoarseLocationProvider {
FakeLocation(this.lat, this.lon);
final double lat;
final double lon;
@override
Future<({double lat, double lon})?> currentCoarseLatLon() async =>
(lat: lat, lon: lon);
}
/// In-memory offer transport: discover matches by geohash prefix.
class FakeOfferTransport implements OfferTransport {
final List<Offer> offers;
@ -120,4 +131,40 @@ void main() {
expect(find.text('Seeds near you'), findsOneWidget); // app bar title
expect(find.text('Set up sharing'), findsOneWidget); // offline prompt
});
testWidgets('"use my location" fills the area with a coarse geohash',
(tester) async {
final social = await SocialService.fromRootSeedHex('00' * 32);
final settings = SocialSettings(InMemorySecretStore());
LocaleSettings.setLocaleSync(AppLocale.en);
await tester.pumpWidget(
TranslationProvider(
child: MaterialApp(
locale: AppLocale.en.flutterLocale,
supportedLocales: AppLocaleUtils.supportedLocales,
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
home: MarketScreen(
social: social,
settings: settings,
location: FakeLocation(41.39, 2.16), // Barcelona-ish
),
),
),
);
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('market.config')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('market.useLocation')));
await tester.pumpAndSettle();
final area = tester.widget<TextField>(find.byKey(const Key('market.area')));
expect(area.controller!.text, hasLength(5));
expect(area.controller!.text, matches(RegExp(r'^[0-9a-z]+$')));
});
}