Skip to content
Draft
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
20 changes: 20 additions & 0 deletions lib/models/exchange/response_objects/trade.dart
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,26 @@ class Trade {
);
}

static const Set<String> terminalStatuses = {
"finished",
"completed",
"success",
"failed",
"error",
"refunded",
"overdue",
"expired",
"closed",
"cancelled",
"canceled",
"not found",
};

static bool isTerminalStatusValue(String status) =>
terminalStatuses.contains(status.trim().toLowerCase());

bool get isTerminalStatus => isTerminalStatusValue(status);

@override
String toString() {
return toMap().toString();
Expand Down
88 changes: 88 additions & 0 deletions lib/pages/exchange_view/delete_trade_confirmation_dialog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import 'package:flutter/material.dart';

import '../../utilities/text_styles.dart';
import '../../utilities/util.dart';
import '../../widgets/desktop/primary_button.dart';
import '../../widgets/desktop/secondary_button.dart';
import '../../widgets/dialogs/s_dialog.dart';

Future<bool> showDeleteTradeConfirmationDialog({
required BuildContext context,
required bool isTerminalStatus,
}) async {
return await showDialog<bool>(
context: context,
useSafeArea: true,
builder: (_) =>
DeleteTradeConfirmationDialog(isTerminalStatus: isTerminalStatus),
) ??
false;
}

class DeleteTradeConfirmationDialog extends StatelessWidget {
const DeleteTradeConfirmationDialog({
super.key,
required this.isTerminalStatus,
});

final bool isTerminalStatus;

@override
Widget build(BuildContext context) {
final isDesktop = Util.isDesktop;

return SDialog(
padding: EdgeInsets.all(isDesktop ? 32 : 20),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 386),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
isTerminalStatus
? "Delete this trade?"
: "Delete an active trade?",
style: isDesktop
? STextStyles.desktopH3(context)
: STextStyles.pageTitleH2(context),
),
const SizedBox(height: 16),
Text(
isTerminalStatus
? "This trade will be permanently deleted."
: "This trade is still active. Deleting it will remove it "
"from this device, so you will no longer be able to "
"track its status here.",
style: isDesktop
? STextStyles.desktopTextSmall(context)
: STextStyles.itemSubtitle(context),
),
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: SecondaryButton(
key: const Key("cancelDeleteTradeButton"),
label: "Cancel",
buttonHeight: ButtonHeight.l,
onPressed: () => Navigator.of(context).pop(false),
),
),
const SizedBox(width: 16),
Expanded(
child: PrimaryButton(
key: const Key("confirmDeleteTradeButton"),
label: "Delete",
buttonHeight: ButtonHeight.l,
onPressed: () => Navigator.of(context).pop(true),
),
),
],
),
],
),
),
);
}
}
134 changes: 114 additions & 20 deletions lib/pages/exchange_view/trade_details_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import 'package:url_launcher/url_launcher.dart';

import '../../app_config.dart';
import '../../models/exchange/change_now/cn_exchange_transaction_status.dart';
import '../../models/exchange/response_objects/trade.dart';
import '../../models/isar/models/blockchain_data/transaction.dart';
import '../../models/isar/stack_theme.dart';
import '../../notifications/show_flush_bar.dart';
Expand All @@ -44,6 +45,7 @@ import '../../utilities/assets.dart';
import '../../utilities/clipboard_interface.dart';
import '../../utilities/constants.dart';
import '../../utilities/format.dart';
import '../../utilities/logger.dart';
import '../../utilities/text_styles.dart';
import '../../utilities/util.dart';
import '../../wallets/crypto_currency/crypto_currency.dart';
Expand All @@ -59,8 +61,10 @@ import '../../widgets/rounded_white_container.dart';
import '../../widgets/stack_dialog.dart';
import '../wallet_view/transaction_views/edit_note_view.dart';
import '../wallet_view/transaction_views/transaction_details_view.dart' as tdv;
import 'delete_trade_confirmation_dialog.dart';
import 'edit_trade_note_view.dart';
import 'send_from_view.dart';
import 'trade_operation_guard.dart';

