feat(block2): market screen — discover seeds near you (offers UI)

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.
This commit is contained in:
vjrj 2026-07-10 03:05:12 +02:00
parent 528b499209
commit c696956ee2
8 changed files with 637 additions and 16 deletions

View file

@ -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<Offer> offers;
FakeOfferTransport(this.offers);
@override
Future<PublishResult> publish(Offer offer) async =>
PublishResult(accepted: true, transportRef: offer.id);
@override
Stream<Offer> discover(DiscoveryQuery query) {
final c = StreamController<Offer>();
for (final o in offers) {
if (o.approxGeohash.startsWith(query.geohashPrefix)) c.add(o);
}
return c.stream;
}
@override
Future<void> retract(String offerId) async {}
@override
Future<void> 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
});
}