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 2e25e34d11
commit f9c3dc018d
11 changed files with 2223 additions and 1459 deletions

View file

@ -1809,6 +1809,75 @@ class Translations$plantare$en {
/// en: 'Commitments'
String get sectionTitle => 'Commitments';
/// en: 'Propose a signed Plantaré'
String get propose => 'Propose a signed Plantaré';
/// en: 'Both of you keep the same promise, signed by both — proof that this seed changed hands and will be grown out and returned.'
String get proposeHelp => 'Both of you keep the same promise, signed by both — proof that this seed changed hands and will be grown out and returned.';
/// en: 'With {name}'
String proposeTo({required Object name}) => 'With ${name}';
/// en: 'Proposal sent — waiting for them to sign'
String get sent => 'Proposal sent — waiting for them to sign';
/// en: 'Which seed?'
String get seedLabel => 'Which seed?';
/// en: 'The variety this promise is about'
String get seedHint => 'The variety this promise is about';
/// en: 'What comes back?'
String get returnKindLabel => 'What comes back?';
/// en: 'A similar amount of seed'
String get returnSimilar => 'A similar amount of seed';
/// en: 'open-pollinated · non-GMO · organically grown'
String get returnSimilarNote => 'open-pollinated · non-GMO · organically grown';
/// en: 'Some hours of work'
String get returnWork => 'Some hours of work';
/// en: 'Something else'
String get returnOther => 'Something else';
/// en: 'How many hours?'
String get workHoursLabel => 'How many hours?';
/// en: 'Waiting for your reply'
String get proposalsSection => 'Waiting for your reply';
/// en: '{name} proposes a Plantaré'
String incomingFrom({required Object name}) => '${name} proposes a Plantaré';
/// en: 'Accept & sign'
String get accept => 'Accept & sign';
/// en: 'Decline'
String get declineAction => 'Decline';
/// en: 'Reason (optional)'
String get declineReasonHint => 'Reason (optional)';
/// en: 'Signed — you both hold it now'
String get acceptedToast => 'Signed — you both hold it now';
/// en: 'Declined'
String get declinedToast => 'Declined';
/// en: 'Awaiting signature'
String get badgeAwaiting => 'Awaiting signature';
/// en: 'Signed by both'
String get badgeSigned => 'Signed by both';
/// en: 'Declined'
String get badgeDeclined => 'Declined';
/// en: 'You're offline it'll be sent when you reconnect'
String get offline => 'You\'re offline — it\'ll be sent when you reconnect';
}
// Path: handover
@ -3020,6 +3089,31 @@ extension on Translations {
'plantare.pickDate' => 'Pick a date',
'plantare.clearDate' => 'Clear date',
'plantare.sectionTitle' => 'Commitments',
'plantare.propose' => 'Propose a signed Plantaré',
'plantare.proposeHelp' => 'Both of you keep the same promise, signed by both — proof that this seed changed hands and will be grown out and returned.',
'plantare.proposeTo' => ({required Object name}) => 'With ${name}',
'plantare.sent' => 'Proposal sent — waiting for them to sign',
'plantare.seedLabel' => 'Which seed?',
'plantare.seedHint' => 'The variety this promise is about',
'plantare.returnKindLabel' => 'What comes back?',
'plantare.returnSimilar' => 'A similar amount of seed',
'plantare.returnSimilarNote' => 'open-pollinated · non-GMO · organically grown',
'plantare.returnWork' => 'Some hours of work',
'plantare.returnOther' => 'Something else',
'plantare.workHoursLabel' => 'How many hours?',
'plantare.proposalsSection' => 'Waiting for your reply',
'plantare.incomingFrom' => ({required Object name}) => '${name} proposes a Plantaré',
'plantare.accept' => 'Accept & sign',
_ => null,
} ?? switch (path) {
'plantare.declineAction' => 'Decline',
'plantare.declineReasonHint' => 'Reason (optional)',
'plantare.acceptedToast' => 'Signed — you both hold it now',
'plantare.declinedToast' => 'Declined',
'plantare.badgeAwaiting' => 'Awaiting signature',
'plantare.badgeSigned' => 'Signed by both',
'plantare.badgeDeclined' => 'Declined',
'plantare.offline' => 'You\'re offline — it\'ll be sent when you reconnect',
'handover.title' => 'Seeds changed hands',
'handover.help' => 'A gift, a swap or a sale — note it down, with a return promise or without.',
'handover.iGave' => 'I gave seeds',
@ -3035,8 +3129,6 @@ extension on Translations {
'sale.help' => 'Record seed you sold or bought — money, Ğ1, or any currency. A separate model from a gift or a Plantare. No commission is ever taken on seeds.',
'sale.add' => 'Record a sale',
'sale.empty' => 'No sales yet. Note here what you sell or buy.',
_ => null,
} ?? switch (path) {
'sale.iSold' => 'I sold',
'sale.iBought' => 'I bought',
'sale.direction' => 'Sold or bought?',