Skip to content
Merged
16 changes: 16 additions & 0 deletions lib/features/order/providers/trade_state_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,22 @@ final tradeStatusProvider =
}
});

/// Trade lifecycle updates pushed from Rust (daemon-driven cancellations).
///
/// Complements [tradeStatusProvider]'s polling, which cannot observe a
/// cancellation anymore: a never-active trade is wiped from the DB on the
/// daemon's Canceled, and after a timeout republish the order book reads
/// `pending` again. Screens filter by `orderId`.
final tradeUpdatesProvider =
StreamProvider.autoDispose<TradeUpdate>((ref) async* {
final stream = await orders_api.onTradeUpdated();
while (true) {
final update = await stream.next();
if (update == null) break;
yield update;
}
});

/// Whether a status is terminal (no further changes possible).
bool _isTerminal(OrderStatus s) => const {
OrderStatus.success,
Expand Down
30 changes: 30 additions & 0 deletions lib/features/order/screens/add_lightning_invoice_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ import 'package:mostro/core/daemon_errors.dart';
import 'package:mostro/l10n/app_localizations.dart';
import 'package:mostro/features/order/providers/trade_state_provider.dart';
import 'package:mostro/features/settings/providers/nwc_provider.dart';
import 'package:mostro/features/trades/providers/trades_providers.dart'
show refreshTrades;
import 'package:mostro/shared/widgets/nwc_invoice_widget.dart';
import 'package:mostro/src/rust/api/orders.dart' as orders_api;
import 'package:mostro/src/rust/api/types.dart' show OrderStatus, TradeUpdate;

/// Add Lightning Invoice screen — Route `/add_invoice/:orderId`.
///
Expand Down Expand Up @@ -37,6 +40,8 @@ class _AddLightningInvoiceScreenState
bool _submitting = false;
/// `true` when NWC is connected but generation failed → show manual form.
bool _manualMode = false;
/// One-shot guard so we don't navigate twice as further updates stream in.
bool _navigated = false;

@override
void dispose() {
Expand Down Expand Up @@ -115,6 +120,31 @@ class _AddLightningInvoiceScreenState

final isWalletConnected = ref.watch(isWalletConnectedProvider);

// Leave the screen when mostrod cancels the order (e.g. the buyer let the
// waiting-state window expire): the daemon ignores messages for a
// canceled order, so without this the form just sits here and every
// submit dies with a 10s NoDaemonResponse.
ref.listen<AsyncValue<TradeUpdate>>(tradeUpdatesProvider, (prev, next) {
final update = next.valueOrNull;
if (update == null || _navigated || !mounted) return;
if (update.orderId != widget.orderId) return;
switch (update.status) {
case OrderStatus.canceled:
case OrderStatus.cooperativelyCanceled:
case OrderStatus.canceledByAdmin:
case OrderStatus.expired:
_navigated = true;
// The wiped trade must also disappear from the My Trades cache.
refreshTrades(ref);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.orderNoLongerActive)),
);
context.go(AppRoute.home);
default:
break;
}
});

// Resolve sats: provider first (live polling), fall back to constructor param.
final sats = _resolvedSats(ref);

Expand Down
28 changes: 27 additions & 1 deletion lib/features/order/screens/pay_lightning_invoice_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import 'package:mostro/core/app_routes.dart';
import 'package:mostro/core/app_theme.dart';
import 'package:mostro/features/order/providers/trade_state_provider.dart';
import 'package:mostro/features/settings/providers/nwc_provider.dart';
import 'package:mostro/features/trades/providers/trades_providers.dart'
show refreshTrades;
import 'package:mostro/l10n/app_localizations.dart';
import 'package:mostro/src/rust/api/types.dart' show OrderStatus;
import 'package:mostro/src/rust/api/types.dart' show OrderStatus, TradeUpdate;
import 'package:mostro/shared/widgets/nwc_payment_widget.dart';

/// Pay Lightning Invoice screen — Route `/pay_invoice/:orderId`.
Expand Down Expand Up @@ -95,6 +97,30 @@ class _PayLightningInvoiceScreenState
},
);

// Push-based cancellation signal. The polling listener above cannot see
// a daemon cancel anymore: the wiped trade has no DB row left, and after
// a timeout republish the book reads `pending` — a status the switch
// above deliberately ignores.
ref.listen<AsyncValue<TradeUpdate>>(tradeUpdatesProvider, (prev, next) {
final update = next.valueOrNull;
if (update == null || _navigated || !mounted) return;
if (update.orderId != widget.orderId) return;
switch (update.status) {
case OrderStatus.canceled:
case OrderStatus.cooperativelyCanceled:
case OrderStatus.canceledByAdmin:
case OrderStatus.expired:
_navigated = true;
refreshTrades(ref);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.orderNoLongerActive)),
);
context.go(AppRoute.home);
default:
break;
}
});

return tradeAsync.when(
loading: () => Scaffold(
appBar: AppBar(title: Text(l10n.payLightningInvoiceTitle)),
Expand Down
5 changes: 5 additions & 0 deletions lib/features/trades/providers/trades_providers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,11 @@ String _formatFiat(double? amount, double? min, double? max) {
/// Exposed so callers (e.g. [refreshTrades]) can invalidate it when new trades
/// are added. Per-row live status comes from [tradeStatusProvider].
final rawTradesProvider = FutureProvider<List<rust_types.TradeInfo>>((ref) {
// Refetch whenever Rust pushes a trade lifecycle change: a daemon cancel
// wipes the row (it must leave My Trades no matter which screen is open)
// and a sweep resync rewrites its status — pull-to-refresh must not be
// the only way to observe either.
ref.listen(tradeUpdatesProvider, (_, __) => ref.invalidateSelf());
return orders_api.listTrades();
});

Expand Down
Loading
Loading