From 78a1ad3c400731315e4c23064212ec52525e29bb Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 20 Aug 2026 16:26:29 -0500 Subject: [PATCH 1/2] fix(navigation): migrate deprecated WillPopScope to PopScope WillPopScope is deprecated and cannot take part in Android predictive back. Static vetoes become canPop, callbacks that navigate on their own move to onPopInvokedWithResult, and the sheets that pop a value declare the result type of the route they close. --- .../confirm_recovery_dialog.dart | 30 +- .../mnemonic_word_count_select_sheet.dart | 63 +- .../sub_widgets/restore_failed_dialog.dart | 27 +- .../sub_widgets/restoring_dialog.dart | 54 +- .../select_wallet_for_sol_token_view.dart | 98 +-- .../select_wallet_for_token_view.dart | 90 +- lib/pages/buy_view/buy_form.dart | 24 +- .../fusion_rounds_selection_sheet.dart | 61 +- .../churning_rounds_selection_sheet.dart | 61 +- lib/pages/coin_control/coin_control_view.dart | 780 +++++++++--------- .../exchange_currency_selection_view.dart | 217 +++-- lib/pages/exchange_view/exchange_form.dart | 4 +- .../exchange_step_views/step_3_view.dart | 65 +- .../sub_widgets/fetch_monkey_dialog.dart | 54 +- .../dialogs/claiming_paynym_dialog.dart | 33 +- .../paynym/dialogs/paynym_details_popup.dart | 118 +-- .../subwidgets/desktop_paynym_details.dart | 159 ++-- lib/pages/pinpad_views/lock_screen_view.dart | 55 +- lib/pages/receive_view/receive_view.dart | 17 +- .../building_transaction_dialog.dart | 6 +- .../sending_transaction_dialog.dart | 43 +- .../dialogs/cancel_stack_restore_dialog.dart | 27 +- .../restore_from_encrypted_string_view.dart | 23 +- .../restore_from_file_view.dart | 16 +- .../backup_frequency_type_select_sheet.dart | 48 +- .../sub_widgets/confirm_full_rescan.dart | 40 +- .../sub_widgets/rescanning_dialog.dart | 18 +- .../firo_rescan_recovery_error_dialog.dart | 4 +- lib/pages/token_view/sol_token_view.dart | 9 +- lib/pages/token_view/token_view.dart | 35 +- ...ancelling_transaction_progress_dialog.dart | 15 +- .../exchange_steps/step_scaffold.dart | 4 +- .../sub_widgets/desktop_receive.dart | 33 +- .../sub_widgets/desktop_wallet_features.dart | 10 +- ...forgotten_passphrase_restore_from_swb.dart | 8 +- lib/utilities/show_loading.dart | 4 +- lib/widgets/dialogs/basic_dialog.dart | 29 +- .../basic_dialog_pop_scope_test.dart | 77 ++ .../helpers/navigation_test_helpers.dart | 51 ++ .../pop_scope_migrated_widgets_test.dart | 143 ++++ .../pop_scope_navigation_contract_test.dart | 341 ++++++++ 41 files changed, 1621 insertions(+), 1373 deletions(-) create mode 100644 test/widget_tests/basic_dialog_pop_scope_test.dart create mode 100644 test/widget_tests/helpers/navigation_test_helpers.dart create mode 100644 test/widget_tests/pop_scope_migrated_widgets_test.dart create mode 100644 test/widget_tests/pop_scope_navigation_contract_test.dart diff --git a/lib/pages/add_wallet_views/restore_wallet_view/confirm_recovery_dialog.dart b/lib/pages/add_wallet_views/restore_wallet_view/confirm_recovery_dialog.dart index fe11d35026..08039cc0a2 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/confirm_recovery_dialog.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/confirm_recovery_dialog.dart @@ -32,23 +32,15 @@ class ConfirmRecoveryDialog extends StatelessWidget { child: Column( children: [ const DesktopDialogCloseButton(), - const SizedBox( - height: 5, - ), - SvgPicture.asset( - Assets.svg.drd, - width: 99, - height: 70, - ), + const SizedBox(height: 5), + SvgPicture.asset(Assets.svg.drd, width: 99, height: 70), const Spacer(), Text( "Restore wallet", style: STextStyles.desktopH2(context), textAlign: TextAlign.center, ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), Text( "Restoring your wallet may take a while.\nPlease do not exit this screen once the process is started.", style: STextStyles.desktopTextMedium(context).copyWith( @@ -58,11 +50,7 @@ class ConfirmRecoveryDialog extends StatelessWidget { ), const Spacer(), Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), + padding: const EdgeInsets.only(left: 32, right: 32, bottom: 32), child: Row( children: [ Expanded( @@ -73,9 +61,7 @@ class ConfirmRecoveryDialog extends StatelessWidget { }, ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Expanded( child: PrimaryButton( label: "Restore", @@ -92,10 +78,8 @@ class ConfirmRecoveryDialog extends StatelessWidget { ), ); } else { - return WillPopScope( - onWillPop: () async { - return true; - }, + return PopScope( + canPop: true, child: StackDialog( title: "Are you ready?", message: diff --git a/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/mnemonic_word_count_select_sheet.dart b/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/mnemonic_word_count_select_sheet.dart index 6cdaa54239..ef43c60c6b 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/mnemonic_word_count_select_sheet.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/mnemonic_word_count_select_sheet.dart @@ -23,11 +23,14 @@ class MnemonicWordCountSelectSheet extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return WillPopScope( - onWillPop: () async { + return PopScope( + canPop: false, + onPopInvokedWithResult: (bool didPop, int? result) { + if (didPop) { + return; + } final length = ref.read(mnemonicWordCountStateProvider.state).state; Navigator.of(context).pop(length); - return false; }, child: Container( decoration: BoxDecoration( @@ -49,10 +52,9 @@ class MnemonicWordCountSelectSheet extends ConsumerWidget { Center( child: Container( decoration: BoxDecoration( - color: - Theme.of( - context, - ).extension()!.textFieldDefaultBG, + color: Theme.of( + context, + ).extension()!.textFieldDefaultBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), @@ -79,16 +81,16 @@ class MnemonicWordCountSelectSheet extends ConsumerWidget { children: [ GestureDetector( onTap: () { - final state = - ref - .read( - mnemonicWordCountStateProvider.state, - ) - .state; + final state = ref + .read(mnemonicWordCountStateProvider.state) + .state; if (state != lengthOptions[i]) { ref - .read(mnemonicWordCountStateProvider.state) - .state = lengthOptions[i]; + .read( + mnemonicWordCountStateProvider.state, + ) + .state = + lengthOptions[i]; } Navigator.of(context).pop(); @@ -105,25 +107,24 @@ class MnemonicWordCountSelectSheet extends ConsumerWidget { width: 20, height: 20, child: Radio( - activeColor: - Theme.of(context) - .extension()! - .radioButtonIconEnabled, + activeColor: Theme.of(context) + .extension()! + .radioButtonIconEnabled, value: lengthOptions[i], - groupValue: - ref - .watch( - mnemonicWordCountStateProvider - .state, - ) - .state, + groupValue: ref + .watch( + mnemonicWordCountStateProvider + .state, + ) + .state, onChanged: (x) { ref - .read( - mnemonicWordCountStateProvider - .state, - ) - .state = lengthOptions[i]; + .read( + mnemonicWordCountStateProvider + .state, + ) + .state = + lengthOptions[i]; Navigator.of(context).pop(); }, ), diff --git a/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/restore_failed_dialog.dart b/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/restore_failed_dialog.dart index 7419583e73..741abac353 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/restore_failed_dialog.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/restore_failed_dialog.dart @@ -51,30 +51,29 @@ class _RestoreFailedDialogState extends ConsumerState { @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { - return false; - }, + return PopScope( + canPop: false, child: StackDialog( title: "Restore failed", message: errorMessage, rightButton: TextButton( - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle(context), - child: Text( - "Ok", - style: STextStyles.itemSubtitle12(context), - ), + style: Theme.of( + context, + ).extension()!.getSecondaryEnabledButtonStyle(context), + child: Text("Ok", style: STextStyles.itemSubtitle12(context)), onPressed: () async { try { - await ref.read(pWallets).deleteWallet( + await ref + .read(pWallets) + .deleteWallet( ref.read(pWalletInfo(walletId)), ref.read(secureStoreProvider), ); } catch (e, s) { - Logging.instance.e("Error while getting wallet info in restore failed dialog\n" - "Error: $e\nStack trace: $s"); + Logging.instance.e( + "Error while getting wallet info in restore failed dialog\n" + "Error: $e\nStack trace: $s", + ); } finally { if (mounted) { Navigator.of(context).pop(); diff --git a/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/restoring_dialog.dart b/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/restoring_dialog.dart index 38004caad4..7029d711cd 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/restoring_dialog.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/restoring_dialog.dart @@ -19,10 +19,7 @@ import '../../../../widgets/desktop/secondary_button.dart'; import '../../../../widgets/stack_dialog.dart'; class RestoringDialog extends StatefulWidget { - const RestoringDialog({ - super.key, - required this.onCancel, - }); + const RestoringDialog({super.key, required this.onCancel}); final Future Function() onCancel; @@ -53,24 +50,15 @@ class _RestoringDialogState extends State { } }, ), - const Spacer( - flex: 1, - ), - const RotatingArrows( - width: 40, - height: 40, - ), - const Spacer( - flex: 2, - ), + const Spacer(flex: 1), + const RotatingArrows(width: 40, height: 40), + const Spacer(flex: 2), Text( "Restoring wallet...", style: STextStyles.desktopH2(context), textAlign: TextAlign.center, ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), Text( "Restoring your wallet may take a while.\nPlease do not exit this screen.", style: STextStyles.desktopTextMedium(context).copyWith( @@ -78,15 +66,9 @@ class _RestoringDialogState extends State { ), textAlign: TextAlign.center, ), - const Spacer( - flex: 2, - ), + const Spacer(flex: 2), Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), + padding: const EdgeInsets.only(left: 32, right: 32, bottom: 32), child: SecondaryButton( label: "Cancel", width: 272.5, @@ -102,25 +84,17 @@ class _RestoringDialogState extends State { ), ); } else { - return WillPopScope( - onWillPop: () async { - return false; - }, + return PopScope( + canPop: false, child: StackDialog( title: "Restoring wallet", message: "This may take a while. Please do not exit this screen.", - icon: const RotatingArrows( - width: 24, - height: 24, - ), + icon: const RotatingArrows(width: 24, height: 24), rightButton: TextButton( - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle(context), - child: Text( - "Cancel", - style: STextStyles.itemSubtitle12(context), - ), + style: Theme.of( + context, + ).extension()!.getSecondaryEnabledButtonStyle(context), + child: Text("Cancel", style: STextStyles.itemSubtitle12(context)), onPressed: () async { await onCancel.call(); if (mounted) { diff --git a/lib/pages/add_wallet_views/select_wallet_for_sol_token_view.dart b/lib/pages/add_wallet_views/select_wallet_for_sol_token_view.dart index d5c012b53a..ce154fd1a3 100644 --- a/lib/pages/add_wallet_views/select_wallet_for_sol_token_view.dart +++ b/lib/pages/add_wallet_views/select_wallet_for_sol_token_view.dart @@ -31,14 +31,12 @@ import '../../widgets/rounded_white_container.dart'; import '../../widgets/wallet_info_row/wallet_info_row.dart'; import 'package:tuple/tuple.dart'; -final newSolWalletTriggerTempUntilHiveCompletelyDeleted = - StateProvider((ref) => false); +final newSolWalletTriggerTempUntilHiveCompletelyDeleted = StateProvider( + (ref) => false, +); class SelectWalletForSolTokenView extends ConsumerStatefulWidget { - const SelectWalletForSolTokenView({ - super.key, - required this.entity, - }); + const SelectWalletForSolTokenView({super.key, required this.entity}); static const String routeName = "/selectWalletForSolTokenView"; @@ -58,15 +56,13 @@ class _SelectWalletForSolTokenViewState void _onContinue() { Navigator.of(context).pushNamed( EditWalletTokensView.routeName, - arguments: Tuple2( - _selectedWalletId!, - [widget.entity.token.address], - ), + arguments: Tuple2(_selectedWalletId!, [widget.entity.token.address]), ); } void _onAddNewSolWallet() { - ref.read(newSolWalletTriggerTempUntilHiveCompletelyDeleted.notifier).state = true; + ref.read(newSolWalletTriggerTempUntilHiveCompletelyDeleted.notifier).state = + true; Navigator.of(context).pushNamed( CreateOrRestoreWalletView.routeName, arguments: CoinEntity(widget.entity.cryptoCurrency), @@ -91,17 +87,25 @@ class _SelectWalletForSolTokenViewState } } - return WillPopScope( - onWillPop: () async { - ref.read(newSolWalletTriggerTempUntilHiveCompletelyDeleted.notifier).state = false; - return true; + return PopScope( + canPop: true, + onPopInvokedWithResult: (bool didPop, dynamic result) { + if (didPop) { + ref + .read( + newSolWalletTriggerTempUntilHiveCompletelyDeleted.notifier, + ) + .state = + false; + } }, child: ConditionalParent( condition: !isDesktop, builder: (child) => Background( child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, + backgroundColor: Theme.of( + context, + ).extension()!.background, appBar: AppBar( leading: AppBarBackButton( onPressed: () { @@ -110,10 +114,7 @@ class _SelectWalletForSolTokenViewState ), ), body: SafeArea( - child: Padding( - padding: const EdgeInsets.all(16), - child: child, - ), + child: Padding(padding: const EdgeInsets.all(16), child: child), ), ), ), @@ -124,18 +125,12 @@ class _SelectWalletForSolTokenViewState isCompactHeight: false, leading: AppBarBackButton(), ), - body: SizedBox( - width: 500, - child: child, - ), + body: SizedBox(width: 500, child: child), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (isDesktop) - const SizedBox( - height: 24, - ), + if (isDesktop) const SizedBox(height: 24), Text( "Select Solana wallet", textAlign: TextAlign.center, @@ -143,9 +138,7 @@ class _SelectWalletForSolTokenViewState ? STextStyles.desktopH2(context) : STextStyles.pageTitleH1(context), ), - SizedBox( - height: isDesktop ? 16 : 8, - ), + SizedBox(height: isDesktop ? 16 : 8), Text( "You are adding a Solana token.", textAlign: TextAlign.center, @@ -153,9 +146,7 @@ class _SelectWalletForSolTokenViewState ? STextStyles.desktopSubtitleH2(context) : STextStyles.subtitle(context), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), Text( "You must choose a Solana wallet in order to use ${widget.entity.name}", textAlign: TextAlign.center, @@ -163,9 +154,7 @@ class _SelectWalletForSolTokenViewState ? STextStyles.desktopSubtitleH2(context) : STextStyles.subtitle(context), ), - SizedBox( - height: isDesktop ? 60 : 16, - ), + SizedBox(height: isDesktop ? 60 : 16), solWalletIds.isEmpty ? RoundedWhiteContainer( padding: EdgeInsets.all(isDesktop ? 16 : 12), @@ -193,9 +182,8 @@ class _SelectWalletForSolTokenViewState child: ListView.separated( itemCount: solWalletIds.length, shrinkWrap: true, - separatorBuilder: (_, __) => SizedBox( - height: isDesktop ? 12 : 6, - ), + separatorBuilder: (_, __) => + SizedBox(height: isDesktop ? 12 : 6), itemBuilder: (_, index) { return RoundedContainer( padding: EdgeInsets.all(isDesktop ? 16 : 8), @@ -205,34 +193,26 @@ class _SelectWalletForSolTokenViewState }); }, color: isDesktop - ? Theme.of(context) - .extension()! - .popupBG + ? Theme.of( + context, + ).extension()!.popupBG : _selectedWalletId == solWalletIds[index] - ? Theme.of(context) - .extension()! - .highlight - : Colors.transparent, + ? Theme.of( + context, + ).extension()!.highlight + : Colors.transparent, child: isDesktop ? EthWalletRadio( walletId: solWalletIds[index], selectedWalletId: _selectedWalletId, ) - : WalletInfoRow( - walletId: solWalletIds[index], - ), + : WalletInfoRow(walletId: solWalletIds[index]), ); }, ), ), - if (solWalletIds.isEmpty || isDesktop) - const SizedBox( - height: 16, - ), - if (isDesktop) - const SizedBox( - height: 16, - ), + if (solWalletIds.isEmpty || isDesktop) const SizedBox(height: 16), + if (isDesktop) const SizedBox(height: 16), solWalletIds.isEmpty ? PrimaryButton( label: "Add new Solana wallet", diff --git a/lib/pages/add_wallet_views/select_wallet_for_token_view.dart b/lib/pages/add_wallet_views/select_wallet_for_token_view.dart index debe8731c8..71a4236299 100644 --- a/lib/pages/add_wallet_views/select_wallet_for_token_view.dart +++ b/lib/pages/add_wallet_views/select_wallet_for_token_view.dart @@ -32,14 +32,12 @@ import '../../widgets/rounded_white_container.dart'; import '../../widgets/wallet_info_row/wallet_info_row.dart'; import 'package:tuple/tuple.dart'; -final newEthWalletTriggerTempUntilHiveCompletelyDeleted = - StateProvider((ref) => false); +final newEthWalletTriggerTempUntilHiveCompletelyDeleted = StateProvider( + (ref) => false, +); class SelectWalletForTokenView extends ConsumerStatefulWidget { - const SelectWalletForTokenView({ - super.key, - required this.entity, - }); + const SelectWalletForTokenView({super.key, required this.entity}); static const String routeName = "/selectWalletForTokenView"; @@ -59,10 +57,7 @@ class _SelectWalletForTokenViewState void _onContinue() { Navigator.of(context).pushNamed( EditWalletTokensView.routeName, - arguments: Tuple2( - _selectedWalletId!, - [widget.entity.token.address], - ), + arguments: Tuple2(_selectedWalletId!, [widget.entity.token.address]), ); } @@ -92,17 +87,20 @@ class _SelectWalletForTokenViewState } } - return WillPopScope( - onWillPop: () async { - ref.read(createSpecialEthWalletRoutingFlag.notifier).state = false; - return true; + return PopScope( + canPop: true, + onPopInvokedWithResult: (bool didPop, dynamic result) { + if (didPop) { + ref.read(createSpecialEthWalletRoutingFlag.notifier).state = false; + } }, child: ConditionalParent( condition: !isDesktop, builder: (child) => Background( child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, + backgroundColor: Theme.of( + context, + ).extension()!.background, appBar: AppBar( leading: AppBarBackButton( onPressed: () { @@ -111,10 +109,7 @@ class _SelectWalletForTokenViewState ), ), body: SafeArea( - child: Padding( - padding: const EdgeInsets.all(16), - child: child, - ), + child: Padding(padding: const EdgeInsets.all(16), child: child), // child: LayoutBuilder( // builder: (ctx, constraints) { @@ -139,18 +134,12 @@ class _SelectWalletForTokenViewState isCompactHeight: false, leading: AppBarBackButton(), ), - body: SizedBox( - width: 500, - child: child, - ), + body: SizedBox(width: 500, child: child), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (isDesktop) - const SizedBox( - height: 24, - ), + if (isDesktop) const SizedBox(height: 24), Text( "Select Ethereum wallet", textAlign: TextAlign.center, @@ -158,9 +147,7 @@ class _SelectWalletForTokenViewState ? STextStyles.desktopH2(context) : STextStyles.pageTitleH1(context), ), - SizedBox( - height: isDesktop ? 16 : 8, - ), + SizedBox(height: isDesktop ? 16 : 8), Text( "You are adding an ETH token.", textAlign: TextAlign.center, @@ -168,9 +155,7 @@ class _SelectWalletForTokenViewState ? STextStyles.desktopSubtitleH2(context) : STextStyles.subtitle(context), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), Text( "You must choose an Ethereum wallet in order to use ${widget.entity.name}", textAlign: TextAlign.center, @@ -178,9 +163,7 @@ class _SelectWalletForTokenViewState ? STextStyles.desktopSubtitleH2(context) : STextStyles.subtitle(context), ), - SizedBox( - height: isDesktop ? 60 : 16, - ), + SizedBox(height: isDesktop ? 60 : 16), ethWalletIds.isEmpty ? RoundedWhiteContainer( padding: EdgeInsets.all(isDesktop ? 16 : 12), @@ -208,9 +191,8 @@ class _SelectWalletForTokenViewState child: ListView.separated( itemCount: ethWalletIds.length, shrinkWrap: true, - separatorBuilder: (_, __) => SizedBox( - height: isDesktop ? 12 : 6, - ), + separatorBuilder: (_, __) => + SizedBox(height: isDesktop ? 12 : 6), itemBuilder: (_, index) { return RoundedContainer( padding: EdgeInsets.all(isDesktop ? 16 : 8), @@ -220,34 +202,26 @@ class _SelectWalletForTokenViewState }); }, color: isDesktop - ? Theme.of(context) - .extension()! - .popupBG + ? Theme.of( + context, + ).extension()!.popupBG : _selectedWalletId == ethWalletIds[index] - ? Theme.of(context) - .extension()! - .highlight - : Colors.transparent, + ? Theme.of( + context, + ).extension()!.highlight + : Colors.transparent, child: isDesktop ? EthWalletRadio( walletId: ethWalletIds[index], selectedWalletId: _selectedWalletId, ) - : WalletInfoRow( - walletId: ethWalletIds[index], - ), + : WalletInfoRow(walletId: ethWalletIds[index]), ); }, ), ), - if (ethWalletIds.isEmpty || isDesktop) - const SizedBox( - height: 16, - ), - if (isDesktop) - const SizedBox( - height: 16, - ), + if (ethWalletIds.isEmpty || isDesktop) const SizedBox(height: 16), + if (isDesktop) const SizedBox(height: 16), ethWalletIds.isEmpty ? PrimaryButton( label: "Add new Ethereum wallet", diff --git a/lib/pages/buy_view/buy_form.dart b/lib/pages/buy_view/buy_form.dart index 93b64400d4..a03b4324dc 100644 --- a/lib/pages/buy_view/buy_form.dart +++ b/lib/pages/buy_view/buy_form.dart @@ -159,21 +159,19 @@ class _BuyFormState extends ConsumerState { void selectCrypto() async { if (ref.read(simplexProvider).supportedCryptos.isEmpty) { - bool shouldPop = false; unawaited( showDialog( context: context, - builder: (context) => WillPopScope( - child: const CustomLoadingOverlay( + builder: (context) => const PopScope( + canPop: false, + child: CustomLoadingOverlay( message: "Loading currency data", eventBus: null, ), - onWillPop: () async => shouldPop, ), ), ); await _loadSimplexCryptos(); - shouldPop = true; if (mounted) { Navigator.of(context, rootNavigator: isDesktop).pop(); } @@ -265,21 +263,19 @@ class _BuyFormState extends ConsumerState { Future selectFiat() async { if (ref.read(simplexProvider).supportedFiats.isEmpty) { - bool shouldPop = false; unawaited( showDialog( context: context, - builder: (context) => WillPopScope( - child: const CustomLoadingOverlay( + builder: (context) => const PopScope( + canPop: false, + child: CustomLoadingOverlay( message: "Loading currency data", eventBus: null, ), - onWillPop: () async => shouldPop, ), ), ); await _loadSimplexFiats(); - shouldPop = true; if (mounted) { Navigator.of(context, rootNavigator: isDesktop).pop(); } @@ -396,16 +392,15 @@ class _BuyFormState extends ConsumerState { // } Future previewQuote(SimplexQuote quote) async { - bool shouldPop = false; unawaited( showDialog( context: context, - builder: (context) => WillPopScope( - child: const CustomLoadingOverlay( + builder: (context) => const PopScope( + canPop: false, + child: CustomLoadingOverlay( message: "Loading quote data", eventBus: null, ), - onWillPop: () async => shouldPop, ), ), ); @@ -425,7 +420,6 @@ class _BuyFormState extends ConsumerState { ); final BuyResponse quoteResponse = await _loadQuote(quote); - shouldPop = true; if (mounted) { Navigator.of(context, rootNavigator: isDesktop).pop(); } diff --git a/lib/pages/cashfusion/fusion_rounds_selection_sheet.dart b/lib/pages/cashfusion/fusion_rounds_selection_sheet.dart index 72cd027dd3..f7256c8e05 100644 --- a/lib/pages/cashfusion/fusion_rounds_selection_sheet.dart +++ b/lib/pages/cashfusion/fusion_rounds_selection_sheet.dart @@ -16,16 +16,10 @@ import '../../utilities/constants.dart'; import '../../utilities/extensions/extensions.dart'; import '../../utilities/text_styles.dart'; -enum FusionOption { - continuous, - custom; -} +enum FusionOption { continuous, custom } class FusionRoundCountSelectSheet extends HookWidget { - const FusionRoundCountSelectSheet({ - super.key, - required this.currentOption, - }); + const FusionRoundCountSelectSheet({super.key, required this.currentOption}); final FusionOption currentOption; @@ -33,17 +27,18 @@ class FusionRoundCountSelectSheet extends HookWidget { Widget build(BuildContext context) { final option = useState(currentOption); - return WillPopScope( - onWillPop: () async { + return PopScope( + canPop: false, + onPopInvokedWithResult: (bool didPop, FusionOption? result) { + if (didPop) { + return; + } Navigator.of(context).pop(option.value); - return false; }, child: Container( decoration: BoxDecoration( color: Theme.of(context).extension()!.popupBG, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(20), - ), + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), child: Padding( padding: const EdgeInsets.only( @@ -59,9 +54,9 @@ class FusionRoundCountSelectSheet extends HookWidget { Center( child: Container( decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: Theme.of( + context, + ).extension()!.textFieldDefaultBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), @@ -70,9 +65,7 @@ class FusionRoundCountSelectSheet extends HookWidget { height: 4, ), ), - const SizedBox( - height: 36, - ), + const SizedBox(height: 36), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -81,9 +74,7 @@ class FusionRoundCountSelectSheet extends HookWidget { style: STextStyles.pageTitleH2(context), textAlign: TextAlign.left, ), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), for (int i = 0; i < FusionOption.values.length; i++) Column( children: [ @@ -117,9 +108,7 @@ class FusionRoundCountSelectSheet extends HookWidget { ), // ], // ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -128,9 +117,7 @@ class FusionRoundCountSelectSheet extends HookWidget { style: STextStyles.titleBold12(context), textAlign: TextAlign.left, ), - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), Text( FusionOption.values[i] == FusionOption.continuous @@ -138,10 +125,10 @@ class FusionRoundCountSelectSheet extends HookWidget { : "Stop after a set number of fusions", style: STextStyles.itemSubtitle12(context) .copyWith( - color: Theme.of(context) - .extension()! - .textDark3, - ), + color: Theme.of(context) + .extension()! + .textDark3, + ), textAlign: TextAlign.left, ), ], @@ -150,14 +137,10 @@ class FusionRoundCountSelectSheet extends HookWidget { ), ), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), ], ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), ], ), ], diff --git a/lib/pages/churning/churning_rounds_selection_sheet.dart b/lib/pages/churning/churning_rounds_selection_sheet.dart index de21964e87..f030dd52a1 100644 --- a/lib/pages/churning/churning_rounds_selection_sheet.dart +++ b/lib/pages/churning/churning_rounds_selection_sheet.dart @@ -6,16 +6,10 @@ import '../../utilities/constants.dart'; import '../../utilities/extensions/extensions.dart'; import '../../utilities/text_styles.dart'; -enum ChurnOption { - continuous, - custom; -} +enum ChurnOption { continuous, custom } class ChurnRoundCountSelectSheet extends HookWidget { - const ChurnRoundCountSelectSheet({ - super.key, - required this.currentOption, - }); + const ChurnRoundCountSelectSheet({super.key, required this.currentOption}); final ChurnOption currentOption; @@ -23,17 +17,18 @@ class ChurnRoundCountSelectSheet extends HookWidget { Widget build(BuildContext context) { final option = useState(currentOption); - return WillPopScope( - onWillPop: () async { + return PopScope( + canPop: false, + onPopInvokedWithResult: (bool didPop, ChurnOption? result) { + if (didPop) { + return; + } Navigator.of(context).pop(option.value); - return false; }, child: Container( decoration: BoxDecoration( color: Theme.of(context).extension()!.popupBG, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(20), - ), + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), child: Padding( padding: const EdgeInsets.only( @@ -49,9 +44,9 @@ class ChurnRoundCountSelectSheet extends HookWidget { Center( child: Container( decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: Theme.of( + context, + ).extension()!.textFieldDefaultBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), @@ -60,9 +55,7 @@ class ChurnRoundCountSelectSheet extends HookWidget { height: 4, ), ), - const SizedBox( - height: 36, - ), + const SizedBox(height: 36), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -71,9 +64,7 @@ class ChurnRoundCountSelectSheet extends HookWidget { style: STextStyles.pageTitleH2(context), textAlign: TextAlign.left, ), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), for (int i = 0; i < ChurnOption.values.length; i++) Column( children: [ @@ -107,9 +98,7 @@ class ChurnRoundCountSelectSheet extends HookWidget { ), // ], // ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -118,9 +107,7 @@ class ChurnRoundCountSelectSheet extends HookWidget { style: STextStyles.titleBold12(context), textAlign: TextAlign.left, ), - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), Text( ChurnOption.values[i] == ChurnOption.continuous @@ -128,10 +115,10 @@ class ChurnRoundCountSelectSheet extends HookWidget { : "Stop after a set number of churns", style: STextStyles.itemSubtitle12(context) .copyWith( - color: Theme.of(context) - .extension()! - .textDark3, - ), + color: Theme.of(context) + .extension()! + .textDark3, + ), textAlign: TextAlign.left, ), ], @@ -140,14 +127,10 @@ class ChurnRoundCountSelectSheet extends HookWidget { ), ), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), ], ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), ], ), ], diff --git a/lib/pages/coin_control/coin_control_view.dart b/lib/pages/coin_control/coin_control_view.dart index 7960733643..7f83d487dd 100644 --- a/lib/pages/coin_control/coin_control_view.dart +++ b/lib/pages/coin_control/coin_control_view.dart @@ -123,12 +123,11 @@ class _CoinControlViewState extends ConsumerState { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - final minConfirms = - ref - .watch(pWallets) - .getWallet(widget.walletId) - .cryptoCurrency - .minConfirms; + final minConfirms = ref + .watch(pWallets) + .getWallet(widget.walletId) + .cryptoCurrency + .minConfirms; final coin = ref.watch(pWalletCoin(widget.walletId)); final currentHeight = ref.watch(pWalletChainHeight(widget.walletId)); @@ -146,137 +145,131 @@ class _CoinControlViewState extends ConsumerState { _map = null; _list = MainDB.instance.queryUTXOsSync( walletId: widget.walletId, - filter: - _isSearching - ? CCFilter.all - : _showBlocked - ? CCFilter.frozen - : CCFilter.available, + filter: _isSearching + ? CCFilter.all + : _showBlocked + ? CCFilter.frozen + : CCFilter.available, sort: _sort, searchTerm: _isSearching ? searchController.text : "", cryptoCurrency: coin, ); } - return WillPopScope( - onWillPop: () async { + return PopScope>( + canPop: false, + onPopInvokedWithResult: (bool didPop, Set? result) { + if (didPop) { + return; + } unawaited(_refreshBalance()); Navigator.of(context).pop( widget.type == CoinControlViewType.use ? _selectedAvailable : null, ); - return false; }, child: Background( child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, + backgroundColor: Theme.of( + context, + ).extension()!.background, appBar: AppBar( automaticallyImplyLeading: false, - leading: - _isSearching - ? null - : widget.type == CoinControlViewType.use && - _selectedAvailable.isNotEmpty - ? AppBarIconButton( - icon: XIcon( - width: 24, - height: 24, - color: - Theme.of( - context, - ).extension()!.topNavIconPrimary, - ), - onPressed: () { - setState(() { - _selectedAvailable.clear(); - }); - }, - ) - : AppBarBackButton( - onPressed: () { - unawaited(_refreshBalance()); - Navigator.of(context).pop( - widget.type == CoinControlViewType.use - ? _selectedAvailable - : null, - ); - }, - ), - title: - _isSearching - ? AppBarSearchField( - controller: searchController, - focusNode: searchFocus, - ) - : Text( - "Coin control", - style: STextStyles.navBarTitle(context), + leading: _isSearching + ? null + : widget.type == CoinControlViewType.use && + _selectedAvailable.isNotEmpty + ? AppBarIconButton( + icon: XIcon( + width: 24, + height: 24, + color: Theme.of( + context, + ).extension()!.topNavIconPrimary, ), + onPressed: () { + setState(() { + _selectedAvailable.clear(); + }); + }, + ) + : AppBarBackButton( + onPressed: () { + unawaited(_refreshBalance()); + Navigator.of(context).pop( + widget.type == CoinControlViewType.use + ? _selectedAvailable + : null, + ); + }, + ), + title: _isSearching + ? AppBarSearchField( + controller: searchController, + focusNode: searchFocus, + ) + : Text("Coin control", style: STextStyles.navBarTitle(context)), titleSpacing: 0, - actions: - _isSearching - ? [ - AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - size: 36, - icon: SvgPicture.asset( - Assets.svg.x, - width: 20, - height: 20, - color: - Theme.of( - context, - ).extension()!.topNavIconPrimary, - ), - onPressed: () { - // show search - setState(() { - _isSearching = false; - }); - }, + actions: _isSearching + ? [ + AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + size: 36, + icon: SvgPicture.asset( + Assets.svg.x, + width: 20, + height: 20, + color: Theme.of( + context, + ).extension()!.topNavIconPrimary, ), + onPressed: () { + // show search + setState(() { + _isSearching = false; + }); + }, ), - ] - : [ - AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - size: 36, - icon: SvgPicture.asset( - Assets.svg.search, - width: 20, - height: 20, - color: - Theme.of( - context, - ).extension()!.topNavIconPrimary, - ), - onPressed: () { - // show search - setState(() { - _isSearching = true; - }); - }, + ), + ] + : [ + AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + size: 36, + icon: SvgPicture.asset( + Assets.svg.search, + width: 20, + height: 20, + color: Theme.of( + context, + ).extension()!.topNavIconPrimary, ), + onPressed: () { + // show search + setState(() { + _isSearching = true; + }); + }, ), - AspectRatio( - aspectRatio: 1, - child: JDropdownIconButton( - mobileAppBar: true, - groupValue: _sort, - items: CCSortDescriptor.values.toSet(), - onSelectionChanged: (CCSortDescriptor? newValue) { - if (newValue != null && newValue != _sort) { - setState(() { - _sort = newValue; - }); - } - }, - displayPrefix: "Sort by", - ), + ), + AspectRatio( + aspectRatio: 1, + child: JDropdownIconButton( + mobileAppBar: true, + groupValue: _sort, + items: CCSortDescriptor.values.toSet(), + onSelectionChanged: (CCSortDescriptor? newValue) { + if (newValue != null && newValue != _sort) { + setState(() { + _sort = newValue; + }); + } + }, + displayPrefix: "Sort by", ), - ], + ), + ], ), body: SafeArea( child: Column( @@ -294,10 +287,9 @@ class _CoinControlViewState extends ConsumerState { "outputs at your discretion. Tap the output circle to " "select.", style: STextStyles.w500_14(context).copyWith( - color: - Theme.of( - context, - ).extension()!.textSubtitle1, + color: Theme.of( + context, + ).extension()!.textSubtitle1, ), ), ), @@ -307,15 +299,13 @@ class _CoinControlViewState extends ConsumerState { height: 48, child: Toggle( key: UniqueKey(), - onColor: - Theme.of( - context, - ).extension()!.popupBG, + onColor: Theme.of( + context, + ).extension()!.popupBG, onText: "Available outputs", - offColor: - Theme.of(context) - .extension()! - .textFieldDefaultBG, + offColor: Theme.of( + context, + ).extension()!.textFieldDefaultBG, offText: "Frozen outputs", isOn: _showBlocked, onValueChanged: (value) { @@ -336,14 +326,13 @@ class _CoinControlViewState extends ConsumerState { Expanded( child: ListView.separated( itemCount: _list!.length, - separatorBuilder: - (context, _) => const SizedBox(height: 10), + separatorBuilder: (context, _) => + const SizedBox(height: 10), itemBuilder: (context, index) { - final utxo = - MainDB.instance.isar.utxos - .where() - .idEqualTo(_list![index]) - .findFirstSync()!; + final utxo = MainDB.instance.isar.utxos + .where() + .idEqualTo(_list![index]) + .findFirstSync()!; final isSelected = _selectedBlocked.contains(utxo) || @@ -385,15 +374,14 @@ class _CoinControlViewState extends ConsumerState { setState(() {}); }, onPressed: () async { - final result = await Navigator.of( - context, - ).pushNamed( - UtxoDetailsView.routeName, - arguments: Tuple2( - utxo.id, - widget.walletId, - ), - ); + final result = await Navigator.of(context) + .pushNamed( + UtxoDetailsView.routeName, + arguments: Tuple2( + utxo.id, + widget.walletId, + ), + ); if (mounted && result == "refresh") { setState(() {}); } @@ -405,244 +393,236 @@ class _CoinControlViewState extends ConsumerState { if (!_isSearching) _list != null ? Expanded( - child: ListView.separated( - itemCount: _list!.length, - separatorBuilder: - (context, _) => - const SizedBox(height: 10), - itemBuilder: (context, index) { - final utxo = - MainDB.instance.isar.utxos - .where() - .idEqualTo(_list![index]) - .findFirstSync()!; + child: ListView.separated( + itemCount: _list!.length, + separatorBuilder: (context, _) => + const SizedBox(height: 10), + itemBuilder: (context, index) { + final utxo = MainDB.instance.isar.utxos + .where() + .idEqualTo(_list![index]) + .findFirstSync()!; - final isSelected = - _showBlocked - ? _selectedBlocked.contains(utxo) - : _selectedAvailable.contains(utxo); + final isSelected = _showBlocked + ? _selectedBlocked.contains(utxo) + : _selectedAvailable.contains(utxo); - return UtxoCard( - key: Key( - "${utxo.walletId}_${utxo.id}_$isSelected", - ), - walletId: widget.walletId, - utxo: utxo, - canSelect: - widget.type == - CoinControlViewType.manage || - (widget.type == - CoinControlViewType.use && - !_showBlocked && - _isConfirmed( - utxo, - currentHeight, - ref.watch( - pWallets.select( - (s) => s.getWallet( - widget.walletId, + return UtxoCard( + key: Key( + "${utxo.walletId}_${utxo.id}_$isSelected", + ), + walletId: widget.walletId, + utxo: utxo, + canSelect: + widget.type == + CoinControlViewType.manage || + (widget.type == + CoinControlViewType.use && + !_showBlocked && + _isConfirmed( + utxo, + currentHeight, + ref.watch( + pWallets.select( + (s) => s.getWallet( + widget.walletId, + ), ), ), - ), - )), - initialSelectedState: isSelected, - onSelectedChanged: (value) { - if (value) { - _showBlocked - ? _selectedBlocked.add(utxo) - : _selectedAvailable.add(utxo); - } else { - _showBlocked - ? _selectedBlocked.remove(utxo) - : _selectedAvailable.remove(utxo); - } - setState(() {}); - }, - onPressed: () async { - final result = await Navigator.of( - context, - ).pushNamed( - UtxoDetailsView.routeName, - arguments: Tuple2( - utxo.id, - widget.walletId, - ), - ); - if (mounted && result == "refresh") { + )), + initialSelectedState: isSelected, + onSelectedChanged: (value) { + if (value) { + _showBlocked + ? _selectedBlocked.add(utxo) + : _selectedAvailable.add(utxo); + } else { + _showBlocked + ? _selectedBlocked.remove(utxo) + : _selectedAvailable.remove( + utxo, + ); + } setState(() {}); - } - }, - ); - }, - ), - ) + }, + onPressed: () async { + final result = + await Navigator.of( + context, + ).pushNamed( + UtxoDetailsView.routeName, + arguments: Tuple2( + utxo.id, + widget.walletId, + ), + ); + if (mounted && result == "refresh") { + setState(() {}); + } + }, + ); + }, + ), + ) : Expanded( - child: ListView.separated( - itemCount: _map!.entries.length, - separatorBuilder: - (context, _) => - const SizedBox(height: 10), - itemBuilder: (context, index) { - final entry = _map!.entries.elementAt( - index, - ); - final _controller = RotateIconController(); + child: ListView.separated( + itemCount: _map!.entries.length, + separatorBuilder: (context, _) => + const SizedBox(height: 10), + itemBuilder: (context, index) { + final entry = _map!.entries.elementAt( + index, + ); + final _controller = + RotateIconController(); - return Expandable2( - border: - Theme.of(context) - .extension()! - .backgroundAppBar, - background: - Theme.of( - context, - ).extension()!.popupBG, - animationDurationMultiplier: - 0.2 * entry.value.length, - onExpandWillChange: (state) { - if (state == - Expandable2State.expanded) { - _controller.forward?.call(); - } else { - _controller.reverse?.call(); - } - }, - header: RoundedContainer( - padding: const EdgeInsets.all(14), - color: Colors.transparent, - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - entry.key, - style: STextStyles.w600_14( - context, + return Expandable2( + border: Theme.of(context) + .extension()! + .backgroundAppBar, + background: Theme.of( + context, + ).extension()!.popupBG, + animationDurationMultiplier: + 0.2 * entry.value.length, + onExpandWillChange: (state) { + if (state == + Expandable2State.expanded) { + _controller.forward?.call(); + } else { + _controller.reverse?.call(); + } + }, + header: RoundedContainer( + padding: const EdgeInsets.all(14), + color: Colors.transparent, + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + entry.key, + style: + STextStyles.w600_14( + context, + ), ), - ), - const SizedBox(height: 2), - Text( - "${entry.value.length} " - "output${entry.value.length > 1 ? "s" : ""}", - style: STextStyles.w500_12( - context, - ).copyWith( - color: - Theme.of(context) - .extension< - StackColors - >()! - .textSubtitle1, + const SizedBox(height: 2), + Text( + "${entry.value.length} " + "output${entry.value.length > 1 ? "s" : ""}", + style: + STextStyles.w500_12( + context, + ).copyWith( + color: Theme.of(context) + .extension< + StackColors + >()! + .textSubtitle1, + ), ), - ), - ], + ], + ), ), - ), - RotateIcon( - animationDurationMultiplier: - 0.2 * entry.value.length, - icon: SvgPicture.asset( - Assets.svg.chevronDown, - width: 14, - color: - Theme.of(context) - .extension< - StackColors - >()! - .textSubtitle1, + RotateIcon( + animationDurationMultiplier: + 0.2 * entry.value.length, + icon: SvgPicture.asset( + Assets.svg.chevronDown, + width: 14, + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + curve: Curves.easeInOut, + controller: _controller, ), - curve: Curves.easeInOut, - controller: _controller, - ), - ], + ], + ), ), - ), - children: - entry.value.map((id) { - final utxo = - MainDB.instance.isar.utxos - .where() - .idEqualTo(id) - .findFirstSync()!; + children: entry.value.map((id) { + final utxo = MainDB + .instance + .isar + .utxos + .where() + .idEqualTo(id) + .findFirstSync()!; - final isSelected = - _selectedBlocked.contains( - utxo, - ) || - _selectedAvailable.contains( - utxo, - ); + final isSelected = + _selectedBlocked.contains(utxo) || + _selectedAvailable.contains(utxo); - return UtxoCard( - key: Key( - "${utxo.walletId}_${utxo.id}_$isSelected", - ), - walletId: widget.walletId, - utxo: utxo, - canSelect: - widget.type == - CoinControlViewType - .manage || - (widget.type == - CoinControlViewType - .use && - !utxo.isBlocked && - _isConfirmed( - utxo, - currentHeight, - ref.watch( - pWallets.select( - (s) => s.getWallet( - widget.walletId, - ), + return UtxoCard( + key: Key( + "${utxo.walletId}_${utxo.id}_$isSelected", + ), + walletId: widget.walletId, + utxo: utxo, + canSelect: + widget.type == + CoinControlViewType + .manage || + (widget.type == + CoinControlViewType + .use && + !utxo.isBlocked && + _isConfirmed( + utxo, + currentHeight, + ref.watch( + pWallets.select( + (s) => s.getWallet( + widget.walletId, ), ), - )), - initialSelectedState: isSelected, - onSelectedChanged: (value) { - if (value) { - utxo.isBlocked - ? _selectedBlocked.add( - utxo, - ) - : _selectedAvailable.add( + ), + )), + initialSelectedState: isSelected, + onSelectedChanged: (value) { + if (value) { + utxo.isBlocked + ? _selectedBlocked.add(utxo) + : _selectedAvailable.add( utxo, ); - } else { - utxo.isBlocked - ? _selectedBlocked.remove( + } else { + utxo.isBlocked + ? _selectedBlocked.remove( utxo, ) - : _selectedAvailable - .remove(utxo); - } + : _selectedAvailable.remove( + utxo, + ); + } + setState(() {}); + }, + onPressed: () async { + final result = + await Navigator.of( + context, + ).pushNamed( + UtxoDetailsView.routeName, + arguments: Tuple2( + utxo.id, + widget.walletId, + ), + ); + if (mounted && + result == "refresh") { setState(() {}); - }, - onPressed: () async { - final result = - await Navigator.of( - context, - ).pushNamed( - UtxoDetailsView.routeName, - arguments: Tuple2( - utxo.id, - widget.walletId, - ), - ); - if (mounted && - result == "refresh") { - setState(() {}); - } - }, - ); - }).toList(), - ); - }, + } + }, + ); + }).toList(), + ); + }, + ), ), - ), ], ), ), @@ -652,10 +632,9 @@ class _CoinControlViewState extends ConsumerState { widget.type == CoinControlViewType.manage) Container( decoration: BoxDecoration( - color: - Theme.of( - context, - ).extension()!.backgroundAppBar, + color: Theme.of( + context, + ).extension()!.backgroundAppBar, boxShadow: [ Theme.of( context, @@ -690,10 +669,9 @@ class _CoinControlViewState extends ConsumerState { if (!_showBlocked && widget.type == CoinControlViewType.use) Container( decoration: BoxDecoration( - color: - Theme.of( - context, - ).extension()!.backgroundAppBar, + color: Theme.of( + context, + ).extension()!.backgroundAppBar, boxShadow: [ Theme.of( context, @@ -722,13 +700,13 @@ class _CoinControlViewState extends ConsumerState { builder: (context) { final int selectedSumInt = _selectedAvailable.isEmpty - ? 0 - : _selectedAvailable - .map((e) => e.value) - .reduce( - (value, element) => - value += element, - ); + ? 0 + : _selectedAvailable + .map((e) => e.value) + .reduce( + (value, element) => + value += element, + ); final selectedSum = selectedSumInt .toAmountAsRaw( fractionDigits: @@ -738,33 +716,26 @@ class _CoinControlViewState extends ConsumerState { ref .watch(pAmountFormatter(coin)) .format(selectedSum), - style: - widget.requestedTotal == null - ? STextStyles.w600_14( - context, - ) - : STextStyles.w600_14( - context, - ).copyWith( - color: - selectedSum >= - widget - .requestedTotal! - ? Theme.of( - context, - ) - .extension< - StackColors - >()! - .accentColorGreen - : Theme.of( - context, - ) - .extension< - StackColors - >()! - .accentColorRed, - ), + style: widget.requestedTotal == null + ? STextStyles.w600_14(context) + : STextStyles.w600_14( + context, + ).copyWith( + color: + selectedSum >= + widget + .requestedTotal! + ? Theme.of(context) + .extension< + StackColors + >()! + .accentColorGreen + : Theme.of(context) + .extension< + StackColors + >()! + .accentColorRed, + ), ); }, ), @@ -775,10 +746,9 @@ class _CoinControlViewState extends ConsumerState { Container( width: double.infinity, height: 1.5, - color: - Theme.of(context) - .extension()! - .backgroundAppBar, + color: Theme.of(context) + .extension()! + .backgroundAppBar, ), if (widget.requestedTotal != null) Padding( diff --git a/lib/pages/exchange_view/exchange_coin_selection/exchange_currency_selection_view.dart b/lib/pages/exchange_view/exchange_coin_selection/exchange_currency_selection_view.dart index 4a539ec9d4..97cce4ffcd 100644 --- a/lib/pages/exchange_view/exchange_coin_selection/exchange_currency_selection_view.dart +++ b/lib/pages/exchange_view/exchange_coin_selection/exchange_currency_selection_view.dart @@ -73,19 +73,18 @@ class _ExchangeCurrencySelectionViewState showDialog( context: context, barrierDismissible: false, - builder: - (_) => WillPopScope( - onWillPop: () async => false, - child: Container( - color: Theme.of( - context, - ).extension()!.overlay.withOpacity(0.6), - child: const CustomLoadingOverlay( - message: "Loading currencies", - eventBus: null, - ), - ), + builder: (_) => PopScope( + canPop: false, + child: Container( + color: Theme.of( + context, + ).extension()!.overlay.withOpacity(0.6), + child: const CustomLoadingOverlay( + message: "Loading currencies", + eventBus: null, ), + ), + ), ), ); @@ -101,36 +100,33 @@ class _ExchangeCurrencySelectionViewState Future> _loadCurrencies() async { await ExchangeDataLoadingService.instance.initDB(); final isar = await ExchangeDataLoadingService.instance.isar; - final currencies = - await isar.currencies - .where() - .filter() - .isFiatEqualTo(false) - .and() - .group( - (q) => - widget.isFixedRate - ? q - .rateTypeEqualTo(SupportedRateType.both) - .or() - .rateTypeEqualTo(SupportedRateType.fixed) - : q - .rateTypeEqualTo(SupportedRateType.both) - .or() - .rateTypeEqualTo(SupportedRateType.estimated), - ) - .sortByIsStackCoin() - .thenByName() - .findAll(); + final currencies = await isar.currencies + .where() + .filter() + .isFiatEqualTo(false) + .and() + .group( + (q) => widget.isFixedRate + ? q + .rateTypeEqualTo(SupportedRateType.both) + .or() + .rateTypeEqualTo(SupportedRateType.fixed) + : q + .rateTypeEqualTo(SupportedRateType.both) + .or() + .rateTypeEqualTo(SupportedRateType.estimated), + ) + .sortByIsStackCoin() + .thenByName() + .findAll(); // If using Tor, filter exchanges which do not support Tor. if (Prefs.instance.useTor) { if (Exchange.exchangeNamesWithTorSupport.isNotEmpty) { currencies.removeWhere( - (element) => - !Exchange.exchangeNamesWithTorSupport.contains( - element.exchangeName, - ), + (element) => !Exchange.exchangeNamesWithTorSupport.contains( + element.exchangeName, + ), ); } } @@ -163,20 +159,18 @@ class _ExchangeCurrencySelectionViewState results.remove(widget.pairedCurrency); } - final walletCoins = - results - .where( - (currency) => - AppConfig.coins - .where( - (coin) => - coin.ticker.toLowerCase() == - currency.ticker.toLowerCase() && - currency.fuzzyNet == coin.ticker.toLowerCase(), - ) - .isNotEmpty, - ) - .toList(); + final walletCoins = results + .where( + (currency) => AppConfig.coins + .where( + (coin) => + coin.ticker.toLowerCase() == + currency.ticker.toLowerCase() && + currency.fuzzyNet == coin.ticker.toLowerCase(), + ) + .isNotEmpty, + ) + .toList(); final list = results.toList(); @@ -240,8 +234,9 @@ class _ExchangeCurrencySelectionViewState builder: (child) { return Background( child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, + backgroundColor: Theme.of( + context, + ).extension()!.background, appBar: AppBar( leading: AppBarBackButton( onPressed: () async { @@ -286,45 +281,45 @@ class _ExchangeCurrencySelectionViewState focusNode: _searchFocusNode, onChanged: (value) => setState(() => _searchString = value), style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - _searchFocusNode, - context, - desktopMed: isDesktop, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, - ), - ), - suffixIcon: - _searchController.text.isNotEmpty + decoration: + standardInputDecoration( + "Search", + _searchFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), + ), + suffixIcon: _searchController.text.isNotEmpty ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchController.text = ""; - _searchString = ""; - }); - }, - ), - ], + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + _searchString = ""; + }); + }, + ), + ], + ), ), - ), - ) + ) : null, - ), + ), ), ), const SizedBox(height: 20), @@ -358,23 +353,20 @@ class _ExchangeCurrencySelectionViewState height: 24, child: AppConfig.isStackCoin( - items[index].ticker, - ) - ? CoinIconForTicker( - ticker: items[index].ticker, - size: 24, - ) - : hasImageUrl - ? _NetImage( - url: image, - key: ValueKey( - image + items[index].fuzzyNet, - ), - ) - : const SizedBox( - width: 24, - height: 24, + items[index].ticker, + ) + ? CoinIconForTicker( + ticker: items[index].ticker, + size: 24, + ) + : hasImageUrl + ? _NetImage( + url: image, + key: ValueKey( + image + items[index].fuzzyNet, ), + ) + : const SizedBox(width: 24, height: 24), ), const SizedBox(width: 10), Expanded( @@ -399,9 +391,8 @@ class _ExchangeCurrencySelectionViewState left: 12, ), child: CoinTickerTag( - ticker: - items[index].fuzzyNet - .toUpperCase(), + ticker: items[index].fuzzyNet + .toUpperCase(), ), ), ], @@ -409,14 +400,12 @@ class _ExchangeCurrencySelectionViewState const SizedBox(height: 2), Text( items[index].ticker.toUpperCase(), - style: STextStyles.smallMed12( - context, - ).copyWith( - color: - Theme.of(context) + style: STextStyles.smallMed12(context) + .copyWith( + color: Theme.of(context) .extension()! .textSubtitle1, - ), + ), ), ], ), diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart index c328e5e4f7..da2fb2a350 100644 --- a/lib/pages/exchange_view/exchange_form.dart +++ b/lib/pages/exchange_view/exchange_form.dart @@ -112,8 +112,8 @@ class _ExchangeFormState extends ConsumerState { showDialog( context: context, barrierDismissible: false, - builder: (_) => WillPopScope( - onWillPop: () async => false, + builder: (_) => PopScope( + canPop: false, child: Container( color: Theme.of( context, diff --git a/lib/pages/exchange_view/exchange_step_views/step_3_view.dart b/lib/pages/exchange_view/exchange_step_views/step_3_view.dart index 4f0b352c3c..32403e6191 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_3_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_3_view.dart @@ -205,14 +205,12 @@ class _Step3ViewState extends ConsumerState { ), child: Text( "Back", - style: STextStyles.button( - context, - ).copyWith( - color: - Theme.of(context) + style: STextStyles.button(context) + .copyWith( + color: Theme.of(context) .extension()! .buttonTextSecondary, - ), + ), ), ), ), @@ -224,22 +222,19 @@ class _Step3ViewState extends ConsumerState { showDialog( context: context, barrierDismissible: false, - builder: - (_) => WillPopScope( - onWillPop: () async => false, - child: Container( - color: Theme.of(context) - .extension()! - .overlay - .withOpacity(0.6), - child: - const CustomLoadingOverlay( - message: - "Creating a trade", - eventBus: null, - ), - ), + builder: (_) => PopScope( + canPop: false, + child: Container( + color: Theme.of(context) + .extension()! + .overlay + .withOpacity(0.6), + child: const CustomLoadingOverlay( + message: "Creating a trade", + eventBus: null, ), + ), + ), ), ); @@ -256,16 +251,14 @@ class _Step3ViewState extends ConsumerState { fixedRate: model.rateType != ExchangeRateType.estimated, - amount: - model.reversed - ? model.receiveAmount - : model.sendAmount, + amount: model.reversed + ? model.receiveAmount + : model.sendAmount, addressTo: model.recipientAddress!, extraId: null, - addressRefund: - supportsRefund - ? model.refundAddress! - : "", + addressRefund: supportsRefund + ? model.refundAddress! + : "", refundExtraId: "", estimate: model.estimate, reversed: model.reversed, @@ -278,8 +271,8 @@ class _Step3ViewState extends ConsumerState { // TODO: better errors String? message; if (response.exception != null) { - message = - response.exception!.toString(); + message = response.exception! + .toString(); if (message.startsWith( "FormatException:", ) && @@ -293,12 +286,10 @@ class _Step3ViewState extends ConsumerState { showDialog( context: context, barrierDismissible: true, - builder: - (_) => StackDialog( - title: - "Failed to create trade", - message: message ?? "", - ), + builder: (_) => StackDialog( + title: "Failed to create trade", + message: message ?? "", + ), ), ); } diff --git a/lib/pages/monkey/sub_widgets/fetch_monkey_dialog.dart b/lib/pages/monkey/sub_widgets/fetch_monkey_dialog.dart index b3b5d7e6d9..cba21ee001 100644 --- a/lib/pages/monkey/sub_widgets/fetch_monkey_dialog.dart +++ b/lib/pages/monkey/sub_widgets/fetch_monkey_dialog.dart @@ -19,10 +19,7 @@ import '../../../widgets/desktop/secondary_button.dart'; import '../../../widgets/stack_dialog.dart'; class FetchMonkeyDialog extends StatefulWidget { - const FetchMonkeyDialog({ - super.key, - required this.onCancel, - }); + const FetchMonkeyDialog({super.key, required this.onCancel}); final Future Function() onCancel; @@ -53,24 +50,15 @@ class _FetchMonkeyDialogState extends State { } }, ), - const Spacer( - flex: 1, - ), - const RotatingArrows( - width: 40, - height: 40, - ), - const Spacer( - flex: 2, - ), + const Spacer(flex: 1), + const RotatingArrows(width: 40, height: 40), + const Spacer(flex: 2), Text( "Fetching MonKey", style: STextStyles.desktopH2(context), textAlign: TextAlign.center, ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), Text( "We are fetching your MonKey", style: STextStyles.desktopTextMedium(context).copyWith( @@ -78,15 +66,9 @@ class _FetchMonkeyDialogState extends State { ), textAlign: TextAlign.center, ), - const Spacer( - flex: 2, - ), + const Spacer(flex: 2), Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), + padding: const EdgeInsets.only(left: 32, right: 32, bottom: 32), child: SecondaryButton( label: "Cancel", width: 272.5, @@ -102,25 +84,17 @@ class _FetchMonkeyDialogState extends State { ), ); } else { - return WillPopScope( - onWillPop: () async { - return false; - }, + return PopScope( + canPop: false, child: StackDialog( title: "Fetching MonKey", message: "We are fetching your MonKey", - icon: const RotatingArrows( - width: 24, - height: 24, - ), + icon: const RotatingArrows(width: 24, height: 24), rightButton: TextButton( - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle(context), - child: Text( - "Cancel", - style: STextStyles.itemSubtitle12(context), - ), + style: Theme.of( + context, + ).extension()!.getSecondaryEnabledButtonStyle(context), + child: Text("Cancel", style: STextStyles.itemSubtitle12(context)), onPressed: () async { await onCancel.call(); if (mounted) { diff --git a/lib/pages/paynym/dialogs/claiming_paynym_dialog.dart b/lib/pages/paynym/dialogs/claiming_paynym_dialog.dart index d10ad52fd3..29947de1db 100644 --- a/lib/pages/paynym/dialogs/claiming_paynym_dialog.dart +++ b/lib/pages/paynym/dialogs/claiming_paynym_dialog.dart @@ -19,9 +19,7 @@ import '../../../widgets/desktop/secondary_button.dart'; import '../../../widgets/stack_dialog.dart'; class ClaimingPaynymDialog extends StatefulWidget { - const ClaimingPaynymDialog({ - super.key, - }); + const ClaimingPaynymDialog({super.key}); @override State createState() => _RestoringDialogState(); @@ -44,10 +42,7 @@ class _RestoringDialogState extends State { ), ], ), - const RotatingArrows( - width: 40, - height: 40, - ), + const RotatingArrows(width: 40, height: 40), Padding( padding: const EdgeInsets.all(40), child: Column( @@ -57,19 +52,16 @@ class _RestoringDialogState extends State { "Claiming PayNym", style: STextStyles.desktopH2(context), ), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), Text( "We are generating your PayNym", style: STextStyles.desktopTextMedium(context).copyWith( - color: - Theme.of(context).extension()!.textDark3, + color: Theme.of( + context, + ).extension()!.textDark3, ), ), - const SizedBox( - height: 40, - ), + const SizedBox(height: 40), SecondaryButton( label: "Cancel", width: 272, @@ -84,17 +76,12 @@ class _RestoringDialogState extends State { ), ); } else { - return WillPopScope( - onWillPop: () async { - return false; - }, + return PopScope( + canPop: false, child: StackDialog( title: "Claiming PayNym", message: "We are generating your PayNym", - icon: const RotatingArrows( - width: 24, - height: 24, - ), + icon: const RotatingArrows(width: 24, height: 24), rightButton: SecondaryButton( label: "Cancel", onPressed: () { diff --git a/lib/pages/paynym/dialogs/paynym_details_popup.dart b/lib/pages/paynym/dialogs/paynym_details_popup.dart index b3e800a17d..dd3d9e7973 100644 --- a/lib/pages/paynym/dialogs/paynym_details_popup.dart +++ b/lib/pages/paynym/dialogs/paynym_details_popup.dart @@ -63,25 +63,16 @@ class _PaynymDetailsPopupState extends ConsumerState { final wallet = ref.read(pWallets).getWallet(widget.walletId); await Navigator.of(context).pushNamed( SendView.routeName, - arguments: Tuple3( - wallet.walletId, - wallet.info.coin, - widget.accountLite, - ), + arguments: Tuple3(wallet.walletId, wallet.info.coin, widget.accountLite), ); } Future _onConnectPressed() async { - bool canPop = false; unawaited( showDialog( context: context, - builder: (context) => WillPopScope( - onWillPop: () async => canPop, - child: const LoadingIndicator( - width: 200, - ), - ), + builder: (context) => + const PopScope(canPop: false, child: LoadingIndicator(width: 200)), ), ); @@ -90,7 +81,6 @@ class _PaynymDetailsPopupState extends ConsumerState { final coin = ref.read(pWalletCoin(widget.walletId)); if (await wallet.hasConnected(widget.accountLite.code)) { - canPop = true; Navigator.of(context).pop(); // TODO show info popup return; @@ -107,7 +97,6 @@ class _PaynymDetailsPopupState extends ConsumerState { ); } on InsufficientBalanceException catch (_) { if (mounted) { - canPop = true; Navigator.of(context).pop(); } setState(() { @@ -116,16 +105,13 @@ class _PaynymDetailsPopupState extends ConsumerState { return; } catch (e) { if (mounted) { - canPop = true; Navigator.of(context).pop(); } await showDialog( context: context, - builder: (context) => StackOkDialog( - title: "Error", - message: e.toString(), - ), + builder: (context) => + StackOkDialog(title: "Error", message: e.toString()), ); return; } @@ -133,7 +119,6 @@ class _PaynymDetailsPopupState extends ConsumerState { if (mounted) { // We have enough balance and prepared tx should be good to go. - canPop = true; // close loading Navigator.of(context).pop(); @@ -196,9 +181,7 @@ class _PaynymDetailsPopupState extends ConsumerState { paymentCodeString: widget.accountLite.code, size: 36, ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -207,8 +190,9 @@ class _PaynymDetailsPopupState extends ConsumerState { style: STextStyles.w600_14(context), ), FutureBuilder( - future: - wallet.hasConnected(widget.accountLite.code), + future: wallet.hasConnected( + widget.accountLite.code, + ), builder: (context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done && @@ -217,17 +201,15 @@ class _PaynymDetailsPopupState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), Text( "Connected", style: STextStyles.w500_12(context) .copyWith( - color: Theme.of(context) - .extension()! - .accentColorGreen, - ), + color: Theme.of(context) + .extension()! + .accentColorGreen, + ), ), ], ); @@ -253,9 +235,9 @@ class _PaynymDetailsPopupState extends ConsumerState { Assets.svg.circleArrowUpRight, width: 14, height: 14, - color: Theme.of(context) - .extension()! - .buttonTextPrimary, + color: Theme.of( + context, + ).extension()!.buttonTextPrimary, ), iconSpacing: 8, width: 100, @@ -269,9 +251,9 @@ class _PaynymDetailsPopupState extends ConsumerState { Assets.svg.circlePlusFilled, width: 13, height: 13, - color: Theme.of(context) - .extension()! - .buttonTextPrimary, + color: Theme.of( + context, + ).extension()!.buttonTextPrimary, ), iconSpacing: 8, width: 128, @@ -292,13 +274,11 @@ class _PaynymDetailsPopupState extends ConsumerState { Column( mainAxisSize: MainAxisSize.min, children: [ - const SizedBox( - height: 24, - ), + const SizedBox(height: 24), RoundedContainer( - color: Theme.of(context) - .extension()! - .warningBackground, + color: Theme.of( + context, + ).extension()!.warningBackground, child: Text( "Adding a PayNym to your contacts requires a one-time " "transaction fee for creating the record on the " @@ -306,9 +286,9 @@ class _PaynymDetailsPopupState extends ConsumerState { "${ref.watch(pWalletCoin(widget.walletId)).ticker} " "into your wallet and try again.", style: STextStyles.infoSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .warningForeground, + color: Theme.of( + context, + ).extension()!.warningForeground, fontSize: 12, ), ), @@ -340,32 +320,26 @@ class _PaynymDetailsPopupState extends ConsumerState { children: [ Text( "PayNym address", - style: STextStyles.infoSmall(context).copyWith( - fontSize: 12, - ), - ), - const SizedBox( - height: 6, + style: STextStyles.infoSmall( + context, + ).copyWith(fontSize: 12), ), + const SizedBox(height: 6), Text( widget.accountLite.code, style: STextStyles.infoSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark, + color: Theme.of( + context, + ).extension()!.textDark, fontSize: 12, ), ), - const SizedBox( - height: 6, - ), + const SizedBox(height: 6), ], ), ), ), - const SizedBox( - width: 20, - ), + const SizedBox(width: 20), QR( padding: const EdgeInsets.all(0), size: 100, @@ -375,11 +349,7 @@ class _PaynymDetailsPopupState extends ConsumerState { ), ), Padding( - padding: const EdgeInsets.only( - left: 24, - right: 24, - bottom: 24, - ), + padding: const EdgeInsets.only(left: 24, right: 24, bottom: 24), child: Row( children: [ kDisableFollowing @@ -391,9 +361,7 @@ class _PaynymDetailsPopupState extends ConsumerState { style: PaynymFollowToggleButtonStyle.detailsPopup, ), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Expanded( child: SecondaryButton( label: "Copy", @@ -403,15 +371,13 @@ class _PaynymDetailsPopupState extends ConsumerState { Assets.svg.copy, width: 12, height: 12, - color: Theme.of(context) - .extension()! - .buttonTextSecondary, + color: Theme.of( + context, + ).extension()!.buttonTextSecondary, ), onPressed: () async { await Clipboard.setData( - ClipboardData( - text: widget.accountLite.code, - ), + ClipboardData(text: widget.accountLite.code), ); unawaited( showFloatingFlushBar( diff --git a/lib/pages/paynym/subwidgets/desktop_paynym_details.dart b/lib/pages/paynym/subwidgets/desktop_paynym_details.dart index 3185144f28..766ab11722 100644 --- a/lib/pages/paynym/subwidgets/desktop_paynym_details.dart +++ b/lib/pages/paynym/subwidgets/desktop_paynym_details.dart @@ -58,15 +58,11 @@ class _PaynymDetailsPopupState extends ConsumerState { bool _showInsufficientFundsInfo = false; Future _onConnectPressed() async { - bool canPop = false; unawaited( showDialog( context: context, - builder: - (context) => WillPopScope( - onWillPop: () async => canPop, - child: const LoadingIndicator(width: 200), - ), + builder: (context) => + const PopScope(canPop: false, child: LoadingIndicator(width: 200)), ), ); @@ -74,7 +70,6 @@ class _PaynymDetailsPopupState extends ConsumerState { ref.read(pWallets).getWallet(widget.walletId) as PaynymInterface; if (await wallet.hasConnected(widget.accountLite.code)) { - canPop = true; Navigator.of(context, rootNavigator: true).pop(); // TODO show info popup return; @@ -91,7 +86,6 @@ class _PaynymDetailsPopupState extends ConsumerState { ); } on InsufficientBalanceException catch (e) { if (mounted) { - canPop = true; Navigator.of(context, rootNavigator: true).pop(); } setState(() { @@ -103,54 +97,51 @@ class _PaynymDetailsPopupState extends ConsumerState { if (mounted) { // We have enough balance and prepared tx should be good to go. - canPop = true; // close loading Navigator.of(context, rootNavigator: true).pop(); // show info pop up await showDialog( context: context, - builder: - (context) => ConfirmPaynymConnectDialog( - nymName: widget.accountLite.nymName, - locale: ref.read(localeServiceChangeNotifierProvider).locale, - onConfirmPressed: () { - Navigator.of(context, rootNavigator: true).pop(); - unawaited( - showDialog( - context: context, - builder: - (context) => DesktopDialog( - maxHeight: MediaQuery.of(context).size.height - 64, - maxWidth: 580, - child: ConfirmTransactionView( - walletId: widget.walletId, - isPaynymNotificationTransaction: true, - txData: preparedTx, - onSuccess: () { - // do nothing extra - }, - onSuccessInsteadOfRouteOnSuccess: () { - Navigator.of(context, rootNavigator: true).pop(); - Navigator.of(context, rootNavigator: true).pop(); - unawaited( - showFloatingFlushBar( - type: FlushBarType.success, - message: - "Connection initiated to ${widget.accountLite.nymName}", - iconAsset: Assets.svg.copy, - context: context, - ), - ); - }, - ), + builder: (context) => ConfirmPaynymConnectDialog( + nymName: widget.accountLite.nymName, + locale: ref.read(localeServiceChangeNotifierProvider).locale, + onConfirmPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + unawaited( + showDialog( + context: context, + builder: (context) => DesktopDialog( + maxHeight: MediaQuery.of(context).size.height - 64, + maxWidth: 580, + child: ConfirmTransactionView( + walletId: widget.walletId, + isPaynymNotificationTransaction: true, + txData: preparedTx, + onSuccess: () { + // do nothing extra + }, + onSuccessInsteadOfRouteOnSuccess: () { + Navigator.of(context, rootNavigator: true).pop(); + Navigator.of(context, rootNavigator: true).pop(); + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: + "Connection initiated to ${widget.accountLite.nymName}", + iconAsset: Assets.svg.copy, + context: context, ), + ); + }, ), - ); - }, - amount: preparedTx.amount! + preparedTx.fee!, - coin: ref.read(pWalletCoin(widget.walletId)), - ), + ), + ), + ); + }, + amount: preparedTx.amount! + preparedTx.fee!, + coin: ref.read(pWalletCoin(widget.walletId)), + ), ); } } @@ -158,11 +149,10 @@ class _PaynymDetailsPopupState extends ConsumerState { Future _onSend() async { await showDialog( context: context, - builder: - (context) => DesktopPaynymSendDialog( - walletId: widget.walletId, - accountLite: widget.accountLite, - ), + builder: (context) => DesktopPaynymSendDialog( + walletId: widget.walletId, + accountLite: widget.accountLite, + ), ); } @@ -209,14 +199,12 @@ class _PaynymDetailsPopupState extends ConsumerState { const SizedBox(height: 2), Text( "Connected", - style: STextStyles.desktopTextSmall( - context, - ).copyWith( - color: - Theme.of(context) + style: STextStyles.desktopTextSmall(context) + .copyWith( + color: Theme.of(context) .extension()! .accentColorGreen, - ), + ), ), ], ); @@ -249,10 +237,9 @@ class _PaynymDetailsPopupState extends ConsumerState { Assets.svg.circleArrowUpRight, width: 16, height: 16, - color: - Theme.of(context) - .extension()! - .buttonTextPrimary, + color: Theme.of( + context, + ).extension()!.buttonTextPrimary, ), iconSpacing: 6, onPressed: _onSend, @@ -265,10 +252,9 @@ class _PaynymDetailsPopupState extends ConsumerState { Assets.svg.circlePlusFilled, width: 16, height: 16, - color: - Theme.of(context) - .extension()! - .buttonTextPrimary, + color: Theme.of( + context, + ).extension()!.buttonTextPrimary, ), iconSpacing: 6, onPressed: _onConnectPressed, @@ -287,12 +273,14 @@ class _PaynymDetailsPopupState extends ConsumerState { kDisableFollowing ? const Spacer() : Expanded( - child: PaynymFollowToggleButton( - walletId: widget.walletId, - paymentCodeStringToFollow: widget.accountLite.code, - style: PaynymFollowToggleButtonStyle.detailsDesktop, + child: PaynymFollowToggleButton( + walletId: widget.walletId, + paymentCodeStringToFollow: + widget.accountLite.code, + style: + PaynymFollowToggleButtonStyle.detailsDesktop, + ), ), - ), ], ), if (_showInsufficientFundsInfo) @@ -301,24 +289,21 @@ class _PaynymDetailsPopupState extends ConsumerState { children: [ const SizedBox(height: 24), RoundedContainer( - color: - Theme.of( - context, - ).extension()!.warningBackground, + color: Theme.of( + context, + ).extension()!.warningBackground, child: Text( "Adding a PayNym to your contacts requires a one-time " "transaction fee for creating the record on the " "blockchain. Please deposit more " "${ref.watch(pWalletCoin(widget.walletId)).ticker} " "into your wallet and try again.", - style: STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: - Theme.of( + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of( context, ).extension()!.warningForeground, - ), + ), ), ), ], @@ -348,14 +333,12 @@ class _PaynymDetailsPopupState extends ConsumerState { constraints: const BoxConstraints(minHeight: 100), child: Text( widget.accountLite.code, - style: STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: - Theme.of( + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of( context, ).extension()!.textDark, - ), + ), ), ), ), diff --git a/lib/pages/pinpad_views/lock_screen_view.dart b/lib/pages/pinpad_views/lock_screen_view.dart index 7a21a99c39..7818ffbc90 100644 --- a/lib/pages/pinpad_views/lock_screen_view.dart +++ b/lib/pages/pinpad_views/lock_screen_view.dart @@ -191,8 +191,9 @@ class _LockscreenViewState extends ConsumerState { await ref.read(prefsChangeNotifierProvider).init(); } - final bool useBiometrics = - ref.read(prefsChangeNotifierProvider).useBiometrics; + final bool useBiometrics = ref + .read(prefsChangeNotifierProvider) + .useBiometrics; final title = widget.biometricsAuthenticationTitle; final localizedReason = widget.biometricsLocalizedReason; @@ -423,22 +424,21 @@ class _LockscreenViewState extends ConsumerState { extendBodyBehindAppBar: true, backgroundColor: Theme.of(context).extension()!.background, appBar: AppBar( - leading: - widget.showBackButton - ? AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 70), - ); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ) - : Container(), + leading: widget.showBackButton + ? AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 70), + ); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ) + : Container(), actions: [ // check prefs and hide if user has biometrics toggle off? Column( @@ -500,15 +500,15 @@ class _LockscreenViewState extends ConsumerState { disabledBorder: InputBorder.none, errorBorder: InputBorder.none, focusedErrorBorder: InputBorder.none, - fillColor: - Theme.of( - context, - ).extension()!.background, + fillColor: Theme.of( + context, + ).extension()!.background, counterText: "", ), submittedFieldDecoration: _pinPutDecoration, - isRandom: - ref.read(prefsChangeNotifierProvider).randomizePIN, + isRandom: ref + .read(prefsChangeNotifierProvider) + .randomizePIN, onSubmit: (pin) { if (!_autoPinCheckLock.isLocked) { _onSubmitPin(pin); @@ -528,11 +528,6 @@ class _LockscreenViewState extends ConsumerState { Widget build(BuildContext context) { return widget.showBackButton ? _body - : WillPopScope( - onWillPop: () async { - return widget.showBackButton; - }, - child: _body, - ); + : PopScope(canPop: widget.showBackButton, child: _body); } } diff --git a/lib/pages/receive_view/receive_view.dart b/lib/pages/receive_view/receive_view.dart index 61f8ae5fbe..b5403570e9 100644 --- a/lib/pages/receive_view/receive_view.dart +++ b/lib/pages/receive_view/receive_view.dart @@ -161,8 +161,7 @@ class _ReceiveViewState extends ConsumerState { if (slatepackString == null) return; if (mounted) { - final wallet = - ref.read(pWallets).getWallet(walletId) as EpiccashWallet; + final wallet = ref.read(pWallets).getWallet(walletId) as EpiccashWallet; Exception? ex; final result = await showLoading( @@ -218,13 +217,12 @@ class _ReceiveViewState extends ConsumerState { final wallet = ref.read(pWallets).getWallet(walletId); if (wallet is MultiAddressInterface) { - bool shouldPop = false; unawaited( showDialog( context: context, builder: (_) { - return WillPopScope( - onWillPop: () async => shouldPop, + return PopScope( + canPop: false, child: Container( color: Theme.of( context, @@ -282,8 +280,6 @@ class _ReceiveViewState extends ConsumerState { address = null; } - shouldPop = true; - if (mounted) { Navigator.of( context, @@ -300,13 +296,12 @@ class _ReceiveViewState extends ConsumerState { Future generateNewSparkAddress() async { final wallet = ref.read(pWallets).getWallet(walletId); if (wallet is SparkInterface) { - bool shouldPop = false; unawaited( showDialog( context: context, builder: (_) { - return WillPopScope( - onWillPop: () async => shouldPop, + return PopScope( + canPop: false, child: Container( color: Theme.of( context, @@ -323,8 +318,6 @@ class _ReceiveViewState extends ConsumerState { final address = await wallet.generateNextSparkAddress(saveToDB: true); - shouldPop = true; - if (mounted) { Navigator.of(context, rootNavigator: true).pop(); setState(() { 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 0d1e9ef344..55488d3bbc 100644 --- a/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart +++ b/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart @@ -78,10 +78,8 @@ class _RestoringDialogState extends ConsumerState { ], ); } else { - return WillPopScope( - onWillPop: () async { - return false; - }, + return PopScope( + canPop: false, child: assetPath.endsWith(".gif") ? StackDialogBase( child: Column( diff --git a/lib/pages/send_view/sub_widgets/sending_transaction_dialog.dart b/lib/pages/send_view/sub_widgets/sending_transaction_dialog.dart index c16792d116..82d9e30d12 100644 --- a/lib/pages/send_view/sub_widgets/sending_transaction_dialog.dart +++ b/lib/pages/send_view/sub_widgets/sending_transaction_dialog.dart @@ -57,11 +57,7 @@ class _RestoringDialogState extends ConsumerState { @override Widget build(BuildContext context) { - final assetPath = ref.watch( - coinImageSecondaryProvider( - widget.coin, - ), - ); + final assetPath = ref.watch(coinImageSecondaryProvider(widget.coin)); if (Util.isDesktop) { return DesktopDialog( @@ -75,15 +71,9 @@ class _RestoringDialogState extends ConsumerState { "Sending transaction", style: STextStyles.desktopH3(context), ), - const SizedBox( - height: 40, - ), + const SizedBox(height: 40), assetPath.endsWith(".gif") - ? Flexible( - child: Image.file( - File(assetPath), - ), - ) + ? Flexible(child: Image.file(File(assetPath))) : ProgressAndSuccess( controller: _progressAndSuccessController!, ), @@ -92,27 +82,21 @@ class _RestoringDialogState extends ConsumerState { ), ); } else { - return WillPopScope( - onWillPop: () async { - return false; - }, + return PopScope( + canPop: false, child: assetPath.endsWith(".gif") ? StackDialogBase( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ - Image.file( - File(assetPath), - ), + Image.file(File(assetPath)), Text( "Sending transaction", textAlign: TextAlign.center, style: STextStyles.pageTitleH2(context), ), - const SizedBox( - height: 32, - ), + const SizedBox(height: 32), ], ), ) @@ -201,13 +185,15 @@ class _ProgressAndSuccessState extends State values: [ ValueDelegate.color( const ["**"], - value: - Theme.of(context).extension()!.accentColorDark, + value: Theme.of( + context, + ).extension()!.accentColorDark, ), ValueDelegate.strokeColor( const ["**"], - value: - Theme.of(context).extension()!.accentColorDark, + value: Theme.of( + context, + ).extension()!.accentColorDark, ), ], ), @@ -233,7 +219,8 @@ class _ProgressAndSuccessState extends State height: widget.height, onLoaded: (composition) { setState(() { - controller2.duration = composition.duration * + controller2.duration = + composition.duration * (composition.markers.last.end - composition.markers[1].start); controller2.value = composition.markers[1].start; }); diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/dialogs/cancel_stack_restore_dialog.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/dialogs/cancel_stack_restore_dialog.dart index 7d944e2785..ce7aecf3b3 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/dialogs/cancel_stack_restore_dialog.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/dialogs/cancel_stack_restore_dialog.dart @@ -21,16 +21,12 @@ import '../../../../../widgets/rounded_container.dart'; import '../../../../../widgets/stack_dialog.dart'; class CancelStackRestoreDialog extends StatelessWidget { - const CancelStackRestoreDialog({ - super.key, - }); + const CancelStackRestoreDialog({super.key}); @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { - return false; - }, + return PopScope( + canPop: false, child: !Util.isDesktop ? StackDialog( title: "Cancel restore process", @@ -40,10 +36,7 @@ class CancelStackRestoreDialog extends StatelessWidget { style: Theme.of(context) .extension()! .getSecondaryEnabledButtonStyle(context), - child: Text( - "Back", - style: STextStyles.itemSubtitle12(context), - ), + child: Text("Back", style: STextStyles.itemSubtitle12(context)), onPressed: () { Navigator.of(context).pop(false); }, @@ -55,9 +48,9 @@ class CancelStackRestoreDialog extends StatelessWidget { child: Text( "Yes, cancel", style: STextStyles.itemSubtitle12(context).copyWith( - color: Theme.of(context) - .extension()! - .buttonTextPrimary, + color: Theme.of( + context, + ).extension()!.buttonTextPrimary, ), ), onPressed: () { @@ -85,9 +78,9 @@ class CancelStackRestoreDialog extends StatelessWidget { SizedBox( width: 500, child: RoundedContainer( - color: Theme.of(context) - .extension()! - .snackBarBackError, + color: Theme.of( + context, + ).extension()!.snackBarBackError, child: Text( "If you cancel, the restore will not complete, and " "the wallets will not appear in your ${AppConfig.prefix}.", diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart index 77ed42e0f5..0bdfc9fc6a 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart @@ -47,9 +47,8 @@ class _RestoreFromEncryptedStringViewState bool hidePassword = true; - Future _onWillPop() async { + Future _onPopInvoked() async { Navigator.of(context).pushReplacementNamed(HomeView.routeName); - return false; } @override @@ -68,8 +67,14 @@ class _RestoreFromEncryptedStringViewState @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: _onWillPop, + return PopScope( + canPop: false, + onPopInvokedWithResult: (bool didPop, dynamic result) async { + if (didPop) { + return; + } + await _onPopInvoked(); + }, child: Background( child: Scaffold( backgroundColor: Theme.of( @@ -83,7 +88,7 @@ class _RestoreFromEncryptedStringViewState await Future.delayed(const Duration(milliseconds: 75)); } if (mounted) { - _onWillPop(); + _onPopInvoked(); } }, ), @@ -183,14 +188,11 @@ class _RestoreFromEncryptedStringViewState ); } - bool shouldPop = false; showDialog( barrierDismissible: false, context: context, - builder: (_) => WillPopScope( - onWillPop: () async { - return shouldPop; - }, + builder: (_) => PopScope( + canPop: false, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -239,7 +241,6 @@ class _RestoreFromEncryptedStringViewState if (mounted) { // pop LoadingIndicator - shouldPop = true; Navigator.of(context).pop(); passwordController.text = ""; diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart index 1b8f4283be..f89ed8c2aa 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart @@ -315,15 +315,12 @@ class _RestoreFromFileViewState extends ConsumerState { return; } - bool shouldPop = false; unawaited( showDialog( barrierDismissible: false, context: context, - builder: (_) => WillPopScope( - onWillPop: () async { - return shouldPop; - }, + builder: (_) => PopScope( + canPop: false, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -370,7 +367,6 @@ class _RestoreFromFileViewState extends ConsumerState { if (mounted) { // pop LoadingIndicator - shouldPop = true; Navigator.of(context).pop(); passwordController.text = ""; @@ -431,15 +427,12 @@ class _RestoreFromFileViewState extends ConsumerState { return; } - bool shouldPop = false; unawaited( showDialog( barrierDismissible: false, context: context, - builder: (_) => WillPopScope( - onWillPop: () async { - return shouldPop; - }, + builder: (_) => PopScope( + canPop: false, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -489,7 +482,6 @@ class _RestoreFromFileViewState extends ConsumerState { if (context.mounted) { // pop LoadingIndicator - shouldPop = true; Navigator.of( context, rootNavigator: true, diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/backup_frequency_type_select_sheet.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/backup_frequency_type_select_sheet.dart index d61c93c626..156523bb40 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/backup_frequency_type_select_sheet.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/backup_frequency_type_select_sheet.dart @@ -18,9 +18,7 @@ import '../../../../../utilities/enums/backup_frequency_type.dart'; import '../../../../../utilities/text_styles.dart'; class BackupFrequencyTypeSelectSheet extends ConsumerWidget { - const BackupFrequencyTypeSelectSheet({ - super.key, - }); + const BackupFrequencyTypeSelectSheet({super.key}); String prettyFrequencyType(BackupFrequencyType type) { switch (type) { @@ -35,18 +33,20 @@ class BackupFrequencyTypeSelectSheet extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return WillPopScope( - onWillPop: () async { - Navigator.of(context) - .pop(ref.read(prefsChangeNotifierProvider).backupFrequencyType); - return false; + return PopScope( + canPop: false, + onPopInvokedWithResult: (bool didPop, BackupFrequencyType? result) { + if (didPop) { + return; + } + Navigator.of( + context, + ).pop(ref.read(prefsChangeNotifierProvider).backupFrequencyType); }, child: Container( decoration: BoxDecoration( color: Theme.of(context).extension()!.popupBG, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(20), - ), + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), child: Padding( padding: const EdgeInsets.only( @@ -62,9 +62,9 @@ class BackupFrequencyTypeSelectSheet extends ConsumerWidget { Center( child: Container( decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: Theme.of( + context, + ).extension()!.textFieldDefaultBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), @@ -73,9 +73,7 @@ class BackupFrequencyTypeSelectSheet extends ConsumerWidget { height: 4, ), ), - const SizedBox( - height: 36, - ), + const SizedBox(height: 36), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -84,9 +82,7 @@ class BackupFrequencyTypeSelectSheet extends ConsumerWidget { style: STextStyles.pageTitleH2(context), textAlign: TextAlign.left, ), - const SizedBox( - height: 24, - ), + const SizedBox(height: 24), for (int i = 0; i < BackupFrequencyType.values.length; i++) Column( children: [ @@ -131,9 +127,7 @@ class BackupFrequencyTypeSelectSheet extends ConsumerWidget { }, ), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Flexible( child: Column( children: [ @@ -151,14 +145,10 @@ class BackupFrequencyTypeSelectSheet extends ConsumerWidget { ), ), ), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), ], ), - const SizedBox( - height: 24, - ), + const SizedBox(height: 24), ], ), ], diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/sub_widgets/confirm_full_rescan.dart b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/sub_widgets/confirm_full_rescan.dart index 530233ec4d..17948b7e70 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/sub_widgets/confirm_full_rescan.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/sub_widgets/confirm_full_rescan.dart @@ -35,9 +35,7 @@ class ConfirmFullRescanDialog extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Padding( - padding: const EdgeInsets.only( - left: 32, - ), + padding: const EdgeInsets.only(left: 32), child: Text( "Rescan blockchain", style: STextStyles.desktopH3(context), @@ -60,9 +58,7 @@ class ConfirmFullRescanDialog extends StatelessWidget { "Warning! It may take a while. If you exit before completion, you will have to redo the process.", style: STextStyles.desktopTextSmall(context), ), - const SizedBox( - height: 43, - ), + const SizedBox(height: 43), Row( children: [ Expanded( @@ -72,9 +68,7 @@ class ConfirmFullRescanDialog extends StatelessWidget { label: "Cancel", ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Expanded( child: PrimaryButton( buttonHeight: ButtonHeight.l, @@ -94,34 +88,26 @@ class ConfirmFullRescanDialog extends StatelessWidget { ), ); } else { - return WillPopScope( - onWillPop: () async { - return true; - }, + return PopScope( + canPop: true, child: StackDialog( title: "Rescan blockchain", message: "Warning! It may take a while. If you exit before completion, you will have to redo the process.", leftButton: TextButton( - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle(context), - child: Text( - "Cancel", - style: STextStyles.itemSubtitle12(context), - ), + style: Theme.of( + context, + ).extension()!.getSecondaryEnabledButtonStyle(context), + child: Text("Cancel", style: STextStyles.itemSubtitle12(context)), onPressed: () { Navigator.of(context).pop(); }, ), rightButton: TextButton( - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), - child: Text( - "Rescan", - style: STextStyles.button(context), - ), + style: Theme.of( + context, + ).extension()!.getPrimaryEnabledButtonStyle(context), + child: Text("Rescan", style: STextStyles.button(context)), onPressed: () { Navigator.of(context).pop(); onConfirm.call(); diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/sub_widgets/rescanning_dialog.dart b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/sub_widgets/rescanning_dialog.dart index 647eb2e331..12f261b524 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/sub_widgets/rescanning_dialog.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/sub_widgets/rescanning_dialog.dart @@ -63,24 +63,16 @@ class _RescanningDialogState extends State @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { - return false; - }, + return PopScope( + canPop: false, child: ConditionalParent( condition: isDesktop, - builder: (child) => DesktopDialog( - maxHeight: 150, - maxWidth: 500, - child: child, - ), + builder: (child) => + DesktopDialog(maxHeight: 150, maxWidth: 500, child: child), child: const StackDialog( title: "Rescanning blockchain", message: "This may take a while. Please do not exit this screen.", - icon: RotatingArrows( - width: 24, - height: 24, - ), + icon: RotatingArrows(width: 24, height: 24), // rightButton: TextButton( // style: Theme.of(context).textButtonTheme.style?.copyWith( // backgroundColor: MaterialStateProperty.all( diff --git a/lib/pages/special/firo_rescan_recovery_error_dialog.dart b/lib/pages/special/firo_rescan_recovery_error_dialog.dart index 8e8c21f6a1..accf967e54 100644 --- a/lib/pages/special/firo_rescan_recovery_error_dialog.dart +++ b/lib/pages/special/firo_rescan_recovery_error_dialog.dart @@ -46,8 +46,8 @@ class _FiroRescanRecoveryErrorViewState extends ConsumerState { @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async => false, + return PopScope( + canPop: false, child: ConditionalParent( condition: Util.isDesktop, builder: (child) { diff --git a/lib/pages/token_view/sol_token_view.dart b/lib/pages/token_view/sol_token_view.dart index ab8b462ef2..1ec7ee27c1 100644 --- a/lib/pages/token_view/sol_token_view.dart +++ b/lib/pages/token_view/sol_token_view.dart @@ -70,14 +70,17 @@ class _SolTokenViewState extends ConsumerState { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - return WillPopScope( - onWillPop: () async { + return PopScope( + canPop: false, + onPopInvokedWithResult: (bool didPop, dynamic result) { + if (didPop) { + return; + } final nav = Navigator.of(context); if (widget.popPrevious) { nav.pop(); } nav.pop(); - return false; }, child: Background( child: Scaffold( diff --git a/lib/pages/token_view/token_view.dart b/lib/pages/token_view/token_view.dart index 6063bd49f3..9dedbd8a37 100644 --- a/lib/pages/token_view/token_view.dart +++ b/lib/pages/token_view/token_view.dart @@ -53,10 +53,9 @@ class _TokenViewState extends ConsumerState { @override void initState() { - initialSyncStatus = - ref.read(pCurrentTokenWallet)!.refreshMutex.isLocked - ? WalletSyncStatus.syncing - : WalletSyncStatus.synced; + initialSyncStatus = ref.read(pCurrentTokenWallet)!.refreshMutex.isLocked + ? WalletSyncStatus.syncing + : WalletSyncStatus.synced; super.initState(); } @@ -69,19 +68,23 @@ class _TokenViewState extends ConsumerState { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - return WillPopScope( - onWillPop: () async { + return PopScope( + canPop: false, + onPopInvokedWithResult: (bool didPop, dynamic result) { + if (didPop) { + return; + } final nav = Navigator.of(context); if (widget.popPrevious) { nav.pop(); } nav.pop(); - return false; }, child: Background( child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, + backgroundColor: Theme.of( + context, + ).extension()!.background, appBar: AppBar( leading: AppBarBackButton( onPressed: () { @@ -135,10 +138,9 @@ class _TokenViewState extends ConsumerState { child: AppBarIconButton( icon: SvgPicture.asset( Assets.svg.verticalEllipsis, - color: - Theme.of( - context, - ).extension()!.topNavIconPrimary, + color: Theme.of( + context, + ).extension()!.topNavIconPrimary, ), onPressed: () { // todo: context menu @@ -181,10 +183,9 @@ class _TokenViewState extends ConsumerState { Text( "Transactions", style: STextStyles.itemSubtitle(context).copyWith( - color: - Theme.of( - context, - ).extension()!.textDark3, + color: Theme.of( + context, + ).extension()!.textDark3, ), ), CustomTextButton( diff --git a/lib/pages/wallet_view/transaction_views/dialogs/cancelling_transaction_progress_dialog.dart b/lib/pages/wallet_view/transaction_views/dialogs/cancelling_transaction_progress_dialog.dart index da9c45a68f..a25ff10ef4 100644 --- a/lib/pages/wallet_view/transaction_views/dialogs/cancelling_transaction_progress_dialog.dart +++ b/lib/pages/wallet_view/transaction_views/dialogs/cancelling_transaction_progress_dialog.dart @@ -27,17 +27,14 @@ class _CancellingTransactionProgressDialogState extends State { @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { - return false; - }, + return PopScope( + canPop: false, child: ConditionalParent( condition: Util.isDesktop, - builder: - (child) => Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [SizedBox(width: 400, child: child)], - ), + builder: (child) => Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [SizedBox(width: 400, child: child)], + ), child: const StackDialog( title: "Cancelling transaction", message: "This may take a while. Please do not exit this screen.", 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 27d0b68a64..ceb1e09ece 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 @@ -78,8 +78,8 @@ class _StepScaffoldState extends ConsumerState { showDialog( context: context, barrierDismissible: false, - builder: (_) => WillPopScope( - onWillPop: () async => false, + builder: (_) => PopScope( + canPop: false, child: Container( color: Theme.of( context, diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart index eb354eb1e0..2ae7c1d224 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart @@ -174,8 +174,7 @@ class _DesktopReceiveState extends ConsumerState { } Future _onEpicReceiveSlatePressed() async { - final wallet = - ref.read(pWallets).getWallet(walletId) as EpiccashWallet; + final wallet = ref.read(pWallets).getWallet(walletId) as EpiccashWallet; Exception? ex; final result = await showLoading( @@ -236,13 +235,12 @@ class _DesktopReceiveState extends ConsumerState { Future generateNewAddress() async { final wallet = ref.read(pWallets).getWallet(walletId); if (wallet is MultiAddressInterface) { - bool shouldPop = false; unawaited( showDialog( context: context, builder: (_) { - return WillPopScope( - onWillPop: () async => shouldPop, + return PopScope( + canPop: false, child: Container( color: Theme.of( context, @@ -299,8 +297,6 @@ class _DesktopReceiveState extends ConsumerState { address = null; } - shouldPop = true; - if (mounted) { Navigator.of(context, rootNavigator: true).pop(); @@ -315,13 +311,12 @@ class _DesktopReceiveState extends ConsumerState { Future generateNewSparkAddress() async { final wallet = ref.read(pWallets).getWallet(walletId); if (wallet is SparkInterface) { - bool shouldPop = false; unawaited( showDialog( context: context, builder: (_) { - return WillPopScope( - onWillPop: () async => shouldPop, + return PopScope( + canPop: false, child: Container( color: Theme.of( context, @@ -338,8 +333,6 @@ class _DesktopReceiveState extends ConsumerState { final address = await wallet.generateNextSparkAddress(saveToDB: true); - shouldPop = true; - if (mounted) { Navigator.of(context, rootNavigator: true).pop(); setState(() { @@ -605,9 +598,11 @@ class _DesktopReceiveState extends ConsumerState { ), ), ), - if (!((isMimblewimblecoin || isEpiccash) && ref.watch(pIsSlatepack(widget.walletId)))) + if (!((isMimblewimblecoin || isEpiccash) && + ref.watch(pIsSlatepack(widget.walletId)))) const SizedBox(height: 20), - if (!((isMimblewimblecoin || isEpiccash) && ref.watch(pIsSlatepack(widget.walletId)))) + if (!((isMimblewimblecoin || isEpiccash) && + ref.watch(pIsSlatepack(widget.walletId)))) ConditionalParent( condition: showMultiType, builder: (child) => Column( @@ -778,7 +773,8 @@ class _DesktopReceiveState extends ConsumerState { label: "Generate new address", ), const SizedBox(height: 20), - if ((isMimblewimblecoin || isEpiccash) && ref.watch(pIsSlatepack(widget.walletId))) + if ((isMimblewimblecoin || isEpiccash) && + ref.watch(pIsSlatepack(widget.walletId))) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -909,7 +905,8 @@ class _DesktopReceiveState extends ConsumerState { // TODO: create transparent button class to account for hover // Conditional logic for 'Submit' button or QR code - if ((isMimblewimblecoin || isEpiccash) && ref.watch(pIsSlatepack(widget.walletId))) + if ((isMimblewimblecoin || isEpiccash) && + ref.watch(pIsSlatepack(widget.walletId))) Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: PrimaryButton( @@ -917,7 +914,9 @@ class _DesktopReceiveState extends ConsumerState { label: "Receive Slatepack", enabled: _slateToggleFlag, onPressed: _slateToggleFlag - ? (isEpiccash ? _onEpicReceiveSlatePressed : _onReceiveSlatePressed) + ? (isEpiccash + ? _onEpicReceiveSlatePressed + : _onReceiveSlatePressed) : null, ), ) diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart index 4793f0ada3..f95d53b032 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart @@ -210,16 +210,15 @@ class _DesktopWalletFeaturesState extends ConsumerState { } Future _attemptAnonymize() async { - bool shouldPop = false; unawaited( showDialog( context: context, - builder: (context) => WillPopScope( - child: const CustomLoadingOverlay( + builder: (context) => const PopScope( + canPop: false, + child: CustomLoadingOverlay( message: "Privatizing balance", eventBus: null, ), - onWillPop: () async => shouldPop, ), ), ); @@ -227,7 +226,6 @@ class _DesktopWalletFeaturesState extends ConsumerState { final wallet = ref.read(pWallets).getWallet(widget.walletId); final publicBalance = wallet.info.cachedBalance.spendable; if (publicBalance <= Amount.zero) { - shouldPop = true; if (context.mounted) { Navigator.of(context, rootNavigator: true).pop(); Navigator.of( @@ -250,7 +248,6 @@ class _DesktopWalletFeaturesState extends ConsumerState { } else { await (wallet as FiroWallet).anonymizeAllSpark(); } - shouldPop = true; if (mounted) { Navigator.of(context, rootNavigator: true).pop(); Navigator.of( @@ -265,7 +262,6 @@ class _DesktopWalletFeaturesState extends ConsumerState { ); } } catch (e) { - shouldPop = true; if (mounted) { Navigator.of(context, rootNavigator: true).pop(); Navigator.of( diff --git a/lib/pages_desktop_specific/password/forgotten_passphrase_restore_from_swb.dart b/lib/pages_desktop_specific/password/forgotten_passphrase_restore_from_swb.dart index f4f4de39e8..4cbaca4f60 100644 --- a/lib/pages_desktop_specific/password/forgotten_passphrase_restore_from_swb.dart +++ b/lib/pages_desktop_specific/password/forgotten_passphrase_restore_from_swb.dart @@ -76,15 +76,12 @@ class _ForgottenPassphraseRestoreFromSWBState return; } - bool shouldPop = false; unawaited( showDialog( barrierDismissible: false, context: context, - builder: (_) => WillPopScope( - onWillPop: () async { - return shouldPop; - }, + builder: (_) => PopScope( + canPop: false, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisAlignment: MainAxisAlignment.center, @@ -120,7 +117,6 @@ class _ForgottenPassphraseRestoreFromSWBState if (mounted) { // pop LoadingIndicator - shouldPop = true; Navigator.of(context).pop(); passwordController.text = ""; diff --git a/lib/utilities/show_loading.dart b/lib/utilities/show_loading.dart index 39537e37d1..a160997806 100644 --- a/lib/utilities/show_loading.dart +++ b/lib/utilities/show_loading.dart @@ -43,8 +43,8 @@ Future showLoading({ showDialog( context: context, barrierDismissible: false, - builder: (_) => WillPopScope( - onWillPop: () async => false, + builder: (_) => PopScope( + canPop: false, child: Container( color: Theme.of( context, diff --git a/lib/widgets/dialogs/basic_dialog.dart b/lib/widgets/dialogs/basic_dialog.dart index 8071ec53c2..c24dc840a4 100644 --- a/lib/widgets/dialogs/basic_dialog.dart +++ b/lib/widgets/dialogs/basic_dialog.dart @@ -59,18 +59,12 @@ class BasicDialog extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - title, - style: STextStyles.desktopH3(context), - ), + Text(title, style: STextStyles.desktopH3(context)), const DesktopDialogCloseButton(), ], ), ), - if (flex) - const Spacer( - flex: 2, - ), + if (flex) const Spacer(flex: 2), if (message != null) Padding( padding: const EdgeInsets.symmetric(horizontal: 32), @@ -79,14 +73,9 @@ class BasicDialog extends StatelessWidget { style: STextStyles.desktopTextSmall(context), ), ), - if (flex) - const Spacer( - flex: 3, - ), + if (flex) const Spacer(flex: 3), if (leftButton != null || rightButton != null) - const SizedBox( - height: 32, - ), + const SizedBox(height: 32), if (leftButton != null || rightButton != null) Padding( padding: const EdgeInsets.all(32), @@ -95,9 +84,7 @@ class BasicDialog extends StatelessWidget { leftButton != null ? Expanded(child: leftButton!) : const Spacer(), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), rightButton != null ? Expanded(child: rightButton!) : const Spacer(), @@ -108,10 +95,8 @@ class BasicDialog extends StatelessWidget { ), ); } else { - return WillPopScope( - onWillPop: () async { - return canPopWithBackButton; - }, + return PopScope( + canPop: canPopWithBackButton, child: StackDialog( title: title, leftButton: leftButton, diff --git a/test/widget_tests/basic_dialog_pop_scope_test.dart b/test/widget_tests/basic_dialog_pop_scope_test.dart new file mode 100644 index 0000000000..daf415a01c --- /dev/null +++ b/test/widget_tests/basic_dialog_pop_scope_test.dart @@ -0,0 +1,77 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stackwallet/models/isar/stack_theme.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/dialogs/basic_dialog.dart'; + +import '../sample_data/theme_json.dart'; +import 'helpers/navigation_test_helpers.dart'; + +void main() { + final navigatorKey = GlobalKey(); + + setUp(() { + Util.screenWidth = 400; + }); + + tearDown(() { + Util.screenWidth = null; + }); + + Future pumpApp(WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + navigatorKey: navigatorKey, + theme: ThemeData( + extensions: [ + StackColors.fromStackColorTheme( + StackTheme.fromJson(json: lightThemeJsonMap), + ), + ], + ), + home: const Scaffold(body: Text("Home")), + ), + ); + } + + Future pushDialog(WidgetTester tester, {required bool canPop}) async { + unawaited( + navigatorKey.currentState!.push( + MaterialPageRoute( + builder: (_) => + BasicDialog(title: "Dialog", canPopWithBackButton: canPop), + ), + ), + ); + await tester.pumpAndSettle(); + } + + testWidgets("blocks back navigation when disabled", (tester) async { + await pumpApp(tester); + await pushDialog(tester, canPop: false); + + await simulateSystemBack(); + await tester.pumpAndSettle(); + + expect(find.text("Dialog"), findsOneWidget); + + navigatorKey.currentState!.pop(); + await tester.pumpAndSettle(); + + expect(find.text("Dialog"), findsNothing); + }, variant: TargetPlatformVariant.all()); + + testWidgets("allows back navigation when enabled", (tester) async { + await pumpApp(tester); + await pushDialog(tester, canPop: true); + + await simulateSystemBack(); + await tester.pumpAndSettle(); + + expect(find.text("Dialog"), findsNothing); + expect(find.text("Home"), findsOneWidget); + }, variant: TargetPlatformVariant.all()); +} diff --git a/test/widget_tests/helpers/navigation_test_helpers.dart b/test/widget_tests/helpers/navigation_test_helpers.dart new file mode 100644 index 0000000000..732fa89ffb --- /dev/null +++ b/test/widget_tests/helpers/navigation_test_helpers.dart @@ -0,0 +1,51 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +Future simulateSystemBack() { + return TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .handlePlatformMessage( + 'flutter/navigation', + const JSONMessageCodec().encodeMessage({ + 'method': 'popRoute', + }), + (ByteData? _) {}, + ); +} + +Future startPredictiveBackGesture() { + return _sendBackGesture( + const MethodCall('startBackGesture', { + 'touchOffset': [5, 300], + 'progress': 0.0, + 'swipeEdge': 0, + }), + ); +} + +Future updatePredictiveBackGesture(double progress) { + return _sendBackGesture( + MethodCall('updateBackGestureProgress', { + 'x': 5 + (300 * progress), + 'y': 300.0, + 'progress': progress, + 'swipeEdge': 0, + }), + ); +} + +Future commitPredictiveBackGesture() { + return _sendBackGesture(const MethodCall('commitBackGesture')); +} + +Future cancelPredictiveBackGesture() { + return _sendBackGesture(const MethodCall('cancelBackGesture')); +} + +Future _sendBackGesture(MethodCall call) { + return TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .handlePlatformMessage( + 'flutter/backgesture', + const StandardMethodCodec().encodeMethodCall(call), + (ByteData? _) {}, + ); +} diff --git a/test/widget_tests/pop_scope_migrated_widgets_test.dart b/test/widget_tests/pop_scope_migrated_widgets_test.dart new file mode 100644 index 0000000000..05584dd1d7 --- /dev/null +++ b/test/widget_tests/pop_scope_migrated_widgets_test.dart @@ -0,0 +1,143 @@ +import 'dart:async'; + +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/add_wallet_views/restore_wallet_view/sub_widgets/mnemonic_word_count_select_sheet.dart'; +import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/sub_widgets/restoring_dialog.dart'; +import 'package:stackwallet/pages/cashfusion/fusion_rounds_selection_sheet.dart'; +import 'package:stackwallet/pages/churning/churning_rounds_selection_sheet.dart'; +import 'package:stackwallet/providers/ui/verify_recovery_phrase/mnemonic_word_count_state_provider.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; + +import '../sample_data/theme_json.dart'; +import 'helpers/navigation_test_helpers.dart'; + +void main() { + final navigatorKey = GlobalKey(); + + setUp(() { + Util.screenWidth = 400; + }); + + tearDown(() { + Util.screenWidth = null; + }); + + Future pumpApp( + WidgetTester tester, { + List overrides = const [], + }) async { + await tester.pumpWidget( + ProviderScope( + overrides: overrides, + child: MaterialApp( + navigatorKey: navigatorKey, + theme: ThemeData( + extensions: >[ + StackColors.fromStackColorTheme( + StackTheme.fromJson(json: lightThemeJsonMap), + ), + ], + ), + home: const Scaffold(body: Text('home')), + ), + ), + ); + } + + Future push(Widget child) { + return navigatorKey.currentState!.push( + MaterialPageRoute(builder: (_) => Material(child: child)), + ); + } + + testWidgets('mnemonic sheet returns its selection on system back', ( + tester, + ) async { + await pumpApp( + tester, + overrides: [ + mnemonicWordCountStateProvider.overrideWithValue(StateController(24)), + ], + ); + final result = push( + const MnemonicWordCountSelectSheet(lengthOptions: [12, 24]), + ); + await tester.pumpAndSettle(); + + await simulateSystemBack(); + await tester.pumpAndSettle(); + + await expectLater(result, completion(24)); + expect(find.text('home'), findsOneWidget); + }); + + testWidgets('fusion sheet returns its current option on system back', ( + tester, + ) async { + await pumpApp(tester); + final result = push( + const FusionRoundCountSelectSheet(currentOption: FusionOption.custom), + ); + await tester.pumpAndSettle(); + + await simulateSystemBack(); + await tester.pumpAndSettle(); + + await expectLater(result, completion(FusionOption.custom)); + }); + + testWidgets('churning sheet returns its current option on system back', ( + tester, + ) async { + await pumpApp(tester); + final result = push( + const ChurnRoundCountSelectSheet(currentOption: ChurnOption.continuous), + ); + await tester.pumpAndSettle(); + + await simulateSystemBack(); + await tester.pumpAndSettle(); + + await expectLater(result, completion(ChurnOption.continuous)); + }); + + testWidgets( + 'restoring dialog blocks system back and its button still cancels', + (tester) async { + await pumpApp(tester); + var cancellations = 0; + unawaited( + push( + RestoringDialog( + onCancel: () async { + cancellations++; + }, + ), + ), + ); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 300)); + + await simulateSystemBack(); + await tester.pump(); + + expect(find.text('Restoring wallet'), findsOneWidget); + expect(cancellations, 0); + + await tester.tap(find.text('Cancel')); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + + expect(cancellations, 1); + expect(find.text('Restoring wallet'), findsNothing); + }, + variant: const TargetPlatformVariant({ + TargetPlatform.android, + TargetPlatform.iOS, + }), + ); +} diff --git a/test/widget_tests/pop_scope_navigation_contract_test.dart b/test/widget_tests/pop_scope_navigation_contract_test.dart new file mode 100644 index 0000000000..550e1776f4 --- /dev/null +++ b/test/widget_tests/pop_scope_navigation_contract_test.dart @@ -0,0 +1,341 @@ +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'helpers/navigation_test_helpers.dart'; + +void main() { + final navigatorKey = GlobalKey(); + + Future pumpApp(WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + navigatorKey: navigatorKey, + home: const Scaffold(body: Text('home')), + ), + ); + } + + testWidgets( + 'dynamic canPop controls system back and reports the result', + (tester) async { + await pumpApp(tester); + var canPop = false; + late StateSetter setPageState; + late BuildContext pageContext; + final callbacks = <(bool, int?)>[]; + final result = navigatorKey.currentState!.push( + MaterialPageRoute( + builder: (_) => StatefulBuilder( + builder: (context, setState) { + setPageState = setState; + pageContext = context; + return PopScope( + canPop: canPop, + onPopInvokedWithResult: (didPop, result) { + callbacks.add((didPop, result)); + }, + child: const Scaffold(body: Text('page')), + ); + }, + ), + ), + ); + await tester.pumpAndSettle(); + + expect( + ModalRoute.of(pageContext)!.popDisposition, + RoutePopDisposition.doNotPop, + ); + await simulateSystemBack(); + await tester.pumpAndSettle(); + + expect(callbacks, <(bool, int?)>[(false, null)]); + expect(find.text('page'), findsOneWidget); + + setPageState(() => canPop = true); + await tester.pump(); + expect( + ModalRoute.of(pageContext)!.popDisposition, + RoutePopDisposition.pop, + ); + + await navigatorKey.currentState!.maybePop(19); + await tester.pumpAndSettle(); + + expect(callbacks.last, (true, 19)); + await expectLater(result, completion(19)); + }, + variant: TargetPlatformVariant.all(), + ); + + testWidgets('direct pop reports didPop without replaying side effects', ( + tester, + ) async { + await pumpApp(tester); + var successfulPopSideEffects = 0; + final callbacks = <(bool, String?)>[]; + final result = navigatorKey.currentState!.push( + MaterialPageRoute( + builder: (_) => PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) { + callbacks.add((didPop, result)); + if (didPop) { + successfulPopSideEffects++; + } + }, + child: const Scaffold(body: Text('page')), + ), + ), + ); + await tester.pumpAndSettle(); + + navigatorKey.currentState!.pop('done'); + await tester.pumpAndSettle(); + + expect(callbacks, <(bool, String?)>[(true, 'done')]); + expect(successfulPopSideEffects, 1); + await expectLater(result, completion('done')); + }); + + testWidgets('a blocking nested scope blocks the whole route', (tester) async { + await pumpApp(tester); + var innerCanPop = false; + late StateSetter setPageState; + final outerCallbacks = []; + final innerCallbacks = []; + unawaited( + navigatorKey.currentState!.push( + MaterialPageRoute( + builder: (_) => StatefulBuilder( + builder: (context, setState) { + setPageState = setState; + return PopScope( + canPop: true, + onPopInvokedWithResult: (didPop, _) { + outerCallbacks.add(didPop); + }, + child: PopScope( + canPop: innerCanPop, + onPopInvokedWithResult: (didPop, _) { + innerCallbacks.add(didPop); + }, + child: const Scaffold(body: Text('nested')), + ), + ); + }, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + await simulateSystemBack(); + await tester.pumpAndSettle(); + expect(find.text('nested'), findsOneWidget); + expect(outerCallbacks, [false]); + expect(innerCallbacks, [false]); + + setPageState(() => innerCanPop = true); + await tester.pump(); + await simulateSystemBack(); + await tester.pumpAndSettle(); + expect(find.text('home'), findsOneWidget); + expect(outerCallbacks.last, isTrue); + expect(innerCallbacks.last, isTrue); + }); + + for (final popPrevious in [false, true]) { + testWidgets('token-style back pops ${popPrevious ? 2 : 1} route(s)', ( + tester, + ) async { + await pumpApp(tester); + unawaited( + navigatorKey.currentState!.push( + MaterialPageRoute( + builder: (_) => const Scaffold(body: Text('previous')), + ), + ), + ); + await tester.pumpAndSettle(); + unawaited( + navigatorKey.currentState!.push( + MaterialPageRoute( + builder: (context) => PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, _) { + if (didPop) { + return; + } + final navigator = Navigator.of(context); + if (popPrevious) { + navigator.pop(); + } + navigator.pop(); + }, + child: const Scaffold(body: Text('token')), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + await simulateSystemBack(); + await tester.pumpAndSettle(); + + expect(find.text('token'), findsNothing); + expect( + find.text('previous'), + popPrevious ? findsNothing : findsOneWidget, + ); + expect(find.text('home'), popPrevious ? findsOneWidget : findsNothing); + }); + } + + testWidgets('recovery-style back stops at its named route', (tester) async { + await tester.pumpWidget( + MaterialApp( + navigatorKey: navigatorKey, + initialRoute: '/', + routes: { + '/': (_) => const Scaffold(body: Text('home')), + '/recovery': (_) => const Scaffold(body: Text('recovery')), + '/middle': (_) => const Scaffold(body: Text('middle')), + '/verify': (context) => PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, _) { + if (!didPop) { + Navigator.of( + context, + ).popUntil(ModalRoute.withName('/recovery')); + } + }, + child: const Scaffold(body: Text('verify')), + ), + }, + ), + ); + unawaited(navigatorKey.currentState!.pushNamed('/recovery')); + unawaited(navigatorKey.currentState!.pushNamed('/middle')); + unawaited(navigatorKey.currentState!.pushNamed('/verify')); + await tester.pumpAndSettle(); + + await simulateSystemBack(); + await tester.pumpAndSettle(); + + expect(find.text('recovery'), findsOneWidget); + expect(find.text('middle'), findsNothing); + expect(find.text('verify'), findsNothing); + }); + + testWidgets( + 'Android predictive back cancel keeps the route and commit pops it', + (tester) async { + await pumpApp(tester); + final callbacks = []; + unawaited( + navigatorKey.currentState!.push( + MaterialPageRoute( + builder: (_) => PopScope( + onPopInvokedWithResult: (didPop, _) { + callbacks.add(didPop); + }, + child: const Scaffold(body: Text('predictive')), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + await startPredictiveBackGesture(); + await updatePredictiveBackGesture(0.5); + await cancelPredictiveBackGesture(); + await tester.pumpAndSettle(); + + expect(find.text('predictive'), findsOneWidget); + expect(callbacks, isEmpty); + + await startPredictiveBackGesture(); + await updatePredictiveBackGesture(0.5); + await commitPredictiveBackGesture(); + await tester.pumpAndSettle(); + + expect(find.text('predictive'), findsNothing); + expect(callbacks, [true]); + }, + variant: const TargetPlatformVariant({ + TargetPlatform.android, + }), + ); + + testWidgets( + 'blocked Android predictive gesture stays blocked', + (tester) async { + await pumpApp(tester); + final callbacks = []; + unawaited( + navigatorKey.currentState!.push( + MaterialPageRoute( + builder: (_) => PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, _) { + callbacks.add(didPop); + }, + child: const Scaffold(body: Text('blocked')), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + await startPredictiveBackGesture(); + await updatePredictiveBackGesture(0.5); + await commitPredictiveBackGesture(); + await tester.pumpAndSettle(); + + expect(find.text('blocked'), findsOneWidget); + expect(callbacks, [false]); + + await simulateSystemBack(); + await tester.pumpAndSettle(); + expect(callbacks, [false, false]); + }, + variant: const TargetPlatformVariant({ + TargetPlatform.android, + }), + ); + + testWidgets( + 'iOS edge gesture is not detected when canPop is false', + (tester) async { + await pumpApp(tester); + final callbacks = []; + unawaited( + navigatorKey.currentState!.push( + CupertinoPageRoute( + builder: (_) => PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, _) { + callbacks.add(didPop); + }, + child: const CupertinoPageScaffold( + child: Center(child: Text('cupertino')), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + await tester.dragFrom(const Offset(5, 300), const Offset(500, 0)); + await tester.pumpAndSettle(); + + expect(find.text('cupertino'), findsOneWidget); + expect(callbacks, isEmpty); + }, + variant: const TargetPlatformVariant({TargetPlatform.iOS}), + ); +} From be5f6f30320f39d277c78685fe64a27c76494caa Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 21 Aug 2026 11:57:43 -0500 Subject: [PATCH 2/2] fix(navigation): approve async back presses with AsyncPopScope PopScope decides canPop synchronously, so the eight screens that awaited a decision in onWillPop get a wrapper that runs the callback from onPopInvokedWithResult, coalesces repeated presses, and reproduces Route.popDisposition: an approved back on the first route is handed to the platform rather than emptying the navigator. --- .../new_wallet_recovery_phrase_view.dart | 99 ++--- .../verify_recovery_phrase_view.dart | 7 +- .../cashfusion/fusion_progress_view.dart | 95 +++-- .../churning/churning_progress_view.dart | 94 ++--- .../exchange_step_views/step_4_view.dart | 5 +- lib/pages/home_view/home_view.dart | 30 +- .../stack_restore_progress_view.dart | 16 +- lib/pages/wallet_view/wallet_view.dart | 19 +- lib/widgets/async_pop_scope.dart | 72 ++++ test/widget_tests/async_pop_scope_test.dart | 376 ++++++++++++++++++ 10 files changed, 609 insertions(+), 204 deletions(-) create mode 100644 lib/widgets/async_pop_scope.dart create mode 100644 test/widget_tests/async_pop_scope_test.dart diff --git a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart index ee80421dc1..14ae639be9 100644 --- a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart +++ b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart @@ -30,6 +30,7 @@ import '../../../utilities/text_styles.dart'; import '../../../utilities/util.dart'; import '../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../wallets/wallet/wallet.dart'; +import '../../../widgets/async_pop_scope.dart'; import '../../../widgets/conditional_parent.dart'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../widgets/desktop/desktop_app_bar.dart'; @@ -76,7 +77,7 @@ class _NewWalletRecoveryPhraseViewState super.initState(); } - Future onWillPop() async { + Future _onWillPop() async { await delete(); return true; } @@ -104,8 +105,8 @@ class _NewWalletRecoveryPhraseViewState @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - return WillPopScope( - onWillPop: onWillPop, + return AsyncPopScope( + onPopAttempt: _onWillPop, child: MasterScaffold( isDesktop: isDesktop, appBar: isDesktop @@ -158,17 +159,17 @@ class _NewWalletRecoveryPhraseViewState child: AppBarIconButton( semanticsLabel: "Copy Button. Copies The Recovery Phrase To Clipboard.", - color: Theme.of(context) - .extension()! - .background, + color: Theme.of( + context, + ).extension()!.background, shadows: const [], icon: SvgPicture.asset( Assets.svg.copy, width: 24, height: 24, - color: Theme.of(context) - .extension()! - .topNavIconPrimary, + color: Theme.of( + context, + ).extension()!.topNavIconPrimary, ), onPressed: () async { await _copy(); @@ -182,8 +183,9 @@ class _NewWalletRecoveryPhraseViewState color: Theme.of(context).extension()!.background, width: isDesktop ? 600 : null, child: Padding( - padding: - isDesktop ? const EdgeInsets.all(0) : const EdgeInsets.all(16), + padding: isDesktop + ? const EdgeInsets.all(0) + : const EdgeInsets.all(16), child: ConditionalParent( condition: Util.isDesktop, builder: (child) => LayoutBuilder( @@ -193,9 +195,7 @@ class _NewWalletRecoveryPhraseViewState constraints: BoxConstraints( minHeight: constraints.maxHeight, ), - child: IntrinsicHeight( - child: child, - ), + child: IntrinsicHeight(child: child), ), ); }, @@ -203,25 +203,15 @@ class _NewWalletRecoveryPhraseViewState child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (isDesktop) - const Spacer( - flex: 10, - ), - if (!isDesktop) - const SizedBox( - height: 4, - ), + if (isDesktop) const Spacer(flex: 10), + if (!isDesktop) const SizedBox(height: 4), if (!isDesktop) Text( ref.watch(pWalletName(_wallet.walletId)), textAlign: TextAlign.center, - style: STextStyles.label(context).copyWith( - fontSize: 12, - ), + style: STextStyles.label(context).copyWith(fontSize: 12), ), - SizedBox( - height: isDesktop ? 24 : 4, - ), + SizedBox(height: isDesktop ? 24 : 4), Text( "Recovery Phrase", textAlign: TextAlign.center, @@ -229,15 +219,13 @@ class _NewWalletRecoveryPhraseViewState ? STextStyles.desktopH2(context) : STextStyles.pageTitleH1(context), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), Container( decoration: BoxDecoration( color: isDesktop - ? Theme.of(context) - .extension()! - .background + ? Theme.of( + context, + ).extension()!.background : Theme.of(context).extension()!.popupBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -253,16 +241,14 @@ class _NewWalletRecoveryPhraseViewState style: isDesktop ? STextStyles.desktopSubtitleH2(context) : STextStyles.label(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + color: Theme.of( + context, + ).extension()!.accentColorDark, ), ), ), ), - SizedBox( - height: isDesktop ? 21 : 8, - ), + SizedBox(height: isDesktop ? 21 : 8), if (!isDesktop) Expanded( child: SingleChildScrollView( @@ -273,13 +259,8 @@ class _NewWalletRecoveryPhraseViewState ), ), if (isDesktop) - MnemonicTable( - words: _mnemonic, - isDesktop: isDesktop, - ), - SizedBox( - height: isDesktop ? 24 : 16, - ), + MnemonicTable(words: _mnemonic, isDesktop: isDesktop), + SizedBox(height: isDesktop ? 24 : 16), if (isDesktop) SizedBox( height: 70, @@ -294,13 +275,11 @@ class _NewWalletRecoveryPhraseViewState Assets.svg.copy, width: 20, height: 20, - color: Theme.of(context) - .extension()! - .buttonTextSecondary, - ), - const SizedBox( - width: 10, + color: Theme.of( + context, + ).extension()!.buttonTextSecondary, ), + const SizedBox(width: 10), Text( "Copy to clipboard", style: STextStyles.desktopButtonSecondaryEnabled( @@ -311,14 +290,9 @@ class _NewWalletRecoveryPhraseViewState ), ), ), - if (isDesktop) - const SizedBox( - height: 16, - ), + if (isDesktop) const SizedBox(height: 16), ConstrainedBox( - constraints: BoxConstraints( - minHeight: isDesktop ? 70 : 0, - ), + constraints: BoxConstraints(minHeight: isDesktop ? 70 : 0), child: TextButton( onPressed: () async { final int next = Random().nextInt(_mnemonic.length); @@ -348,10 +322,7 @@ class _NewWalletRecoveryPhraseViewState ), ), ), - if (isDesktop) - const Spacer( - flex: 15, - ), + if (isDesktop) const Spacer(flex: 15), ], ), ), diff --git a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart index 2b9802b0dd..1c0120b064 100644 --- a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart +++ b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart @@ -43,6 +43,7 @@ import '../../../wallets/wallet/wallet.dart'; import '../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart'; import '../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import '../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart'; +import '../../../widgets/async_pop_scope.dart'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../widgets/desktop/desktop_app_bar.dart'; import '../../../widgets/desktop/desktop_scaffold.dart'; @@ -458,7 +459,7 @@ class _VerifyRecoveryPhraseViewState return Tuple2(result, chosenWord); } - Future onWillPop() async { + Future _onWillPop() async { // await delete(); Navigator.of(context).popUntil( ModalRoute.withName( @@ -482,8 +483,8 @@ class _VerifyRecoveryPhraseViewState .watch(verifyMnemonicWordIndexStateProvider.state) .state; - return WillPopScope( - onWillPop: onWillPop, + return AsyncPopScope( + onPopAttempt: _onWillPop, child: MasterScaffold( isDesktop: isDesktop, appBar: isDesktop diff --git a/lib/pages/cashfusion/fusion_progress_view.dart b/lib/pages/cashfusion/fusion_progress_view.dart index 021a8ef690..3e775894cd 100644 --- a/lib/pages/cashfusion/fusion_progress_view.dart +++ b/lib/pages/cashfusion/fusion_progress_view.dart @@ -26,6 +26,7 @@ import '../../wallets/crypto_currency/crypto_currency.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart'; import '../../widgets/background.dart'; +import '../../widgets/async_pop_scope.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/desktop/primary_button.dart'; import '../../widgets/desktop/secondary_button.dart'; @@ -49,24 +50,23 @@ class _FusionProgressViewState extends ConsumerState { final shouldCancel = await showDialog( context: context, barrierDismissible: false, - builder: - (_) => StackDialog( - title: "Cancel fusion?", - leftButton: SecondaryButton( - label: "No", - buttonHeight: null, - onPressed: () { - Navigator.of(context).pop(false); - }, - ), - rightButton: PrimaryButton( - label: "Yes", - buttonHeight: null, - onPressed: () { - Navigator.of(context).pop(true); - }, - ), - ), + builder: (_) => StackDialog( + title: "Cancel fusion?", + leftButton: SecondaryButton( + label: "No", + buttonHeight: null, + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + rightButton: PrimaryButton( + label: "Yes", + buttonHeight: null, + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ), ); if (shouldCancel == true && mounted) { @@ -105,27 +105,27 @@ class _FusionProgressViewState extends ConsumerState { @override Widget build(BuildContext context) { - final bool _succeeded = - ref.watch(fusionProgressUIStateProvider(widget.walletId)).succeeded; + final bool _succeeded = ref + .watch(fusionProgressUIStateProvider(widget.walletId)) + .succeeded; - final bool _failed = - ref.watch(fusionProgressUIStateProvider(widget.walletId)).failed; + final bool _failed = ref + .watch(fusionProgressUIStateProvider(widget.walletId)) + .failed; - final int _fusionRoundsCompleted = - ref - .watch(fusionProgressUIStateProvider(widget.walletId)) - .fusionRoundsCompleted; + final int _fusionRoundsCompleted = ref + .watch(fusionProgressUIStateProvider(widget.walletId)) + .fusionRoundsCompleted; WakelockPlus.enable(); - return WillPopScope( - onWillPop: () async { - return await _requestAndProcessCancel(); - }, + return AsyncPopScope( + onPopAttempt: _requestAndProcessCancel, child: Background( child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, + backgroundColor: Theme.of( + context, + ).extension()!.background, appBar: AppBar( automaticallyImplyLeading: false, leading: AppBarBackButton( @@ -159,37 +159,32 @@ class _FusionProgressViewState extends ConsumerState { children: [ if (_fusionRoundsCompleted == 0) RoundedContainer( - color: - Theme.of(context) - .extension()! - .snackBarBackError, + color: Theme.of( + context, + ).extension()!.snackBarBackError, child: Text( "Do not close this window. If you exit, " "the process will be canceled.", - style: STextStyles.smallMed14( - context, - ).copyWith( - color: - Theme.of(context) + style: STextStyles.smallMed14(context) + .copyWith( + color: Theme.of(context) .extension()! .snackBarTextError, - ), + ), textAlign: TextAlign.center, ), ), if (_fusionRoundsCompleted > 0) RoundedContainer( - color: - Theme.of(context) - .extension()! - .snackBarBackInfo, + color: Theme.of( + context, + ).extension()!.snackBarBackInfo, child: Text( "Fusion rounds completed: $_fusionRoundsCompleted", style: STextStyles.w500_14(context).copyWith( - color: - Theme.of(context) - .extension()! - .snackBarTextInfo, + color: Theme.of(context) + .extension()! + .snackBarTextInfo, ), textAlign: TextAlign.center, ), diff --git a/lib/pages/churning/churning_progress_view.dart b/lib/pages/churning/churning_progress_view.dart index a214640c6f..2c7a865d16 100644 --- a/lib/pages/churning/churning_progress_view.dart +++ b/lib/pages/churning/churning_progress_view.dart @@ -9,6 +9,7 @@ import '../../themes/stack_colors.dart'; import '../../utilities/assets.dart'; import '../../utilities/text_styles.dart'; import '../../widgets/background.dart'; +import '../../widgets/async_pop_scope.dart'; import '../../widgets/churning/churn_progress_item.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/desktop/primary_button.dart'; @@ -34,24 +35,23 @@ class _ChurningProgressViewState extends ConsumerState { final shouldCancel = await showDialog( context: context, barrierDismissible: false, - builder: - (_) => StackDialog( - title: "Cancel churning?", - leftButton: SecondaryButton( - label: "No", - buttonHeight: null, - onPressed: () { - Navigator.of(context).pop(false); - }, - ), - rightButton: PrimaryButton( - label: "Yes", - buttonHeight: null, - onPressed: () { - Navigator.of(context).pop(true); - }, - ), - ), + builder: (_) => StackDialog( + title: "Cancel churning?", + leftButton: SecondaryButton( + label: "No", + buttonHeight: null, + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + rightButton: PrimaryButton( + label: "Yes", + buttonHeight: null, + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ), ); if (shouldCancel == true && mounted) { @@ -100,25 +100,23 @@ class _ChurningProgressViewState extends ConsumerState { if (context.mounted) { showDialog( context: context, - builder: - (context) => ChurnErrorDialog( - error: n.toString(), - walletId: widget.walletId, - ), + builder: (context) => ChurnErrorDialog( + error: n.toString(), + walletId: widget.walletId, + ), ); } } }, ); - return WillPopScope( - onWillPop: () async { - return await _requestAndProcessCancel(); - }, + return AsyncPopScope( + onPopAttempt: _requestAndProcessCancel, child: Background( child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, + backgroundColor: Theme.of( + context, + ).extension()!.background, appBar: AppBar( automaticallyImplyLeading: false, leading: AppBarBackButton( @@ -152,37 +150,32 @@ class _ChurningProgressViewState extends ConsumerState { children: [ if (_roundsCompleted == 0) RoundedContainer( - color: - Theme.of(context) - .extension()! - .snackBarBackError, + color: Theme.of( + context, + ).extension()!.snackBarBackError, child: Text( "Do not close this window. If you exit, " "the process will be canceled.", - style: STextStyles.smallMed14( - context, - ).copyWith( - color: - Theme.of(context) + style: STextStyles.smallMed14(context) + .copyWith( + color: Theme.of(context) .extension()! .snackBarTextError, - ), + ), textAlign: TextAlign.center, ), ), if (_roundsCompleted > 0) RoundedContainer( - color: - Theme.of(context) - .extension()! - .snackBarBackInfo, + color: Theme.of( + context, + ).extension()!.snackBarBackInfo, child: Text( "Churning rounds completed: $_roundsCompleted", style: STextStyles.w500_14(context).copyWith( - color: - Theme.of(context) - .extension()! - .snackBarTextInfo, + color: Theme.of(context) + .extension()! + .snackBarTextInfo, ), textAlign: TextAlign.center, ), @@ -225,10 +218,9 @@ class _ChurningProgressViewState extends ConsumerState { if (_succeeded) PrimaryButton( label: "Churn again", - onPressed: - ref - .read(pChurningService(widget.walletId)) - .churn, + onPressed: ref + .read(pChurningService(widget.walletId)) + .churn, ), if (_succeeded) const SizedBox(height: 16), SecondaryButton( 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 a48b85b23c..b9d98656bd 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 @@ -34,6 +34,7 @@ import '../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../wallets/models/tx_data.dart'; import '../../../wallets/wallet/impl/firo_wallet.dart'; import '../../../widgets/background.dart'; +import '../../../widgets/async_pop_scope.dart'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../widgets/custom_buttons/simple_copy_button.dart'; import '../../../widgets/desktop/primary_button.dart'; @@ -382,8 +383,8 @@ class _Step4ViewState extends ConsumerState { @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { + return AsyncPopScope( + onPopAttempt: () async { await _close(); return false; }, diff --git a/lib/pages/home_view/home_view.dart b/lib/pages/home_view/home_view.dart index 12867fc2f0..853af21a4c 100644 --- a/lib/pages/home_view/home_view.dart +++ b/lib/pages/home_view/home_view.dart @@ -33,6 +33,7 @@ import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; import '../../widgets/animated_widgets/rotate_icon.dart'; import '../../widgets/app_icon.dart'; +import '../../widgets/async_pop_scope.dart'; import '../../widgets/background.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/small_tor_icon.dart'; @@ -135,21 +136,27 @@ class _HomeViewState extends ConsumerState { const timeout = Duration(milliseconds: 1500); if (_cachedTime == null || now.difference(_cachedTime!) > timeout) { _cachedTime = now; + var timedOut = false; await showDialog( context: context, barrierDismissible: false, - builder: (_) => WillPopScope( - onWillPop: () async { - _exitEnabled = true; - return true; + builder: (_) => PopScope( + canPop: true, + onPopInvokedWithResult: (didPop, result) { + if (didPop && !timedOut) { + _exitEnabled = true; + } }, child: const StackDialog(title: "Tap back again to exit"), ), ).timeout( timeout, onTimeout: () { + timedOut = true; _exitEnabled = false; - Navigator.of(context).pop(); + if (mounted) { + Navigator.of(context).pop(); + } }, ); } @@ -298,8 +305,8 @@ class _HomeViewState extends ConsumerState { }, ); - return WillPopScope( - onWillPop: _onWillPop, + return AsyncPopScope( + onPopAttempt: _onWillPop, child: Background( child: Scaffold( backgroundColor: Colors.transparent, @@ -346,8 +353,7 @@ class _HomeViewState extends ConsumerState { color: Theme.of( context, ).extension()!.backgroundAppBar, - icon: - ref.watch(pAnyGlobalUnreadNotifications) + icon: ref.watch(pAnyGlobalUnreadNotifications) ? SvgPicture.file( File( ref.watch( @@ -358,8 +364,7 @@ class _HomeViewState extends ConsumerState { ), width: 20, height: 20, - color: - ref.watch(pAnyGlobalUnreadNotifications) + color: ref.watch(pAnyGlobalUnreadNotifications) ? null : Theme.of( context, @@ -369,8 +374,7 @@ class _HomeViewState extends ConsumerState { Assets.svg.bell, width: 20, height: 20, - color: - ref.watch(pAnyGlobalUnreadNotifications) + color: ref.watch(pAnyGlobalUnreadNotifications) ? null : Theme.of( context, diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/stack_restore_progress_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/stack_restore_progress_view.dart index 2062ef3f9b..b6f4bfeb43 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/stack_restore_progress_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/stack_restore_progress_view.dart @@ -28,6 +28,7 @@ import '../../../../../utilities/enums/stack_restoring_status.dart'; import '../../../../../utilities/logger.dart'; import '../../../../../utilities/text_styles.dart'; import '../../../../../utilities/util.dart'; +import '../../../../../widgets/async_pop_scope.dart'; import '../../../../../widgets/conditional_parent.dart'; import '../../../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../../../widgets/desktop/primary_button.dart'; @@ -65,15 +66,12 @@ class _StackRestoreProgressViewState bool isDesktop = Util.isDesktop; Future _cancel() async { - bool shouldPop = false; unawaited( showDialog( barrierDismissible: false, context: context, - builder: (_) => WillPopScope( - onWillPop: () async { - return shouldPop; - }, + builder: (_) => PopScope( + canPop: false, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisAlignment: MainAxisAlignment.center, @@ -100,7 +98,6 @@ class _StackRestoreProgressViewState ); await SWB.cancelRestore(); - shouldPop = true; int count = 0; @@ -189,9 +186,8 @@ class _StackRestoreProgressViewState if (shouldCancel) { await _cancel(); return true; - } else { - return false; } + return false; } Widget _getIconForState(StackRestoringStatus state) { @@ -246,8 +242,8 @@ class _StackRestoreProgressViewState return ConditionalParent( condition: !isDesktop, builder: (child) { - return WillPopScope( - onWillPop: _onWillPop, + return AsyncPopScope( + onPopAttempt: _onWillPop, child: Scaffold( backgroundColor: Theme.of( context, diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index c40963153d..62ea9eb81d 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -66,6 +66,7 @@ import '../../wallets/wallet/wallet_mixin_interfaces/sign_verify_interface.dart' import '../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart'; import '../../widgets/background.dart'; +import '../../widgets/async_pop_scope.dart'; import '../../widgets/conditional_parent.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/custom_buttons/blue_text_button.dart'; @@ -427,16 +428,15 @@ class _WalletViewState extends ConsumerState { } Future attemptAnonymize() async { - bool shouldPop = false; unawaited( showDialog( context: context, - builder: (context) => WillPopScope( - child: const CustomLoadingOverlay( + builder: (context) => const PopScope( + canPop: false, + child: CustomLoadingOverlay( message: "Anonymizing balance", eventBus: null, ), - onWillPop: () async => shouldPop, ), ), ); @@ -444,7 +444,6 @@ class _WalletViewState extends ConsumerState { final Amount publicBalance = wallet.info.cachedBalance.spendable; if (publicBalance <= Amount.zero) { - shouldPop = true; if (mounted) { Navigator.of( context, @@ -466,7 +465,6 @@ class _WalletViewState extends ConsumerState { } else { await (wallet as FiroWallet).anonymizeAllSpark(); } - shouldPop = true; if (mounted) { Navigator.of( context, @@ -480,7 +478,6 @@ class _WalletViewState extends ConsumerState { ); } } catch (e) { - shouldPop = true; if (mounted) { Navigator.of( context, @@ -512,8 +509,8 @@ class _WalletViewState extends ConsumerState { return ConditionalParent( condition: _rescanningOnOpen, builder: (child) { - return WillPopScope( - onWillPop: () async => !_rescanningOnOpen, + return PopScope( + canPop: !_rescanningOnOpen, child: Stack( children: [ child, @@ -562,8 +559,8 @@ class _WalletViewState extends ConsumerState { ), ); }, - child: WillPopScope( - onWillPop: _onWillPop, + child: AsyncPopScope( + onPopAttempt: _onWillPop, child: Background( child: Stack( children: [ diff --git a/lib/widgets/async_pop_scope.dart b/lib/widgets/async_pop_scope.dart new file mode 100644 index 0000000000..65b930b50a --- /dev/null +++ b/lib/widgets/async_pop_scope.dart @@ -0,0 +1,72 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2026 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + */ + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// Adapts async pop approval to [PopScope] and coalesces repeated attempts. +class AsyncPopScope extends StatefulWidget { + const AsyncPopScope({ + super.key, + required this.onPopAttempt, + required this.child, + }); + + final Future Function() onPopAttempt; + final Widget child; + + @override + State> createState() => _AsyncPopScopeState(); +} + +class _AsyncPopScopeState extends State> { + bool _handlingPop = false; + + Future _handlePop(T? result) async { + try { + final shouldPop = await widget.onPopAttempt(); + if (!shouldPop || !mounted) { + return; + } + final route = ModalRoute.of(context); + if (route == null || !route.isCurrent) { + return; + } + // `Route.popDisposition` is `bubble` on the first route, so the replaced + // `WillPopScope` handed an approved back press to the platform (Android + // backgrounds the app) rather than emptying the navigator. + if (route.isFirst) { + await SystemNavigator.pop(); + return; + } + Navigator.of(context).pop(result); + } finally { + _handlingPop = false; + } + } + + void _onPopInvoked(bool didPop, T? result) { + if (didPop || _handlingPop) { + return; + } + + _handlingPop = true; + unawaited(_handlePop(result)); + } + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: false, + onPopInvokedWithResult: _onPopInvoked, + child: widget.child, + ); + } +} diff --git a/test/widget_tests/async_pop_scope_test.dart b/test/widget_tests/async_pop_scope_test.dart new file mode 100644 index 0000000000..8ae80c7d92 --- /dev/null +++ b/test/widget_tests/async_pop_scope_test.dart @@ -0,0 +1,376 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stackwallet/widgets/async_pop_scope.dart'; + +import 'helpers/navigation_test_helpers.dart'; + +void main() { + final navigatorKey = GlobalKey(); + + Future pumpApp(WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + navigatorKey: navigatorKey, + home: const Scaffold(body: Text('home')), + ), + ); + } + + Future pushGuardedRoute({ + required Future Function() onPopAttempt, + }) { + final result = navigatorKey.currentState!.push( + MaterialPageRoute( + builder: (_) => AsyncPopScope( + onPopAttempt: onPopAttempt, + child: const Scaffold(body: Text('guarded')), + ), + ), + ); + return result; + } + + testWidgets('system back waits for approval before popping', (tester) async { + await pumpApp(tester); + final approval = Completer(); + var attempts = 0; + final routeResult = pushGuardedRoute( + onPopAttempt: () { + attempts++; + return approval.future; + }, + ); + await tester.pumpAndSettle(); + + await simulateSystemBack(); + await tester.pump(); + + expect(attempts, 1); + expect(find.text('guarded'), findsOneWidget); + + approval.complete(true); + await tester.pumpAndSettle(); + + expect(find.text('guarded'), findsNothing); + await expectLater(routeResult, completion(isNull)); + }); + + testWidgets('rapid back attempts run one callback', (tester) async { + await pumpApp(tester); + final approval = Completer(); + var attempts = 0; + unawaited( + pushGuardedRoute( + onPopAttempt: () { + attempts++; + return approval.future; + }, + ), + ); + await tester.pumpAndSettle(); + + await Future.wait(>[ + simulateSystemBack(), + simulateSystemBack(), + simulateSystemBack(), + ]); + await tester.pump(); + + expect(attempts, 1); + + approval.complete(false); + await tester.pump(); + await simulateSystemBack(); + await tester.pump(); + + expect(attempts, 2); + expect(find.text('guarded'), findsOneWidget); + }); + + testWidgets('approved maybePop preserves its result', (tester) async { + await pumpApp(tester); + final routeResult = pushGuardedRoute(onPopAttempt: () async => true); + await tester.pumpAndSettle(); + + await navigatorKey.currentState!.maybePop(42); + await tester.pumpAndSettle(); + + await expectLater(routeResult, completion(42)); + expect(find.text('home'), findsOneWidget); + }); + + testWidgets('direct Navigator.pop bypasses approval', (tester) async { + await pumpApp(tester); + var attempts = 0; + final routeResult = pushGuardedRoute( + onPopAttempt: () async { + attempts++; + return false; + }, + ); + await tester.pumpAndSettle(); + + navigatorKey.currentState!.pop(7); + await tester.pumpAndSettle(); + + expect(attempts, 0); + await expectLater(routeResult, completion(7)); + }); + + testWidgets('callback navigation is not followed by another pop', ( + tester, + ) async { + await pumpApp(tester); + unawaited( + pushGuardedRoute( + onPopAttempt: () async { + unawaited( + navigatorKey.currentState!.push( + MaterialPageRoute( + builder: (_) => const Scaffold(body: Text('replacement')), + ), + ), + ); + return true; + }, + ), + ); + await tester.pumpAndSettle(); + + await simulateSystemBack(); + await tester.pumpAndSettle(); + + expect(find.text('replacement'), findsOneWidget); + navigatorKey.currentState!.pop(); + await tester.pumpAndSettle(); + expect(find.text('guarded'), findsOneWidget); + }); + + for (final approval in [false, true]) { + testWidgets('matches legacy async pop when approval is $approval', ( + tester, + ) async { + final outcomes = <(bool, int)>[]; + + for (final legacy in [true, false]) { + await pumpApp(tester); + var attempts = 0; + unawaited( + navigatorKey.currentState!.push( + MaterialPageRoute( + builder: (_) { + Future onPopAttempt() async { + attempts++; + return approval; + } + + const child = Scaffold(body: Text('compared')); + if (legacy) { + // ignore: deprecated_member_use + return WillPopScope(onWillPop: onPopAttempt, child: child); + } + return AsyncPopScope( + onPopAttempt: onPopAttempt, + child: child, + ); + }, + ), + ), + ); + await tester.pumpAndSettle(); + + await simulateSystemBack(); + await tester.pumpAndSettle(); + + outcomes.add((find.text('compared').evaluate().isNotEmpty, attempts)); + } + + expect(outcomes[1], outcomes[0]); + }); + } + + testWidgets('approval finishes before route completion', (tester) async { + await pumpApp(tester); + final events = []; + final routeResult = pushGuardedRoute( + onPopAttempt: () async { + events.add('approval started'); + await Future.delayed(Duration.zero); + events.add('approval finished'); + return true; + }, + ); + unawaited(routeResult.then((_) => events.add('route completed'))); + await tester.pumpAndSettle(); + + await simulateSystemBack(); + await tester.pumpAndSettle(); + await routeResult; + + expect(events, [ + 'approval started', + 'approval finished', + 'route completed', + ]); + }); + + group('first route', () { + late List platformCalls; + + setUp(() { + platformCalls = []; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, (call) async { + platformCalls.add(call.method); + return null; + }); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, null); + }); + + for (final legacy in [true, false]) { + final label = legacy ? 'legacy' : 'async pop scope'; + + for (final approval in [true, false]) { + testWidgets('$label back with approval $approval', (tester) async { + Future onPopAttempt() async => approval; + const child = Scaffold(body: Text('root')); + + await tester.pumpWidget( + MaterialApp( + home: legacy + // ignore: deprecated_member_use + ? WillPopScope(onWillPop: onPopAttempt, child: child) + : AsyncPopScope( + onPopAttempt: onPopAttempt, + child: child, + ), + ), + ); + await tester.pumpAndSettle(); + + await simulateSystemBack(); + await tester.pumpAndSettle(); + + expect( + platformCalls.contains('SystemNavigator.pop'), + approval, + reason: + 'an approved back on the only route must reach the ' + 'platform instead of emptying the navigator', + ); + expect(find.text('root'), findsOneWidget); + }); + } + + testWidgets('$label tap back again to exit', (tester) async { + await tester.pumpWidget(MaterialApp(home: _ExitPrompt(legacy: legacy))); + await tester.pumpAndSettle(); + + unawaited(simulateSystemBack()); + await tester.pumpAndSettle(); + expect(find.text('tap back again to exit'), findsOneWidget); + + unawaited(simulateSystemBack()); + await tester.pumpAndSettle(); + + expect(platformCalls, contains('SystemNavigator.pop')); + expect(find.text('home'), findsOneWidget); + }); + } + + testWidgets('a pushed route still pops without exiting', (tester) async { + await pumpApp(tester); + unawaited(pushGuardedRoute(onPopAttempt: () async => true)); + await tester.pumpAndSettle(); + + await simulateSystemBack(); + await tester.pumpAndSettle(); + + expect(find.text('guarded'), findsNothing); + expect(find.text('home'), findsOneWidget); + expect(platformCalls, isNot(contains('SystemNavigator.pop'))); + }); + }); +} + +/// Mirrors `HomeView`'s double back to exit prompt, the first route flow the +/// migration has to preserve. +class _ExitPrompt extends StatefulWidget { + const _ExitPrompt({required this.legacy}); + + final bool legacy; + + @override + State<_ExitPrompt> createState() => _ExitPromptState(); +} + +class _ExitPromptState extends State<_ExitPrompt> { + bool _exitEnabled = false; + DateTime? _cachedTime; + + Future _onWillPop() async { + if (_exitEnabled) { + return true; + } + + final now = DateTime.now(); + const timeout = Duration(milliseconds: 1500); + if (_cachedTime == null || now.difference(_cachedTime!) > timeout) { + _cachedTime = now; + var timedOut = false; + await showDialog( + context: context, + barrierDismissible: false, + builder: (_) { + const dialog = Material(child: Text('tap back again to exit')); + if (widget.legacy) { + // ignore: deprecated_member_use + return WillPopScope( + onWillPop: () async { + _exitEnabled = !timedOut; + return true; + }, + child: dialog, + ); + } + return PopScope( + onPopInvokedWithResult: (didPop, result) { + if (didPop && !timedOut) { + _exitEnabled = true; + } + }, + child: dialog, + ); + }, + ).timeout( + timeout, + onTimeout: () { + timedOut = true; + _exitEnabled = false; + if (mounted) { + Navigator.of(context).pop(); + } + }, + ); + } + + return _exitEnabled; + } + + @override + Widget build(BuildContext context) { + const child = Scaffold(body: Text('home')); + if (widget.legacy) { + // ignore: deprecated_member_use + return WillPopScope(onWillPop: _onWillPop, child: child); + } + return AsyncPopScope(onPopAttempt: _onWillPop, child: child); + } +}