feat(block2): publish my seeds to the network (completes publish->discover)

The other half of the market: share your marked lots as offers.
- VarietyRepository.shareableLots(): non-private lots WITH their id (stable,
  updatable offer id) + the variety label. Private lots never leave the device.
- OffersCubit.publishLots(): maps each via OfferMapper and publishes, tagged
  with the coarse area + signed by the derived key; counts acceptances; no-op
  offline or with no area.
- Market screen: a 'share my seeds' action (online only) — publishes, then
  re-discovers to show them; 'mark some seeds first' when nothing's shared.
- i18n en/es/pt.

14 tests green (repo query, cubit publish, market UI); analyzer clean for new code.
This commit is contained in:
vjrj 2026-07-10 10:18:30 +02:00
parent cb5f55e146
commit f8f73c4153
12 changed files with 284 additions and 5 deletions

View file

@ -4,6 +4,8 @@ import 'package:commons_core/commons_core.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../data/variety_repository.dart';
import '../services/offer_mapper.dart';
import '../services/social_service.dart';
import '../services/social_settings.dart';
@ -119,6 +121,39 @@ class OffersCubit extends Cubit<OffersState> {
}
}
/// Publishes the user's [lots] as offers, each tagged with the coarse
/// [areaGeohash] and signed by [authorPubkeyHex]. Returns how many the relay
/// accepted. No-op (returns 0) when offline or with no area set.
Future<int> publishLots(
List<ShareableLot> lots, {
required String authorPubkeyHex,
required String areaGeohash,
}) async {
final transport = _transport;
if (transport == null || areaGeohash.isEmpty || lots.isEmpty) return 0;
emit(state.copyWith(publishing: true, error: () => null));
var accepted = 0;
try {
for (final lot in lots) {
final offer = OfferMapper.fromSharedLot(
lotId: lot.lotId,
authorPubkeyHex: authorPubkeyHex,
summary: lot.summary,
sharing: lot.offerStatus,
areaGeohash: areaGeohash,
category: lot.category,
);
final result = await transport.publish(offer);
if (result.accepted) accepted++;
}
emit(state.copyWith(publishing: false));
} catch (e) {
emit(state.copyWith(publishing: false, error: () => '$e'));
rethrow;
}
return accepted;
}
@override
Future<void> close() async {
await _sub?.cancel();