Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
21 changes: 18 additions & 3 deletions lib/features/trades/providers/trades_providers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,10 @@ TradeStatusFilter orderStatusToFilter(rust_types.OrderStatus status) {
// ── Internal helpers ──────────────────────────────────────────────────────────

/// Converts a [rust_types.TradeInfo] to a [TradeListItem].
TradeListItem _tradeInfoToItem(rust_types.TradeInfo trade) {
TradeListItem _tradeInfoToItem(
rust_types.TradeInfo trade, {
TradeStatusFilter? statusOverride,
}) {
final fiatDisplay = _formatFiat(
trade.order.fiatAmount,
trade.order.fiatAmountMin,
Expand All @@ -128,7 +131,7 @@ TradeListItem _tradeInfoToItem(rust_types.TradeInfo trade) {
// TradeRole.buyer = the user is buying Bitcoin (took a sell order or
// created a buy order). TradeRole.seller = selling Bitcoin.
isSelling: trade.role == rust_types.TradeRole.seller,
status: orderStatusToFilter(trade.order.status),
status: statusOverride ?? orderStatusToFilter(trade.order.status),
// order.isMine is true when the local user published this order (maker).
role: trade.order.isMine ? TradeRole.creator : TradeRole.taker,
fiatAmount: fiatDisplay,
Expand Down Expand Up @@ -197,7 +200,19 @@ final filteredTradesWithOrderStateProvider =
final filter = ref.watch(selectedStatusFilterProvider);
final trades = await ref.watch(rawTradesProvider.future);

final items = trades.map(_tradeInfoToItem).toList();
// Bucket each trade by the SAME live status its row chip shows, so the filter
// and the chip can never disagree (issue #269). The persisted snapshot in the
// DB can lag behind gift-wrap / 38383 updates (and, for own orders, is not
// always synced back to Pending), so tradeStatusProvider is the single source
// of truth. Until the live status has loaded we fall back to the snapshot
// bucket, so nothing briefly escapes the active filter.
final items = trades.map((trade) {
final live = ref.watch(tradeStatusProvider(trade.order.id)).valueOrNull;
return _tradeInfoToItem(
trade,
statusOverride: live == null ? null : orderStatusToFilter(live),
);
}).toList();

final filtered = filter == TradeStatusFilter.all
? items
Expand Down
57 changes: 57 additions & 0 deletions test/features/trades/filtered_trades_provider_test.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mostro/features/trades/providers/trades_providers.dart';
import 'package:mostro/features/order/providers/trade_state_provider.dart';
import 'package:mostro/src/rust/api/types.dart' show TradeInfo, OrderStatus;

import '../../support/fake_trades.dart';
Expand Down Expand Up @@ -95,4 +96,60 @@ void main() {
});
});
});

group('live status buckets the filter (issue #269)', () {
// A trade whose persisted snapshot is Pending, but whose live status (the
// one the row chip shows) has already moved to waitingBuyerInvoice. The
// filter must follow the live status, not the stale snapshot.
ProviderContainer staleSnapshotContainer() => createContainer(overrides: [
rawTradesProvider.overrideWith(
(ref) async => [fakeTrade(id: 'x', status: OrderStatus.pending)],
),
tradeStatusProvider('order-x').overrideWith(
(ref) => Stream.value(OrderStatus.waitingBuyerInvoice),
),
]);

// Wait for the overridden tradeStatusProvider stream to emit its first
// value, so the derived filter provider sees the live status rather than
// racing the snapshot fallback (which only applies until live loads).
Future<void> primeLiveStatus(ProviderContainer c) async {
await c.read(tradeStatusProvider('order-x').future);
}

test('trade does NOT appear under the stale Pending bucket', () async {
final container = staleSnapshotContainer();
await primeLiveStatus(container);
expect(
await _orderIds(container, filter: TradeStatusFilter.pending),
isEmpty,
);
});

test('trade appears under the live Waiting Invoice bucket', () async {
final container = staleSnapshotContainer();
await primeLiveStatus(container);
expect(
await _orderIds(container, filter: TradeStatusFilter.waitingInvoice),
['order-x'],
);
});

test('falls back to the snapshot bucket until live status loads', () async {
// No tradeStatusProvider override: live is unavailable, so the snapshot
// status (Pending) is used. This preserves behaviour before first poll.
final container = createContainer(overrides: [
rawTradesProvider.overrideWith(
(ref) async => [fakeTrade(id: 'y', status: OrderStatus.pending)],
),
tradeStatusProvider('order-y').overrideWith(
(ref) => const Stream.empty(),
),
]);
expect(
await _orderIds(container, filter: TradeStatusFilter.pending),
['order-y'],
);
});
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Loading