From c696956ee2f6923c56eebb21e22b9af5afabdbb3 Mon Sep 17 00:00:00 2001 From: vjrj Date: Fri, 10 Jul 2026 03:05:12 +0200 Subject: [PATCH] =?UTF-8?q?feat(block2):=20market=20screen=20=E2=80=94=20d?= =?UTF-8?q?iscover=20seeds=20near=20you=20(offers=20UI)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wires the 'coming soon' market card to a real discovery screen (mockups 04/05): - MarketScreen: opens an OfferTransport lazily; local-first, so it degrades to a human 'set up sharing' prompt when no relay/area is set or the net is down. - Discovers offers by the saved coarse area, lists them with human reciprocity labels (gift/swap/for sale), 'near you' (never exact location), price only for sales. Config sheet to set area + community servers (no bundled public relays). - Threaded via TaneApp(social, socialSettings) — null keeps the card 'coming soon' (so Block 1 tests pass unchanged); main wires it live. - i18n en/es/pt (market.*), reuses share.* for type labels. 3 market widget tests + home tests green; app_seeds analyzes clean. --- apps/app_seeds/lib/app.dart | 32 +- apps/app_seeds/lib/i18n/en.i18n.json | 18 + apps/app_seeds/lib/i18n/es.i18n.json | 18 + apps/app_seeds/lib/i18n/pt.i18n.json | 18 + apps/app_seeds/lib/main.dart | 4 + apps/app_seeds/lib/ui/home_screen.dart | 56 ++- apps/app_seeds/lib/ui/market_screen.dart | 384 ++++++++++++++++++ .../app_seeds/test/ui/market_screen_test.dart | 123 ++++++ 8 files changed, 637 insertions(+), 16 deletions(-) create mode 100644 apps/app_seeds/lib/ui/market_screen.dart create mode 100644 apps/app_seeds/test/ui/market_screen_test.dart diff --git a/apps/app_seeds/lib/app.dart b/apps/app_seeds/lib/app.dart index b80561a..babc54a 100644 --- a/apps/app_seeds/lib/app.dart +++ b/apps/app_seeds/lib/app.dart @@ -8,12 +8,15 @@ import 'data/species_repository.dart'; import 'data/variety_repository.dart'; import 'i18n/strings.g.dart'; import 'services/onboarding_store.dart'; +import 'services/social_service.dart'; +import 'services/social_settings.dart'; import 'state/inventory_cubit.dart'; import 'state/variety_detail_cubit.dart'; import 'ui/about_screen.dart'; import 'ui/home_screen.dart'; import 'ui/intro_screen.dart'; import 'ui/inventory_list_screen.dart'; +import 'ui/market_screen.dart'; import 'ui/settings_screen.dart'; import 'ui/theme.dart'; import 'ui/variety_detail_screen.dart'; @@ -26,13 +29,26 @@ class TaneApp extends StatelessWidget { required this.repository, required this.species, required this.onboarding, + this.social, + this.socialSettings, this.showIntro = false, super.key, - }) : _router = _buildRouter(repository, onboarding, showIntro); + }) : _router = _buildRouter( + repository, + onboarding, + showIntro, + social, + socialSettings, + ); final VarietyRepository repository; final SpeciesRepository species; final OnboardingStore onboarding; + + /// Block 2 social layer. Null until the social round is wired in `main`; when + /// null the market stays a "coming soon" card and `/market` is not routed. + final SocialService? social; + final SocialSettings? socialSettings; final bool showIntro; final GoRouter _router; @@ -40,11 +56,23 @@ class TaneApp extends StatelessWidget { VarietyRepository repository, OnboardingStore onboarding, bool showIntro, + SocialService? social, + SocialSettings? socialSettings, ) { return GoRouter( initialLocation: showIntro ? '/intro' : '/', routes: [ - GoRoute(path: '/', builder: (context, state) => const HomeScreen()), + GoRoute( + path: '/', + builder: (context, state) => + HomeScreen(marketEnabled: social != null), + ), + if (social != null && socialSettings != null) + GoRoute( + path: '/market', + builder: (context, state) => + MarketScreen(social: social, settings: socialSettings), + ), GoRoute( path: '/intro', builder: (context, state) => IntroScreen( diff --git a/apps/app_seeds/lib/i18n/en.i18n.json b/apps/app_seeds/lib/i18n/en.i18n.json index fef6edc..a416376 100644 --- a/apps/app_seeds/lib/i18n/en.i18n.json +++ b/apps/app_seeds/lib/i18n/en.i18n.json @@ -327,5 +327,23 @@ "cutting": { "singular": "cutting", "plural": "cuttings" }, "grams": { "singular": "gram", "plural": "grams" }, "count": { "singular": "seed", "plural": "seeds" } + }, + "market": { + "title": "Seeds near you", + "subtitle": "What others are sharing nearby", + "notSetUp": "Sharing isn't set up yet", + "notSetUpBody": "Add a community server and your area to see and share seeds nearby. Everything stays coarse — never your exact address.", + "setUp": "Set up sharing", + "searching": "Looking around your area…", + "empty": "No seeds shared near you yet", + "near": "Near you", + "configTitle": "Sharing setup", + "areaLabel": "Area code", + "areaHelp": "Kept coarse on purpose — roughly your zone, never an exact spot.", + "serversLabel": "Community servers", + "serversHelp": "One address per line — run by seed networks or collectives, not a company.", + "save": "Save", + "saved": "Saved", + "wanted": "Wanted" } } diff --git a/apps/app_seeds/lib/i18n/es.i18n.json b/apps/app_seeds/lib/i18n/es.i18n.json index 533a96b..f218689 100644 --- a/apps/app_seeds/lib/i18n/es.i18n.json +++ b/apps/app_seeds/lib/i18n/es.i18n.json @@ -327,5 +327,23 @@ "cutting": { "singular": "esqueje", "plural": "esquejes" }, "grams": { "singular": "gramo", "plural": "gramos" }, "count": { "singular": "semilla", "plural": "semillas" } + }, + "market": { + "title": "Semillas cerca de ti", + "subtitle": "Lo que otras personas comparten cerca", + "notSetUp": "Aún no has configurado el compartir", + "notSetUpBody": "Añade un servidor comunitario y tu zona para ver y compartir semillas cerca. Todo se mantiene aproximado — nunca tu dirección exacta.", + "setUp": "Configurar el compartir", + "searching": "Buscando por tu zona…", + "empty": "Aún no hay semillas compartidas cerca de ti", + "near": "Cerca de ti", + "configTitle": "Configuración de compartir", + "areaLabel": "Código de zona", + "areaHelp": "Se mantiene aproximado a propósito — tu zona a grandes rasgos, nunca un punto exacto.", + "serversLabel": "Servidores comunitarios", + "serversHelp": "Una dirección por línea — de redes de semillas o colectivos, no de una empresa.", + "save": "Guardar", + "saved": "Guardado", + "wanted": "Busco" } } diff --git a/apps/app_seeds/lib/i18n/pt.i18n.json b/apps/app_seeds/lib/i18n/pt.i18n.json index 9c7f66f..bbdc337 100644 --- a/apps/app_seeds/lib/i18n/pt.i18n.json +++ b/apps/app_seeds/lib/i18n/pt.i18n.json @@ -323,5 +323,23 @@ "cutting": { "singular": "estaca", "plural": "estacas" }, "grams": { "singular": "grama", "plural": "gramas" }, "count": { "singular": "semente", "plural": "sementes" } + }, + "market": { + "title": "Sementes perto de ti", + "subtitle": "O que outras pessoas partilham por perto", + "notSetUp": "Ainda não configuraste a partilha", + "notSetUpBody": "Adiciona um servidor comunitário e a tua zona para ver e partilhar sementes por perto. Tudo fica aproximado — nunca a tua morada exata.", + "setUp": "Configurar a partilha", + "searching": "A procurar pela tua zona…", + "empty": "Ainda não há sementes partilhadas perto de ti", + "near": "Perto de ti", + "configTitle": "Configuração de partilha", + "areaLabel": "Código de zona", + "areaHelp": "Mantém-se aproximado de propósito — a tua zona em geral, nunca um ponto exato.", + "serversLabel": "Servidores comunitários", + "serversHelp": "Um endereço por linha — de redes de sementes ou coletivos, não de uma empresa.", + "save": "Guardar", + "saved": "Guardado", + "wanted": "Procuro" } } diff --git a/apps/app_seeds/lib/main.dart b/apps/app_seeds/lib/main.dart index 3239bd7..a16db49 100644 --- a/apps/app_seeds/lib/main.dart +++ b/apps/app_seeds/lib/main.dart @@ -6,6 +6,8 @@ import 'data/variety_repository.dart'; import 'di/injector.dart'; import 'i18n/strings.g.dart'; import 'services/onboarding_store.dart'; +import 'services/social_service.dart'; +import 'services/social_settings.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -18,6 +20,8 @@ Future main() async { repository: getIt(), species: getIt(), onboarding: onboarding, + social: getIt(), + socialSettings: getIt(), showIntro: !await onboarding.introSeen(), ), ), diff --git a/apps/app_seeds/lib/ui/home_screen.dart b/apps/app_seeds/lib/ui/home_screen.dart index 42cb960..58826ce 100644 --- a/apps/app_seeds/lib/ui/home_screen.dart +++ b/apps/app_seeds/lib/ui/home_screen.dart @@ -11,7 +11,11 @@ import 'theme.dart'; /// to action and "Open market" (Block 2) as a disabled outlined card. The /// hamburger opens [AppDrawer]. class HomeScreen extends StatelessWidget { - const HomeScreen({super.key}); + const HomeScreen({this.marketEnabled = false, super.key}); + + /// When the Block 2 social layer is wired, the market becomes a live + /// destination; otherwise it stays a disabled "coming soon" card. + final bool marketEnabled; @override Widget build(BuildContext context) { @@ -77,10 +81,14 @@ class HomeScreen extends StatelessWidget { ), const SizedBox(height: 16), _OutlinedMenuCard( + key: const Key('home.market'), icon: Icons.storefront_outlined, label: t.home.openMarket, subtitle: t.home.openMarketSubtitle, - tag: t.common.comingSoon, + tag: marketEnabled ? null : t.common.comingSoon, + onTap: marketEnabled + ? () => context.go('/market') + : null, ), ], ), @@ -151,17 +159,24 @@ class _OutlinedMenuCard extends StatelessWidget { required this.icon, required this.label, required this.subtitle, - required this.tag, + this.tag, + this.onTap, + super.key, }); final IconData icon; final String label; final String subtitle; - final String tag; + + /// A small trailing tag (e.g. "coming soon"); omitted for live cards. + final String? tag; + + /// When set, the card is tappable; otherwise it reads as disabled. + final VoidCallback? onTap; @override Widget build(BuildContext context) { - return Container( + final card = Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(18), @@ -183,18 +198,31 @@ class _OutlinedMenuCard extends StatelessWidget { subtitleColor: seedMuted, ), ), - Text( - tag.toUpperCase(), - style: const TextStyle( - color: Color(0xFFB3BDA8), - fontSize: 11, - fontWeight: FontWeight.w500, - letterSpacing: 0.5, - ), - ), + if (tag != null) + Text( + tag!.toUpperCase(), + style: const TextStyle( + color: Color(0xFFB3BDA8), + fontSize: 11, + fontWeight: FontWeight.w500, + letterSpacing: 0.5, + ), + ) + else if (onTap != null) + const Icon(Icons.chevron_right, color: seedMuted), ], ), ); + if (onTap == null) return card; + return Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(18), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(18), + child: card, + ), + ); } } diff --git a/apps/app_seeds/lib/ui/market_screen.dart b/apps/app_seeds/lib/ui/market_screen.dart new file mode 100644 index 0000000..b5fde6c --- /dev/null +++ b/apps/app_seeds/lib/ui/market_screen.dart @@ -0,0 +1,384 @@ +import 'package:commons_core/commons_core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../i18n/strings.g.dart'; +import '../services/social_service.dart'; +import '../services/social_settings.dart'; +import '../state/offers_cubit.dart'; +import 'theme.dart'; + +/// The market (redesign screens 04/05): discover seeds shared near you. Opens +/// 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}); + + final SocialService social; + final SocialSettings settings; + + @override + State createState() => _MarketScreenState(); +} + +class _MarketScreenState extends State { + OffersCubit? _cubit; + bool _loading = true; + + @override + void initState() { + super.initState(); + _init(); + } + + Future _init() async { + setState(() => _loading = true); + final cubit = await createOffersCubit(widget.social, widget.settings); + final area = await widget.settings.areaGeohash(); + if (!mounted) { + await cubit.close(); + return; + } + final previous = _cubit; + setState(() { + _cubit = cubit; + _loading = false; + }); + await previous?.close(); + if (area.isNotEmpty && cubit.isOnline) await cubit.discover(area); + } + + Future _openConfig() async { + final changed = await showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (_) => _ConfigSheet(settings: widget.settings), + ); + if (changed == true) await _init(); + } + + @override + void dispose() { + _cubit?.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final t = context.t; + final cubit = _cubit; + return Scaffold( + appBar: AppBar( + title: Text(t.market.title), + actions: [ + IconButton( + key: const Key('market.config'), + icon: const Icon(Icons.tune), + tooltip: t.market.configTitle, + onPressed: _openConfig, + ), + ], + ), + body: _loading || cubit == null + ? const Center(child: CircularProgressIndicator()) + : BlocProvider.value( + value: cubit, + child: MarketBody(onConfigure: _openConfig), + ), + ); + } +} + +class MarketBody extends StatelessWidget { + const MarketBody({required this.onConfigure, super.key}); + + final VoidCallback onConfigure; + + @override + Widget build(BuildContext context) { + final t = context.t; + return BlocBuilder( + builder: (context, state) { + if (!context.read().isOnline) { + return _EmptyState( + icon: Icons.wifi_tethering_off_outlined, + title: t.market.notSetUp, + body: t.market.notSetUpBody, + actionLabel: t.market.setUp, + onAction: onConfigure, + ); + } + if (state.searching && state.offers.isEmpty) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 16), + Text(t.market.searching, + style: const TextStyle(color: seedMuted)), + ], + ), + ); + } + if (state.offers.isEmpty) { + return _EmptyState( + icon: Icons.grass_outlined, + title: t.market.empty, + body: '', + ); + } + return ListView.separated( + padding: const EdgeInsets.all(16), + itemCount: state.offers.length, + separatorBuilder: (_, _) => const SizedBox(height: 12), + itemBuilder: (context, i) => _OfferCard(offer: state.offers[i]), + ); + }, + ); + } +} + +/// One discovered offer. Human words for the reciprocity mode; coarse "near you" +/// instead of any precise location; price only when it's a sale. +class _OfferCard extends StatelessWidget { + const _OfferCard({required this.offer}); + + final Offer offer; + + @override + Widget build(BuildContext context) { + final t = context.t; + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: seedOutline), + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + offer.summary, + style: const TextStyle( + fontSize: 17, + fontWeight: FontWeight.w500, + color: seedOnSurface, + ), + ), + ), + _TypeChip(label: _offerTypeLabel(t, offer.type)), + ], + ), + const SizedBox(height: 6), + Row( + children: [ + const Icon(Icons.place_outlined, size: 15, color: seedMuted), + const SizedBox(width: 4), + Text(t.market.near, style: const TextStyle(color: seedMuted, fontSize: 13)), + if (offer.type == OfferType.sale && offer.priceAmount != null) ...[ + const Spacer(), + Text( + '${offer.priceAmount} ${offer.priceCurrency ?? ''}'.trim(), + style: const TextStyle( + color: seedOnSurface, + fontWeight: FontWeight.w600, + ), + ), + ], + ], + ), + ], + ), + ); + } +} + +String _offerTypeLabel(Translations t, OfferType type) => switch (type) { + OfferType.gift => t.share.gift, + OfferType.exchange => t.share.exchange, + OfferType.sale => t.share.sell, + OfferType.wanted => t.market.wanted, + OfferType.lend => t.share.exchange, + }; + +class _TypeChip extends StatelessWidget { + const _TypeChip({required this.label}); + + final String label; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: seedPrimaryContainer, + borderRadius: BorderRadius.circular(20), + ), + child: Text( + label, + style: const TextStyle( + color: seedOnPrimaryContainer, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ); + } +} + +class _EmptyState extends StatelessWidget { + const _EmptyState({ + required this.icon, + required this.title, + required this.body, + this.actionLabel, + this.onAction, + }); + + final IconData icon; + final String title; + final String body; + final String? actionLabel; + final VoidCallback? onAction; + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 56, color: seedMuted), + const SizedBox(height: 16), + Text( + title, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + color: seedOnSurface, + ), + ), + if (body.isNotEmpty) ...[ + const SizedBox(height: 8), + Text( + body, + textAlign: TextAlign.center, + style: const TextStyle(color: seedMuted, fontSize: 14), + ), + ], + if (actionLabel != null && onAction != null) ...[ + const SizedBox(height: 20), + FilledButton(onPressed: onAction, child: Text(actionLabel!)), + ], + ], + ), + ), + ); + } +} + +/// 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}); + + final SocialSettings settings; + + @override + State<_ConfigSheet> createState() => _ConfigSheetState(); +} + +class _ConfigSheetState extends State<_ConfigSheet> { + final _area = TextEditingController(); + final _servers = TextEditingController(); + bool _loading = true; + + @override + void initState() { + super.initState(); + _load(); + } + + Future _load() async { + _area.text = await widget.settings.areaGeohash(); + _servers.text = (await widget.settings.relayUrls()).join('\n'); + if (mounted) setState(() => _loading = false); + } + + Future _save() async { + await widget.settings.setAreaGeohash(_area.text); + await widget.settings.setRelayUrls(_servers.text.split('\n')); + if (mounted) Navigator.of(context).pop(true); + } + + @override + void dispose() { + _area.dispose(); + _servers.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final t = context.t; + return Padding( + padding: EdgeInsets.only( + left: 20, + right: 20, + top: 20, + bottom: MediaQuery.of(context).viewInsets.bottom + 20, + ), + child: _loading + ? const SizedBox(height: 120, child: Center(child: CircularProgressIndicator())) + : Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + t.market.configTitle, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: seedTitle, + ), + ), + const SizedBox(height: 16), + TextField( + key: const Key('market.area'), + controller: _area, + decoration: InputDecoration( + labelText: t.market.areaLabel, + helperText: t.market.areaHelp, + helperMaxLines: 3, + ), + ), + const SizedBox(height: 16), + TextField( + key: const Key('market.servers'), + controller: _servers, + minLines: 2, + maxLines: 4, + decoration: InputDecoration( + labelText: t.market.serversLabel, + helperText: t.market.serversHelp, + helperMaxLines: 3, + ), + ), + const SizedBox(height: 20), + FilledButton( + key: const Key('market.save'), + onPressed: _save, + child: Text(t.market.save), + ), + ], + ), + ); + } +} diff --git a/apps/app_seeds/test/ui/market_screen_test.dart b/apps/app_seeds/test/ui/market_screen_test.dart new file mode 100644 index 0000000..d94a1f7 --- /dev/null +++ b/apps/app_seeds/test/ui/market_screen_test.dart @@ -0,0 +1,123 @@ +import 'dart:async'; + +import 'package:commons_core/commons_core.dart'; +import 'package:flutter/material.dart'; +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/social_service.dart'; +import 'package:tane/services/social_settings.dart'; +import 'package:tane/state/offers_cubit.dart'; +import 'package:tane/ui/market_screen.dart'; + +import '../support/test_support.dart'; + +/// In-memory offer transport: discover matches by geohash prefix. +class FakeOfferTransport implements OfferTransport { + final List offers; + FakeOfferTransport(this.offers); + + @override + Future publish(Offer offer) async => + PublishResult(accepted: true, transportRef: offer.id); + + @override + Stream discover(DiscoveryQuery query) { + final c = StreamController(); + for (final o in offers) { + if (o.approxGeohash.startsWith(query.geohashPrefix)) c.add(o); + } + return c.stream; + } + + @override + Future retract(String offerId) async {} + @override + Future close() async {} +} + +Widget _wrapBody(OffersCubit cubit) { + LocaleSettings.setLocaleSync(AppLocale.en); + return TranslationProvider( + child: MaterialApp( + locale: AppLocale.en.flutterLocale, + supportedLocales: AppLocaleUtils.supportedLocales, + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + home: Scaffold( + body: BlocProvider.value( + value: cubit, + child: MarketBody(onConfigure: () {}), + ), + ), + ), + ); +} + +Offer _offer(String id, String summary, OfferType type) => Offer( + id: id, + authorPubkeyHex: 'ab' * 32, + summary: summary, + type: type, + approxGeohash: 'sp3e9', + ); + +void main() { + testWidgets('renders discovered offers with their type', (tester) async { + final cubit = OffersCubit(FakeOfferTransport([ + _offer('1', 'Tomate rosa de Barbastro', OfferType.gift), + _offer('2', 'Judía del ganxet', OfferType.exchange), + ])); + addTearDown(cubit.close); + + await tester.pumpWidget(_wrapBody(cubit)); + await cubit.discover('sp3'); + await tester.pumpAndSettle(); + + expect(find.text('Tomate rosa de Barbastro'), findsOneWidget); + expect(find.text('Judía del ganxet'), findsOneWidget); + expect(find.text('Near you'), findsWidgets); + }); + + testWidgets('offline (no transport) shows the "set up sharing" prompt', + (tester) async { + final cubit = OffersCubit(null); + addTearDown(cubit.close); + + await tester.pumpWidget(_wrapBody(cubit)); + await tester.pumpAndSettle(); + + expect(find.text("Sharing isn't set up yet"), findsOneWidget); + expect(find.text('Set up sharing'), findsOneWidget); + }); + + testWidgets('MarketScreen with no relays configured degrades to offline', + (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), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.text('Seeds near you'), findsOneWidget); // app bar title + expect(find.text('Set up sharing'), findsOneWidget); // offline prompt + }); +}