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..ac9c2fec82 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 @@ -156,8 +157,7 @@ class _NewWalletRecoveryPhraseViewState child: AspectRatio( aspectRatio: 1, child: AppBarIconButton( - semanticsLabel: - "Copy Button. Copies The Recovery Phrase To Clipboard.", + semanticsLabel: "Copy Button. Copies The Recovery Phrase To Clipboard.", color: Theme.of(context) .extension()! .background, @@ -182,8 +182,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 +194,7 @@ class _NewWalletRecoveryPhraseViewState constraints: BoxConstraints( minHeight: constraints.maxHeight, ), - child: IntrinsicHeight( - child: child, - ), + child: IntrinsicHeight(child: child), ), ); }, @@ -203,25 +202,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 +218,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 + .extension()! + .background : Theme.of(context).extension()!.popupBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -260,9 +247,7 @@ class _NewWalletRecoveryPhraseViewState ), ), ), - SizedBox( - height: isDesktop ? 21 : 8, - ), + SizedBox(height: isDesktop ? 21 : 8), if (!isDesktop) Expanded( child: SingleChildScrollView( @@ -273,13 +258,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, @@ -298,9 +278,7 @@ class _NewWalletRecoveryPhraseViewState .extension()! .buttonTextSecondary, ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), Text( "Copy to clipboard", style: STextStyles.desktopButtonSecondaryEnabled( @@ -311,14 +289,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 +321,7 @@ class _NewWalletRecoveryPhraseViewState ), ), ), - if (isDesktop) - const Spacer( - flex: 15, - ), + if (isDesktop) const Spacer(flex: 15), ], ), ), 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..ed2b1eb713 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 @@ -10,6 +10,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; + import '../../../themes/stack_colors.dart'; import '../../../utilities/assets.dart'; import '../../../utilities/text_styles.dart'; @@ -32,23 +33,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 +51,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 +62,7 @@ class ConfirmRecoveryDialog extends StatelessWidget { }, ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Expanded( child: PrimaryButton( label: "Restore", @@ -92,14 +79,11 @@ class ConfirmRecoveryDialog extends StatelessWidget { ), ); } else { - return WillPopScope( - onWillPop: () async { - return true; - }, + return PopScope( + canPop: true, child: StackDialog( title: "Are you ready?", - message: - "Restoring your wallet may take a while. Please do not exit this screen once the process is started.", + message: "Restoring your wallet may take a while. Please do not exit this screen once the process is started.", leftButton: SecondaryButton( label: "Cancel", onPressed: () { 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..1f3641ebbc 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..c6964da60e 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,10 +51,8 @@ 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, @@ -62,19 +60,20 @@ class _RestoreFailedDialogState extends ConsumerState { style: Theme.of(context) .extension()! .getSecondaryEnabledButtonStyle(context), - child: Text( - "Ok", - style: STextStyles.itemSubtitle12(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..a255ea97ce 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 @@ -9,6 +9,7 @@ */ import 'package:flutter/material.dart'; + import '../../../../themes/stack_colors.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../utilities/util.dart'; @@ -19,10 +20,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 +51,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 +67,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 +85,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), - ), + 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..50d232091e 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 @@ -9,6 +9,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; + import '../../models/add_wallet_list_entity/sub_classes/coin_entity.dart'; import '../../models/add_wallet_list_entity/sub_classes/sol_token_entity.dart'; import 'add_token_view/edit_wallet_tokens_view.dart'; @@ -29,16 +30,15 @@ import '../../widgets/eth_wallet_radio.dart'; import '../../widgets/rounded_container.dart'; 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 +58,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 +89,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 +116,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 +127,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 +140,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 +148,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 +156,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 +184,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), @@ -206,33 +196,25 @@ class _SelectWalletForSolTokenViewState }, color: isDesktop ? Theme.of(context) - .extension()! - .popupBG + .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..b13bd91eec 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 @@ -10,6 +10,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; + import '../../models/add_wallet_list_entity/sub_classes/coin_entity.dart'; import '../../models/add_wallet_list_entity/sub_classes/eth_token_entity.dart'; import 'add_token_view/edit_wallet_tokens_view.dart'; @@ -30,16 +31,15 @@ import '../../widgets/eth_wallet_radio.dart'; import '../../widgets/rounded_container.dart'; 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 +59,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 +89,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 +111,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 +136,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 +149,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 +157,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 +165,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 +193,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), @@ -221,33 +205,25 @@ class _SelectWalletForTokenViewState }, color: isDesktop ? Theme.of(context) - .extension()! - .popupBG + .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/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 808bbb98e6..0fb2600bbd 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 @@ -42,6 +42,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'; @@ -452,7 +453,7 @@ class _VerifyRecoveryPhraseViewState return Tuple2(result, chosenWord); } - Future onWillPop() async { + Future _onWillPop() async { // await delete(); Navigator.of(context).popUntil( ModalRoute.withName( @@ -476,8 +477,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/buy_view/buy_form.dart b/lib/pages/buy_view/buy_form.dart index 7973bea163..6db17fa3f9 100644 --- a/lib/pages/buy_view/buy_form.dart +++ b/lib/pages/buy_view/buy_form.dart @@ -179,21 +179,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(); } @@ -285,21 +283,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(); } @@ -421,16 +417,15 @@ class _BuyFormState extends ConsumerState { return; } - 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, ), ), ); @@ -450,7 +445,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_progress_view.dart b/lib/pages/cashfusion/fusion_progress_view.dart index 021a8ef690..a54f397799 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/cashfusion/fusion_rounds_selection_sheet.dart b/lib/pages/cashfusion/fusion_rounds_selection_sheet.dart index 72cd027dd3..48a2011029 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( @@ -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_progress_view.dart b/lib/pages/churning/churning_progress_view.dart index a214640c6f..261546606b 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, ), @@ -195,9 +188,8 @@ class _ChurningProgressViewState extends ConsumerState { label: "Waiting for balance to unlock ${ref.watch(pChurningService(widget.walletId).select((s) => s.confirmsInfo)) ?? ""}", status: ref.watch( - pChurningService( - widget.walletId, - ).select((s) => s.waitingForUnlockedBalance), + pChurningService(widget.walletId) + .select((s) => s.waitingForUnlockedBalance), ), ), const SizedBox(height: 12), @@ -205,9 +197,8 @@ class _ChurningProgressViewState extends ConsumerState { iconAsset: Assets.svg.churn, label: "Creating churn transaction", status: ref.watch( - pChurningService( - widget.walletId, - ).select((s) => s.makingChurnTransaction), + pChurningService(widget.walletId) + .select((s) => s.makingChurnTransaction), ), ), const SizedBox(height: 12), @@ -215,9 +206,8 @@ class _ChurningProgressViewState extends ConsumerState { iconAsset: Assets.svg.checkCircle, label: "Complete", status: ref.watch( - pChurningService( - widget.walletId, - ).select((s) => s.completedStatus), + pChurningService(widget.walletId) + .select((s) => s.completedStatus), ), ), const Spacer(), @@ -225,10 +215,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/churning/churning_rounds_selection_sheet.dart b/lib/pages/churning/churning_rounds_selection_sheet.dart index de21964e87..63a0c60321 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( @@ -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 e7e58beb4a..586ade67cc 100644 --- a/lib/pages/coin_control/coin_control_view.dart +++ b/lib/pages/coin_control/coin_control_view.dart @@ -162,13 +162,16 @@ class _CoinControlViewState extends ConsumerState { ); } - 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( 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 b5ebfc9b34..14a72e2fe1 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 @@ -74,8 +74,8 @@ class _ExchangeCurrencySelectionViewState showDialog( context: context, barrierDismissible: false, - builder: (_) => WillPopScope( - onWillPop: () async => false, + builder: (_) => PopScope( + canPop: false, child: Container( color: Theme.of(context) .extension()! diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart index 5040f3b9eb..10b036955c 100644 --- a/lib/pages/exchange_view/exchange_form.dart +++ b/lib/pages/exchange_view/exchange_form.dart @@ -116,8 +116,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) .extension()! 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 b649d56121..e64d236be7 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 @@ -266,8 +266,8 @@ class _Step3ViewState extends ConsumerState { showDialog( context: context, barrierDismissible: false, - builder: (_) => WillPopScope( - onWillPop: () async => false, + builder: (_) => PopScope( + canPop: false, child: Container( color: Theme.of(context) .extension()! 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 c23cd61178..87ad0d3a82 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'; @@ -404,8 +405,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 f1f3e04290..2d3dc78a51 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/onetime_popups/native_library_warning_dialog.dart'; @@ -136,21 +137,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(); + } }, ); } @@ -303,8 +310,8 @@ class _HomeViewState extends ConsumerState { }, ); - return WillPopScope( - onWillPop: _onWillPop, + return AsyncPopScope( + onPopAttempt: _onWillPop, child: Background( child: Scaffold( backgroundColor: Colors.transparent, diff --git a/lib/pages/monkey/sub_widgets/fetch_monkey_dialog.dart b/lib/pages/monkey/sub_widgets/fetch_monkey_dialog.dart index b3b5d7e6d9..692cdd4deb 100644 --- a/lib/pages/monkey/sub_widgets/fetch_monkey_dialog.dart +++ b/lib/pages/monkey/sub_widgets/fetch_monkey_dialog.dart @@ -9,6 +9,7 @@ */ import 'package:flutter/material.dart'; + import '../../../themes/stack_colors.dart'; import '../../../utilities/text_styles.dart'; import '../../../utilities/util.dart'; @@ -19,10 +20,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 +51,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 +67,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 +85,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), - ), + 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..a9c8e04cc1 100644 --- a/lib/pages/paynym/dialogs/claiming_paynym_dialog.dart +++ b/lib/pages/paynym/dialogs/claiming_paynym_dialog.dart @@ -9,6 +9,7 @@ */ import 'package:flutter/material.dart'; + import '../../../themes/stack_colors.dart'; import '../../../utilities/text_styles.dart'; import '../../../utilities/util.dart'; @@ -19,9 +20,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 +43,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 +53,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 +77,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..81269b8e00 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, + ), ), ], ); @@ -292,9 +274,7 @@ class _PaynymDetailsPopupState extends ConsumerState { Column( mainAxisSize: MainAxisSize.min, children: [ - const SizedBox( - height: 24, - ), + const SizedBox(height: 24), RoundedContainer( color: Theme.of(context) .extension()! @@ -340,13 +320,10 @@ 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( @@ -356,16 +333,12 @@ class _PaynymDetailsPopupState extends ConsumerState { 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 +348,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 +360,7 @@ class _PaynymDetailsPopupState extends ConsumerState { style: PaynymFollowToggleButtonStyle.detailsPopup, ), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Expanded( child: SecondaryButton( label: "Copy", @@ -409,9 +376,7 @@ class _PaynymDetailsPopupState extends ConsumerState { ), 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..caa391a044 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( - context, - ).extension()!.warningForeground, - ), + 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( - context, - ).extension()!.textDark, - ), + 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..8d75c15d74 100644 --- a/lib/pages/pinpad_views/lock_screen_view.dart +++ b/lib/pages/pinpad_views/lock_screen_view.dart @@ -177,9 +177,8 @@ class _LockscreenViewState extends ConsumerState { final walletId = widget.routeOnSuccessArguments as String; unawaited( - Navigator.of( - context, - ).pushNamed(WalletView.routeName, arguments: walletId), + Navigator.of(context) + .pushNamed(WalletView.routeName, arguments: walletId), ); } } @@ -191,8 +190,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 +423,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( @@ -486,9 +485,8 @@ class _LockscreenViewState extends ConsumerState { fieldsCount: pinCount, eachFieldHeight: 12, eachFieldWidth: 12, - textStyle: STextStyles.label( - context, - ).copyWith(fontSize: 1), + textStyle: STextStyles.label(context) + .copyWith(fontSize: 1), focusNode: _pinFocusNode, controller: _pinTextController, useNativeKeyboard: false, @@ -500,15 +498,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 +526,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..6b4aaebf0a 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,17 +217,17 @@ 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, - ).extension()!.overlay.withOpacity(0.5), + color: Theme.of(context) + .extension()! + .overlay + .withOpacity(0.5), child: const CustomLoadingOverlay( message: "Generating address", eventBus: null, @@ -282,12 +281,9 @@ class _ReceiveViewState extends ConsumerState { address = null; } - shouldPop = true; - if (mounted) { - Navigator.of( - context, - ).popUntil(ModalRoute.withName(ReceiveView.routeName)); + Navigator.of(context) + .popUntil(ModalRoute.withName(ReceiveView.routeName)); setState(() { _addressMap[_walletAddressTypes[_currentIndex]] = @@ -300,17 +296,17 @@ 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, - ).extension()!.overlay.withOpacity(0.5), + color: Theme.of(context) + .extension()! + .overlay + .withOpacity(0.5), child: const CustomLoadingOverlay( message: "Generating address", eventBus: null, @@ -323,8 +319,6 @@ class _ReceiveViewState extends ConsumerState { final address = await wallet.generateNextSparkAddress(saveToDB: true); - shouldPop = true; - if (mounted) { Navigator.of(context, rootNavigator: true).pop(); setState(() { @@ -553,17 +547,16 @@ class _ReceiveViewState extends ConsumerState { child: AspectRatio( aspectRatio: 1, child: AppBarIconButton( - semanticsLabel: - "Address List Pop-up Button. Opens A Pop-up For Address List Button.", + semanticsLabel: "Address List Pop-up Button. Opens A Pop-up For Address List Button.", key: const Key("walletNetworkSettingsAddNewNodeViewButton"), size: 36, shadows: const [], color: Theme.of(context).extension()!.background, icon: SvgPicture.asset( Assets.svg.verticalEllipsis, - color: Theme.of( - context, - ).extension()!.accentColorDark, + color: Theme.of(context) + .extension()! + .accentColorDark, width: 20, height: 20, ), @@ -580,9 +573,9 @@ class _ReceiveViewState extends ConsumerState { right: 10, child: Container( decoration: BoxDecoration( - color: Theme.of( - context, - ).extension()!.popupBG, + color: Theme.of(context) + .extension()! + .popupBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), @@ -651,9 +644,9 @@ class _ReceiveViewState extends ConsumerState { Text( "Address type", style: STextStyles.w500_14(context).copyWith( - color: Theme.of( - context, - ).extension()!.infoItemLabel, + color: Theme.of(context) + .extension()! + .infoItemLabel, ), ), const SizedBox(height: 10), 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 68c9c8bd20..b79c571962 100644 --- a/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart +++ b/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart @@ -79,10 +79,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..baee517512 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..5ab85aa1fb 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,29 +21,21 @@ 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", - message: - "Cancelling will revert any changes that may have been applied", + message: "Cancelling will revert any changes that may have been applied", leftButton: TextButton( 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); }, 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..f5c9e37fac 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,13 +67,19 @@ 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( - context, - ).extension()!.background, + backgroundColor: Theme.of(context) + .extension()! + .background, appBar: AppBar( leading: AppBarBackButton( onPressed: () async { @@ -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..e1b471194b 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 @@ -88,9 +88,9 @@ class _RestoreFromFileViewState extends ConsumerState { builder: (child) { return Background( child: Scaffold( - backgroundColor: Theme.of( - context, - ).extension()!.background, + backgroundColor: Theme.of(context) + .extension()! + .background, appBar: AppBar( leading: AppBarBackButton( onPressed: () async { @@ -142,9 +142,9 @@ class _RestoreFromFileViewState extends ConsumerState { "Choose file location", style: STextStyles.desktopTextExtraExtraSmall(context) .copyWith( - color: Theme.of( - context, - ).extension()!.textDark3, + color: Theme.of(context) + .extension()! + .textDark3, ), textAlign: TextAlign.left, ), @@ -186,9 +186,9 @@ class _RestoreFromFileViewState extends ConsumerState { const SizedBox(width: 16), SvgPicture.asset( Assets.svg.folder, - color: Theme.of( - context, - ).extension()!.textDark3, + color: Theme.of(context) + .extension()! + .textDark3, width: 16, height: 16, ), @@ -215,9 +215,9 @@ class _RestoreFromFileViewState extends ConsumerState { "Enter passphrase", style: STextStyles.desktopTextExtraExtraSmall(context) .copyWith( - color: Theme.of( - context, - ).extension()!.textDark3, + color: Theme.of(context) + .extension()! + .textDark3, ), textAlign: TextAlign.left, ), @@ -260,9 +260,9 @@ class _RestoreFromFileViewState extends ConsumerState { hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash, - color: Theme.of( - context, - ).extension()!.textDark3, + color: Theme.of(context) + .extension()! + .textDark3, width: 16, height: 16, ), @@ -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, @@ -355,9 +352,8 @@ class _RestoreFromFileViewState extends ConsumerState { ), ); - final encryptedText = await File( - fileToRestore, - ).readAsString(); + final encryptedText = await File(fileToRestore) + .readAsString(); final String? jsonString = await compute( SWB.decryptStackWalletStringWithPassphrase, @@ -370,7 +366,6 @@ class _RestoreFromFileViewState extends ConsumerState { if (mounted) { // pop LoadingIndicator - shouldPop = true; Navigator.of(context).pop(); passwordController.text = ""; @@ -431,15 +426,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, @@ -474,9 +466,8 @@ class _RestoreFromFileViewState extends ConsumerState { ), ); - final encryptedText = await File( - fileToRestore, - ).readAsString(); + final encryptedText = await File(fileToRestore) + .readAsString(); final String? jsonString = await compute( SWB.decryptStackWalletStringWithPassphrase, @@ -489,7 +480,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..3aa8a585e6 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,19 @@ class BackupFrequencyTypeSelectSheet extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return WillPopScope( - onWillPop: () async { + return PopScope( + canPop: false, + onPopInvokedWithResult: (bool didPop, BackupFrequencyType? result) { + if (didPop) { + return; + } Navigator.of(context) .pop(ref.read(prefsChangeNotifierProvider).backupFrequencyType); - 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( @@ -73,9 +72,7 @@ class BackupFrequencyTypeSelectSheet extends ConsumerWidget { height: 4, ), ), - const SizedBox( - height: 36, - ), + const SizedBox(height: 36), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -84,9 +81,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 +126,7 @@ class BackupFrequencyTypeSelectSheet extends ConsumerWidget { }, ), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Flexible( child: Column( children: [ @@ -151,14 +144,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/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..99c9c46496 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, @@ -84,9 +82,9 @@ class _StackRestoreProgressViewState child: Text( "Cancelling restore. Please wait.", style: STextStyles.pageTitleH2(context).copyWith( - color: Theme.of( - context, - ).extension()!.textWhite, + color: Theme.of(context) + .extension()! + .textWhite, ), ), ), @@ -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) { @@ -199,9 +195,9 @@ class _StackRestoreProgressViewState case StackRestoringStatus.waiting: return SvgPicture.asset( Assets.svg.loader, - color: Theme.of( - context, - ).extension()!.buttonBackSecondary, + color: Theme.of(context) + .extension()! + .buttonBackSecondary, ); case StackRestoringStatus.restoring: return SvgPicture.asset( @@ -246,12 +242,12 @@ class _StackRestoreProgressViewState return ConditionalParent( condition: !isDesktop, builder: (child) { - return WillPopScope( - onWillPop: _onWillPop, + return AsyncPopScope( + onPopAttempt: _onWillPop, child: Scaffold( - backgroundColor: Theme.of( - context, - ).extension()!.background, + backgroundColor: Theme.of(context) + .extension()! + .background, appBar: AppBar( leading: AppBarBackButton( onPressed: () async { @@ -309,17 +305,17 @@ class _StackRestoreProgressViewState height: 32, child: RoundedContainer( padding: const EdgeInsets.all(0), - color: Theme.of( - context, - ).extension()!.buttonBackSecondary, + color: Theme.of(context) + .extension()! + .buttonBackSecondary, child: Center( child: SvgPicture.asset( Assets.svg.gear, width: 16, height: 16, - color: Theme.of( - context, - ).extension()!.accentColorDark, + color: Theme.of(context) + .extension()! + .accentColorDark, ), ), ), @@ -339,29 +335,29 @@ class _StackRestoreProgressViewState ) : RoundedContainer( padding: EdgeInsets.zero, - color: Theme.of( - context, - ).extension()!.popupBG, - borderColor: Theme.of( - context, - ).extension()!.background, + color: Theme.of(context) + .extension()! + .popupBG, + borderColor: Theme.of(context) + .extension()! + .background, child: RestoringItemCard( left: SizedBox( width: 32, height: 32, child: RoundedContainer( padding: const EdgeInsets.all(0), - color: Theme.of( - context, - ).extension()!.buttonBackSecondary, + color: Theme.of(context) + .extension()! + .buttonBackSecondary, child: Center( child: SvgPicture.asset( Assets.svg.gear, width: 16, height: 16, - color: Theme.of( - context, - ).extension()!.accentColorDark, + color: Theme.of(context) + .extension()! + .accentColorDark, ), ), ), @@ -397,16 +393,16 @@ class _StackRestoreProgressViewState height: 32, child: RoundedContainer( padding: const EdgeInsets.all(0), - color: Theme.of( - context, - ).extension()!.buttonBackSecondary, + color: Theme.of(context) + .extension()! + .buttonBackSecondary, child: Center( child: AddressBookIcon( width: 16, height: 16, - color: Theme.of( - context, - ).extension()!.accentColorDark, + color: Theme.of(context) + .extension()! + .accentColorDark, ), ), ), @@ -426,28 +422,28 @@ class _StackRestoreProgressViewState ) : RoundedContainer( padding: EdgeInsets.zero, - color: Theme.of( - context, - ).extension()!.popupBG, - borderColor: Theme.of( - context, - ).extension()!.background, + color: Theme.of(context) + .extension()! + .popupBG, + borderColor: Theme.of(context) + .extension()! + .background, child: RestoringItemCard( left: SizedBox( width: 32, height: 32, child: RoundedContainer( padding: const EdgeInsets.all(0), - color: Theme.of( - context, - ).extension()!.buttonBackSecondary, + color: Theme.of(context) + .extension()! + .buttonBackSecondary, child: Center( child: AddressBookIcon( width: 16, height: 16, - color: Theme.of( - context, - ).extension()!.accentColorDark, + color: Theme.of(context) + .extension()! + .accentColorDark, ), ), ), @@ -483,17 +479,17 @@ class _StackRestoreProgressViewState height: 32, child: RoundedContainer( padding: const EdgeInsets.all(0), - color: Theme.of( - context, - ).extension()!.buttonBackSecondary, + color: Theme.of(context) + .extension()! + .buttonBackSecondary, child: Center( child: SvgPicture.asset( Assets.svg.node, width: 16, height: 16, - color: Theme.of( - context, - ).extension()!.accentColorDark, + color: Theme.of(context) + .extension()! + .accentColorDark, ), ), ), @@ -513,29 +509,29 @@ class _StackRestoreProgressViewState ) : RoundedContainer( padding: EdgeInsets.zero, - color: Theme.of( - context, - ).extension()!.popupBG, - borderColor: Theme.of( - context, - ).extension()!.background, + color: Theme.of(context) + .extension()! + .popupBG, + borderColor: Theme.of(context) + .extension()! + .background, child: RestoringItemCard( left: SizedBox( width: 32, height: 32, child: RoundedContainer( padding: const EdgeInsets.all(0), - color: Theme.of( - context, - ).extension()!.buttonBackSecondary, + color: Theme.of(context) + .extension()! + .buttonBackSecondary, child: Center( child: SvgPicture.asset( Assets.svg.node, width: 16, height: 16, - color: Theme.of( - context, - ).extension()!.accentColorDark, + color: Theme.of(context) + .extension()! + .accentColorDark, ), ), ), @@ -571,17 +567,17 @@ class _StackRestoreProgressViewState height: 32, child: RoundedContainer( padding: const EdgeInsets.all(0), - color: Theme.of( - context, - ).extension()!.buttonBackSecondary, + color: Theme.of(context) + .extension()! + .buttonBackSecondary, child: Center( child: SvgPicture.asset( Assets.svg.arrowsTwoWay, width: 16, height: 16, - color: Theme.of( - context, - ).extension()!.accentColorDark, + color: Theme.of(context) + .extension()! + .accentColorDark, ), ), ), @@ -601,29 +597,29 @@ class _StackRestoreProgressViewState ) : RoundedContainer( padding: EdgeInsets.zero, - color: Theme.of( - context, - ).extension()!.popupBG, - borderColor: Theme.of( - context, - ).extension()!.background, + color: Theme.of(context) + .extension()! + .popupBG, + borderColor: Theme.of(context) + .extension()! + .background, child: RestoringItemCard( left: SizedBox( width: 32, height: 32, child: RoundedContainer( padding: const EdgeInsets.all(0), - color: Theme.of( - context, - ).extension()!.buttonBackSecondary, + color: Theme.of(context) + .extension()! + .buttonBackSecondary, child: Center( child: SvgPicture.asset( Assets.svg.arrowsTwoWay, width: 16, height: 16, - color: Theme.of( - context, - ).extension()!.accentColorDark, + color: Theme.of(context) + .extension()! + .accentColorDark, ), ), ), @@ -686,9 +682,9 @@ class _StackRestoreProgressViewState child: Text( _success ? "OK" : "Cancel restore process", style: STextStyles.button(context).copyWith( - color: Theme.of( - context, - ).extension()!.buttonTextPrimary, + color: Theme.of(context) + .extension()! + .buttonTextPrimary, ), ), ) @@ -714,12 +710,11 @@ class _StackRestoreProgressViewState if (widget.shouldPushToHome) { unawaited( - Navigator.of( - context, - ).pushNamedAndRemoveUntil( - DesktopHomeView.routeName, - (route) => false, - ), + Navigator.of(context) + .pushNamedAndRemoveUntil( + DesktopHomeView.routeName, + (route) => false, + ), ); } else { Navigator.of( 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..1260e64190 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 @@ -9,6 +9,7 @@ */ import 'package:flutter/material.dart'; + import '../../../../../themes/stack_colors.dart'; import '../../../../../utilities/text_styles.dart'; import '../../../../../utilities/util.dart'; @@ -35,9 +36,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 +59,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 +69,7 @@ class ConfirmFullRescanDialog extends StatelessWidget { label: "Cancel", ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Expanded( child: PrimaryButton( buttonHeight: ButtonHeight.l, @@ -94,22 +89,16 @@ 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.", + 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), - ), + child: Text("Cancel", style: STextStyles.itemSubtitle12(context)), onPressed: () { Navigator.of(context).pop(); }, @@ -118,10 +107,7 @@ class ConfirmFullRescanDialog extends StatelessWidget { style: Theme.of(context) .extension()! .getPrimaryEnabledButtonStyle(context), - child: Text( - "Rescan", - style: STextStyles.button(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..7991b1a8ef 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 @@ -9,6 +9,7 @@ */ import 'package:flutter/material.dart'; + import '../../../../../utilities/util.dart'; import '../../../../../widgets/animated_widgets/rotating_arrows.dart'; import '../../../../../widgets/conditional_parent.dart'; @@ -63,24 +64,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..49d0f7dcbb 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) { @@ -98,9 +98,9 @@ class _FiroRescanRecoveryErrorViewState builder: (child) { return Background( child: Scaffold( - backgroundColor: Theme.of( - context, - ).extension()!.background, + backgroundColor: Theme.of(context) + .extension()! + .background, appBar: AppBar( automaticallyImplyLeading: false, actions: [ @@ -119,16 +119,16 @@ class _FiroRescanRecoveryErrorViewState key: const Key("walletViewRadioButton"), size: 36, shadows: const [], - color: Theme.of( - context, - ).extension()!.background, + color: Theme.of(context) + .extension()! + .background, icon: SvgPicture.asset( Assets.svg.trash, width: 20, height: 20, - color: Theme.of( - context, - ).extension()!.topNavIconPrimary, + color: Theme.of(context) + .extension()! + .topNavIconPrimary, ), onPressed: () async { final walletName = ref.read( diff --git a/lib/pages/token_view/sol_token_view.dart b/lib/pages/token_view/sol_token_view.dart index ab8b462ef2..484e214195 100644 --- a/lib/pages/token_view/sol_token_view.dart +++ b/lib/pages/token_view/sol_token_view.dart @@ -70,20 +70,23 @@ 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( - backgroundColor: Theme.of( - context, - ).extension()!.background, + backgroundColor: Theme.of(context) + .extension()! + .background, appBar: AppBar( leading: AppBarBackButton( onPressed: () { @@ -135,9 +138,9 @@ class _SolTokenViewState extends ConsumerState { child: AppBarIconButton( icon: SvgPicture.asset( Assets.svg.verticalEllipsis, - color: Theme.of( - context, - ).extension()!.topNavIconPrimary, + color: Theme.of(context) + .extension()! + .topNavIconPrimary, ), onPressed: () { Navigator.of(context).pushNamed( @@ -178,9 +181,9 @@ class _SolTokenViewState 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/token_view/token_view.dart b/lib/pages/token_view/token_view.dart index 6063bd49f3..ae4c8621be 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/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 9ced7e8546..2ac84d97f6 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) .popUntil(ModalRoute.withName(WalletView.routeName)); @@ -465,7 +464,6 @@ class _WalletViewState extends ConsumerState { } else { await (wallet as FiroWallet).anonymizeAllSpark(); } - shouldPop = true; if (mounted) { Navigator.of(context) .popUntil(ModalRoute.withName(WalletView.routeName)); @@ -478,7 +476,6 @@ class _WalletViewState extends ConsumerState { ); } } catch (e) { - shouldPop = true; if (mounted) { Navigator.of(context) .popUntil(ModalRoute.withName(WalletView.routeName)); @@ -509,8 +506,8 @@ class _WalletViewState extends ConsumerState { return ConditionalParent( condition: _rescanningOnOpen, builder: (child) { - return WillPopScope( - onWillPop: () async => !_rescanningOnOpen, + return PopScope( + canPop: !_rescanningOnOpen, child: Stack( children: [ child, @@ -558,8 +555,8 @@ class _WalletViewState extends ConsumerState { ), ); }, - child: WillPopScope( - onWillPop: _onWillPop, + child: AsyncPopScope( + onPopAttempt: _onWillPop, child: Background( child: Stack( children: [ 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 0cf8770a0b..49c7bb4f34 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 @@ -77,8 +77,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) .extension()! 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..87478aef10 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,17 +235,17 @@ 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, - ).extension()!.overlay.withOpacity(0.5), + color: Theme.of(context) + .extension()! + .overlay + .withOpacity(0.5), child: const CustomLoadingOverlay( message: "Generating address", eventBus: null, @@ -299,8 +298,6 @@ class _DesktopReceiveState extends ConsumerState { address = null; } - shouldPop = true; - if (mounted) { Navigator.of(context, rootNavigator: true).pop(); @@ -315,17 +312,17 @@ 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, - ).extension()!.overlay.withOpacity(0.5), + color: Theme.of(context) + .extension()! + .overlay + .withOpacity(0.5), child: const CustomLoadingOverlay( message: "Generating address", eventBus: null, @@ -338,8 +335,6 @@ class _DesktopReceiveState extends ConsumerState { final address = await wallet.generateNextSparkAddress(saveToDB: true); - shouldPop = true; - if (mounted) { Navigator.of(context, rootNavigator: true).pop(); setState(() { @@ -557,23 +552,22 @@ class _DesktopReceiveState extends ConsumerState { child: Container( decoration: BoxDecoration( color: - Theme.of( - context, - ).extension()?.textFieldDefaultBG ?? + Theme.of(context) + .extension() + ?.textFieldDefaultBG ?? Colors.white, // Fallback color borderRadius: BorderRadius.circular(8), border: Border.all( color: - Theme.of( - context, - ).extension()?.backgroundAppBar ?? + Theme.of(context) + .extension() + ?.backgroundAppBar ?? Colors.grey, // Fallback color width: 1, ), ), child: const SizedBox( - height: - 60, // Provide an explicit height to avoid infinite constraints + height: 60, // Provide an explicit height to avoid infinite constraints child: MwcTxsMethodToggle(), ), ), @@ -584,30 +578,31 @@ class _DesktopReceiveState extends ConsumerState { child: Container( decoration: BoxDecoration( color: - Theme.of( - context, - ).extension()?.textFieldDefaultBG ?? + Theme.of(context) + .extension() + ?.textFieldDefaultBG ?? Colors.white, // Fallback color borderRadius: BorderRadius.circular(8), border: Border.all( color: - Theme.of( - context, - ).extension()?.backgroundAppBar ?? + Theme.of(context) + .extension() + ?.backgroundAppBar ?? Colors.grey, // Fallback color width: 1, ), ), child: const SizedBox( - height: - 60, // Provide an explicit height to avoid infinite constraints + height: 60, // Provide an explicit height to avoid infinite constraints child: EpicTxsMethodToggle(), ), ), ), - 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( @@ -652,9 +647,9 @@ class _DesktopReceiveState extends ConsumerState { ), buttonStyleData: ButtonStyleData( decoration: BoxDecoration( - color: Theme.of( - context, - ).extension()!.textFieldDefaultBG, + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), @@ -664,9 +659,9 @@ class _DesktopReceiveState extends ConsumerState { offset: const Offset(0, -10), elevation: 0, decoration: BoxDecoration( - color: Theme.of( - context, - ).extension()!.textFieldDefaultBG, + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), @@ -699,9 +694,9 @@ class _DesktopReceiveState extends ConsumerState { child: Container( decoration: BoxDecoration( border: Border.all( - color: Theme.of( - context, - ).extension()!.backgroundAppBar, + color: Theme.of(context) + .extension()! + .backgroundAppBar, width: 1, ), borderRadius: BorderRadius.circular( @@ -726,9 +721,9 @@ class _DesktopReceiveState extends ConsumerState { Assets.svg.copy, width: 15, height: 15, - color: Theme.of( - context, - ).extension()!.infoItemIcons, + color: Theme.of(context) + .extension()! + .infoItemIcons, ), const SizedBox(width: 4), Text("Copy", style: STextStyles.link2(context)), @@ -746,9 +741,9 @@ class _DesktopReceiveState extends ConsumerState { STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: Theme.of( - context, - ).extension()!.textDark, + color: Theme.of(context) + .extension()! + .textDark, ), ), ), @@ -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: [ @@ -786,9 +782,9 @@ class _DesktopReceiveState extends ConsumerState { Text( "Receive Slatepack", style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of( - context, - ).extension()!.textFieldActiveSearchIconRight, + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, ), textAlign: TextAlign.left, ), @@ -819,9 +815,9 @@ class _DesktopReceiveState extends ConsumerState { }, focusNode: _slateFocusNode, style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of( - context, - ).extension()!.textFieldActiveText, + color: Theme.of(context) + .extension()! + .textFieldActiveText, height: 1.8, ), decoration: @@ -833,8 +829,7 @@ class _DesktopReceiveState extends ConsumerState { ).copyWith( contentPadding: const EdgeInsets.symmetric( horizontal: 16, - vertical: - 12, // Adjust vertical padding for better alignment + vertical: 12, // Adjust vertical padding for better alignment ), suffixIcon: Padding( padding: _receiveSlateController.text.isEmpty @@ -909,7 +904,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 +913,9 @@ class _DesktopReceiveState extends ConsumerState { label: "Receive Slatepack", enabled: _slateToggleFlag, onPressed: _slateToggleFlag - ? (isEpiccash ? _onEpicReceiveSlatePressed : _onReceiveSlatePressed) + ? (isEpiccash + ? _onEpicReceiveSlatePressed + : _onReceiveSlatePressed) : null, ), ) @@ -986,17 +984,17 @@ class _DesktopReceiveState extends ConsumerState { Assets.svg.qrcode, width: 14, height: 16, - color: Theme.of( - context, - ).extension()!.accentColorBlue, + color: Theme.of(context) + .extension()! + .accentColorBlue, ), const SizedBox(width: 8), Text( "Create new QR code", style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of( - context, - ).extension()!.accentColorBlue, + color: Theme.of(context) + .extension()! + .accentColorBlue, ), ), ], 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 936e079e10..6eda1a92ac 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 @@ -208,16 +208,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, ), ), ); @@ -225,7 +224,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(context) @@ -247,7 +245,6 @@ class _DesktopWalletFeaturesState extends ConsumerState { } else { await (wallet as FiroWallet).anonymizeAllSpark(); } - shouldPop = true; if (mounted) { Navigator.of(context, rootNavigator: true).pop(); Navigator.of(context) @@ -261,7 +258,6 @@ class _DesktopWalletFeaturesState extends ConsumerState { ); } } catch (e) { - shouldPop = true; if (mounted) { Navigator.of(context, rootNavigator: true).pop(); Navigator.of(context) 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 a36be4ad3a..2775a10631 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 @@ -77,15 +77,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, @@ -121,7 +118,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..d2649587a1 100644 --- a/lib/utilities/show_loading.dart +++ b/lib/utilities/show_loading.dart @@ -43,12 +43,13 @@ Future showLoading({ showDialog( context: context, barrierDismissible: false, - builder: (_) => WillPopScope( - onWillPop: () async => false, + builder: (_) => PopScope( + canPop: false, child: Container( - color: Theme.of( - context, - ).extension()!.overlay.withOpacity(opaqueBG ? 1.0 : 0.6), + color: Theme.of(context) + .extension()! + .overlay + .withOpacity(opaqueBG ? 1.0 : 0.6), child: CustomLoadingOverlay( message: message, subMessage: subMessage, 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/lib/widgets/dialogs/basic_dialog.dart b/lib/widgets/dialogs/basic_dialog.dart index 8071ec53c2..ae97e9d021 100644 --- a/lib/widgets/dialogs/basic_dialog.dart +++ b/lib/widgets/dialogs/basic_dialog.dart @@ -9,6 +9,7 @@ */ import 'package:flutter/material.dart'; + import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; import '../desktop/desktop_dialog.dart'; @@ -59,18 +60,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 +74,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 +85,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 +96,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/lib/widgets/frost_scaffold.dart b/lib/widgets/frost_scaffold.dart index 9a527eb57f..2c9f9d1079 100644 --- a/lib/widgets/frost_scaffold.dart +++ b/lib/widgets/frost_scaffold.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; + import '../frost_route_generator.dart'; import '../themes/stack_colors.dart'; import '../utilities/text_styles.dart'; @@ -57,28 +58,23 @@ class _FrostScaffoldState extends ConsumerState { label: "No", onPressed: () { // pop dialog - Navigator.of( - context, - rootNavigator: Util.isDesktop, - ).pop("no"); + Navigator.of(context, rootNavigator: Util.isDesktop).pop("no"); }, ), rightButton: PrimaryButton( label: "Yes", onPressed: () { // pop dialog - Navigator.of( - context, - rootNavigator: Util.isDesktop, - ).pop("yes"); + Navigator.of(context, rootNavigator: Util.isDesktop).pop("yes"); }, ), ), ); // make sure to at least delay some time otherwise flutter pops back more than a single route lol... - final minTimeFuture = - Future.delayed(const Duration(milliseconds: 200)); + final minTimeFuture = Future.delayed( + const Duration(milliseconds: 200), + ); final result = await Future.wait([resultFuture, minTimeFuture]); @@ -100,7 +96,11 @@ class _FrostScaffoldState extends ConsumerState { Widget build(BuildContext context) { return PopScope( canPop: Util.isDesktop && ref.watch(pFrostScaffoldCanPopDesktop), - onPopInvoked: (_) => _requestPop(context), + onPopInvokedWithResult: (didPop, _) { + if (!didPop) { + _requestPop(context); + } + }, child: Material( child: ConditionalParent( condition: Util.isDesktop, @@ -109,11 +109,10 @@ class _FrostScaffoldState extends ConsumerState { condition: !Util.isDesktop, builder: (child) => Background( child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - body: SafeArea( - child: child, - ), + backgroundColor: Theme.of(context) + .extension()! + .background, + body: SafeArea(child: child), ), ), child: Column( @@ -122,9 +121,7 @@ class _FrostScaffoldState extends ConsumerState { SizedBox( height: 56, child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), + padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( children: [ Text( @@ -136,21 +133,16 @@ class _FrostScaffoldState extends ConsumerState { .customTextButtonEnabledText, ), ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), Expanded( child: Text( _routes[ref.watch(pFrostCreateCurrentStep) - 1] .title, - style: STextStyles.navBarTitle(context).copyWith( - fontSize: _titleTextSize, - ), + style: STextStyles.navBarTitle(context) + .copyWith(fontSize: _titleTextSize), ), ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), CustomTextButton( text: "Exit", textSize: _titleTextSize, @@ -181,12 +173,7 @@ class _FrostScaffoldState extends ConsumerState { builder: (child) => Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Flexible( - child: SizedBox( - width: 500, - child: child, - ), - ), + Flexible(child: SizedBox(width: 500, child: child)), ], ), child: ConditionalParent( @@ -198,9 +185,7 @@ class _FrostScaffoldState extends ConsumerState { constraints: BoxConstraints( minHeight: constraints.maxHeight, ), - child: IntrinsicHeight( - child: child, - ), + child: IntrinsicHeight(child: 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); + } +} 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..0a1ef42a97 --- /dev/null +++ b/test/widget_tests/pop_scope_migrated_widgets_test.dart @@ -0,0 +1,176 @@ +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/frost_route_generator.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; +import 'package:stackwallet/widgets/frost_scaffold.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, + }), + ); + testWidgets('completed FROST route pop does not ask to cancel', ( + tester, + ) async { + Util.screenWidth = 1400; + await pumpApp(tester); + final container = ProviderScope.containerOf( + navigatorKey.currentContext!, + listen: false, + ); + container.read(pFrostScaffoldArgs.state).state = ( + info: ( + walletName: 'test', + frostCurrency: BitcoinFrost(CryptoCurrencyNetwork.main), + ), + walletId: null, + stepRoutes: [(routeName: '/test-placeholder', title: 'Test step')], + frostInterruptionDialogType: FrostInterruptionDialogType.walletCreation, + parentNav: navigatorKey.currentState!, + callerRouteName: '/', + ); + final result = push(const FrostStepScaffold()); + await tester.pumpAndSettle(); + expect(find.text('Test step'), findsOneWidget); + navigatorKey.currentState!.pop(); + await tester.pumpAndSettle(); + await result; + expect(find.text('home'), findsOneWidget); + expect(find.text('Cancel wallet creation process'), findsNothing); + expect(tester.takeException(), isNull); + }); +} 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..f37b00bc15 --- /dev/null +++ b/test/widget_tests/pop_scope_navigation_contract_test.dart @@ -0,0 +1,335 @@ +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}), + ); +}