CW-959: Swap Status on Transaction Screen (#2299)

* feat(swap-status-monitor): add real-time swap status monitoring and UI updates

- Introduce SwapManager for automatic tracking of active-wallet swaps.
- Automatically queues new or updated trades from the Hive box.
- Periodically fetch and persist swap statuses via the corresponding trade provider.
- Implement start(wallet, providers), stop(), and dispose() for lifecycle control.
- Apply user's ExchangeApiMode(disabled, tor-only, enabled) when fetching updates.
- Remove swaps from the watchlist on any final state (completed, expired, failed).
- Dispose SwapManager in AppState.dispose() to cancel polling and the Hive subscription.

* refactor(swap-status): replace SwapManager with TradeMonitor for improved trade monitoring.

This change improves the flow by simplifying the trade monitoring logic.

- Removes SwapManager class and replace with TradeMonitor implementation
- Update di and Appstate to register and dispose TradeMonitor
- Modify DashboardViewModel to use TradeMonitor instead of SwapManager

* fix: Modify trade monitoring logic to ensure trade timers are properly disposed when wallet switching occurs

* fix(swap-status): Fix receive amount for exchanges showing as .00 because of null values

* feat(swap-status): Enhance Trade Monitoring

This change:
- Adds a privacy settings option to disable automatic exchange status updates.
- Prevents trade monitoring when privacy settings option is enabled.
- Disables trade monitoring when the app is in background, we only want to run these checks in foreground.
- Refactors the trade monitoring logic to remove unneccessary checks and use of resources.

* feat(swap-status): Enhance Trade Monitoring

This change:
- Adds a privacy settings option to disable automatic exchange status updates.
- Prevents trade monitoring when privacy settings option is enabled.
- Disables trade monitoring when the app is in background, we only want to run these checks in foreground.
- Refactors the trade monitoring logic to remove unneccessary checks and use of resources.

* fix(swap-staus): Prevent unneccessary calls

* feat(swap-status): Prevent api request calls as long as last update time is less than specified interval
This commit is contained in:
David Adegoke
2025-06-04 18:24:56 +03:00
committed by GitHub
parent 1134915920
commit 1d6e594e04
39 changed files with 455 additions and 82 deletions
@@ -166,17 +166,19 @@ class TransactionsPage extends StatelessWidget {
return Observer(
builder: (_) => TradeRow(
key: item.key,
onTap: () => Navigator.of(context)
.pushNamed(Routes.tradeDetails, arguments: trade),
key: item.key,
onTap: () => Navigator.of(context)
.pushNamed(Routes.tradeDetails, arguments: trade),
swapState: trade.state,
provider: trade.provider,
from: trade.from,
to: trade.to,
createdAtFormattedDate: trade.createdAt != null
? DateFormat('HH:mm').format(trade.createdAt!)
: null,
formattedAmount: item.tradeFormattedAmount,
formattedReceiveAmount: item.tradeFormattedReceiveAmount),
from: trade.from,
to: trade.to,
createdAtFormattedDate: trade.createdAt != null
? DateFormat('HH:mm').format(trade.createdAt!)
: null,
formattedAmount: item.tradeFormattedAmount,
formattedReceiveAmount: item.tradeFormattedReceiveAmount
),
);
}
if (item is OrderListItem) {
@@ -1,7 +1,9 @@
import 'package:cake_wallet/exchange/trade_state.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/utils/image_utill.dart';
import 'package:flutter/material.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
import 'package:cw_core/crypto_currency.dart';
class TradeRow extends StatelessWidget {
TradeRow({
@@ -12,6 +14,7 @@ class TradeRow extends StatelessWidget {
this.onTap,
this.formattedAmount,
this.formattedReceiveAmount,
required this.swapState,
super.key,
});
@@ -22,6 +25,7 @@ class TradeRow extends StatelessWidget {
final String? createdAtFormattedDate;
final String? formattedAmount;
final String? formattedReceiveAmount;
final TradeState swapState;
@override
Widget build(BuildContext context) {
@@ -29,25 +33,39 @@ class TradeRow extends StatelessWidget {
final receiveAmountCrypto = to.toString();
return InkWell(
onTap: onTap,
child: Container(
padding: EdgeInsets.fromLTRB(24, 8, 24, 8),
color: Colors.transparent,
child: Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(50),
child: ImageUtil.getImageFromPath(
imagePath: provider.image,
height: 36,
width: 36,
onTap: onTap,
child: Container(
padding: EdgeInsets.fromLTRB(24, 8, 24, 8),
color: Colors.transparent,
child: Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Stack(
clipBehavior: Clip.none,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(50),
child: ImageUtil.getImageFromPath(
imagePath: provider.image, height: 36, width: 36),),
Positioned(
right: 0,
bottom: 2,
child: Container(
height: 8,
width: 8,
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: _statusColor(context, swapState),
borderRadius: BorderRadius.circular(12),
),
),
),
],
),
),
SizedBox(width: 12),
Expanded(
child: Column(
SizedBox(width: 12),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
@@ -104,4 +122,21 @@ class TradeRow extends StatelessWidget {
),
);
}
Color _statusColor(BuildContext context, TradeState status) {
switch (status) {
case TradeState.complete:
case TradeState.completed:
case TradeState.finished:
case TradeState.success:
case TradeState.settled:
return PaletteDark.brightGreen;
case TradeState.failed:
case TradeState.expired:
case TradeState.notFound:
return Palette.darkRed;
default:
return const Color(0xffff6600);
}
}
}
+7
View File
@@ -3,6 +3,7 @@ import 'dart:io';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/core/totp_request_details.dart';
import 'package:cake_wallet/core/trade_monitor.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/view_model/link_view_model.dart';
import 'package:cw_core/utils/print_verbose.dart';
@@ -27,6 +28,7 @@ class Root extends StatefulWidget {
required this.navigatorKey,
required this.authService,
required this.linkViewModel,
required this.tradeMonitor,
}) : super(key: key);
final AuthenticationStore authenticationStore;
@@ -35,6 +37,7 @@ class Root extends StatefulWidget {
final AuthService authService;
final Widget child;
final LinkViewModel linkViewModel;
final TradeMonitor tradeMonitor;
@override
RootState createState() => RootState();
@@ -141,6 +144,8 @@ class RootState extends State<Root> with WidgetsBindingObserver {
bitcoin!.stopPayjoinSessions(widget.appStore.wallet!);
}
widget.tradeMonitor.stopTradeMonitoring();
break;
case AppLifecycleState.resumed:
widget.authService.requireAuth().then((value) {
@@ -154,6 +159,8 @@ class RootState extends State<Root> with WidgetsBindingObserver {
widget.appStore.settingsStore.usePayjoin) {
bitcoin!.resumePayjoinSessions(widget.appStore.wallet!);
}
widget.tradeMonitor.resumeTradeMonitoring();
break;
default:
break;
+31 -20
View File
@@ -75,30 +75,41 @@ class PrivacyPage extends BasePage {
),
if (DeviceInfo.instance.isMobile)
SettingsSwitcherCell(
title: S.current.prevent_screenshots,
value: _privacySettingsViewModel.isAppSecure,
onValueChange: (BuildContext _, bool value) {
_privacySettingsViewModel.setIsAppSecure(value);
}),
SettingsSwitcherCell(
title: S.current.disable_buy,
value: _privacySettingsViewModel.disableTradeOption,
title: S.current.prevent_screenshots,
value: _privacySettingsViewModel.isAppSecure,
onValueChange: (BuildContext _, bool value) {
_privacySettingsViewModel.setDisableTradeOption(value);
}),
_privacySettingsViewModel.setIsAppSecure(value);
},
),
SettingsSwitcherCell(
title: S.current.disable_bulletin,
value: _privacySettingsViewModel.disableBulletin,
onValueChange: (BuildContext _, bool value) {
_privacySettingsViewModel.setDisableBulletin(value);
}),
title: S.current.disable_buy,
value: _privacySettingsViewModel.disableTradeOption,
onValueChange: (BuildContext _, bool value) {
_privacySettingsViewModel.setDisableTradeOption(value);
},
),
SettingsSwitcherCell(
title: S.current.disable_automatic_exchange_status_updates,
value: _privacySettingsViewModel.disableAutomaticExchangeStatusUpdates,
onValueChange: (BuildContext _, bool value) {
_privacySettingsViewModel.setDisableAutomaticExchangeStatusUpdates(value);
},
),
SettingsSwitcherCell(
title: S.current.disable_bulletin,
value: _privacySettingsViewModel.disableBulletin,
onValueChange: (BuildContext _, bool value) {
_privacySettingsViewModel.setDisableBulletin(value);
},
),
if (_privacySettingsViewModel.canUseEtherscan)
SettingsSwitcherCell(
title: S.current.etherscan_history,
value: _privacySettingsViewModel.useEtherscan,
onValueChange: (BuildContext _, bool value) {
_privacySettingsViewModel.setUseEtherscan(value);
}),
title: S.current.etherscan_history,
value: _privacySettingsViewModel.useEtherscan,
onValueChange: (BuildContext _, bool value) {
_privacySettingsViewModel.setUseEtherscan(value);
},
),
if (_privacySettingsViewModel.canUsePolygonScan)
SettingsSwitcherCell(
title: S.current.polygonscan_history,