From 503790f7d0d22a4ab2bef0235284e94214992bd5 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 6 Oct 2026 21:44:44 -0500 Subject: [PATCH 1/5] style(exchange): dart format send_from_view --- lib/pages/exchange_view/send_from_view.dart | 36 ++++++++++----------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart index 9fe4121ee..d49dfdb4e 100644 --- a/lib/pages/exchange_view/send_from_view.dart +++ b/lib/pages/exchange_view/send_from_view.dart @@ -106,9 +106,9 @@ class _SendFromViewState extends ConsumerState { builder: (child) { return Background( child: Scaffold( - backgroundColor: Theme.of( - context, - ).extension()!.background, + backgroundColor: Theme.of(context) + .extension()! + .background, appBar: AppBar( leading: AppBarBackButton( onPressed: () { @@ -381,9 +381,9 @@ class _SendFromCardState extends ConsumerState { child: Text( "Ok", style: STextStyles.button(context).copyWith( - color: Theme.of( - context, - ).extension()!.buttonTextSecondary, + color: Theme.of(context) + .extension()! + .buttonTextSecondary, ), ), onPressed: () { @@ -427,9 +427,9 @@ class _SendFromCardState extends ConsumerState { children: [ if (!trade.exchangeName.startsWith(TrocadorExchange.exchangeName)) MaterialButton( - splashColor: Theme.of( - context, - ).extension()!.highlight, + splashColor: Theme.of(context) + .extension()! + .highlight, key: Key("walletsSheetItemButtonFiroPrivateKey_$walletId"), padding: const EdgeInsets.all(0), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, @@ -481,9 +481,9 @@ class _SendFromCardState extends ConsumerState { Assets.svg.chevronRight, height: 14, width: 7, - color: Theme.of( - context, - ).extension()!.infoItemLabel, + color: Theme.of(context) + .extension()! + .infoItemLabel, ), ], ), @@ -491,9 +491,9 @@ class _SendFromCardState extends ConsumerState { ), ), MaterialButton( - splashColor: Theme.of( - context, - ).extension()!.highlight, + splashColor: Theme.of(context) + .extension()! + .highlight, key: Key("walletsSheetItemButtonFiroPublicKey_$walletId"), padding: const EdgeInsets.all(0), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, @@ -543,9 +543,9 @@ class _SendFromCardState extends ConsumerState { Assets.svg.chevronRight, height: 14, width: 7, - color: Theme.of( - context, - ).extension()!.infoItemLabel, + color: Theme.of(context) + .extension()! + .infoItemLabel, ), ], ), From 0346331fc7289198d0903d36145a778e3ead25b1 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 6 Oct 2026 21:54:27 -0500 Subject: [PATCH 2/5] style(send,namecoin): dart format building dialog and name option widgets --- .../sub_widgets/transfer_option_widget.dart | 12 ++--- .../sub_widgets/update_option_widget.dart | 50 +++++++++---------- .../building_transaction_dialog.dart | 1 + 3 files changed, 29 insertions(+), 34 deletions(-) diff --git a/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart b/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart index 5dbc649bc..9e3e632ba 100644 --- a/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart +++ b/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart @@ -361,8 +361,7 @@ class _TransferOptionWidgetState extends ConsumerState { children: [ _addressController.text.isNotEmpty ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Address Field Input.", + semanticsLabel: "Clear Button. Clears The Address Field Input.", key: const Key( "nameTransferClearAddressFieldButtonKey", ), @@ -375,8 +374,7 @@ class _TransferOptionWidgetState extends ConsumerState { child: const XIcon(), ) : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Address Field Input.", + semanticsLabel: "Paste Button. Pastes From Clipboard To Address Field Input.", key: const Key( "nameTransferPasteAddressFieldButtonKey", ), @@ -405,8 +403,7 @@ class _TransferOptionWidgetState extends ConsumerState { ), if (_addressController.text.isEmpty) TextFieldIconButton( - semanticsLabel: - "Address Book Button. Opens Address Book For Address Field.", + semanticsLabel: "Address Book Button. Opens Address Book For Address Field.", key: const Key( "nameTransferAddressBookButtonKey", ), @@ -420,8 +417,7 @@ class _TransferOptionWidgetState extends ConsumerState { ), if (_addressController.text.isEmpty) TextFieldIconButton( - semanticsLabel: - "Scan QR Button. Opens Camera For Scanning QR Code.", + semanticsLabel: "Scan QR Button. Opens Camera For Scanning QR Code.", key: const Key("nameTransferScanQrButtonKey"), onTap: _scanQr, child: const QrCodeIcon(), diff --git a/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart b/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart index 4fd78fb70..dd48c0819 100644 --- a/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart +++ b/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart @@ -183,16 +183,15 @@ class _BuyDomainWidgetState extends ConsumerState { if (Util.isDesktop) { await showDialog( context: context, - builder: - (context) => SDialog( - child: SizedBox( - width: 580, - child: ConfirmNameTransactionView( - txData: txData, - walletId: widget.walletId, - ), - ), + builder: (context) => SDialog( + child: SizedBox( + width: 580, + child: ConfirmNameTransactionView( + txData: txData, + walletId: widget.walletId, ), + ), + ), ); } else { await Navigator.of(context).pushNamed( @@ -217,13 +216,12 @@ class _BuyDomainWidgetState extends ConsumerState { if (mounted) { await showDialog( context: context, - builder: - (_) => StackOkDialog( - title: "Update failed", - message: err, - desktopPopRootNavigator: Util.isDesktop, - maxWidth: Util.isDesktop ? 600 : null, - ), + builder: (_) => StackOkDialog( + title: "Update failed", + message: err, + desktopPopRootNavigator: Util.isDesktop, + maxWidth: Util.isDesktop ? 600 : null, + ), ); } } finally { @@ -260,10 +258,9 @@ class _BuyDomainWidgetState extends ConsumerState { Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: - Util.isDesktop - ? CrossAxisAlignment.start - : CrossAxisAlignment.stretch, + crossAxisAlignment: Util.isDesktop + ? CrossAxisAlignment.start + : CrossAxisAlignment.stretch, children: [ Text("Edit value", style: STextStyles.label(context)), const SizedBox(height: 6), @@ -293,10 +290,9 @@ class _BuyDomainWidgetState extends ConsumerState { return Text( "$length/$valueMaxLength", style: STextStyles.w500_10(context).copyWith( - color: - Theme.of( - context, - ).extension()!.textSubtitle2, + color: Theme.of(context) + .extension()! + .textSubtitle2, ), ); }, @@ -311,8 +307,10 @@ class _BuyDomainWidgetState extends ConsumerState { child: SecondaryButton( label: "Cancel", buttonHeight: Util.isDesktop ? ButtonHeight.l : null, - onPressed: - Navigator.of(context, rootNavigator: Util.isDesktop).pop, + onPressed: Navigator.of( + context, + rootNavigator: Util.isDesktop, + ).pop, ), ), const SizedBox(width: 16), diff --git a/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart b/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart index 0d1e9ef34..38464ad96 100644 --- a/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart +++ b/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart @@ -12,6 +12,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; + import '../../../themes/coin_image_provider.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/text_styles.dart'; From 47c15a28cd25594fffb425f6aaeded4fb2443881 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Sat, 28 Feb 2026 18:23:33 -0600 Subject: [PATCH 3/5] fix: remove double-pop from BuildingTransactionDialog cancel The dialog's cancel button popped the navigator itself and then invoked onCancel, while callers' onCancel handlers also popped, causing a double pop. Drop the internal pop so the caller owns the dismissal, and add the missing pop to Step4View's onCancel. --- .../exchange_step_views/step_4_view.dart | 1 + .../building_transaction_dialog.dart | 2 - .../building_transaction_dialog_test.dart | 118 ++++++++++++++++++ 3 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 test/pages/send_view/building_transaction_dialog_test.dart 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 dc75032f9..c23cd6117 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 @@ -286,6 +286,7 @@ class _Step4ViewState extends ConsumerState { isSpark: wallet is FiroWallet && !firoPublicSend, onCancel: () { wasCancelled = true; + Navigator.of(context).pop(); }, ); }, diff --git a/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart b/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart index 38464ad96..68c9c8bd2 100644 --- a/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart +++ b/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart @@ -116,7 +116,6 @@ class _RestoringDialogState extends ConsumerState { style: STextStyles.itemSubtitle12(context), ), onPressed: () { - Navigator.of(context).pop(); onCancel.call(); }, ), @@ -141,7 +140,6 @@ class _RestoringDialogState extends ConsumerState { style: STextStyles.itemSubtitle12(context), ), onPressed: () { - Navigator.of(context).pop(); onCancel.call(); }, ), diff --git a/test/pages/send_view/building_transaction_dialog_test.dart b/test/pages/send_view/building_transaction_dialog_test.dart new file mode 100644 index 000000000..56b2dcb96 --- /dev/null +++ b/test/pages/send_view/building_transaction_dialog_test.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stackwallet/models/isar/stack_theme.dart'; +import 'package:stackwallet/pages/send_view/sub_widgets/building_transaction_dialog.dart'; +import 'package:stackwallet/themes/coin_image_provider.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; + +import '../../sample_data/theme_json.dart'; + +void main() { + for (final isDesktop in [false, true]) { + testWidgets( + 'cancel dismisses only the ${isDesktop ? 'desktop' : 'mobile'} dialog', + (tester) async { + Util.screenWidth = isDesktop ? null : 400; + addTearDown(() => Util.screenWidth = null); + + var cancelCount = 0; + await tester.pumpWidget( + ProviderScope( + overrides: [ + coinImageSecondaryProvider.overrideWithProvider( + (_) => Provider((_) => 'coin.svg'), + ), + ], + child: MaterialApp( + theme: ThemeData( + extensions: [ + StackColors.fromStackColorTheme( + StackTheme.fromJson(json: lightThemeJsonMap), + ), + ], + ), + home: _RootPage( + isDesktop: isDesktop, + onCancel: () => cancelCount++, + ), + ), + ), + ); + + await tester.tap(find.text('Open caller')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Build transaction')); + await tester.pump(const Duration(milliseconds: 300)); + await tester.tap(find.text('Cancel')); + await tester.pumpAndSettle(); + + expect(cancelCount, 1); + expect(find.byKey(const Key('caller page')), findsOneWidget); + expect(find.text('Generating transaction'), findsNothing); + expect( + tester.state(find.byType(Navigator)).canPop(), + isTrue, + ); + }, + ); + } +} + +class _RootPage extends StatelessWidget { + const _RootPage({required this.isDesktop, required this.onCancel}); + + final bool isDesktop; + final VoidCallback onCancel; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: TextButton( + onPressed: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => + _CallerPage(isDesktop: isDesktop, onCancel: onCancel), + ), + ), + child: const Text('Open caller'), + ), + ); + } +} + +class _CallerPage extends StatelessWidget { + const _CallerPage({required this.isDesktop, required this.onCancel}); + + final bool isDesktop; + final VoidCallback onCancel; + + @override + Widget build(BuildContext context) { + return Scaffold( + key: const Key('caller page'), + body: TextButton( + onPressed: () => showDialog( + context: context, + barrierDismissible: false, + builder: (dialogContext) { + final child = BuildingTransactionDialog( + coin: Bitcoin(CryptoCurrencyNetwork.main), + isSpark: false, + onCancel: () { + onCancel(); + Navigator.of(dialogContext).pop(); + }, + ); + + return isDesktop ? DesktopDialog(child: child) : child; + }, + ), + child: const Text('Build transaction'), + ), + ); + } +} From 7c96fccfa18539446025c9e226be71a0331bad35 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Mon, 24 Aug 2026 15:58:00 -0500 Subject: [PATCH 4/5] fix: keep the send caller route when a build fails after cancel Seven send flows pop the caller's route from their catch block whenever a prepareSend failure arrives after the user already dismissed the building transaction dialog. On desktop that pop empties the root navigator and blanks the window; on mobile it drops the user back to the wallet view. Hoist wasCancelled out of the try so the catch can see it, as step_4_view and the ShopinBit/CakePay send flows already do. --- .../salvium_create_stake_view.dart | 5 +- lib/pages/send_view/send_view.dart | 5 +- lib/pages/send_view/sol_token_send_view.dart | 5 +- lib/pages/send_view/token_send_view.dart | 5 +- .../wallet_view/sub_widgets/desktop_send.dart | 5 +- .../sub_widgets/desktop_sol_token_send.dart | 5 +- .../sub_widgets/desktop_token_send.dart | 5 +- .../building_transaction_dialog_test.dart | 224 ++++++++++++++---- 8 files changed, 189 insertions(+), 70 deletions(-) diff --git a/lib/pages/salvium_stake/salvium_create_stake_view.dart b/lib/pages/salvium_stake/salvium_create_stake_view.dart index ca62f242d..567918513 100644 --- a/lib/pages/salvium_stake/salvium_create_stake_view.dart +++ b/lib/pages/salvium_stake/salvium_create_stake_view.dart @@ -74,9 +74,8 @@ class _SalviumCreateStakeViewState if (_lock) return; _lock = true; + bool wasCancelled = false; try { - bool wasCancelled = false; - unawaited( showDialog( context: context, @@ -170,7 +169,7 @@ class _SalviumCreateStakeViewState } catch (e, s) { Logging.instance.e("Salvium stake preview: ", error: e, stackTrace: s); - if (mounted) { + if (mounted && !wasCancelled) { // pop building dialog Navigator.of(context, rootNavigator: Util.isDesktop).pop(); diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index e6249006a..3fe1e2c45 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -935,9 +935,8 @@ class _SendViewState extends ConsumerState { } } + bool wasCancelled = false; try { - bool wasCancelled = false; - if (mounted) { unawaited( showDialog( @@ -1167,7 +1166,7 @@ class _SendViewState extends ConsumerState { } } catch (e, s) { Logging.instance.e("$e\n$s", error: e, stackTrace: s); - if (mounted) { + if (mounted && !wasCancelled) { // pop building dialog Navigator.of(context, rootNavigator: true).pop(); diff --git a/lib/pages/send_view/sol_token_send_view.dart b/lib/pages/send_view/sol_token_send_view.dart index 13c66ff01..dd8ccb5c7 100644 --- a/lib/pages/send_view/sol_token_send_view.dart +++ b/lib/pages/send_view/sol_token_send_view.dart @@ -464,9 +464,8 @@ class _SolTokenSendViewState extends ConsumerState { final wallet = ref.read(pWallets).getWallet(walletId); final Amount amount = _amountToSend!; + bool wasCancelled = false; try { - bool wasCancelled = false; - if (mounted) { unawaited( showDialog( @@ -543,7 +542,7 @@ class _SolTokenSendViewState extends ConsumerState { } } catch (e, s) { Logging.instance.e("$e\n$s", error: e, stackTrace: s); - if (mounted) { + if (mounted && !wasCancelled) { // pop building dialog Navigator.of(context).pop(); diff --git a/lib/pages/send_view/token_send_view.dart b/lib/pages/send_view/token_send_view.dart index 5601d41c9..fe8a80518 100644 --- a/lib/pages/send_view/token_send_view.dart +++ b/lib/pages/send_view/token_send_view.dart @@ -463,9 +463,8 @@ class _TokenSendViewState extends ConsumerState { // } // } + bool wasCancelled = false; try { - bool wasCancelled = false; - if (mounted) { unawaited( showDialog( @@ -538,7 +537,7 @@ class _TokenSendViewState extends ConsumerState { } } catch (e, s) { Logging.instance.e("$e\n$s", error: e, stackTrace: s); - if (mounted) { + if (mounted && !wasCancelled) { // pop building dialog Navigator.of(context).pop(); diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index b411775ef..9ce1582d5 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -549,9 +549,8 @@ class _DesktopSendState extends ConsumerState { } } + bool wasCancelled = false; try { - bool wasCancelled = false; - if (mounted) { unawaited( showDialog( @@ -793,7 +792,7 @@ class _DesktopSendState extends ConsumerState { } } catch (e, s) { Logging.instance.e("Desktop send: ", error: e, stackTrace: s); - if (mounted) { + if (mounted && !wasCancelled) { // pop building dialog Navigator.of(context, rootNavigator: true).pop(); diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_sol_token_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_sol_token_send.dart index 9c8475196..2ac7b7bbe 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_sol_token_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_sol_token_send.dart @@ -225,9 +225,8 @@ class _DesktopSolTokenSendState extends ConsumerState { } } + bool wasCancelled = false; try { - bool wasCancelled = false; - if (mounted) { unawaited( showDialog( @@ -310,7 +309,7 @@ class _DesktopSolTokenSendState extends ConsumerState { ); } } catch (e) { - if (mounted) { + if (mounted && !wasCancelled) { // pop building dialog Navigator.of(context, rootNavigator: true).pop(); diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart index 9388a2230..8bb54b00b 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart @@ -207,9 +207,8 @@ class _DesktopTokenSendState extends ConsumerState { } } + bool wasCancelled = false; try { - bool wasCancelled = false; - if (mounted) { unawaited( showDialog( @@ -289,7 +288,7 @@ class _DesktopTokenSendState extends ConsumerState { ); } } catch (e) { - if (mounted) { + if (mounted && !wasCancelled) { // pop building dialog Navigator.of(context, rootNavigator: true).pop(); diff --git a/test/pages/send_view/building_transaction_dialog_test.dart b/test/pages/send_view/building_transaction_dialog_test.dart index 56b2dcb96..eb2b151df 100644 --- a/test/pages/send_view/building_transaction_dialog_test.dart +++ b/test/pages/send_view/building_transaction_dialog_test.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -11,62 +13,93 @@ import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import '../../sample_data/theme_json.dart'; +Widget _app(Widget home) { + return ProviderScope( + overrides: [ + coinImageSecondaryProvider.overrideWithProvider( + (_) => Provider((_) => 'coin.svg'), + ), + ], + child: MaterialApp( + theme: ThemeData( + extensions: [ + StackColors.fromStackColorTheme( + StackTheme.fromJson(json: lightThemeJsonMap), + ), + ], + ), + home: home, + ), + ); +} + void main() { for (final isDesktop in [false, true]) { - testWidgets( - 'cancel dismisses only the ${isDesktop ? 'desktop' : 'mobile'} dialog', - (tester) async { - Util.screenWidth = isDesktop ? null : 400; - addTearDown(() => Util.screenWidth = null); - - var cancelCount = 0; - await tester.pumpWidget( - ProviderScope( - overrides: [ - coinImageSecondaryProvider.overrideWithProvider( - (_) => Provider((_) => 'coin.svg'), - ), - ], - child: MaterialApp( - theme: ThemeData( - extensions: [ - StackColors.fromStackColorTheme( - StackTheme.fromJson(json: lightThemeJsonMap), - ), - ], - ), - home: _RootPage( - isDesktop: isDesktop, - onCancel: () => cancelCount++, - ), - ), - ), - ); + final label = isDesktop ? 'desktop' : 'mobile'; - await tester.tap(find.text('Open caller')); - await tester.pumpAndSettle(); - await tester.tap(find.text('Build transaction')); - await tester.pump(const Duration(milliseconds: 300)); - await tester.tap(find.text('Cancel')); - await tester.pumpAndSettle(); - - expect(cancelCount, 1); - expect(find.byKey(const Key('caller page')), findsOneWidget); - expect(find.text('Generating transaction'), findsNothing); - expect( - tester.state(find.byType(Navigator)).canPop(), - isTrue, - ); - }, - ); + testWidgets('cancel dismisses only the $label dialog', (tester) async { + Util.screenWidth = isDesktop ? null : 400; + addTearDown(() => Util.screenWidth = null); + + var cancelCount = 0; + await tester.pumpWidget( + _app(_RootPage(isDesktop: isDesktop, onCancel: () => cancelCount++)), + ); + + await tester.tap(find.text('Open caller')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Build transaction')); + await tester.pump(const Duration(milliseconds: 300)); + await tester.tap(find.text('Cancel')); + await tester.pumpAndSettle(); + + expect(cancelCount, 1); + expect(find.byKey(const Key('caller page')), findsOneWidget); + expect(find.text('Generating transaction'), findsNothing); + expect( + tester.state(find.byType(Navigator)).canPop(), + isTrue, + ); + }); + + testWidgets('a build failing after cancel keeps the $label caller route', ( + tester, + ) async { + Util.screenWidth = isDesktop ? null : 400; + addTearDown(() => Util.screenWidth = null); + + final txData = Completer(); + await tester.pumpWidget( + _app(_RootPage(isDesktop: isDesktop, txData: txData)), + ); + + await tester.tap(find.text('Open caller')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Build transaction')); + await tester.pump(const Duration(milliseconds: 300)); + await tester.tap(find.text('Cancel')); + await tester.pumpAndSettle(); + + expect(find.text('Generating transaction'), findsNothing); + expect(find.byKey(const Key('caller page')), findsOneWidget); + + txData.completeError(Exception('insufficient funds')); + await tester.pump(const Duration(milliseconds: 2600)); + await tester.pumpAndSettle(); + + expect(find.byKey(const Key('caller page')), findsOneWidget); + expect(find.byKey(const Key('failed dialog')), findsNothing); + expect(tester.takeException(), isNull); + }); } } class _RootPage extends StatelessWidget { - const _RootPage({required this.isDesktop, required this.onCancel}); + const _RootPage({required this.isDesktop, this.onCancel, this.txData}); final bool isDesktop; - final VoidCallback onCancel; + final VoidCallback? onCancel; + final Completer? txData; @override Widget build(BuildContext context) { @@ -74,8 +107,9 @@ class _RootPage extends StatelessWidget { body: TextButton( onPressed: () => Navigator.of(context).push( MaterialPageRoute( - builder: (_) => - _CallerPage(isDesktop: isDesktop, onCancel: onCancel), + builder: (_) => txData == null + ? _CallerPage(isDesktop: isDesktop, onCancel: onCancel!) + : _PreviewCallerPage(isDesktop: isDesktop, txData: txData!), ), ), child: const Text('Open caller'), @@ -116,3 +150,95 @@ class _CallerPage extends StatelessWidget { ); } } + +/// Mirrors the preview flow shared by the send views, which cannot be pumped +/// directly here because they need live wallet providers. +class _PreviewCallerPage extends StatefulWidget { + const _PreviewCallerPage({required this.isDesktop, required this.txData}); + + final bool isDesktop; + final Completer txData; + + @override + State<_PreviewCallerPage> createState() => _PreviewCallerPageState(); +} + +class _PreviewCallerPageState extends State<_PreviewCallerPage> { + bool wasCancelled = false; + + Future _preview() async { + try { + unawaited( + showDialog( + context: context, + barrierDismissible: false, + builder: (dialogContext) { + final child = BuildingTransactionDialog( + coin: Bitcoin(CryptoCurrencyNetwork.main), + isSpark: false, + onCancel: () { + wasCancelled = true; + + Navigator.of(dialogContext).pop(); + }, + ); + + return widget.isDesktop ? DesktopDialog(child: child) : child; + }, + ), + ); + + final time = Future.delayed(const Duration(milliseconds: 2500)); + final results = await Future.wait([widget.txData.future, time]); + + if (!wasCancelled && mounted) { + // pop building dialog + Navigator.of(context, rootNavigator: true).pop(); + + unawaited( + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => Scaffold( + key: const Key('confirm page'), + body: Text('${results.first}'), + ), + ), + ), + ); + } + } catch (_) { + if (mounted && !wasCancelled) { + // pop building dialog + Navigator.of(context, rootNavigator: true).pop(); + + unawaited( + showDialog( + context: context, + barrierDismissible: true, + builder: (dialogContext) => AlertDialog( + key: const Key('failed dialog'), + title: const Text('Transaction failed'), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(), + child: const Text('Ok'), + ), + ], + ), + ), + ); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + key: const Key('caller page'), + body: TextButton( + onPressed: () => unawaited(_preview()), + child: const Text('Build transaction'), + ), + ); + } +} From b09968ac18aa8be6449579de84190d704c3f224d Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 27 Aug 2026 21:46:38 -0500 Subject: [PATCH 5/5] fix: keep exchange and namecoin callers when a build fails after cancel Hoist wasCancelled out of the try in send_from_view and the namecoin transfer/update option widgets so a prepare failure arriving after the user cancelled the building dialog no longer pops the caller route or shows a stale error dialog. --- lib/pages/exchange_view/send_from_view.dart | 5 ++--- .../namecoin_names/sub_widgets/transfer_option_widget.dart | 5 ++--- .../namecoin_names/sub_widgets/update_option_widget.dart | 5 ++--- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart index d49dfdb4e..7c0fd140c 100644 --- a/lib/pages/exchange_view/send_from_view.dart +++ b/lib/pages/exchange_view/send_from_view.dart @@ -225,9 +225,8 @@ class _SendFromCardState extends ConsumerState { Future _send({bool? shouldSendPublicFiroFunds}) async { final coin = ref.read(pWalletCoin(walletId)); + bool wasCancelled = false; try { - bool wasCancelled = false; - final wallet = ref.read(pWallets).getWallet(walletId); unawaited( @@ -362,7 +361,7 @@ class _SendFromCardState extends ConsumerState { } } catch (e, s) { Logging.instance.e("$e\n$s", error: e, stackTrace: s); - if (mounted) { + if (mounted && !wasCancelled) { // pop building dialog Navigator.of(context, rootNavigator: Util.isDesktop).pop(); diff --git a/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart b/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart index 9e3e632ba..eba8faa45 100644 --- a/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart +++ b/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart @@ -71,11 +71,10 @@ class _TransferOptionWidgetState extends ConsumerState { FocusScope.of(context).unfocus(); await Future.delayed(const Duration(milliseconds: 100)); + bool wasCancelled = false; try { final wallet = ref.read(pWallets).getWallet(walletId) as NamecoinWallet; - bool wasCancelled = false; - if (mounted) { if (Util.isDesktop) { unawaited( @@ -194,7 +193,7 @@ class _TransferOptionWidgetState extends ConsumerState { stackTrace: s, ); - if (mounted) { + if (mounted && !wasCancelled) { String err = e.toString(); if (err.startsWith("Exception: ")) { err = err.replaceFirst("Exception: ", ""); diff --git a/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart b/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart index dd48c0819..fa21142b0 100644 --- a/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart +++ b/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart @@ -73,6 +73,7 @@ class _BuyDomainWidgetState extends ConsumerState { Future _previewUpdate() async { if (_previewLock) return; _previewLock = true; + bool wasCancelled = false; try { final newValue = _getNewValue(); if (newValue == _currentValue) { @@ -86,8 +87,6 @@ class _BuyDomainWidgetState extends ConsumerState { final wallet = ref.read(pWallets).getWallet(widget.walletId) as NamecoinWallet; - bool wasCancelled = false; - if (mounted) { if (Util.isDesktop) { unawaited( @@ -213,7 +212,7 @@ class _BuyDomainWidgetState extends ConsumerState { err = "Contains invalid characters"; } - if (mounted) { + if (mounted && !wasCancelled) { await showDialog( context: context, builder: (_) => StackOkDialog(