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:
parent
b607ddde41
commit
e78656bc07
11 changed files with 2223 additions and 1459 deletions
100
apps/app_seeds/test/ui/plantare_propose_sheet_test.dart
Normal file
100
apps/app_seeds/test/ui/plantare_propose_sheet_test.dart
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import 'package:commons_core/commons_core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tane/db/enums.dart';
|
||||
import 'package:tane/i18n/strings.g.dart';
|
||||
import 'package:tane/services/plantare_service.dart';
|
||||
import 'package:tane/ui/plantare_propose_sheet.dart';
|
||||
|
||||
import '../support/test_support.dart';
|
||||
|
||||
/// A records-only transport so the sheet can be driven without a relay.
|
||||
class _CapturingTransport implements PlantareTransport {
|
||||
final proposed = <PlantarePledge>[];
|
||||
@override
|
||||
Future<void> propose(PlantarePledge pledge) async => proposed.add(pledge);
|
||||
@override
|
||||
Future<void> accept(PlantarePledge pledge) async {}
|
||||
@override
|
||||
Future<void> decline({
|
||||
required String toPubkey,
|
||||
required String pledgeId,
|
||||
String reason = '',
|
||||
}) async {}
|
||||
@override
|
||||
Stream<PlantareEnvelope> incoming() => const Stream.empty();
|
||||
@override
|
||||
Future<void> close() async {}
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('filling the seed and sending proposes a signed pledge',
|
||||
(tester) async {
|
||||
LocaleSettings.setLocaleSync(AppLocale.en);
|
||||
final db = newTestDatabase();
|
||||
addTearDown(db.close);
|
||||
final repo = newTestRepository(db);
|
||||
final tx = _CapturingTransport();
|
||||
final service = PlantareService(
|
||||
repo: repo,
|
||||
selfPubkey: 'a' * 64,
|
||||
selfSecretKey:
|
||||
'0000000000000000000000000000000000000000000000000000000000000001',
|
||||
)..bindTransport(tx);
|
||||
|
||||
await tester.pumpWidget(
|
||||
TranslationProvider(
|
||||
child: MaterialApp(
|
||||
locale: const Locale('en'),
|
||||
supportedLocales: AppLocaleUtils.supportedLocales,
|
||||
localizationsDelegates: const [
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
home: Builder(
|
||||
builder: (context) => Scaffold(
|
||||
body: Center(
|
||||
child: ElevatedButton(
|
||||
onPressed: () => showProposePlantareSheet(
|
||||
context,
|
||||
service: service,
|
||||
peerPubkey: 'b' * 64,
|
||||
peerName: 'Ana',
|
||||
),
|
||||
child: const Text('open'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('open'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byKey(const Key('propose.seed')), findsOneWidget);
|
||||
|
||||
// The seed name is the one required field.
|
||||
await tester.enterText(
|
||||
find.byKey(const Key('propose.seed')), 'Tomate rosa');
|
||||
await tester.pump();
|
||||
expect(find.text('Tomate rosa'), findsOneWidget);
|
||||
await tester.ensureVisible(find.byKey(const Key('propose.send')));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byKey(const Key('propose.send')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(tx.proposed, hasLength(1));
|
||||
expect(tx.proposed.single.label, 'Tomate rosa');
|
||||
// Default direction is owedToMe → I'm the creditor and signed my stub.
|
||||
expect(tx.proposed.single.creditorSignature, isNotNull);
|
||||
expect(tx.proposed.single.debtorSignature, isNull);
|
||||
// The local row is recorded as proposed.
|
||||
final row = await repo.plantareByPledgeId(tx.proposed.single.pledgeId);
|
||||
expect(row!.remoteState, PlantareRemoteState.proposed);
|
||||
|
||||
await service.stop();
|
||||
});
|
||||
}
|
||||
|
|
@ -63,6 +63,57 @@ void main() {
|
|||
await disposeTree(tester);
|
||||
});
|
||||
|
||||
testWidgets('a fully-signed bilateral pledge shows the "signed by both" badge',
|
||||
(tester) async {
|
||||
LocaleSettings.setLocaleSync(AppLocale.en);
|
||||
final repo = newTestRepository(db);
|
||||
await repo.createPlantare(
|
||||
direction: PlantareDirection.owedToMe,
|
||||
counterparty: 'Ana',
|
||||
pledgeId: 'p-1',
|
||||
debtorKey: 'dk',
|
||||
creditorKey: 'ck',
|
||||
debtorSignature: 'ds',
|
||||
creditorSignature: 'cs',
|
||||
remoteState: PlantareRemoteState.accepted,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
wrapScreen(repository: repo, child: const PlantaresScreen()),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
expect(find.text('Signed by both'), findsOneWidget);
|
||||
// No accept/decline for a closed deal.
|
||||
expect(find.byKey(const Key('plantare.accept.')), findsNothing);
|
||||
await disposeTree(tester);
|
||||
});
|
||||
|
||||
testWidgets('a pending proposal shows the "awaiting signature" badge',
|
||||
(tester) async {
|
||||
LocaleSettings.setLocaleSync(AppLocale.en);
|
||||
final repo = newTestRepository(db);
|
||||
await repo.createPlantare(
|
||||
direction: PlantareDirection.iReturn,
|
||||
counterparty: 'Ben',
|
||||
pledgeId: 'p-2',
|
||||
debtorKey: 'dk',
|
||||
creditorKey: 'ck',
|
||||
creditorSignature: 'cs', // proposer (creditor) signed; I (debtor) haven't
|
||||
remoteState: PlantareRemoteState.proposed,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
wrapScreen(repository: repo, child: const PlantaresScreen()),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
expect(find.text('Awaiting signature'), findsOneWidget);
|
||||
await disposeTree(tester);
|
||||
});
|
||||
|
||||
testWidgets('tapping a linked commitment opens its variety', (tester) async {
|
||||
final repo = newTestRepository(db);
|
||||
final vid = await repo.addQuickVariety(label: 'Judía');
|
||||
|
|
|
|||
|
|
@ -67,6 +67,8 @@ class FakeSession implements SocialSession {
|
|||
@override
|
||||
ProfileTransport get profile => throw UnimplementedError();
|
||||
@override
|
||||
PlantareTransport get plantares => throw UnimplementedError();
|
||||
@override
|
||||
SyncTransport get sync => throw UnimplementedError();
|
||||
@override
|
||||
Future<void> close() async {}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue