From 296581094d0769f306ccc9a92d5d869e5fbb03ea Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 31 Jul 2025 13:16:08 -0600 Subject: [PATCH 1/2] Disable send from stack option for swaps where the wallet is required to be open and/or fully synced. Users will need to manually go into the wallet and send from there. --- .../exchange_step_views/step_4_view.dart | 35 ++++++--- .../exchange_view/trade_details_view.dart | 33 ++++++-- .../exchange_steps/step_scaffold.dart | 76 ++++++++++++++----- 3 files changed, 108 insertions(+), 36 deletions(-) diff --git a/lib/pages/exchange_view/exchange_step_views/step_4_view.dart b/lib/pages/exchange_view/exchange_step_views/step_4_view.dart index d244ded98..973298bd6 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_4_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_4_view.dart @@ -21,6 +21,7 @@ import '../../../models/exchange/incomplete_exchange.dart'; import '../../../notifications/show_flush_bar.dart'; import '../../../providers/providers.dart'; import '../../../route_generator.dart'; +import '../../../services/wallets.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/amount/amount.dart'; import '../../../utilities/amount/amount_formatter.dart'; @@ -34,6 +35,8 @@ import '../../../wallets/crypto_currency/crypto_currency.dart'; import '../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../wallets/models/tx_data.dart'; import '../../../wallets/wallet/impl/firo_wallet.dart'; +import '../../../wallets/wallet/intermediate/external_wallet.dart'; +import '../../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../../widgets/background.dart'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../widgets/desktop/secondary_button.dart'; @@ -65,6 +68,7 @@ class Step4View extends ConsumerStatefulWidget { } class _Step4ViewState extends ConsumerState { + late final bool isWalletCoinAndCanSend; late final IncompleteExchangeModel model; late final ClipboardInterface clipboard; @@ -72,13 +76,20 @@ class _Step4ViewState extends ConsumerState { Timer? _statusTimer; - bool _isWalletCoinAndHasWallet(String ticker, WidgetRef ref) { + bool isWalletCoinAndCanSendWithoutWalletOpened( + String ticker, + Wallets walletsInstance, + ) { try { final coin = AppConfig.getCryptoCurrencyForTicker(ticker); - return ref - .read(pWallets) - .wallets - .where((e) => e.info.coin == coin) + return walletsInstance.wallets + .where( + (e) => + e.info.coin == coin && + (e is! ExternalWallet || + e is MwebInterface), // ltc mweb is external but swaps + // should not use mweb, hence the odd logic check here + ) .isNotEmpty; } catch (_) { return false; @@ -111,6 +122,11 @@ class _Step4ViewState extends ConsumerState { model = widget.model; clipboard = widget.clipboard; + isWalletCoinAndCanSend = isWalletCoinAndCanSendWithoutWalletOpened( + model.trade!.payInCurrency, + ref.read(pWallets), + ); + _statusTimer = Timer.periodic(const Duration(seconds: 60), (_) { _updateStatus(); }); @@ -339,10 +355,6 @@ class _Step4ViewState extends ConsumerState { @override Widget build(BuildContext context) { - final bool isWalletCoin = _isWalletCoinAndHasWallet( - model.trade!.payInCurrency, - ref, - ); return WillPopScope( onWillPop: () async { await _close(); @@ -791,8 +803,9 @@ class _Step4ViewState extends ConsumerState { style: STextStyles.button(context), ), ), - if (isWalletCoin) const SizedBox(height: 12), - if (isWalletCoin) + if (isWalletCoinAndCanSend) + const SizedBox(height: 12), + if (isWalletCoinAndCanSend) Builder( builder: (context) { String buttonTitle = diff --git a/lib/pages/exchange_view/trade_details_view.dart b/lib/pages/exchange_view/trade_details_view.dart index 329f1d572..6e56e5755 100644 --- a/lib/pages/exchange_view/trade_details_view.dart +++ b/lib/pages/exchange_view/trade_details_view.dart @@ -32,6 +32,7 @@ import '../../services/exchange/exchange.dart'; import '../../services/exchange/nanswap/nanswap_exchange.dart'; import '../../services/exchange/simpleswap/simpleswap_exchange.dart'; import '../../services/exchange/trocador/trocador_exchange.dart'; +import '../../services/wallets.dart'; import '../../themes/stack_colors.dart'; import '../../themes/theme_providers.dart'; import '../../utilities/amount/amount.dart'; @@ -43,6 +44,8 @@ import '../../utilities/format.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; import '../../wallets/crypto_currency/crypto_currency.dart'; +import '../../wallets/wallet/intermediate/external_wallet.dart'; +import '../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../widgets/background.dart'; import '../../widgets/conditional_parent.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; @@ -152,6 +155,26 @@ class _TradeDetailsViewState extends ConsumerState { } } + bool isWalletCoinAndCanSendWithoutWalletOpened( + String ticker, + Wallets walletsInstance, + ) { + try { + final coin = AppConfig.getCryptoCurrencyForTicker(ticker); + return walletsInstance.wallets + .where( + (e) => + e.info.coin == coin && + (e is! ExternalWallet || + e is MwebInterface), // ltc mweb is external but swaps + // should not use mweb, hence the odd logic check here + ) + .isNotEmpty; + } catch (_) { + return false; + } + } + @override Widget build(BuildContext context) { final bool sentFromStack = @@ -193,13 +216,11 @@ class _TradeDetailsViewState extends ConsumerState { final showSendFromStackButton = !hasTx && - ![ - "xmr", - "monero", - "wow", - "wownero", - ].contains(trade.payInCurrency.toLowerCase()) && AppConfig.isStackCoin(trade.payInCurrency) && + isWalletCoinAndCanSendWithoutWalletOpened( + trade.payInCurrency, + ref.read(pWallets), + ) && (trade.status == "New" || trade.status == "new" || trade.status == "waiting" || diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart index 606d9fa68..23e925c0c 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart @@ -20,14 +20,18 @@ import '../../../models/exchange/response_objects/trade.dart'; import '../../../pages/exchange_view/send_from_view.dart'; import '../../../providers/exchange/exchange_form_state_provider.dart'; import '../../../providers/global/trades_service_provider.dart'; +import '../../../providers/global/wallets_provider.dart'; import '../../../route_generator.dart'; import '../../../services/exchange/exchange_response.dart'; import '../../../services/notifications_api.dart'; +import '../../../services/wallets.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/amount/amount.dart'; import '../../../utilities/assets.dart'; import '../../../utilities/enums/exchange_rate_type_enum.dart'; import '../../../utilities/text_styles.dart'; +import '../../../wallets/wallet/intermediate/external_wallet.dart'; +import '../../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../widgets/custom_loading_overlay.dart'; import '../../../widgets/desktop/desktop_dialog.dart'; @@ -241,6 +245,26 @@ class _StepScaffoldState extends ConsumerState { ); } + bool isWalletCoinAndCanSendWithoutWalletOpened( + String ticker, + Wallets walletsInstance, + ) { + try { + final coin = AppConfig.getCryptoCurrencyForTicker(ticker); + return walletsInstance.wallets + .where( + (e) => + e.info.coin == coin && + (e is! ExternalWallet || + e is MwebInterface), // ltc mweb is external but swaps + // should not use mweb, hence the odd logic check here + ) + .isNotEmpty; + } catch (_) { + return false; + } + } + @override void initState() { duration = const Duration(milliseconds: 250); @@ -251,6 +275,18 @@ class _StepScaffoldState extends ConsumerState { @override Widget build(BuildContext context) { final model = ref.watch(desktopExchangeModelProvider); + + final bool canSendFromStack; + if (currentStep != 4) { + // set to true anyways to show back button + canSendFromStack = true; + } else { + canSendFromStack = isWalletCoinAndCanSendWithoutWalletOpened( + model?.sendTicker ?? "", + ref.read(pWallets), + ); + } + return Column( mainAxisAlignment: MainAxisAlignment.start, children: [ @@ -307,25 +343,27 @@ class _StepScaffoldState extends ConsumerState { ), child: Row( children: [ - Expanded( - child: AnimatedCrossFade( - duration: const Duration(milliseconds: 250), - crossFadeState: - currentStep == 4 - ? CrossFadeState.showSecond - : CrossFadeState.showFirst, - firstChild: SecondaryButton( - label: "Back", - buttonHeight: ButtonHeight.l, - onPressed: onBack, - ), - secondChild: SecondaryButton( - label: "Send from ${AppConfig.appName}", - buttonHeight: ButtonHeight.l, - onPressed: sendFromStack, - ), - ), - ), + canSendFromStack + ? Expanded( + child: AnimatedCrossFade( + duration: const Duration(milliseconds: 250), + crossFadeState: + currentStep == 4 + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, + firstChild: SecondaryButton( + label: "Back", + buttonHeight: ButtonHeight.l, + onPressed: onBack, + ), + secondChild: SecondaryButton( + label: "Send from ${AppConfig.appName}", + buttonHeight: ButtonHeight.l, + onPressed: sendFromStack, + ), + ), + ) + : const Spacer(), const SizedBox(width: 16), Expanded( child: AnimatedCrossFade( From 22c1e04d0127fe9e3addba18fb4be85dd4c657ee Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 31 Jul 2025 13:51:44 -0600 Subject: [PATCH 2/2] Provide option on desktop baddecryption error to still continue with wallet deletion --- .../desktop_attention_delete_wallet.dart | 250 ++++++++++++------ 1 file changed, 167 insertions(+), 83 deletions(-) diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart index 16032b36b..032a61bf4 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart @@ -10,12 +10,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stack_wallet_backup/secure_storage.dart'; import 'package:tuple/tuple.dart'; import '../../../../app_config.dart'; import '../../../../pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_view_only_wallet_keys_view.dart'; import '../../../../providers/global/wallets_provider.dart'; +import '../../../../route_generator.dart'; import '../../../../themes/stack_colors.dart'; +import '../../../../utilities/logger.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart'; @@ -24,13 +27,11 @@ import '../../../../widgets/desktop/desktop_dialog_close_button.dart'; import '../../../../widgets/desktop/primary_button.dart'; import '../../../../widgets/desktop/secondary_button.dart'; import '../../../../widgets/rounded_container.dart'; +import '../../../../widgets/rounded_white_container.dart'; import 'delete_wallet_keys_popup.dart'; class DesktopAttentionDeleteWallet extends ConsumerStatefulWidget { - const DesktopAttentionDeleteWallet({ - super.key, - required this.walletId, - }); + const DesktopAttentionDeleteWallet({super.key, required this.walletId}); final String walletId; @@ -55,10 +56,7 @@ class _DesktopAttentionDeleteWallet children: [ DesktopDialogCloseButton( onPressedOverride: () { - Navigator.of( - context, - rootNavigator: true, - ).pop(); + Navigator.of(context, rootNavigator: true).pop(); }, ), ], @@ -67,17 +65,13 @@ class _DesktopAttentionDeleteWallet padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 26), child: Column( children: [ - Text( - "Attention!", - style: STextStyles.desktopH2(context), - ), - const SizedBox( - height: 16, - ), + Text("Attention!", style: STextStyles.desktopH2(context)), + const SizedBox(height: 16), RoundedContainer( - color: Theme.of(context) - .extension()! - .snackBarBackError, + color: + Theme.of( + context, + ).extension()!.snackBarBackError, child: Padding( padding: const EdgeInsets.all(10.0), child: Text( @@ -85,11 +79,13 @@ class _DesktopAttentionDeleteWallet "the only way you can have access to your funds is by using your backup key." "\n\n${AppConfig.appName} does not keep nor is able to restore your backup key or your wallet." "\n\nPLEASE SAVE YOUR BACKUP KEY.", - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .snackBarTextError, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.snackBarTextError, ), ), ), @@ -103,10 +99,7 @@ class _DesktopAttentionDeleteWallet buttonHeight: ButtonHeight.xl, label: "Cancel", onPressed: () { - Navigator.of( - context, - rootNavigator: true, - ).pop(); + Navigator.of(context, rootNavigator: true).pop(); }, ), const SizedBox(width: 16), @@ -115,71 +108,96 @@ class _DesktopAttentionDeleteWallet buttonHeight: ButtonHeight.xl, label: "View Backup Key", onPressed: () async { - final wallet = - ref.read(pWallets).getWallet(widget.walletId); + try { + final wallet = ref + .read(pWallets) + .getWallet(widget.walletId); - if (wallet is ViewOnlyOptionInterface && - wallet.isViewOnly) { - final data = await wallet.getViewOnlyWalletData(); - if (context.mounted) { - await Navigator.of(context).push( - MaterialPageRoute( - builder: (builder) => DesktopDialog( - maxWidth: 614, - maxHeight: double.infinity, - child: Column( - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, + if (wallet is ViewOnlyOptionInterface && + wallet.isViewOnly) { + final data = await wallet.getViewOnlyWalletData(); + if (context.mounted) { + await Navigator.of(context).push( + MaterialPageRoute( + builder: + (builder) => DesktopDialog( + maxWidth: 614, + maxHeight: double.infinity, + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Padding( + padding: + const EdgeInsets.only( + left: 32, + ), + child: Text( + "Wallet keys", + style: + STextStyles.desktopH3( + context, + ), + ), + ), + DesktopDialogCloseButton( + onPressedOverride: () { + Navigator.of( + context, + rootNavigator: true, + ).pop(); + }, + ), + ], ), - child: Text( - "Wallet keys", - style: STextStyles.desktopH3( - context, - ), + Padding( + padding: const EdgeInsets.all(32), + child: + DeleteViewOnlyWalletKeysView( + walletId: widget.walletId, + data: data, + ), ), - ), - DesktopDialogCloseButton( - onPressedOverride: () { - Navigator.of( - context, - rootNavigator: true, - ).pop(); - }, - ), - ], - ), - Padding( - padding: const EdgeInsets.all(32), - child: DeleteViewOnlyWalletKeysView( - walletId: widget.walletId, - data: data, + ], ), ), - ], - ), ), - ), - ); - } - } else + ); + } + } else + // TODO: [prio=med] handle other types wallet deletion + // All wallets currently are mnemonic based + if (wallet is MnemonicInterface) { + final words = await wallet.getMnemonicAsWords(); - // TODO: [prio=med] handle other types wallet deletion - // All wallets currently are mnemonic based - if (wallet is MnemonicInterface) { - final words = await wallet.getMnemonicAsWords(); + if (context.mounted) { + await Navigator.of(context).pushNamed( + DeleteWalletKeysPopup.routeName, + arguments: Tuple2(widget.walletId, words), + ); + } + } + } on BadDecryption catch (e, s) { + Logging.instance.f( + "Desktop wallet delete error. Showing decryption error continue dialog.", + error: e, + stackTrace: s, + ); if (context.mounted) { - await Navigator.of(context).pushNamed( - DeleteWalletKeysPopup.routeName, - arguments: Tuple2( - widget.walletId, - words, + await Navigator.of(context).push( + RouteGenerator.getRoute( + builder: (context) { + return ErrorLoadingKeysDialog( + walletId: widget.walletId, + ); + }, + settings: const RouteSettings( + name: "/desktopErrorLoadingKeysDialog", + ), ), ); } @@ -196,3 +214,69 @@ class _DesktopAttentionDeleteWallet ); } } + +class ErrorLoadingKeysDialog extends StatelessWidget { + const ErrorLoadingKeysDialog({super.key, required this.walletId}); + + final String walletId; + + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxWidth: 614, + maxHeight: double.infinity, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Decryption error", + style: STextStyles.desktopH3(context), + ), + ), + DesktopDialogCloseButton( + onPressedOverride: () { + Navigator.of(context, rootNavigator: true).pop(); + }, + ), + ], + ), + Padding( + padding: const EdgeInsets.all(32), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + child: Text( + "Could not retrieve wallet keys/mnemonic phrase/seed.\n\n" + "Are you certain you would like to continue with wallet deletion?", + style: STextStyles.label(context).copyWith(fontSize: 16), + ), + ), + const SizedBox(height: 40), + PrimaryButton( + label: "Continue", + onPressed: () async { + await Navigator.of(context).push( + RouteGenerator.getRoute( + builder: (context) { + return ConfirmDelete(walletId: walletId); + }, + settings: const RouteSettings( + name: "/desktopConfirmDelete", + ), + ), + ); + }, + ), + ], + ), + ), + ], + ), + ); + } +}