diff --git a/lib/pages_desktop_specific/coin_control/desktop_coin_control_view.dart b/lib/pages_desktop_specific/coin_control/desktop_coin_control_view.dart new file mode 100644 index 000000000..85dc75407 --- /dev/null +++ b/lib/pages_desktop_specific/coin_control/desktop_coin_control_view.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; + +class DesktopCoinControlView extends StatefulWidget { + const DesktopCoinControlView({ + Key? key, + required this.walletId, + }) : super(key: key); + + static const String routeName = "/desktopCoinControl"; + + final String walletId; + + @override + State createState() => _DesktopCoinControlViewState(); +} + +class _DesktopCoinControlViewState extends State { + @override + Widget build(BuildContext context) { + return DesktopScaffold( + appBar: DesktopAppBar( + background: Theme.of(context).extension()!.popupBG, + leading: Expanded( + child: Row( + children: [ + const SizedBox( + width: 32, + ), + AppBarIconButton( + size: 32, + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + shadows: const [], + icon: SvgPicture.asset( + Assets.svg.arrowLeft, + width: 18, + height: 18, + color: Theme.of(context) + .extension()! + .topNavIconPrimary, + ), + onPressed: Navigator.of(context).pop, + ), + const SizedBox( + width: 15, + ), + SvgPicture.asset( + Assets.svg.coinControl.gamePad, + width: 32, + height: 32, + color: + Theme.of(context).extension()!.textSubtitle1, + ), + const SizedBox( + width: 12, + ), + Text( + "Coin control", + style: STextStyles.desktopH3(context), + ), + ], + ), + ), + useSpacers: false, + isCompactHeight: true, + ), + body: Column( + children: [ + Padding( + padding: const EdgeInsets.all(24), + child: Row( + children: [ + ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 490, + ), + child: const TextField(), + ), + SecondaryButton( + label: "Show all outputs", + onPressed: () { + // + }, + ), + SecondaryButton( + label: "Sort by", + onPressed: () { + // + }, + ), + ], + ), + ), + Expanded( + child: Container( + color: Colors.green, + ), + ), + ], + ), + ); + } +} 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 26e78e094..98793c931 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 @@ -1,25 +1,35 @@ import 'dart:async'; +import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/paynym/paynym_claim_view.dart'; import 'package:stackwallet/pages/paynym/paynym_home_view.dart'; +import 'package:stackwallet/pages_desktop_specific/coin_control/desktop_coin_control_view.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart'; import 'package:stackwallet/providers/global/paynym_api_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/providers/wallet/my_paynym_account_state_provider.dart'; +import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/custom_loading_overlay.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; -class DesktopWalletFeatures extends ConsumerWidget { +class DesktopWalletFeatures extends ConsumerStatefulWidget { const DesktopWalletFeatures({ Key? key, required this.walletId, @@ -27,19 +37,228 @@ class DesktopWalletFeatures extends ConsumerWidget { final String walletId; - Future onPaynymButtonPressed( - BuildContext context, WidgetRef ref) async { + @override + ConsumerState createState() => + _DesktopWalletFeaturesState(); +} + +class _DesktopWalletFeaturesState extends ConsumerState { + Future _onSwapPressed() async { + // todo + } + + Future _onBuyPressed() async { + // todo + } + + Future _onMorePressed() async { + await showDialog( + context: context, + builder: (_) => MoreFeaturesDialog( + walletId: widget.walletId, + onPaynymPressed: _onPaynymPressed, + onCoinControlPressed: _onCoinControlPressed, + onAnonymizeAllPressed: _onAnonymizeAllPressed, + onWhirlpoolPressed: _onWhirlpoolPressed, + ), + ); + } + + void _onWhirlpoolPressed() { + Navigator.of(context, rootNavigator: true).pop(); + } + + void _onCoinControlPressed() { + Navigator.of(context, rootNavigator: true).pop(); + + Navigator.of(context).pushNamed( + DesktopCoinControlView.routeName, + arguments: widget.walletId, + ); + } + + Future _onAnonymizeAllPressed() async { + Navigator.of(context, rootNavigator: true).pop(); + await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => DesktopDialog( + maxWidth: 500, + maxHeight: 210, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 20), + child: Column( + children: [ + Text( + "Attention!", + style: STextStyles.desktopH2(context), + ), + const SizedBox(height: 16), + Text( + "You're about to anonymize all of your public funds.", + style: STextStyles.desktopTextSmall(context), + ), + const SizedBox(height: 32), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SecondaryButton( + width: 200, + buttonHeight: ButtonHeight.l, + label: "Cancel", + onPressed: () { + Navigator.of(context).pop(); + }, + ), + const SizedBox(width: 20), + PrimaryButton( + width: 200, + buttonHeight: ButtonHeight.l, + label: "Continue", + onPressed: () { + Navigator.of(context).pop(); + + unawaited( + _attemptAnonymize(), + ); + }, + ) + ], + ), + ], + ), + ), + ), + ); + } + + Future _attemptAnonymize() async { + final managerProvider = ref + .read(walletsChangeNotifierProvider) + .getManagerProvider(widget.walletId); + + bool shouldPop = false; unawaited( showDialog( context: context, - builder: (context) => const LoadingIndicator( - width: 100, + builder: (context) => WillPopScope( + child: const CustomLoadingOverlay( + message: "Anonymizing balance", + eventBus: null, + ), + onWillPop: () async => shouldPop, ), ), ); + final firoWallet = ref.read(managerProvider).wallet as FiroWallet; + + final publicBalance = firoWallet.availablePublicBalance(); + if (publicBalance <= Decimal.zero) { + shouldPop = true; + if (context.mounted) { + Navigator.of(context, rootNavigator: true).pop(); + Navigator.of(context).popUntil( + ModalRoute.withName(DesktopWalletView.routeName), + ); + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "No funds available to anonymize!", + context: context, + ), + ); + } + return; + } + + try { + await firoWallet.anonymizeAllPublicFunds(); + shouldPop = true; + if (context.mounted) { + Navigator.of(context, rootNavigator: true).pop(); + Navigator.of(context).popUntil( + ModalRoute.withName(DesktopWalletView.routeName), + ); + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "Anonymize transaction submitted", + context: context, + ), + ); + } + } catch (e) { + shouldPop = true; + if (context.mounted) { + Navigator.of(context, rootNavigator: true).pop(); + Navigator.of(context).popUntil( + ModalRoute.withName(DesktopWalletView.routeName), + ); + await showDialog( + context: context, + builder: (_) => DesktopDialog( + maxWidth: 400, + maxHeight: 300, + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Anonymize all failed", + style: STextStyles.desktopH3(context), + ), + const Spacer( + flex: 1, + ), + Text( + "Reason: $e", + style: STextStyles.desktopTextSmall(context), + ), + const Spacer( + flex: 2, + ), + Row( + children: [ + const Spacer(), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Ok", + buttonHeight: ButtonHeight.l, + onPressed: + Navigator.of(context, rootNavigator: true).pop, + ), + ), + ], + ) + ], + ), + ), + ), + ); + } + } + } + + Future _onPaynymPressed() async { + Navigator.of(context, rootNavigator: true).pop(); + + unawaited( + showDialog( + context: context, + builder: (context) { + return const LoadingIndicator( + width: 100, + ); + }, + ), + ); final manager = - ref.read(walletsChangeNotifierProvider).getManager(walletId); + ref.read(walletsChangeNotifierProvider).getManager(widget.walletId); final wallet = manager.wallet as PaynymWalletInterface; @@ -62,28 +281,22 @@ class DesktopWalletFeatures extends ConsumerWidget { await Navigator.of(context).pushNamed( PaynymHomeView.routeName, - arguments: walletId, + arguments: widget.walletId, ); } else { await Navigator.of(context).pushNamed( PaynymClaimView.routeName, - arguments: walletId, + arguments: widget.walletId, ); } } } - Future _onMorePressed(BuildContext context) async { - await showDialog( - context: context, - builder: (context) => MoreFeaturesDialog(walletId: walletId)); - } - @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { final manager = ref.watch( walletsChangeNotifierProvider.select( - (value) => value.getManager(walletId), + (value) => value.getManager(widget.walletId), ), ); @@ -108,7 +321,7 @@ class DesktopWalletFeatures extends ConsumerWidget { .extension()! .buttonTextSecondary, ), - onPressed: () => onPaynymButtonPressed(context, ref), + onPressed: () => _onSwapPressed(), ), if (Constants.enableExchange) const SizedBox( @@ -127,7 +340,7 @@ class DesktopWalletFeatures extends ConsumerWidget { .extension()! .buttonTextSecondary, ), - onPressed: () => onPaynymButtonPressed(context, ref), + onPressed: () => _onBuyPressed(), ), if (showMore) const SizedBox( @@ -138,19 +351,13 @@ class DesktopWalletFeatures extends ConsumerWidget { width: 160, buttonHeight: ButtonHeight.l, icon: SvgPicture.asset( - Assets.svg.iconFor( - coin: ref.watch( - walletsChangeNotifierProvider.select( - (value) => value.getManager(walletId).coin, - ), - ), - ), + Assets.svg.bars, height: 20, width: 20, color: Theme.of(context).extension()!.buttonTextSecondary, ), - onPressed: () => _onMorePressed(context), + onPressed: () => _onMorePressed(), ), ], ); diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart index 9a267465d..7081b30bb 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart @@ -1,213 +1,41 @@ -import 'dart:async'; - -import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:stackwallet/notifications/show_flush_bar.dart'; -import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; -import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; -import 'package:stackwallet/widgets/custom_loading_overlay.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; -import 'package:stackwallet/widgets/desktop/primary_button.dart'; -import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; -class MoreFeaturesDialog extends ConsumerWidget { +class MoreFeaturesDialog extends ConsumerStatefulWidget { const MoreFeaturesDialog({ Key? key, required this.walletId, + required this.onPaynymPressed, + required this.onCoinControlPressed, + required this.onAnonymizeAllPressed, + required this.onWhirlpoolPressed, }) : super(key: key); final String walletId; - - Future _onAnonymizeAllPressed( - BuildContext context, WidgetRef ref,) async { - Navigator.of(context, rootNavigator: true).pop(); - await showDialog( - context: context, - barrierDismissible: false, - builder: (context) => DesktopDialog( - maxWidth: 500, - maxHeight: 210, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 20), - child: Column( - children: [ - Text( - "Attention!", - style: STextStyles.desktopH2(context), - ), - const SizedBox(height: 16), - Text( - "You're about to anonymize all of your public funds.", - style: STextStyles.desktopTextSmall(context), - ), - const SizedBox(height: 32), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SecondaryButton( - width: 200, - buttonHeight: ButtonHeight.l, - label: "Cancel", - onPressed: () { - Navigator.of(context).pop(); - }, - ), - const SizedBox(width: 20), - PrimaryButton( - width: 200, - buttonHeight: ButtonHeight.l, - label: "Continue", - onPressed: () { - Navigator.of(context).pop(); - - unawaited( - _attemptAnonymize(context, ref), - ); - }, - ) - ], - ), - ], - ), - ), - ), - ); - } - - Future _attemptAnonymize(BuildContext context, WidgetRef ref) async { - final managerProvider = - ref.read(walletsChangeNotifierProvider).getManagerProvider(walletId); - - bool shouldPop = false; - unawaited( - showDialog( - context: context, - builder: (context) => WillPopScope( - child: const CustomLoadingOverlay( - message: "Anonymizing balance", - eventBus: null, - ), - onWillPop: () async => shouldPop, - ), - ), - ); - final firoWallet = ref.read(managerProvider).wallet as FiroWallet; - - final publicBalance = firoWallet.availablePublicBalance(); - if (publicBalance <= Decimal.zero) { - shouldPop = true; - if (context.mounted) { - Navigator.of(context, rootNavigator: true).pop(); - Navigator.of(context).popUntil( - ModalRoute.withName(DesktopWalletView.routeName), - ); - unawaited( - showFloatingFlushBar( - type: FlushBarType.info, - message: "No funds available to anonymize!", - context: context, - ), - ); - } - return; - } - - try { - await firoWallet.anonymizeAllPublicFunds(); - shouldPop = true; - if (context.mounted) { - Navigator.of(context, rootNavigator: true).pop(); - Navigator.of(context).popUntil( - ModalRoute.withName(DesktopWalletView.routeName), - ); - unawaited( - showFloatingFlushBar( - type: FlushBarType.success, - message: "Anonymize transaction submitted", - context: context, - ), - ); - } - } catch (e) { - shouldPop = true; - if (context.mounted) { - Navigator.of(context, rootNavigator: true).pop(); - Navigator.of(context).popUntil( - ModalRoute.withName(DesktopWalletView.routeName), - ); - await showDialog( - context: context, - builder: (_) => DesktopDialog( - maxWidth: 400, - maxHeight: 300, - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Anonymize all failed", - style: STextStyles.desktopH3(context), - ), - const Spacer( - flex: 1, - ), - Text( - "Reason: $e", - style: STextStyles.desktopTextSmall(context), - ), - const Spacer( - flex: 2, - ), - Row( - children: [ - const Spacer(), - const SizedBox( - width: 16, - ), - Expanded( - child: PrimaryButton( - label: "Ok", - buttonHeight: ButtonHeight.l, - onPressed: - Navigator.of(context, rootNavigator: true).pop, - ), - ), - ], - ) - ], - ), - ), - ), - ); - } - } - } - - void _onWhirlpoolPressed( - BuildContext context, WidgetRef ref,) { - Navigator.of(context, rootNavigator: true).pop();} - void _onCoinControlPressed( - BuildContext context, WidgetRef ref,) { - Navigator.of(context, rootNavigator: true).pop();} - void _onPaynymPressed( - BuildContext context, WidgetRef ref,) { - Navigator.of(context, rootNavigator: true).pop();} + final VoidCallback? onPaynymPressed; + final VoidCallback? onCoinControlPressed; + final VoidCallback? onAnonymizeAllPressed; + final VoidCallback? onWhirlpoolPressed; @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => _MoreFeaturesDialogState(); +} + +class _MoreFeaturesDialogState extends ConsumerState { + @override + Widget build(BuildContext context) { final manager = ref.watch( walletsChangeNotifierProvider.select( - (value) => value.getManager(walletId), + (value) => value.getManager(widget.walletId), ), ); @@ -235,28 +63,28 @@ class MoreFeaturesDialog extends ConsumerWidget { label: "Anonymize funds", detail: "Anonymize funds", iconAsset: Assets.svg.anonymize, - onPressed: () => _onAnonymizeAllPressed(context, ref), + onPressed: () => widget.onAnonymizeAllPressed?.call(), ), if (manager.hasWhirlpoolSupport) _MoreFeaturesItem( label: "Whirlpool", detail: "Powerful Bitcoin privacy enhancer", iconAsset: Assets.svg.whirlPool, - onPressed: () =>_onWhirlpoolPressed(context, ref), + onPressed: () => widget.onWhirlpoolPressed?.call(), ), if (manager.hasCoinControlSupport) _MoreFeaturesItem( label: "Coin control", detail: "Control, freeze, and utilize outputs at your discretion", iconAsset: Assets.svg.coinControl.gamePad, - onPressed: () =>_onCoinControlPressed(context, ref), + onPressed: () => widget.onCoinControlPressed?.call(), ), if (manager.hasPaynymSupport) _MoreFeaturesItem( label: "PayNym", detail: "Increased address privacy using BIP47", iconAsset: Assets.svg.robotHead, - onPressed: () => _onPaynymPressed(context, ref), + onPressed: () => widget.onPaynymPressed?.call(), ), const SizedBox( height: 28, @@ -277,7 +105,7 @@ class _MoreFeaturesItem extends StatelessWidget { }) : super(key: key); static const double iconSizeBG = 46; - static const double iconSize = 26; + static const double iconSize = 24; final String label; final String detail; diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 9e0efabfb..64fd1e010 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -98,6 +98,7 @@ import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_sear import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; import 'package:stackwallet/pages/wallets_view/wallets_view.dart'; import 'package:stackwallet/pages_desktop_specific/address_book_view/desktop_address_book.dart'; +import 'package:stackwallet/pages_desktop_specific/coin_control/desktop_coin_control_view.dart'; // import 'package:stackwallet/pages_desktop_specific/desktop_exchange/desktop_all_buys_view.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_buy/desktop_buy_view.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart'; @@ -1287,6 +1288,20 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case DesktopCoinControlView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => DesktopCoinControlView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case BackupRestoreSettings.routeName: return getRoute( shouldUseMaterialRoute: useMaterialPageRoute,