feat(plantare): UI — propose from chat, review & sign on the ledger

The viral loop, made usable end-to-end:

- Propose sheet (plantare_propose_sheet.dart): from a chat with a peer
  (their key already in hand), pick your side, name the seed, choose what
  comes back (similar · non-GMO organic / work hours / other) and an
  optional return-by, then sign and send. Reached from the chat overflow
  menu; confirms "waiting for them to sign".
- Plantares screen: incoming proposals show Accept & sign / Decline right
  on the tile; every bilateral row wears a handshake badge — awaiting
  signature, signed by both, or declined. Accept falls back to a gentle
  "offline" nudge if the proposal isn't in hand yet.
- i18n: full en + es for the bilateral flow; other locales fall back to
  base per slang config.

Wire-up only touches human copy (no jargon) and stays behind the
optional PlantareService, so inventory-only builds are unaffected.

Tests: propose sheet drives a real sign+send; the ledger renders the
signed/awaiting badges. Fake SocialSession in your_people test gains the
new transport getter. Widget tests targeted + timed. All green.
This commit is contained in:
vjrj 2026-07-14 11:19:04 +02:00
parent b607ddde41
commit e78656bc07
11 changed files with 2223 additions and 1459 deletions

View file

@ -12,6 +12,7 @@ import '../domain/message_rules.dart';
import '../i18n/strings.g.dart';
import '../services/message_store.dart';
import '../services/onboarding_store.dart';
import '../services/plantare_service.dart';
import '../services/profile_cache.dart';
import '../services/profile_store.dart';
import '../services/social_connection.dart';
@ -23,6 +24,7 @@ import '../state/peer_rating_cubit.dart';
import '../state/trust_cubit.dart';
import 'market_gate.dart';
import 'peer_avatar.dart';
import 'plantare_propose_sheet.dart';
import 'rating_sheet.dart';
import 'report_sheet.dart';
import 'theme.dart';
@ -205,6 +207,23 @@ class _ChatScreenState extends State<ChatScreen> {
/// Reports this peer (a standard report event to the community servers);
/// when they were also blocked from the sheet, leaves the chat too.
/// Opens the "propose a signed Plantaré" sheet against this peer (their key is
/// in hand from the conversation), and confirms when a proposal is sent.
Future<void> _proposePlantare() async {
if (!getIt.isRegistered<PlantareService>()) return;
final sent = await showProposePlantareSheet(
context,
service: getIt<PlantareService>(),
peerPubkey: widget.peerPubkey,
peerName: _peerName,
);
if (sent == true && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.t.plantare.sent)),
);
}
}
Future<void> _reportPeer() async {
final outcome = await showReportSheet(
context,
@ -305,10 +324,16 @@ class _ChatScreenState extends State<ChatScreen> {
PopupMenuButton<String>(
key: const Key('chat.menu'),
onSelected: (value) {
if (value == 'plantare') _proposePlantare();
if (value == 'report') _reportPeer();
if (value == 'block') _blockPeer();
},
itemBuilder: (context) => [
if (getIt.isRegistered<PlantareService>())
PopupMenuItem(
value: 'plantare',
child: Text(t.plantare.propose),
),
PopupMenuItem(
value: 'report',
child: Text(t.report.person),