Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion lib/features/order/screens/pay_lightning_invoice_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand Down
88 changes: 88 additions & 0 deletions test/features/order/screens/pay_lightning_invoice_screen_test.dart
Original file line number Diff line number Diff line change
@@ -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<NwcPaymentWidget>(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);
},
);
}
2 changes: 2 additions & 0 deletions test/support/fake_trades.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ TradeInfo fakeTrade({
String paymentMethod = 'Wire',
bool isMine = false,
int startedAt = 1000,
BigInt? amountSats,
}) {
final order = OrderInfo(
id: 'order-$id',
Expand All @@ -22,6 +23,7 @@ TradeInfo fakeTrade({
premium: 0,
creatorPubkey: 'pubkey-$id',
createdAt: startedAt,
amountSats: amountSats ?? BigInt.zero,
isMine: isMine,
rating: 0,
totalReviews: 0,
Expand Down
Loading