diff --git a/lib/features/order/screens/pay_lightning_invoice_screen.dart b/lib/features/order/screens/pay_lightning_invoice_screen.dart index bc10c157..5f577d29 100644 --- a/lib/features/order/screens/pay_lightning_invoice_screen.dart +++ b/lib/features/order/screens/pay_lightning_invoice_screen.dart @@ -211,7 +211,31 @@ class _PayLightningInvoiceScreenState ); } - // If NWC wallet is connected and payment hasn't failed yet, show auto-pay. + // A payment has been detected: show a waiting-only state and hide + // every invoice-submission control (NWC widget, QR, pay-external, + // copy/share). NwcPaymentWidget re-arms its own pay button in its + // finally block, and the QR branch would otherwise still expose the + // QR and "pay with wallet" action, either of which lets the user + // re-send an already-settled bolt11. Issue #244. + if (_waiting) { + return Scaffold( + appBar: AppBar(title: Text(l10n.payLightningInvoiceTitle)), + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator(color: green), + const SizedBox(height: 16), + Text( + l10n.waitingForPaymentConfirmation, + style: TextStyle(color: colors?.textSecondary), + ), + ], + ), + ), + ); + } + // NWC wallet connected and payment hasn't failed yet: show auto-pay. if (isWalletConnected && !_manualMode) { return Scaffold( appBar: AppBar(title: Text(l10n.payLightningInvoiceTitle)), diff --git a/test/features/order/screens/pay_lightning_invoice_screen_test.dart b/test/features/order/screens/pay_lightning_invoice_screen_test.dart new file mode 100644 index 00000000..64f76a75 --- /dev/null +++ b/test/features/order/screens/pay_lightning_invoice_screen_test.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mostro/features/order/providers/trade_state_provider.dart'; +import 'package:mostro/features/order/screens/pay_lightning_invoice_screen.dart'; +import 'package:mostro/features/settings/providers/nwc_provider.dart'; +import 'package:mostro/l10n/app_localizations.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:mostro/shared/widgets/nwc_payment_widget.dart'; +import 'package:mostro/src/rust/api/types.dart'; + +import '../../../support/fake_trades.dart'; + +/// A trade with a usable hold invoice so the screen passes the +/// invoice.isEmpty / amountSats guard and reaches the NWC / QR branches. +TradeInfo _payableTrade() { + final base = fakeTrade( + id: 'x', + status: OrderStatus.waitingPayment, + amountSats: BigInt.from(1000), + ); + return TradeInfo( + id: base.id, + order: base.order, + role: base.role, + counterpartyPubkey: base.counterpartyPubkey, + currentStep: base.currentStep, + tradeKeyIndex: base.tradeKeyIndex, + startedAt: base.startedAt, + holdInvoice: 'lnbc1000n1pxxxxxxx', + ); +} + +Widget _app({required bool walletConnected, required TradeInfo trade}) { + return ProviderScope( + overrides: [ + isWalletConnectedProvider.overrideWithValue(walletConnected), + tradeInfoStreamProvider('order-x').overrideWith( + (ref) => Stream.value(trade), + ), + // Keep the status poller quiet so it doesn't fire navigation during the + // test; the NWC success callback is what drives _waiting here. + tradeStatusProvider('order-x').overrideWith( + (ref) => const Stream.empty(), + ), + ], + child: const MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: PayLightningInvoiceScreen(orderId: 'order-x'), + ), + ); +} + +void main() { + testWidgets( + '#244: after payment, all invoice-submission controls are hidden and a waiting spinner is shown', + (tester) async { + await tester.pumpWidget( + _app(walletConnected: true, trade: _payableTrade()), + ); + await tester.pump(); // resolve the trade stream + await tester.pump(const Duration(milliseconds: 50)); // let the stream settle + + final l10n = await AppLocalizations.delegate.load(const Locale('en')); + + // Before payment: the NWC auto-pay widget is shown, no waiting text. + expect(find.byType(NwcPaymentWidget), findsOneWidget); + expect(find.text(l10n.waitingForPaymentConfirmation), findsNothing); + + // Simulate a successful NWC payment by invoking the widget's success + // callback (what the real wallet flow calls). This sets _waiting = true. + final nwc = tester.widget(find.byType(NwcPaymentWidget)); + nwc.onPaymentSuccess(); + await tester.pump(); + + // After payment: the screen shows a waiting-only state. Every + // invoice-submission control is gone (NWC widget, QR, pay-external), so + // the user cannot re-send the already-settled bolt11 (#244). Only the + // confirmation spinner and its label remain. + expect(find.byType(NwcPaymentWidget), findsNothing); + expect(find.byType(QrImageView), findsNothing); + expect(find.text(l10n.payWithLightningWallet), findsNothing); + expect(find.text(l10n.copyButtonLabel), findsNothing); + expect(find.text(l10n.waitingForPaymentConfirmation), findsOneWidget); + }, + ); +} diff --git a/test/support/fake_trades.dart b/test/support/fake_trades.dart index be628750..0ea7b993 100644 --- a/test/support/fake_trades.dart +++ b/test/support/fake_trades.dart @@ -11,6 +11,7 @@ TradeInfo fakeTrade({ String paymentMethod = 'Wire', bool isMine = false, int startedAt = 1000, + BigInt? amountSats, }) { final order = OrderInfo( id: 'order-$id', @@ -22,6 +23,7 @@ TradeInfo fakeTrade({ premium: 0, creatorPubkey: 'pubkey-$id', createdAt: startedAt, + amountSats: amountSats ?? BigInt.zero, isMine: isMine, rating: 0, totalReviews: 0,