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:
parent
f8f73c4153
commit
492bc62025
15 changed files with 176 additions and 10 deletions
|
|
@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||
|
||||
import '../data/variety_repository.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
import '../services/coarse_location.dart';
|
||||
import '../services/social_service.dart';
|
||||
import '../services/social_settings.dart';
|
||||
import '../state/offers_cubit.dart';
|
||||
|
|
@ -13,11 +14,20 @@ import 'theme.dart';
|
|||
/// an offer transport lazily; local-first, so it degrades to a "set up sharing"
|
||||
/// prompt when no relay/area is configured or the network is unreachable.
|
||||
class MarketScreen extends StatefulWidget {
|
||||
const MarketScreen({required this.social, required this.settings, super.key});
|
||||
const MarketScreen({
|
||||
required this.social,
|
||||
required this.settings,
|
||||
this.location,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final SocialService social;
|
||||
final SocialSettings settings;
|
||||
|
||||
/// Optional device-location source for the "use my location" shortcut; when
|
||||
/// null (tests, or a platform without it) the shortcut is hidden.
|
||||
final CoarseLocationProvider? location;
|
||||
|
||||
@override
|
||||
State<MarketScreen> createState() => _MarketScreenState();
|
||||
}
|
||||
|
|
@ -53,7 +63,8 @@ class _MarketScreenState extends State<MarketScreen> {
|
|||
final changed = await showModalBottomSheet<bool>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (_) => _ConfigSheet(settings: widget.settings),
|
||||
builder: (_) =>
|
||||
_ConfigSheet(settings: widget.settings, location: widget.location),
|
||||
);
|
||||
if (changed == true) await _init();
|
||||
}
|
||||
|
|
@ -325,9 +336,10 @@ class _EmptyState extends StatelessWidget {
|
|||
/// Coarse-area + community-server setup. Kept behind progressive disclosure — a
|
||||
/// power-user surface — with human-worded labels.
|
||||
class _ConfigSheet extends StatefulWidget {
|
||||
const _ConfigSheet({required this.settings});
|
||||
const _ConfigSheet({required this.settings, this.location});
|
||||
|
||||
final SocialSettings settings;
|
||||
final CoarseLocationProvider? location;
|
||||
|
||||
@override
|
||||
State<_ConfigSheet> createState() => _ConfigSheetState();
|
||||
|
|
@ -356,6 +368,22 @@ class _ConfigSheetState extends State<_ConfigSheet> {
|
|||
if (mounted) Navigator.of(context).pop(true);
|
||||
}
|
||||
|
||||
/// Fills the area from the device's approximate location, reduced to a coarse
|
||||
/// geohash so no precise fix is stored.
|
||||
Future<void> _useLocation() async {
|
||||
final provider = widget.location;
|
||||
if (provider == null) return;
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final t = context.t;
|
||||
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));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_area.dispose();
|
||||
|
|
@ -402,6 +430,16 @@ class _ConfigSheetState extends State<_ConfigSheet> {
|
|||
helperMaxLines: 3,
|
||||
),
|
||||
),
|
||||
if (widget.location != null)
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: TextButton.icon(
|
||||
key: const Key('market.useLocation'),
|
||||
icon: const Icon(Icons.my_location, size: 18),
|
||||
label: Text(t.market.useLocation),
|
||||
onPressed: _useLocation,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// The technical web address lives under progressive disclosure —
|
||||
// most people won't have one, and jargon shouldn't greet them.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue