From 09da422470f07c834adcd162952fbb572b7363ce Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2026 13:14:15 +0200 Subject: [PATCH] Fix one swap tap creating multiple trades (#3709) TradeIsCreating was only set deep inside createTrade's provider loop, after several awaits, so the swap button stayed tappable while the first call was still running and every extra tap started another trade. createTrade also had no in-progress check of its own. Now createTrade returns early if a trade is already being created, sets TradeIsCreating synchronously before the first await, and resets the state in a finally block so the button is usable again after success, failure or an unexpected exception. The ConcurrentModificationError retry now awaits the inner call instead of re-entering createTrade, so it is not blocked by the new guard. Claude-Session: https://claude.ai/code/session_016HdqYnWEiqnBvmvqCBQgjE Co-authored-by: Claude --- .../exchange/exchange_view_model.dart | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 0373f5e24..f44dff6d2 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -1047,6 +1047,27 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with @action Future createTrade() async { + // Ignore repeated taps while a trade is already being created + if (tradeState is TradeIsCreating) return; + + // Set synchronously, before the first await, so the swap button shows its + // loading state and further taps are ignored right away + tradeState = TradeIsCreating(); + + try { + await _createTrade(); + } finally { + // Never leave the swap button stuck in its loading state + // (e.g. if an unexpected exception is thrown) + if (tradeState is TradeIsCreating) { + tradeState = TradeIsCreatedFailure( + title: S.current.trade_not_created, + error: S.current.none_of_selected_providers_can_exchange); + } + } + } + + Future _createTrade() async { final depositAmountValue = _depositAmount ?? Money.zero(depositCurrency); final receiveAmountValue = _receiveAmount ?? Money.zero(receiveCurrency); @@ -1335,7 +1356,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with /// /// this is because the limitation of the SplayTreeMap that /// you can't modify it while iterating through it - Future.delayed(Duration(milliseconds: 200), createTrade); + await Future.delayed(Duration(milliseconds: 200)); + await _createTrade(); } }