class TradeDetailsView extends ConsumerStatefulWidget {
const TradeDetailsView({
Expand Down Expand Up @@ -89,34 +93,84 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
late final ClipboardInterface clipboard;
late final Transaction? transactionIfSentFromStack;
late final String? walletId;
final _operationGuard = TradeOperationGuard();

@override
initState() {
void initState() {
super.initState();
tradeId = widget.tradeId;
clipboard = widget.clipboard;
transactionIfSentFromStack = widget.transactionIfSentFromStack;
walletId = widget.walletId;

if (ref.read(prefsChangeNotifierProvider).externalCalls) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
final trade = ref
.read(tradesServiceProvider)
.trades
.firstWhere((e) => e.tradeId == tradeId);

if (mounted && trade.exchangeName != "Majestic Bank") {
final exchange = Exchange.fromName(trade.exchangeName);
final response = await exchange.updateTrade(trade);

if (mounted && response.value != null) {
await ref
.read(tradesServiceProvider)
.edit(trade: response.value!, shouldNotifyListeners: true);
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!_operationGuard.deletionRequested) {
_operationGuard.trackStatusRefresh(_refreshTrade());
}
});
}
super.initState();
}

Future<void> _refreshTrade() async {
try {
final tradesService = ref.read(tradesServiceProvider);
final trade = tradesService.get(tradeId);
if (!mounted || trade == null || trade.exchangeName == "Majestic Bank") {
return;
}

final exchange = Exchange.fromName(trade.exchangeName);
final response = await exchange.updateTrade(trade);

if (!mounted ||
_operationGuard.deletionRequested ||
response.value == null ||
tradesService.get(tradeId) == null) {
return;
}

await tradesService.edit(
trade: response.value!,
shouldNotifyListeners: true,
);
} catch (e, s) {
Logging.instance.e("Failed to refresh trade", error: e, stackTrace: s);
}
}

Future<void> _deleteTrade(Trade trade) async {
final shouldDelete = await showDeleteTradeConfirmationDialog(
context: context,
isTerminalStatus: trade.isTerminalStatus,
);
if (!shouldDelete || !mounted) {
return;
}

final tradesService = ref.read(tradesServiceProvider);
setState(() {});

try {
final deleted = await _operationGuard.deleteAfterRefresh(
() => tradesService.delete(trade: trade, shouldNotifyListeners: true),
);
if (deleted && mounted) {
Navigator.of(context).pop();
}
} catch (e, s) {
Logging.instance.e("Failed to delete trade", error: e, stackTrace: s);
if (mounted) {
setState(() {});
unawaited(
showFloatingFlushBar(
type: FlushBarType.warning,
message: "Failed to delete trade. Please try again.",
context: context,
),
);
}
}
}

String _fetchIconAssetForStatus(String statusString, IThemeAssets assets) {
Expand Down Expand Up @@ -166,10 +220,11 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
transactionIfSentFromStack != null && walletId != null;

final trade = ref.watch(
tradesServiceProvider.select(
(value) => value.trades.firstWhere((e) => e.tradeId == tradeId),
),
tradesServiceProvider.select((value) => value.get(tradeId)),
);
if (trade == null) {
return const SizedBox.shrink();
}

final bool hasTx =
sentFromStack ||
Expand Down Expand Up @@ -214,6 +269,8 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
trade.status == "wait" ||
trade.status == "Waiting");

void deleteTrade() => unawaited(_deleteTrade(trade));

return ConditionalParent(
condition: !isDesktop,
builder: (child) => Background(
Expand All @@ -234,6 +291,36 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
"Trade details",
style: STextStyles.navBarTitle(context),
),
actions: [
Padding(
padding: const EdgeInsets.only(top: 10, bottom: 10, right: 10),
child: AspectRatio(
aspectRatio: 1,
child: AppBarIconButton(
key: const Key("tradeDetailsViewDeleteTradeButtonKey"),
size: 36,
shadows: const [],
color: Theme.of(
context,
).extension<StackColors>()!.background,
icon: SvgPicture.asset(
Assets.svg.trash,
colorFilter: ColorFilter.mode(
Theme.of(
context,
).extension<StackColors>()!.accentColorDark,
BlendMode.srcIn,
),
width: 20,
height: 20,
),
onPressed: _operationGuard.deletionRequested
? null
: deleteTrade,
),
),
),
],
),
body: SafeArea(
child: Padding(
Expand Down Expand Up @@ -297,6 +384,13 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
);
},
),
const SizedBox(height: 16),
SecondaryButton(
label: "Delete trade",
buttonHeight: ButtonHeight.l,
enabled: !_operationGuard.deletionRequested,
onPressed: deleteTrade,
),
const SizedBox(height: 32),
],
),
Expand Down
30 changes: 30 additions & 0 deletions lib/pages/exchange_view/trade_operation_guard.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
class TradeOperationGuard {
// A refresh must finish first so its database write cannot follow deletion.
Future<void> _statusRefresh = Future.value();

bool _deletionRequested = false;

bool get deletionRequested => _deletionRequested;

void trackStatusRefresh(Future<void> refresh) {
if (!_deletionRequested) {
_statusRefresh = refresh;
}
}

Future<bool> deleteAfterRefresh(Future<void> Function() delete) async {
if (_deletionRequested) {
return false;
}

_deletionRequested = true;
try {
await _statusRefresh;
await delete();
return true;
} catch (_) {
_deletionRequested = false;
rethrow;
}
}
}
Loading
Loading