From 60ff3ebf6fc93c565bdbf9fab3c198595890c4e2 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Fri, 7 Oct 2022 10:04:56 -0600 Subject: [PATCH 001/426] color doesn't change on hover --- lib/utilities/theme/stack_colors.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/utilities/theme/stack_colors.dart b/lib/utilities/theme/stack_colors.dart index 7790a9f82..c5aaac8c0 100644 --- a/lib/utilities/theme/stack_colors.dart +++ b/lib/utilities/theme/stack_colors.dart @@ -1507,5 +1507,8 @@ class StackColors extends ThemeExtension<StackColors> { backgroundColor: MaterialStateProperty.all<Color>( background, ), + overlayColor: MaterialStateProperty.all<Color>( + Colors.transparent, + ), ); } From 82377f694a5996d28094f0d15e923b2d5ba1701e Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Wed, 12 Oct 2022 11:31:24 -0600 Subject: [PATCH 002/426] some layout fixes --- lib/pages/stack_privacy_calls.dart | 43 +++++++++++++++++++----------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/lib/pages/stack_privacy_calls.dart b/lib/pages/stack_privacy_calls.dart index 841dacf9e..e50bcb7d2 100644 --- a/lib/pages/stack_privacy_calls.dart +++ b/lib/pages/stack_privacy_calls.dart @@ -1,20 +1,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/pages/pinpad_views/create_pin_view.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/advanced_views/advanced_settings_view.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; -import 'package:stackwallet/providers/global/prefs_provider.dart'; -import 'package:stackwallet/utilities/prefs.dart'; - class StackPrivacyCalls extends ConsumerStatefulWidget { const StackPrivacyCalls({ Key? key, @@ -194,6 +192,7 @@ class _PrivacyToggleState extends ConsumerState<PrivacyToggle> { children: [ Expanded( child: RawMaterialButton( + elevation: 0, fillColor: Theme.of(context).extension<StackColors>()!.popupBG, shape: RoundedRectangleBorder( side: !ref.watch( @@ -224,10 +223,13 @@ class _PrivacyToggleState extends ConsumerState<PrivacyToggle> { Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - SvgPicture.asset( - Assets.svg.personaEasy, - width: 140, - height: 140, + Padding( + padding: const EdgeInsets.all(16.0), + child: SvgPicture.asset( + Assets.svg.personaEasy, + width: 140, + height: 140, + ), ), Center( child: Text( @@ -321,10 +323,13 @@ class _PrivacyToggleState extends ConsumerState<PrivacyToggle> { Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - SvgPicture.asset( - Assets.svg.personaIncognito, - width: 140, - height: 140, + Padding( + padding: const EdgeInsets.all(16.0), + child: SvgPicture.asset( + Assets.svg.personaIncognito, + width: 140, + height: 140, + ), ), Center( child: Text( @@ -434,9 +439,17 @@ class ContinueButton extends StatelessWidget { Prefs.instance.externalCalls = isEasy; - if (!isSettings) { - Navigator.of(context).pushNamed(CreatePinView.routeName); - } + !isSettings + ? Navigator.of(context).pushNamed(CreatePinView.routeName) + : Navigator.of(context) + .pushNamed(AdvancedSettingsView.routeName); + + // if (!isSettings) { + // Navigator.of(context).pushNamed(CreatePinView.routeName); + // } + // if (isSettings) { + // Navigator.of(context).pop(); + // } }, child: Text( !isSettings ? "Continue" : "Save changes", From b7d5dfc32e97da120d8efcb65033882a763c9a6c Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Thu, 13 Oct 2022 10:00:00 -0600 Subject: [PATCH 003/426] continue button pushes CreatePasswordView for desktop --- lib/pages/stack_privacy_calls.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pages/stack_privacy_calls.dart b/lib/pages/stack_privacy_calls.dart index e50bcb7d2..e68045311 100644 --- a/lib/pages/stack_privacy_calls.dart +++ b/lib/pages/stack_privacy_calls.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/pinpad_views/create_pin_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/advanced_views/advanced_settings_view.dart'; +import 'package:stackwallet/pages_desktop_specific/create_password/create_password_view.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -418,7 +419,7 @@ class ContinueButton extends StatelessWidget { Prefs.instance.externalCalls = isEasy; if (!isSettings) { - Navigator.of(context).pushNamed(CreatePinView.routeName); + Navigator.of(context).pushNamed(CreatePasswordView.routeName); } }, child: Text( From 3f9111faacfd56390ea9644fdfc7f5d04f7e81d1 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 13 Oct 2022 11:55:04 -0600 Subject: [PATCH 004/426] added keys to const desktop views --- lib/pages_desktop_specific/home/desktop_home_view.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/pages_desktop_specific/home/desktop_home_view.dart b/lib/pages_desktop_specific/home/desktop_home_view.dart index 4ca78894b..12c4b1e8e 100644 --- a/lib/pages_desktop_specific/home/desktop_home_view.dart +++ b/lib/pages_desktop_specific/home/desktop_home_view.dart @@ -19,6 +19,7 @@ class _DesktopHomeViewState extends ConsumerState<DesktopHomeView> { int currentViewIndex = 0; final List<Widget> contentViews = [ const Navigator( + key: Key("desktopStackHomeKey"), onGenerateRoute: RouteGenerator.generateRoute, initialRoute: MyStackView.routeName, ), @@ -32,6 +33,7 @@ class _DesktopHomeViewState extends ConsumerState<DesktopHomeView> { color: Colors.orange, ), const Navigator( + key: Key("desktopSettingHomeKey"), onGenerateRoute: RouteGenerator.generateRoute, initialRoute: SettingsMenu.routeName, ), From 7e6edd4dab91da6e99a376491d85b38c60a6dfdd Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 13 Oct 2022 13:07:48 -0600 Subject: [PATCH 005/426] desktop basic wallet layout ui --- .../my_stack_view/coin_wallets_table.dart | 7 + .../wallet_view/desktop_wallet_view.dart | 294 ++++++++++++++++++ lib/route_generator.dart | 15 + .../wallet_info_row/wallet_info_row.dart | 70 +++-- 4 files changed, 355 insertions(+), 31 deletions(-) create mode 100644 lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart diff --git a/lib/pages_desktop_specific/home/my_stack_view/coin_wallets_table.dart b/lib/pages_desktop_specific/home/my_stack_view/coin_wallets_table.dart index 64e4a23d3..b16a9bc58 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/coin_wallets_table.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/coin_wallets_table.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/wallet_info_row/wallet_info_row.dart'; @@ -37,6 +38,12 @@ class CoinWalletsTable extends ConsumerWidget { ), WalletInfoRow( walletId: walletIds[i], + onPressed: () async { + await Navigator.of(context).pushNamed( + DesktopWalletView.routeName, + arguments: walletIds[i], + ); + }, ), ], ), diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart new file mode 100644 index 000000000..785b94dc4 --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -0,0 +1,294 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/providers/providers.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/custom_buttons/blue_text_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'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class DesktopWalletView extends ConsumerStatefulWidget { + const DesktopWalletView({ + Key? key, + required this.walletId, + }) : super(key: key); + + static const String routeName = "/desktopWalletView"; + + final String walletId; + + @override + ConsumerState<DesktopWalletView> createState() => _DesktopWalletViewState(); +} + +class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { + late final String walletId; + + Future<void> onBackPressed() async { + // TODO log out and close wallet before popping back + Navigator.of(context).pop(); + } + + @override + void initState() { + walletId = widget.walletId; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final manager = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletId))); + final coin = manager.coin; + + return DesktopScaffold( + appBar: DesktopAppBar( + background: Theme.of(context).extension<StackColors>()!.popupBG, + leading: Row( + children: [ + const SizedBox( + width: 32, + ), + AppBarIconButton( + size: 32, + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + shadows: const [], + icon: SvgPicture.asset( + Assets.svg.arrowLeft, + width: 18, + height: 18, + color: Theme.of(context) + .extension<StackColors>()! + .topNavIconPrimary, + ), + onPressed: onBackPressed, + ), + const SizedBox( + width: 15, + ), + SvgPicture.asset( + Assets.svg.iconFor(coin: coin), + width: 32, + height: 32, + ), + const SizedBox( + width: 12, + ), + Text( + manager.walletName, + style: STextStyles.desktopH3(context), + ), + ], + ), + trailing: Row( + children: const [ + NetworkInfoButton(), + SizedBox( + width: 12, + ), + WalletKeysButton(), + SizedBox( + width: 32, + ), + ], + ), + isCompactHeight: true, + ), + body: Padding( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(20), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.iconFor(coin: coin), + width: 40, + height: 40, + ), + const SizedBox( + width: 10, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + "TODO: balance", + style: STextStyles.desktopH3(context), + ), + const SizedBox( + width: 8, + ), + Container( + color: Colors.red, + width: 20, + height: 20, + ), + ], + ), + Text( + "todo: fiat balance", + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ) + ], + ), + const Spacer(), + SecondaryButton( + width: 180, + height: 56, + onPressed: () { + // todo: go to wallet initiated exchange + }, + label: "Exchange", + icon: Container( + color: Colors.red, + width: 20, + height: 20, + ), + ) + ], + ), + ), + const SizedBox( + height: 24, + ), + Expanded( + child: Row( + children: const [ + Expanded( + child: MyWallet(), + ), + SizedBox( + width: 16, + ), + Expanded( + child: RecentDesktopTransactions(), + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +class MyWallet extends StatefulWidget { + const MyWallet({Key? key}) : super(key: key); + + @override + State<MyWallet> createState() => _MyWalletState(); +} + +class _MyWalletState extends State<MyWallet> { + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "My wallet", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveSearchIconLeft, + ), + ), + const SizedBox( + height: 16, + ), + Expanded( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: Container(), + ), + ), + ], + ); + } +} + +class RecentDesktopTransactions extends StatefulWidget { + const RecentDesktopTransactions({Key? key}) : super(key: key); + + @override + State<RecentDesktopTransactions> createState() => + _RecentDesktopTransactionsState(); +} + +class _RecentDesktopTransactionsState extends State<RecentDesktopTransactions> { + @override + Widget build(BuildContext context) { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Recent transactions", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveSearchIconLeft, + ), + ), + BlueTextButton( + text: "See all", + onTap: () { + // todo: show all txns + }, + ), + ], + ), + const SizedBox( + height: 16, + ), + Expanded( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: Container(), + ), + ), + ], + ); + } +} + +class NetworkInfoButton extends StatelessWidget { + const NetworkInfoButton({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + child: Text("todo: sync status"), + ); + } +} + +class WalletKeysButton extends StatelessWidget { + const WalletKeysButton({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + child: Text("todo: wallet keys"), + ); + } +} diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 834432a69..b4ca9ef92 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -86,6 +86,7 @@ import 'package:stackwallet/pages_desktop_specific/create_password/create_passwo import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_settings_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/settings_menu.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; @@ -969,6 +970,20 @@ class RouteGenerator { builder: (_) => const MyStackView(), settings: RouteSettings(name: settings.name)); + case DesktopWalletView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => DesktopWalletView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case SettingsMenu.routeName: return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, diff --git a/lib/widgets/wallet_info_row/wallet_info_row.dart b/lib/widgets/wallet_info_row/wallet_info_row.dart index 4840e9b01..d5e42e814 100644 --- a/lib/widgets/wallet_info_row/wallet_info_row.dart +++ b/lib/widgets/wallet_info_row/wallet_info_row.dart @@ -25,9 +25,13 @@ class WalletInfoRow extends ConsumerWidget { .watch(walletsChangeNotifierProvider.notifier) .getManagerProvider(walletId)); - return Row( - children: Util.isDesktop - ? [ + if (Util.isDesktop) { + return GestureDetector( + onTap: onPressed, + child: Container( + color: Colors.transparent, + child: Row( + children: [ Expanded( flex: 4, child: Row( @@ -38,11 +42,9 @@ class WalletInfoRow extends ConsumerWidget { ), Text( manager.walletName, - style: - STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of(context).extension<StackColors>()!.textDark, ), ), ], @@ -70,29 +72,35 @@ class WalletInfoRow extends ConsumerWidget { ], ), ) - ] - : [ - WalletInfoCoinIcon(coin: manager.coin), - const SizedBox( - width: 12, - ), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - manager.walletName, - style: STextStyles.titleBold12(context), - ), - const SizedBox( - height: 2, - ), - WalletInfoRowBalanceFuture(walletId: walletId), - ], - ), - ), ], - ); + ), + ), + ); + } else { + return Row( + children: [ + WalletInfoCoinIcon(coin: manager.coin), + const SizedBox( + width: 12, + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + manager.walletName, + style: STextStyles.titleBold12(context), + ), + const SizedBox( + height: 2, + ), + WalletInfoRowBalanceFuture(walletId: walletId), + ], + ), + ), + ], + ); + } } } From 146135aad9bea0fbfd025b4af65d8e752ae9eb95 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 13 Oct 2022 13:10:24 -0600 Subject: [PATCH 006/426] main desktop view divider --- lib/pages_desktop_specific/home/desktop_home_view.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/pages_desktop_specific/home/desktop_home_view.dart b/lib/pages_desktop_specific/home/desktop_home_view.dart index 12c4b1e8e..3941fc6de 100644 --- a/lib/pages_desktop_specific/home/desktop_home_view.dart +++ b/lib/pages_desktop_specific/home/desktop_home_view.dart @@ -63,6 +63,10 @@ class _DesktopHomeViewState extends ConsumerState<DesktopHomeView> { DesktopMenu( onSelectionChanged: onMenuSelectionChanged, ), + Container( + width: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), Expanded( child: contentViews[currentViewIndex], ), From 1157b5fd0f27b063a8727c5b4473e6380518ec14 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Thu, 13 Oct 2022 16:42:13 -0600 Subject: [PATCH 007/426] redid settings option layout and added first container --- .../home/settings_menu/settings_menu.dart | 297 +++++++++--------- 1 file changed, 152 insertions(+), 145 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/settings_menu.dart b/lib/pages_desktop_specific/home/settings_menu/settings_menu.dart index de800b51f..82b32be5b 100644 --- a/lib/pages_desktop_specific/home/settings_menu/settings_menu.dart +++ b/lib/pages_desktop_specific/home/settings_menu/settings_menu.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/pages_desktop_specific/home/settings_menu_item.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/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +import '../../../utilities/assets.dart'; +import '../settings_menu_item.dart'; class SettingsMenu extends ConsumerStatefulWidget { const SettingsMenu({ @@ -35,155 +37,160 @@ class _SettingsMenuState extends ConsumerState<SettingsMenu> { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - return Material( - color: Theme.of(context).extension<StackColors>()!.background, - child: SizedBox( - width: 300, - child: Padding( - padding: const EdgeInsets.fromLTRB(24.0, 10.0, 0, 0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + return Column( + children: [ + DesktopAppBar( + isCompactHeight: true, + leading: Row( children: [ - SizedBox( - height: 20, - // width: 300, + const SizedBox( + width: 24, + height: 24, ), Text( "Settings", - style: STextStyles.desktopH3(context).copyWith( - fontSize: 24, - ), - ), - Row( - children: [ - Padding( - padding: const EdgeInsets.fromLTRB( - 3.0, - 30.0, - 55.0, - 0, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SettingsMenuItem( - icon: SvgPicture.asset( - Assets.svg.polygon, - width: 11, - height: 11, - ), - label: "Backup and restore", - value: 0, - group: selectedMenuItem, - onChanged: updateSelectedMenuItem, - ), - const SizedBox( - height: 2, - ), - SettingsMenuItem( - icon: SvgPicture.asset( - Assets.svg.polygon, - width: 11, - height: 11, - ), - label: "Security", - value: 1, - group: selectedMenuItem, - onChanged: updateSelectedMenuItem, - ), - const SizedBox( - height: 2, - ), - SettingsMenuItem( - icon: SvgPicture.asset( - Assets.svg.polygon, - width: 11, - height: 11, - ), - label: "Currency", - value: 2, - group: selectedMenuItem, - onChanged: updateSelectedMenuItem, - ), - const SizedBox( - height: 2, - ), - SettingsMenuItem( - icon: SvgPicture.asset( - Assets.svg.polygon, - width: 11, - height: 11, - ), - label: "Language", - value: 3, - group: selectedMenuItem, - onChanged: updateSelectedMenuItem, - ), - const SizedBox( - height: 2, - ), - SettingsMenuItem( - icon: SvgPicture.asset( - Assets.svg.polygon, - width: 11, - height: 11, - ), - label: "Nodes", - value: 4, - group: selectedMenuItem, - onChanged: updateSelectedMenuItem, - ), - const SizedBox( - height: 2, - ), - SettingsMenuItem( - icon: SvgPicture.asset( - Assets.svg.polygon, - width: 11, - height: 11, - ), - label: "Syncing preferences", - value: 5, - group: selectedMenuItem, - onChanged: updateSelectedMenuItem, - ), - const SizedBox( - height: 2, - ), - SettingsMenuItem( - icon: SvgPicture.asset( - Assets.svg.polygon, - width: 11, - height: 11, - ), - label: "Appearance", - value: 6, - group: selectedMenuItem, - onChanged: updateSelectedMenuItem, - ), - const SizedBox( - height: 2, - ), - SettingsMenuItem( - icon: SvgPicture.asset( - Assets.svg.polygon, - width: 11, - height: 11, - ), - label: "Advanced", - value: 7, - group: selectedMenuItem, - onChanged: updateSelectedMenuItem, - ), - ], - ), - ), - ], - ), + style: STextStyles.desktopH3(context), + ) ], ), ), - ), + Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 15), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SettingsMenuItem( + icon: SvgPicture.asset( + Assets.svg.polygon, + width: 11, + height: 11, + ), + label: "Backup and restore", + value: 0, + group: selectedMenuItem, + onChanged: updateSelectedMenuItem, + ), + const SizedBox( + height: 2, + ), + SettingsMenuItem( + icon: SvgPicture.asset( + Assets.svg.polygon, + width: 11, + height: 11, + ), + label: "Security", + value: 1, + group: selectedMenuItem, + onChanged: updateSelectedMenuItem, + ), + const SizedBox( + height: 2, + ), + SettingsMenuItem( + icon: SvgPicture.asset( + Assets.svg.polygon, + width: 11, + height: 11, + ), + label: "Currency", + value: 2, + group: selectedMenuItem, + onChanged: updateSelectedMenuItem, + ), + const SizedBox( + height: 2, + ), + SettingsMenuItem( + icon: SvgPicture.asset( + Assets.svg.polygon, + width: 11, + height: 11, + ), + label: "Language", + value: 3, + group: selectedMenuItem, + onChanged: updateSelectedMenuItem, + ), + const SizedBox( + height: 2, + ), + SettingsMenuItem( + icon: SvgPicture.asset( + Assets.svg.polygon, + width: 11, + height: 11, + ), + label: "Nodes", + value: 4, + group: selectedMenuItem, + onChanged: updateSelectedMenuItem, + ), + const SizedBox( + height: 2, + ), + SettingsMenuItem( + icon: SvgPicture.asset( + Assets.svg.polygon, + width: 11, + height: 11, + ), + label: "Syncing preferences", + value: 5, + group: selectedMenuItem, + onChanged: updateSelectedMenuItem, + ), + const SizedBox( + height: 2, + ), + SettingsMenuItem( + icon: SvgPicture.asset( + Assets.svg.polygon, + width: 11, + height: 11, + ), + label: "Appearance", + value: 6, + group: selectedMenuItem, + onChanged: updateSelectedMenuItem, + ), + const SizedBox( + height: 2, + ), + SettingsMenuItem( + icon: SvgPicture.asset( + Assets.svg.polygon, + width: 11, + height: 11, + ), + label: "Advanced", + value: 7, + group: selectedMenuItem, + onChanged: updateSelectedMenuItem, + ), + ], + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 10, + right: 40, + ), + child: RoundedWhiteContainer( + child: SvgPicture.asset( + Assets.svg.backupAuto, + width: 48, + height: 48, + ), + ), + ), + ) + ], + ), + ], ); } } From d06219d3d978613378b3a7f47ff34ec2ea42c938 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Thu, 13 Oct 2022 16:42:47 -0600 Subject: [PATCH 008/426] added small text --- lib/utilities/text_styles.dart | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/lib/utilities/text_styles.dart b/lib/utilities/text_styles.dart index c3a6929fe..8175cbde8 100644 --- a/lib/utilities/text_styles.dart +++ b/lib/utilities/text_styles.dart @@ -679,6 +679,25 @@ class STextStyles { } } + static TextStyle desktopTextSmall(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).buttonTextPrimaryDisabled, + fontWeight: FontWeight.w500, + fontSize: 18, + height: 27 / 18, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).buttonTextPrimaryDisabled, + fontWeight: FontWeight.w500, + fontSize: 18, + height: 27 / 18, + ); + } + } + static TextStyle desktopTextExtraSmall(BuildContext context) { switch (_theme(context).themeType) { case ThemeType.light: From 8116b267c1c60c9aeee4f8c1f83f0ae981f8dfeb Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 14 Oct 2022 10:49:46 -0600 Subject: [PATCH 009/426] desktop wallet send/receive tabview --- .../wallet_view/desktop_wallet_view.dart | 73 ++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index 785b94dc4..f8cc7e2dc 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.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'; @@ -217,7 +218,45 @@ class _MyWalletState extends State<MyWallet> { Expanded( child: RoundedWhiteContainer( padding: const EdgeInsets.all(0), - child: Container(), + child: DefaultTabController( + length: 2, + child: Column( + children: [ + TabBar( + indicatorColor: Theme.of(context) + .extension<StackColors>()! + .accentColorBlue, + labelStyle: STextStyles.desktopTextExtraSmall(context), + labelColor: Theme.of(context) + .extension<StackColors>()! + .accentColorBlue, + unselectedLabelColor: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + labelPadding: const EdgeInsets.symmetric( + vertical: 6, + ), + splashBorderRadius: BorderRadius.vertical( + top: Radius.circular( + Constants.size.circularBorderRadius, + ), + ), + tabs: const [ + Tab(text: "Send"), + Tab(text: "Receive"), + ], + ), + const Expanded( + child: TabBarView( + children: [ + DesktopSend(), + DesktopReceive(), + ], + ), + ), + ], + ), + ), ), ), ], @@ -225,6 +264,38 @@ class _MyWalletState extends State<MyWallet> { } } +class DesktopReceive extends StatefulWidget { + const DesktopReceive({Key? key}) : super(key: key); + + @override + State<DesktopReceive> createState() => _DesktopReceiveState(); +} + +class _DesktopReceiveState extends State<DesktopReceive> { + @override + Widget build(BuildContext context) { + return Container( + color: Colors.green, + ); + } +} + +class DesktopSend extends StatefulWidget { + const DesktopSend({Key? key}) : super(key: key); + + @override + State<DesktopSend> createState() => _DesktopSendState(); +} + +class _DesktopSendState extends State<DesktopSend> { + @override + Widget build(BuildContext context) { + return Container( + color: Colors.red, + ); + } +} + class RecentDesktopTransactions extends StatefulWidget { const RecentDesktopTransactions({Key? key}) : super(key: key); From 733c81bf90eec2a9c66f73b774bb0607a2343bc2 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Fri, 14 Oct 2022 13:28:43 -0600 Subject: [PATCH 010/426] save changes button fix --- lib/pages/stack_privacy_calls.dart | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/lib/pages/stack_privacy_calls.dart b/lib/pages/stack_privacy_calls.dart index e68045311..eaf7d18d0 100644 --- a/lib/pages/stack_privacy_calls.dart +++ b/lib/pages/stack_privacy_calls.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/pinpad_views/create_pin_view.dart'; -import 'package:stackwallet/pages/settings_views/global_settings_view/advanced_views/advanced_settings_view.dart'; import 'package:stackwallet/pages_desktop_specific/create_password/create_password_view.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -408,7 +407,7 @@ class ContinueButton extends StatelessWidget { @override Widget build(BuildContext context) { - return !isDesktop + return isDesktop ? TextButton( style: Theme.of(context) .extension<StackColors>()! @@ -442,15 +441,7 @@ class ContinueButton extends StatelessWidget { !isSettings ? Navigator.of(context).pushNamed(CreatePinView.routeName) - : Navigator.of(context) - .pushNamed(AdvancedSettingsView.routeName); - - // if (!isSettings) { - // Navigator.of(context).pushNamed(CreatePinView.routeName); - // } - // if (isSettings) { - // Navigator.of(context).pop(); - // } + : Navigator.of(context).pop(); }, child: Text( !isSettings ? "Continue" : "Save changes", From b822519d588ea13f1cb172be9a11121deb227cf0 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 14 Oct 2022 13:40:44 -0600 Subject: [PATCH 011/426] default to easy mode and button size fix --- lib/pages/stack_privacy_calls.dart | 32 ++++++++++++++++-------------- lib/utilities/prefs.dart | 4 +++- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/lib/pages/stack_privacy_calls.dart b/lib/pages/stack_privacy_calls.dart index eaf7d18d0..bf0a9fddb 100644 --- a/lib/pages/stack_privacy_calls.dart +++ b/lib/pages/stack_privacy_calls.dart @@ -393,21 +393,21 @@ class _PrivacyToggleState extends ConsumerState<PrivacyToggle> { } } -class ContinueButton extends StatelessWidget { - const ContinueButton( - {Key? key, - required this.isDesktop, - required this.isSettings, - required this.isEasy}) - : super(key: key); +class ContinueButton extends ConsumerWidget { + const ContinueButton({ + Key? key, + required this.isDesktop, + required this.isSettings, + required this.isEasy, + }) : super(key: key); final bool isDesktop; final bool isSettings; final bool isEasy; @override - Widget build(BuildContext context) { - return isDesktop + Widget build(BuildContext context, WidgetRef ref) { + return !isDesktop ? TextButton( style: Theme.of(context) .extension<StackColors>()! @@ -416,10 +416,11 @@ class ContinueButton extends StatelessWidget { print("Output of isEasy:"); print(isEasy); - Prefs.instance.externalCalls = isEasy; - if (!isSettings) { - Navigator.of(context).pushNamed(CreatePasswordView.routeName); - } + ref.read(prefsChangeNotifierProvider).externalCalls = isEasy; + + !isSettings + ? Navigator.of(context).pushNamed(CreatePinView.routeName) + : Navigator.of(context).pop(); }, child: Text( !isSettings ? "Continue" : "Save changes", @@ -437,10 +438,11 @@ class ContinueButton extends StatelessWidget { print("Output of isEasy:"); print(isEasy); - Prefs.instance.externalCalls = isEasy; + ref.read(prefsChangeNotifierProvider).externalCalls = isEasy; !isSettings - ? Navigator.of(context).pushNamed(CreatePinView.routeName) + ? Navigator.of(context) + .pushNamed(CreatePasswordView.routeName) : Navigator.of(context).pop(); }, child: Text( diff --git a/lib/utilities/prefs.dart b/lib/utilities/prefs.dart index 7e1096550..dc165d4b6 100644 --- a/lib/utilities/prefs.dart +++ b/lib/utilities/prefs.dart @@ -546,7 +546,9 @@ class Prefs extends ChangeNotifier { boxName: DB.boxNamePrefs, key: "startupWalletId") as String?; } - bool _externalCalls = false; + // incognito mode disabled + + bool _externalCalls = true; bool get externalCalls => _externalCalls; From 289f0b89597d7f5df29ae4b082d7684945c185ec Mon Sep 17 00:00:00 2001 From: sneurlax <sneurlax@gmail.com> Date: Thu, 13 Oct 2022 22:13:42 -0500 Subject: [PATCH 012/426] Add mainnet tests for address generation from test mnemonic TODO: Clean up the wallet file as the tests conclude --- .../coins/monero/monero_wallet_test.dart | 207 ++++++++++++++++++ .../coins/monero/monero_wallet_test_data.dart | 14 ++ 2 files changed, 221 insertions(+) create mode 100644 test/services/coins/monero/monero_wallet_test.dart create mode 100644 test/services/coins/monero/monero_wallet_test_data.dart diff --git a/test/services/coins/monero/monero_wallet_test.dart b/test/services/coins/monero/monero_wallet_test.dart new file mode 100644 index 000000000..1ee2fc44a --- /dev/null +++ b/test/services/coins/monero/monero_wallet_test.dart @@ -0,0 +1,207 @@ +import 'dart:async'; +import 'dart:core'; +import 'dart:core' as core; +import 'dart:io'; +import 'dart:math'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:hive/hive.dart'; +import 'package:hive_test/hive_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'package:cw_core/monero_amount_format.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_monero/api/wallet.dart'; +import 'package:cw_monero/pending_monero_transaction.dart'; +import 'package:cw_monero/monero_wallet.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_libmonero/core/key_service.dart'; +import 'package:flutter_libmonero/core/wallet_creation_service.dart'; +import 'package:flutter_libmonero/view_model/send/output.dart'; +import 'package:flutter_libmonero/monero/monero.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:hive/hive.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; + +import 'dart:developer' as developer; + +// TODO trim down to the minimum imports above + +import 'monero_wallet_test_data.dart'; + +//FlutterSecureStorage? storage; +FakeSecureStorage? storage; +WalletService? walletService; +SharedPreferences? prefs; +KeyService? keysStorage; +MoneroWalletBase? walletBase; +late WalletCreationService _walletCreationService; +dynamic _walletInfoSource; + +String name = 'namee${Random().nextInt(10000000)}'; +int nettype = 0; +WalletType type = WalletType.monero; + +@GenerateMocks([]) +void main() async { + storage = FakeSecureStorage(); + prefs = await SharedPreferences.getInstance(); + keysStorage = KeyService(storage!); + WalletInfo walletInfo = WalletInfo.external( + id: '', + name: '', + type: type, + isRecovery: false, + restoreHeight: 0, + date: DateTime.now(), + path: '', + address: '', + dirPath: ''); + late WalletCredentials credentials; + + WidgetsFlutterBinding.ensureInitialized(); + Directory appDir = (await getApplicationDocumentsDirectory()); + if (Platform.isIOS) { + appDir = (await getLibraryDirectory()); + } + await Hive.close(); + Hive.init(appDir.path); + Hive.registerAdapter(NodeAdapter()); + Hive.registerAdapter(WalletInfoAdapter()); + Hive.registerAdapter(WalletTypeAdapter()); + Hive.registerAdapter(UnspentCoinsInfoAdapter()); + + monero.onStartup(); + _walletInfoSource = await Hive.openBox<WalletInfo>(WalletInfo.boxName); + walletService = monero.createMoneroWalletService(_walletInfoSource); + + group("Mainnet tests", () { + setUp(() async { + try { + // if (name?.isEmpty ?? true) { + // name = await generateName(); + // } + final dirPath = await pathForWalletDir(name: name, type: type); + final path = await pathForWallet(name: name, type: type); + credentials = + // // creating a new wallet + // monero.createMoneroNewWalletCredentials( + // name: name, language: "English"); + // restoring a previous wallet + monero.createMoneroRestoreWalletFromSeedCredentials( + name: name, height: 2580000, mnemonic: testMnemonic); + + walletInfo = WalletInfo.external( + id: WalletBase.idFor(name, type), + name: name, + type: type, + isRecovery: false, + restoreHeight: credentials.height ?? 0, + date: DateTime.now(), + path: path, + address: "", + dirPath: dirPath); + credentials.walletInfo = walletInfo; + + _walletCreationService = WalletCreationService( + secureStorage: storage, + sharedPreferences: prefs, + walletService: walletService, + keyService: keysStorage, + ); + _walletCreationService.changeWalletType(); + } catch (e, s) { + print(e); + print(s); + } + }); + + test("Test mainnet address generation from seed", () async { + final wallet = await + // _walletCreationService.create(credentials); + _walletCreationService.restoreFromSeed(credentials); + walletInfo.address = wallet.walletAddresses.address; + //print(walletInfo.address); + + await _walletInfoSource.add(walletInfo); + walletBase?.close(); + walletBase = wallet as MoneroWalletBase; + //print("${walletBase?.seed}"); + + // print(walletBase); + // loggerPrint(walletBase.toString()); + // loggerPrint("name: ${walletBase!.name} seed: ${walletBase!.seed} id: " + // "${walletBase!.id} walletinfo: ${toStringForinfo(walletBase!.walletInfo)} type: ${walletBase!.type} balance: " + // "${walletBase!.balance.entries.first.value.available} currency: ${walletBase!.currency}"); + + expect(walletInfo.address, mainnetTestData[0][0]); + expect( + await walletBase!.getTransactionAddress(0, 0), mainnetTestData[0][0]); + expect( + await walletBase!.getTransactionAddress(0, 1), mainnetTestData[0][1]); + expect( + await walletBase!.getTransactionAddress(0, 2), mainnetTestData[0][2]); + expect( + await walletBase!.getTransactionAddress(1, 0), mainnetTestData[1][0]); + expect( + await walletBase!.getTransactionAddress(1, 1), mainnetTestData[1][1]); + expect( + await walletBase!.getTransactionAddress(1, 2), mainnetTestData[1][2]); + }); + }); + /* + group("Mainnet node tests", () { + test("Test mainnet node connection", () async { + await walletBase?.connectToNode( + node: Node( + uri: "monero-stagenet.stackwallet.com:38081", + type: WalletType.moneroStageNet)); + await walletBase!.rescan( + height: + credentials.height); // Probably shouldn't be rescanning from 0... + await walletBase!.getNodeHeight(); + int height = await walletBase!.getNodeHeight(); + print('height: $height'); + bool connected = await walletBase!.isConnected(); + print('connected: $connected'); + + //expect... + }); + }); + */ + + // TODO test deletion of wallets ... and delete them +} + +Future<String> pathForWalletDir( + {required String name, required WalletType type}) async { + Directory root = (await getApplicationDocumentsDirectory()); + if (Platform.isIOS) { + root = (await getLibraryDirectory()); + } + final prefix = walletTypeToString(type).toLowerCase(); + final walletsDir = Directory('${root.path}/wallets'); + final walletDire = Directory('${walletsDir.path}/$prefix/$name'); + + if (!walletDire.existsSync()) { + walletDire.createSync(recursive: true); + } + + return walletDire.path; +} + +Future<String> pathForWallet( + {required String name, required WalletType type}) async => + await pathForWalletDir(name: name, type: type) + .then((path) => path + '/$name'); diff --git a/test/services/coins/monero/monero_wallet_test_data.dart b/test/services/coins/monero/monero_wallet_test_data.dart new file mode 100644 index 000000000..dc0a0f4cb --- /dev/null +++ b/test/services/coins/monero/monero_wallet_test_data.dart @@ -0,0 +1,14 @@ +String testMnemonic = + 'agreed aquarium wallets uptight karate wonders afoot guys itself nucleus reduce lamb fully fewest bimonthly dazed skulls magically mocked fugitive imbalance saga calamity dialect itself'; +var mainnetTestData = [ + [ + '4AeRgkWZsMJhAWKMeCZ3h4ZSPnAcW5VBtRFyLd6gBEf6GgJU2FHXDA6i1DnQTd6h8R3VU5AkbGcWSNhtSwNNPgaD48gp4nn', + '82WsoLmbZt3BPwJMF5PfT8GitThJzUq3FFoSQyr4fKfJdxZebgY3mHPcnAqTBA3FFwZRGxC4ZDwkfE1VVULPa55x3xXgCbj', + '84kYPuZ1eaVKGQhf26QPNWbSLQG16BywXdLYYShVrPNMLAUAWce5vcpRc78FxwRphrG6Cda7faCKdUMr8fUCH3peHPenvHy' + ], + [ + '86SF44CsTBYU3vk1X7nGBbQnrUSknGbd6Uw8a9hUUgy3KBeXTDvk3pm8upMzZKw17m3mLPEzbcPp5WLpYVoHR5PKNVtFrHH', + '8Aa9LNGdBHwYUMsy6M9ZVXMEkTBZyEDT7aQmY32trCxbU6dwkZJSCSbcpyL7UiTB9QXXosomZtJYvUJ296vTNX5yQ81KaA2', + '85C5zZRcaD89PKmXEwjcYMVAUqoH5rrAXe3GokvSupXnDmccYvZagz5Qem7bQLteEw4iFEJ9oRk9BNfjTi4K2cyTJbTMMPT' + ] +]; From a3a36d02c3a7e6f4ef6b166a5afd6cad55012a25 Mon Sep 17 00:00:00 2001 From: sneurlax <sneurlax@gmail.com> Date: Fri, 14 Oct 2022 10:09:17 -0500 Subject: [PATCH 013/426] Import wallets service and remove wallet at conclusion of tests --- .../coins/monero/monero_wallet_test.dart | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/services/coins/monero/monero_wallet_test.dart b/test/services/coins/monero/monero_wallet_test.dart index 1ee2fc44a..5b802a089 100644 --- a/test/services/coins/monero/monero_wallet_test.dart +++ b/test/services/coins/monero/monero_wallet_test.dart @@ -34,6 +34,8 @@ import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:stackwallet/services/wallets.dart'; + import 'dart:developer' as developer; // TODO trim down to the minimum imports above @@ -48,6 +50,7 @@ KeyService? keysStorage; MoneroWalletBase? walletBase; late WalletCreationService _walletCreationService; dynamic _walletInfoSource; +Wallets? walletsService; String name = 'namee${Random().nextInt(10000000)}'; int nettype = 0; @@ -160,6 +163,22 @@ void main() async { await walletBase!.getTransactionAddress(1, 2), mainnetTestData[1][2]); }); }); + + group("Mainnet wallet deletion test", () { + test("Test mainnet wallet deletion", () async { + // Remove wallet from wallet service + walletService?.remove(name); + walletsService?.removeWallet(walletId: name); + + // TODO test deletion, get code from generation for checking if it already exists + }); + + /* + // wait for widget tree to dispose of any widgets watching the manager + await Future<void>.delayed(const Duration(seconds: 1)); + walletsInstance.removeWallet(walletId: walletId); + */ + }); /* group("Mainnet node tests", () { test("Test mainnet node connection", () async { From b6e51cb954ff1b90765b582487ecb3141fc5c840 Mon Sep 17 00:00:00 2001 From: sneurlax <sneurlax@gmail.com> Date: Fri, 14 Oct 2022 10:24:05 -0500 Subject: [PATCH 014/426] Add template wallet detection and removal code Not enabled as wallet files are not currently being saved; only an empty folder is created and left. TODO clean that up --- .../coins/monero/monero_wallet_test.dart | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/test/services/coins/monero/monero_wallet_test.dart b/test/services/coins/monero/monero_wallet_test.dart index 5b802a089..d54959ab2 100644 --- a/test/services/coins/monero/monero_wallet_test.dart +++ b/test/services/coins/monero/monero_wallet_test.dart @@ -20,6 +20,7 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_monero/api/wallet.dart'; +import 'package:cw_monero/api/wallet_manager.dart' as monero_wallet_manager; import 'package:cw_monero/pending_monero_transaction.dart'; import 'package:cw_monero/monero_wallet.dart'; import 'package:flutter/material.dart'; @@ -52,6 +53,8 @@ late WalletCreationService _walletCreationService; dynamic _walletInfoSource; Wallets? walletsService; +String path = ''; + String name = 'namee${Random().nextInt(10000000)}'; int nettype = 0; WalletType type = WalletType.monero; @@ -96,7 +99,7 @@ void main() async { // name = await generateName(); // } final dirPath = await pathForWalletDir(name: name, type: type); - final path = await pathForWallet(name: name, type: type); + path = await pathForWallet(name: name, type: type); credentials = // // creating a new wallet // monero.createMoneroNewWalletCredentials( @@ -163,23 +166,21 @@ void main() async { await walletBase!.getTransactionAddress(1, 2), mainnetTestData[1][2]); }); }); - + /* + // Not needed; only folder created, wallet files not saved yet. TODO test saving and deleting wallet files and make sure to clean up leftover folder afterwards group("Mainnet wallet deletion test", () { - test("Test mainnet wallet deletion", () async { + test("Test mainnet wallet existence", () { + expect(monero_wallet_manager.isWalletExistSync(path: path), true); + }); + + test("Test mainnet wallet deletion", () { // Remove wallet from wallet service walletService?.remove(name); walletsService?.removeWallet(walletId: name); - - // TODO test deletion, get code from generation for checking if it already exists + expect(monero_wallet_manager.isWalletExistSync(path: path), false); }); - - /* - // wait for widget tree to dispose of any widgets watching the manager - await Future<void>.delayed(const Duration(seconds: 1)); - walletsInstance.removeWallet(walletId: walletId); - */ }); - /* + group("Mainnet node tests", () { test("Test mainnet node connection", () async { await walletBase?.connectToNode( From d00c15de2603f5f0bf2bee4a0871cd2b266e3624 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Fri, 14 Oct 2022 17:07:58 -0600 Subject: [PATCH 015/426] text color change --- lib/utilities/text_styles.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utilities/text_styles.dart b/lib/utilities/text_styles.dart index 8175cbde8..82e2ad349 100644 --- a/lib/utilities/text_styles.dart +++ b/lib/utilities/text_styles.dart @@ -683,7 +683,7 @@ class STextStyles { switch (_theme(context).themeType) { case ThemeType.light: return GoogleFonts.inter( - color: _theme(context).buttonTextPrimaryDisabled, + color: _theme(context).textDark, fontWeight: FontWeight.w500, fontSize: 18, height: 27 / 18, From 038df8280018472dfaf2196b297743558ca771ed Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Fri, 14 Oct 2022 17:08:24 -0600 Subject: [PATCH 016/426] backup_and_restore page --- .../settings_menu/backup_and_restore.dart | 170 ++++++++++++++++++ .../home/settings_menu/settings_menu.dart | 24 +-- 2 files changed, 174 insertions(+), 20 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore.dart index e69de29bb..9030aa7fa 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore.dart @@ -0,0 +1,170 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_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/rounded_white_container.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class BackupRestore extends ConsumerStatefulWidget { + const BackupRestore({Key? key}) : super(key: key); + + static const String routeName = "/backupRestore"; + + @override + ConsumerState<BackupRestore> createState() => _BackupRestore(); +} + +class _BackupRestore extends ConsumerState<BackupRestore> { + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + return ListView( + shrinkWrap: true, + scrollDirection: Axis.vertical, + children: [ + Padding( + padding: const EdgeInsets.only( + right: 30, + ), + child: RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset( + Assets.svg.backupAuto, + width: 48, + height: 48, + alignment: Alignment.topLeft, + ), + Center( + child: Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.start, + text: TextSpan( + children: [ + TextSpan( + text: "Auto Backup", + style: STextStyles.desktopTextSmall(context), + ), + TextSpan( + text: + "\n\nAuto Backup is a custom Stack Wallet feature that offers a convenient backup of your data." + "To ensure maximum security, we recommend using a unique password that you haven't used anywhere " + "else on the internet before. Your password is not stored.", + style: + STextStyles.desktopTextExtraExtraSmall(context), + ), + TextSpan( + text: + "\n\nFor more information, please see our website ", + style: + STextStyles.desktopTextExtraExtraSmall(context), + ), + TextSpan( + text: "stackwallet.com", + style: STextStyles.richLink(context) + .copyWith(fontSize: 14), + recognizer: TapGestureRecognizer() + ..onTap = () { + launchUrl( + Uri.parse("https://stackwallet.com/"), + mode: LaunchMode.externalApplication, + ); + }, + ), + ], + ), + ), + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Padding( + padding: EdgeInsets.all( + 10, + ), + child: EnableBackupButton(), + ), + ], + ), + ], + ), + ), + ), + const SizedBox( + height: 25, + ), + Padding( + padding: const EdgeInsets.only( + right: 30, + ), + child: RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset( + Assets.svg.backupAdd, + width: 48, + height: 48, + alignment: Alignment.topLeft, + ), + Center( + child: Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.start, + text: TextSpan( + children: [ + TextSpan( + text: "Manual Backup", + style: STextStyles.desktopTextSmall(context), + ), + TextSpan( + text: + "\n\nCreate Manual backup to easily transfer your data between devices. " + "You will create a backup file that can be later used in the Restore option. " + "Use a strong password to encrypt your data.", + style: + STextStyles.desktopTextExtraExtraSmall(context), + ), + ], + ), + ), + ), + ), + ], + ), + ), + ), + ], + ); + } +} + +class EnableBackupButton extends ConsumerWidget { + const EnableBackupButton({ + Key? key, + }) : super(key: key); + @override + Widget build(BuildContext context, WidgetRef ref) { + return SizedBox( + width: 200, + height: 48, + child: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + onPressed: () {}, + child: Text( + "Enable Auto Backup", + style: STextStyles.button(context), + ), + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/settings_menu/settings_menu.dart b/lib/pages_desktop_specific/home/settings_menu/settings_menu.dart index 82b32be5b..fb5444d2a 100644 --- a/lib/pages_desktop_specific/home/settings_menu/settings_menu.dart +++ b/lib/pages_desktop_specific/home/settings_menu/settings_menu.dart @@ -1,12 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu_item.dart'; +import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; -import 'package:stackwallet/widgets/rounded_white_container.dart'; - -import '../../../utilities/assets.dart'; -import '../settings_menu_item.dart'; class SettingsMenu extends ConsumerStatefulWidget { const SettingsMenu({ @@ -54,7 +52,8 @@ class _SettingsMenuState extends ConsumerState<SettingsMenu> { ], ), ), - Row( + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.only(left: 15), @@ -173,21 +172,6 @@ class _SettingsMenuState extends ConsumerState<SettingsMenu> { ], ), ), - Expanded( - child: Padding( - padding: const EdgeInsets.only( - left: 10, - right: 40, - ), - child: RoundedWhiteContainer( - child: SvgPicture.asset( - Assets.svg.backupAuto, - width: 48, - height: 48, - ), - ), - ), - ) ], ), ], From c2aeb5bae831eaa6baa964b66d9a821063889c80 Mon Sep 17 00:00:00 2001 From: sneurlax <sneurlax@gmail.com> Date: Fri, 14 Oct 2022 18:38:46 -0500 Subject: [PATCH 017/426] Add Wownero mainnet wallet address generation test --- .../coins/wownero/wownero_wallet_test.dart | 164 ++++++++++++++++++ .../wownero/wownero_wallet_test_data.dart | 14 ++ 2 files changed, 178 insertions(+) create mode 100644 test/services/coins/wownero/wownero_wallet_test.dart create mode 100644 test/services/coins/wownero/wownero_wallet_test_data.dart diff --git a/test/services/coins/wownero/wownero_wallet_test.dart b/test/services/coins/wownero/wownero_wallet_test.dart new file mode 100644 index 000000000..e64dd772c --- /dev/null +++ b/test/services/coins/wownero/wownero_wallet_test.dart @@ -0,0 +1,164 @@ +import 'dart:async'; +import 'dart:core'; +import 'dart:core' as core; +import 'dart:io'; +import 'dart:math'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:hive/hive.dart'; +import 'package:hive_test/hive_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'package:cw_core/monero_amount_format.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_wownero/api/wallet.dart'; +import 'package:cw_wownero/pending_wownero_transaction.dart'; +import 'package:cw_wownero/wownero_wallet.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_libmonero/core/key_service.dart'; +import 'package:flutter_libmonero/core/wallet_creation_service.dart'; +import 'package:flutter_libmonero/view_model/send/output.dart'; +import 'package:flutter_libmonero/wownero/wownero.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:hive/hive.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'wownero_wallet_test_data.dart'; + +FakeSecureStorage? storage; +WalletService? walletService; +SharedPreferences? prefs; +KeyService? keysStorage; +WowneroWalletBase? walletBase; +late WalletCreationService _walletCreationService; +dynamic _walletInfoSource; + +String path = ''; + +String name = 'namee${Random().nextInt(10000000)}'; +int nettype = 0; +WalletType type = WalletType.wownero; + +@GenerateMocks([]) +void main() async { + storage = FakeSecureStorage(); + prefs = await SharedPreferences.getInstance(); + keysStorage = KeyService(storage!); + WalletInfo walletInfo = WalletInfo.external( + id: '', + name: '', + type: type, + isRecovery: false, + restoreHeight: 0, + date: DateTime.now(), + path: '', + address: '', + dirPath: ''); + late WalletCredentials credentials; + + WidgetsFlutterBinding.ensureInitialized(); + Directory appDir = (await getApplicationDocumentsDirectory()); + if (Platform.isIOS) { + appDir = (await getLibraryDirectory()); + } + await Hive.close(); + Hive.init(appDir.path); + Hive.registerAdapter(NodeAdapter()); + Hive.registerAdapter(WalletInfoAdapter()); + Hive.registerAdapter(WalletTypeAdapter()); + Hive.registerAdapter(UnspentCoinsInfoAdapter()); + + wownero.onStartup(); + _walletInfoSource = await Hive.openBox<WalletInfo>(WalletInfo.boxName); + walletService = wownero.createWowneroWalletService(_walletInfoSource); + + group("Wownero tests", () { + setUp(() async { + try { + final dirPath = await pathForWalletDir(name: name, type: type); + path = await pathForWallet(name: name, type: type); + credentials = wownero.createWowneroRestoreWalletFromSeedCredentials( + name: name, height: 465760, mnemonic: testMnemonic); + + walletInfo = WalletInfo.external( + id: WalletBase.idFor(name, type), + name: name, + type: type, + isRecovery: false, + restoreHeight: credentials.height ?? 0, + date: DateTime.now(), + path: path, + address: "", + dirPath: dirPath); + credentials.walletInfo = walletInfo; + + _walletCreationService = WalletCreationService( + secureStorage: storage, + sharedPreferences: prefs, + walletService: walletService, + keyService: keysStorage, + ); + _walletCreationService.changeWalletType(); + } catch (e, s) { + print(e); + print(s); + } + }); + + test("Test mainnet address generation from seed", () async { + final wallet = await _walletCreationService.restoreFromSeed(credentials); + walletInfo.address = wallet.walletAddresses.address; + + await _walletInfoSource.add(walletInfo); + walletBase?.close(); + walletBase = wallet as WowneroWalletBase; + + expect(walletInfo.address, mainnetTestData[0][0]); + expect( + await walletBase!.getTransactionAddress(0, 0), mainnetTestData[0][0]); + expect( + await walletBase!.getTransactionAddress(0, 1), mainnetTestData[0][1]); + expect( + await walletBase!.getTransactionAddress(0, 2), mainnetTestData[0][2]); + expect( + await walletBase!.getTransactionAddress(1, 0), mainnetTestData[1][0]); + expect( + await walletBase!.getTransactionAddress(1, 1), mainnetTestData[1][1]); + expect( + await walletBase!.getTransactionAddress(1, 2), mainnetTestData[1][2]); + }); + }); +} + +Future<String> pathForWalletDir( + {required String name, required WalletType type}) async { + Directory root = (await getApplicationDocumentsDirectory()); + if (Platform.isIOS) { + root = (await getLibraryDirectory()); + } + final prefix = walletTypeToString(type).toLowerCase(); + final walletsDir = Directory('${root.path}/wallets'); + final walletDire = Directory('${walletsDir.path}/$prefix/$name'); + + if (!walletDire.existsSync()) { + walletDire.createSync(recursive: true); + } + + return walletDire.path; +} + +Future<String> pathForWallet( + {required String name, required WalletType type}) async => + await pathForWalletDir(name: name, type: type) + .then((path) => path + '/$name'); diff --git a/test/services/coins/wownero/wownero_wallet_test_data.dart b/test/services/coins/wownero/wownero_wallet_test_data.dart new file mode 100644 index 000000000..b0d93a448 --- /dev/null +++ b/test/services/coins/wownero/wownero_wallet_test_data.dart @@ -0,0 +1,14 @@ +String testMnemonic = + 'weather cruise school such silly profit clerk wage reduce obtain ill sand episode shadow'; +var mainnetTestData = [ + [ + 'Wo3jmHvTMLwE6h29fpgcb8PbJSpaKuqM7XTXVfiiu8bLCZsJvrQCbQSJR48Vo3BWNQKsMsXZ4VixndXTH25QtorC27NCjmsEi', + 'WW3K54QzmMFB1uTZh3LVvgQYqANLmX1FkJHLJ4sU1E7BQmp8nGizyBnjNXSgsjCa4BQ3Rw3GG5jw1ByUkaUjSywm2KmHAbFvK', + 'WW3e3F51KAojcSW2G5WimmE1WVFsbBHc6HppZFBa6dNiEn21cThXzdGGDbpv89aTKXSRSPSFaetK6HgCozYawaYz2knUi9Hmn' + ], + [ + 'WW2nx7MFruyN2CcXnGnMbDdvqsyZUGQthLWKYPkQ4iM9XCE54RyWVjNjgopryUbyi9WKzYhHDai2wENbh1Jh1UHa28CL72TYt', + 'WW34p57QBMoD6MEZVTu5u9R7G3KeYqvN4eYbvHLYsgbWXpLe992fBvVB7ANJNvaGmPg2uwY5oKjwKbpo4fDU6cGS231PmvXrZ', + 'WW2KQLLt6gjC9gRsC4NGehbAZX6UPU7sK89UQFwSg3NKj3MXPwnjh5BiJVqYYNQb6JNsfa7oP7eDjLagtLa2H6YP11RhUNQqw' + ] +]; From b09ee79865f901c603d9b8c316ec4c2edd14afe4 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 17 Oct 2022 09:07:17 -0600 Subject: [PATCH 018/426] backup and restore page --- .../home/desktop_settings_view.dart | 8 +- .../settings_menu/backup_and_restore.dart | 118 +++++++++++++++++- lib/route_generator.dart | 7 ++ 3 files changed, 127 insertions(+), 6 deletions(-) diff --git a/lib/pages_desktop_specific/home/desktop_settings_view.dart b/lib/pages_desktop_specific/home/desktop_settings_view.dart index bfe9272f2..ce51dfbe7 100644 --- a/lib/pages_desktop_specific/home/desktop_settings_view.dart +++ b/lib/pages_desktop_specific/home/desktop_settings_view.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/settings_menu.dart'; +import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; class DesktopSettingsView extends ConsumerStatefulWidget { @@ -16,8 +18,9 @@ class DesktopSettingsView extends ConsumerStatefulWidget { class _DesktopSettingsViewState extends ConsumerState<DesktopSettingsView> { int currentViewIndex = 0; final List<Widget> contentViews = [ - Container( - color: Colors.lime, + const Navigator( + onGenerateRoute: RouteGenerator.generateRoute, + initialRoute: BackupRestore.routeName, ), //b+r Container( color: Colors.green, @@ -48,7 +51,6 @@ class _DesktopSettingsViewState extends ConsumerState<DesktopSettingsView> { }); } - // will have a row with two items: SettingsMenu and settings contentxd @override Widget build(BuildContext context) { return Material( diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore.dart index 9030aa7fa..37b27bb97 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore.dart @@ -88,7 +88,7 @@ class _BackupRestore extends ConsumerState<BackupRestore> { padding: EdgeInsets.all( 10, ), - child: EnableBackupButton(), + child: AutoBackupButton(), ), ], ), @@ -137,6 +137,72 @@ class _BackupRestore extends ConsumerState<BackupRestore> { ), ), ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Padding( + padding: EdgeInsets.all( + 10, + ), + child: ManualBackupButton(), + ), + ], + ), + ], + ), + ), + ), + const SizedBox( + height: 25, + ), + Padding( + padding: const EdgeInsets.only( + right: 30, + ), + child: RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset( + Assets.svg.backupRestore, + width: 48, + height: 48, + alignment: Alignment.topLeft, + ), + Center( + child: Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.start, + text: TextSpan( + children: [ + TextSpan( + text: "Restore Backup", + style: STextStyles.desktopTextSmall(context), + ), + TextSpan( + text: + "\n\nUse your Stack Wallet backup file to restore your wallets, address book " + "and wallet preferences.", + style: + STextStyles.desktopTextExtraExtraSmall(context), + ), + ], + ), + ), + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Padding( + padding: EdgeInsets.all( + 10, + ), + child: ManualBackupButton(), + ), + ], + ), ], ), ), @@ -146,8 +212,8 @@ class _BackupRestore extends ConsumerState<BackupRestore> { } } -class EnableBackupButton extends ConsumerWidget { - const EnableBackupButton({ +class AutoBackupButton extends ConsumerWidget { + const AutoBackupButton({ Key? key, }) : super(key: key); @override @@ -168,3 +234,49 @@ class EnableBackupButton extends ConsumerWidget { ); } } + +class ManualBackupButton extends ConsumerWidget { + const ManualBackupButton({ + Key? key, + }) : super(key: key); + @override + Widget build(BuildContext context, WidgetRef ref) { + return SizedBox( + width: 200, + height: 48, + child: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + onPressed: () {}, + child: Text( + "Create Manual Backup", + style: STextStyles.button(context), + ), + ), + ); + } +} + +class RestoreBackupButton extends ConsumerWidget { + const RestoreBackupButton({ + Key? key, + }) : super(key: key); + @override + Widget build(BuildContext context, WidgetRef ref) { + return SizedBox( + width: 200, + height: 48, + child: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + onPressed: () {}, + child: Text( + "Restore Backup", + style: STextStyles.button(context), + ), + ), + ); + } +} diff --git a/lib/route_generator.dart b/lib/route_generator.dart index b4ca9ef92..ef3b143bd 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -87,6 +87,7 @@ import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_settings_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/settings_menu.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; @@ -992,6 +993,12 @@ class RouteGenerator { ), settings: RouteSettings(name: settings.name)); + case BackupRestore.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const BackupRestore(), + settings: RouteSettings(name: settings.name)); + // == End of desktop specific routes ===================================== default: From ab28a9369950747c4801a637651bc01529644406 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 17 Oct 2022 10:10:16 -0600 Subject: [PATCH 019/426] WIP: icon color change --- .../home/desktop_settings_view.dart | 1 + .../home/settings_menu/settings_menu.dart | 41 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/lib/pages_desktop_specific/home/desktop_settings_view.dart b/lib/pages_desktop_specific/home/desktop_settings_view.dart index ce51dfbe7..10f5c683b 100644 --- a/lib/pages_desktop_specific/home/desktop_settings_view.dart +++ b/lib/pages_desktop_specific/home/desktop_settings_view.dart @@ -19,6 +19,7 @@ class _DesktopSettingsViewState extends ConsumerState<DesktopSettingsView> { int currentViewIndex = 0; final List<Widget> contentViews = [ const Navigator( + key: Key("settingsBackupRestoreDesktopKey"), onGenerateRoute: RouteGenerator.generateRoute, initialRoute: BackupRestore.routeName, ), //b+r diff --git a/lib/pages_desktop_specific/home/settings_menu/settings_menu.dart b/lib/pages_desktop_specific/home/settings_menu/settings_menu.dart index fb5444d2a..178afb2db 100644 --- a/lib/pages_desktop_specific/home/settings_menu/settings_menu.dart +++ b/lib/pages_desktop_specific/home/settings_menu/settings_menu.dart @@ -4,6 +4,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu_item.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/desktop/desktop_app_bar.dart'; class SettingsMenu extends ConsumerStatefulWidget { @@ -65,6 +66,11 @@ class _SettingsMenuState extends ConsumerState<SettingsMenu> { Assets.svg.polygon, width: 11, height: 11, + color: selectedMenuItem == 0 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Colors.transparent, ), label: "Backup and restore", value: 0, @@ -79,6 +85,11 @@ class _SettingsMenuState extends ConsumerState<SettingsMenu> { Assets.svg.polygon, width: 11, height: 11, + color: selectedMenuItem == 1 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Colors.transparent, ), label: "Security", value: 1, @@ -93,6 +104,11 @@ class _SettingsMenuState extends ConsumerState<SettingsMenu> { Assets.svg.polygon, width: 11, height: 11, + color: selectedMenuItem == 2 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Colors.transparent, ), label: "Currency", value: 2, @@ -107,6 +123,11 @@ class _SettingsMenuState extends ConsumerState<SettingsMenu> { Assets.svg.polygon, width: 11, height: 11, + color: selectedMenuItem == 3 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Colors.transparent, ), label: "Language", value: 3, @@ -121,6 +142,11 @@ class _SettingsMenuState extends ConsumerState<SettingsMenu> { Assets.svg.polygon, width: 11, height: 11, + color: selectedMenuItem == 4 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Colors.transparent, ), label: "Nodes", value: 4, @@ -135,6 +161,11 @@ class _SettingsMenuState extends ConsumerState<SettingsMenu> { Assets.svg.polygon, width: 11, height: 11, + color: selectedMenuItem == 5 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Colors.transparent, ), label: "Syncing preferences", value: 5, @@ -149,6 +180,11 @@ class _SettingsMenuState extends ConsumerState<SettingsMenu> { Assets.svg.polygon, width: 11, height: 11, + color: selectedMenuItem == 6 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Colors.transparent, ), label: "Appearance", value: 6, @@ -163,6 +199,11 @@ class _SettingsMenuState extends ConsumerState<SettingsMenu> { Assets.svg.polygon, width: 11, height: 11, + color: selectedMenuItem == 7 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Colors.transparent, ), label: "Advanced", value: 7, From 22473c16685ffc84788f49e6abebd390c6d744a4 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 17 Oct 2022 10:50:37 -0600 Subject: [PATCH 020/426] desktop settings navigation fix --- .../home/desktop_home_view.dart | 4 +- .../home/desktop_settings_view.dart | 24 +- .../home/settings_menu/settings_menu.dart | 327 ++++++++---------- 3 files changed, 176 insertions(+), 179 deletions(-) diff --git a/lib/pages_desktop_specific/home/desktop_home_view.dart b/lib/pages_desktop_specific/home/desktop_home_view.dart index 3941fc6de..41f4b5041 100644 --- a/lib/pages_desktop_specific/home/desktop_home_view.dart +++ b/lib/pages_desktop_specific/home/desktop_home_view.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_menu.dart'; +import 'package:stackwallet/pages_desktop_specific/home/desktop_settings_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_view.dart'; -import 'package:stackwallet/pages_desktop_specific/home/settings_menu/settings_menu.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -35,7 +35,7 @@ class _DesktopHomeViewState extends ConsumerState<DesktopHomeView> { const Navigator( key: Key("desktopSettingHomeKey"), onGenerateRoute: RouteGenerator.generateRoute, - initialRoute: SettingsMenu.routeName, + initialRoute: DesktopSettingsView.routeName, ), Container( color: Colors.blue, diff --git a/lib/pages_desktop_specific/home/desktop_settings_view.dart b/lib/pages_desktop_specific/home/desktop_settings_view.dart index 10f5c683b..cd11eff3d 100644 --- a/lib/pages_desktop_specific/home/desktop_settings_view.dart +++ b/lib/pages_desktop_specific/home/desktop_settings_view.dart @@ -3,7 +3,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/settings_menu.dart'; import 'package:stackwallet/route_generator.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; class DesktopSettingsView extends ConsumerStatefulWidget { const DesktopSettingsView({Key? key}) : super(key: key); @@ -54,9 +57,24 @@ class _DesktopSettingsViewState extends ConsumerState<DesktopSettingsView> { @override Widget build(BuildContext context) { - return Material( - color: Theme.of(context).extension<StackColors>()!.background, - child: Row( + return DesktopScaffold( + background: Theme.of(context).extension<StackColors>()!.background, + appBar: DesktopAppBar( + isCompactHeight: true, + leading: Row( + children: [ + const SizedBox( + width: 24, + height: 24, + ), + Text( + "Settings", + style: STextStyles.desktopH3(context), + ) + ], + ), + ), + body: Row( children: [ SettingsMenu( onSelectionChanged: onMenuSelectionChanged, diff --git a/lib/pages_desktop_specific/home/settings_menu/settings_menu.dart b/lib/pages_desktop_specific/home/settings_menu/settings_menu.dart index 178afb2db..b743fdb39 100644 --- a/lib/pages_desktop_specific/home/settings_menu/settings_menu.dart +++ b/lib/pages_desktop_specific/home/settings_menu/settings_menu.dart @@ -3,9 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu_item.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/desktop/desktop_app_bar.dart'; class SettingsMenu extends ConsumerStatefulWidget { const SettingsMenu({ @@ -37,184 +35,165 @@ class _SettingsMenuState extends ConsumerState<SettingsMenu> { debugPrint("BUILD: $runtimeType"); return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - DesktopAppBar( - isCompactHeight: true, - leading: Row( + Padding( + padding: const EdgeInsets.only(left: 15), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox( - width: 24, - height: 24, + SettingsMenuItem( + icon: SvgPicture.asset( + Assets.svg.polygon, + width: 11, + height: 11, + color: selectedMenuItem == 0 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Colors.transparent, + ), + label: "Backup and restore", + value: 0, + group: selectedMenuItem, + onChanged: updateSelectedMenuItem, + ), + const SizedBox( + height: 2, + ), + SettingsMenuItem( + icon: SvgPicture.asset( + Assets.svg.polygon, + width: 11, + height: 11, + color: selectedMenuItem == 1 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Colors.transparent, + ), + label: "Security", + value: 1, + group: selectedMenuItem, + onChanged: updateSelectedMenuItem, + ), + const SizedBox( + height: 2, + ), + SettingsMenuItem( + icon: SvgPicture.asset( + Assets.svg.polygon, + width: 11, + height: 11, + color: selectedMenuItem == 2 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Colors.transparent, + ), + label: "Currency", + value: 2, + group: selectedMenuItem, + onChanged: updateSelectedMenuItem, + ), + const SizedBox( + height: 2, + ), + SettingsMenuItem( + icon: SvgPicture.asset( + Assets.svg.polygon, + width: 11, + height: 11, + color: selectedMenuItem == 3 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Colors.transparent, + ), + label: "Language", + value: 3, + group: selectedMenuItem, + onChanged: updateSelectedMenuItem, + ), + const SizedBox( + height: 2, + ), + SettingsMenuItem( + icon: SvgPicture.asset( + Assets.svg.polygon, + width: 11, + height: 11, + color: selectedMenuItem == 4 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Colors.transparent, + ), + label: "Nodes", + value: 4, + group: selectedMenuItem, + onChanged: updateSelectedMenuItem, + ), + const SizedBox( + height: 2, + ), + SettingsMenuItem( + icon: SvgPicture.asset( + Assets.svg.polygon, + width: 11, + height: 11, + color: selectedMenuItem == 5 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Colors.transparent, + ), + label: "Syncing preferences", + value: 5, + group: selectedMenuItem, + onChanged: updateSelectedMenuItem, + ), + const SizedBox( + height: 2, + ), + SettingsMenuItem( + icon: SvgPicture.asset( + Assets.svg.polygon, + width: 11, + height: 11, + color: selectedMenuItem == 6 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Colors.transparent, + ), + label: "Appearance", + value: 6, + group: selectedMenuItem, + onChanged: updateSelectedMenuItem, + ), + const SizedBox( + height: 2, + ), + SettingsMenuItem( + icon: SvgPicture.asset( + Assets.svg.polygon, + width: 11, + height: 11, + color: selectedMenuItem == 7 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Colors.transparent, + ), + label: "Advanced", + value: 7, + group: selectedMenuItem, + onChanged: updateSelectedMenuItem, ), - Text( - "Settings", - style: STextStyles.desktopH3(context), - ) ], ), ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(left: 15), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SettingsMenuItem( - icon: SvgPicture.asset( - Assets.svg.polygon, - width: 11, - height: 11, - color: selectedMenuItem == 0 - ? Theme.of(context) - .extension<StackColors>()! - .accentColorBlue - : Colors.transparent, - ), - label: "Backup and restore", - value: 0, - group: selectedMenuItem, - onChanged: updateSelectedMenuItem, - ), - const SizedBox( - height: 2, - ), - SettingsMenuItem( - icon: SvgPicture.asset( - Assets.svg.polygon, - width: 11, - height: 11, - color: selectedMenuItem == 1 - ? Theme.of(context) - .extension<StackColors>()! - .accentColorBlue - : Colors.transparent, - ), - label: "Security", - value: 1, - group: selectedMenuItem, - onChanged: updateSelectedMenuItem, - ), - const SizedBox( - height: 2, - ), - SettingsMenuItem( - icon: SvgPicture.asset( - Assets.svg.polygon, - width: 11, - height: 11, - color: selectedMenuItem == 2 - ? Theme.of(context) - .extension<StackColors>()! - .accentColorBlue - : Colors.transparent, - ), - label: "Currency", - value: 2, - group: selectedMenuItem, - onChanged: updateSelectedMenuItem, - ), - const SizedBox( - height: 2, - ), - SettingsMenuItem( - icon: SvgPicture.asset( - Assets.svg.polygon, - width: 11, - height: 11, - color: selectedMenuItem == 3 - ? Theme.of(context) - .extension<StackColors>()! - .accentColorBlue - : Colors.transparent, - ), - label: "Language", - value: 3, - group: selectedMenuItem, - onChanged: updateSelectedMenuItem, - ), - const SizedBox( - height: 2, - ), - SettingsMenuItem( - icon: SvgPicture.asset( - Assets.svg.polygon, - width: 11, - height: 11, - color: selectedMenuItem == 4 - ? Theme.of(context) - .extension<StackColors>()! - .accentColorBlue - : Colors.transparent, - ), - label: "Nodes", - value: 4, - group: selectedMenuItem, - onChanged: updateSelectedMenuItem, - ), - const SizedBox( - height: 2, - ), - SettingsMenuItem( - icon: SvgPicture.asset( - Assets.svg.polygon, - width: 11, - height: 11, - color: selectedMenuItem == 5 - ? Theme.of(context) - .extension<StackColors>()! - .accentColorBlue - : Colors.transparent, - ), - label: "Syncing preferences", - value: 5, - group: selectedMenuItem, - onChanged: updateSelectedMenuItem, - ), - const SizedBox( - height: 2, - ), - SettingsMenuItem( - icon: SvgPicture.asset( - Assets.svg.polygon, - width: 11, - height: 11, - color: selectedMenuItem == 6 - ? Theme.of(context) - .extension<StackColors>()! - .accentColorBlue - : Colors.transparent, - ), - label: "Appearance", - value: 6, - group: selectedMenuItem, - onChanged: updateSelectedMenuItem, - ), - const SizedBox( - height: 2, - ), - SettingsMenuItem( - icon: SvgPicture.asset( - Assets.svg.polygon, - width: 11, - height: 11, - color: selectedMenuItem == 7 - ? Theme.of(context) - .extension<StackColors>()! - .accentColorBlue - : Colors.transparent, - ), - label: "Advanced", - value: 7, - group: selectedMenuItem, - onChanged: updateSelectedMenuItem, - ), - ], - ), - ), - ], - ), ], ); } From 5da68eb8571cc6703064aa7de9167c477f3d87c0 Mon Sep 17 00:00:00 2001 From: Marco <marco@cypherstack.com> Date: Wed, 19 Oct 2022 09:34:04 -0600 Subject: [PATCH 021/426] stack privacy --- lib/pages/stack_privacy_calls.dart | 99 +++++++++++------------------- 1 file changed, 37 insertions(+), 62 deletions(-) diff --git a/lib/pages/stack_privacy_calls.dart b/lib/pages/stack_privacy_calls.dart index 7160a60b2..2aa2a5c8a 100644 --- a/lib/pages/stack_privacy_calls.dart +++ b/lib/pages/stack_privacy_calls.dart @@ -6,7 +6,6 @@ import 'package:stackwallet/pages_desktop_specific/create_password/create_passwo import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; @@ -218,7 +217,6 @@ class _PrivacyToggleState extends State<PrivacyToggle> { children: [ Expanded( child: RawMaterialButton( - elevation: 0, fillColor: Theme.of(context).extension<StackColors>()!.popupBG, shape: RoundedRectangleBorder( side: !externalCallsEnabled @@ -250,13 +248,10 @@ class _PrivacyToggleState extends State<PrivacyToggle> { Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: SvgPicture.asset( - Assets.svg.personaEasy, - width: 140, - height: 140, - ), + SvgPicture.asset( + Assets.svg.personaEasy, + width: 140, + height: 140, ), Center( child: Text( @@ -343,13 +338,10 @@ class _PrivacyToggleState extends State<PrivacyToggle> { Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: SvgPicture.asset( - Assets.svg.personaIncognito, - width: 140, - height: 140, - ), + SvgPicture.asset( + Assets.svg.personaIncognito, + width: 140, + height: 140, ), Center( child: Text( @@ -409,8 +401,8 @@ class ContinueButton extends ConsumerWidget { const ContinueButton({ Key? key, required this.isDesktop, - required this.isSettings, - required this.isEasy, + required this.onPressed, + required this.label, }) : super(key: key); final String label; @@ -419,50 +411,33 @@ class ContinueButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return !isDesktop - ? TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () { - print("Output of isEasy:"); - print(isEasy); - - ref.read(prefsChangeNotifierProvider).externalCalls = isEasy; - - !isSettings - ? Navigator.of(context).pushNamed(CreatePinView.routeName) - : Navigator.of(context).pop(); - }, - child: Text( - !isSettings ? "Continue" : "Save changes", - style: STextStyles.button(context), - ), - ) - : SizedBox( - width: 328, - height: 70, - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () { - print("Output of isEasy:"); - print(isEasy); - - ref.read(prefsChangeNotifierProvider).externalCalls = isEasy; - - !isSettings - ? Navigator.of(context) - .pushNamed(CreatePasswordView.routeName) - : Navigator.of(context).pop(); - }, - child: Text( - !isSettings ? "Continue" : "Save changes", - style: STextStyles.button(context).copyWith(fontSize: 20), - ), - ), - ); + if (isDesktop) { + return SizedBox( + width: 328, + height: 70, + child: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + onPressed: onPressed, + child: Text( + label, + style: STextStyles.button(context).copyWith(fontSize: 20), + ), + ), + ); + } else { + return TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + onPressed: onPressed, + child: Text( + label, + style: STextStyles.button(context), + ), + ); + } } } From 78d741374c4c6801980691eb994df9b3d7b3c708 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Wed, 19 Oct 2022 16:10:34 -0600 Subject: [PATCH 022/426] settings menu options ui --- assets/svg/Button.svg | 6 + assets/svg/dollar-sign-circle.svg | 4 + assets/svg/language-circle.svg | 11 ++ assets/svg/lock-circle.svg | 11 ++ assets/svg/rotate-circle.svg | 4 + assets/svg/sun-circle.svg | 11 ++ .../home/settings_menu/advanced_settings.dart | 104 +++++++++++++++++ .../settings_menu/appearance_settings.dart | 71 ++++++++++++ ....dart => backup_and_restore_settings.dart} | 22 ++-- .../home/settings_menu/currency_settings.dart | 102 +++++++++++++++++ .../home/settings_menu/language_settings.dart | 105 +++++++++++++++++ .../home/settings_menu/security_settings.dart | 102 +++++++++++++++++ .../syncing_preferences_settings.dart | 106 ++++++++++++++++++ lib/route_generator.dart | 58 ++++++++-- lib/utilities/assets.dart | 6 + 15 files changed, 704 insertions(+), 19 deletions(-) create mode 100644 assets/svg/Button.svg create mode 100644 assets/svg/dollar-sign-circle.svg create mode 100644 assets/svg/language-circle.svg create mode 100644 assets/svg/lock-circle.svg create mode 100644 assets/svg/rotate-circle.svg create mode 100644 assets/svg/sun-circle.svg create mode 100644 lib/pages_desktop_specific/home/settings_menu/advanced_settings.dart create mode 100644 lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart rename lib/pages_desktop_specific/home/settings_menu/{backup_and_restore.dart => backup_and_restore_settings.dart} (93%) create mode 100644 lib/pages_desktop_specific/home/settings_menu/currency_settings.dart create mode 100644 lib/pages_desktop_specific/home/settings_menu/language_settings.dart create mode 100644 lib/pages_desktop_specific/home/settings_menu/security_settings.dart create mode 100644 lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart diff --git a/assets/svg/Button.svg b/assets/svg/Button.svg new file mode 100644 index 000000000..37e0d359b --- /dev/null +++ b/assets/svg/Button.svg @@ -0,0 +1,6 @@ +<svg width="91" height="38" viewBox="0 0 91 38" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.5"> +<rect y="0.5" width="91" height="37" rx="8" fill="#E0E3E3"/> +<path d="M20.7663 24.5H17.4702V14.3182H20.8707C21.8684 14.3182 22.7251 14.522 23.4411 14.9297C24.157 15.334 24.7055 15.9157 25.0866 16.6747C25.4711 17.4304 25.6634 18.3369 25.6634 19.3942C25.6634 20.4548 25.4695 21.3662 25.0817 22.1286C24.6972 22.8909 24.1404 23.4775 23.4112 23.8885C22.6821 24.2962 21.8004 24.5 20.7663 24.5ZM19.0064 23.1577H20.6818C21.4574 23.1577 22.102 23.0118 22.6158 22.7202C23.1295 22.4252 23.514 21.9993 23.7692 21.4425C24.0244 20.8823 24.152 20.1996 24.152 19.3942C24.152 18.5954 24.0244 17.9176 23.7692 17.3608C23.5173 16.804 23.1411 16.3814 22.6406 16.093C22.1402 15.8047 21.5187 15.6605 20.7763 15.6605H19.0064V23.1577ZM27.4544 24.5V16.8636H28.9409V24.5H27.4544ZM28.2051 15.6854C27.9466 15.6854 27.7245 15.5992 27.5389 15.4268C27.3566 15.2512 27.2654 15.0424 27.2654 14.8004C27.2654 14.5552 27.3566 14.3464 27.5389 14.174C27.7245 13.9983 27.9466 13.9105 28.2051 13.9105C28.4636 13.9105 28.684 13.9983 28.8663 14.174C29.0519 14.3464 29.1447 14.5552 29.1447 14.8004C29.1447 15.0424 29.0519 15.2512 28.8663 15.4268C28.684 15.5992 28.4636 15.6854 28.2051 15.6854ZM36.663 18.728L35.3157 18.9666C35.2594 18.7943 35.1699 18.6302 35.0472 18.4744C34.9279 18.3187 34.7655 18.1911 34.56 18.0916C34.3545 17.9922 34.0977 17.9425 33.7894 17.9425C33.3685 17.9425 33.0172 18.0369 32.7354 18.2259C32.4537 18.4115 32.3129 18.6518 32.3129 18.9467C32.3129 19.2019 32.4073 19.4074 32.5962 19.5632C32.7852 19.719 33.0901 19.8466 33.511 19.946L34.7241 20.2244C35.4267 20.3868 35.9504 20.6371 36.2951 20.9751C36.6398 21.3132 36.8121 21.7524 36.8121 22.2926C36.8121 22.75 36.6796 23.1577 36.4144 23.5156C36.1526 23.8703 35.7863 24.1487 35.3157 24.3509C34.8484 24.553 34.3065 24.6541 33.69 24.6541C32.8349 24.6541 32.1372 24.4718 31.5969 24.1072C31.0567 23.7393 30.7253 23.2173 30.6026 22.5412L32.0394 22.3224C32.1289 22.697 32.3129 22.9804 32.5913 23.1726C32.8697 23.3615 33.2326 23.456 33.68 23.456C34.1673 23.456 34.5567 23.3549 34.8484 23.1527C35.14 22.9472 35.2859 22.697 35.2859 22.402C35.2859 22.1634 35.1964 21.9628 35.0174 21.8004C34.8417 21.638 34.5716 21.5154 34.207 21.4325L32.9144 21.1491C32.2018 20.9867 31.6748 20.7282 31.3335 20.3736C30.9954 20.0189 30.8263 19.5698 30.8263 19.0263C30.8263 18.5755 30.9523 18.1811 31.2042 17.843C31.4561 17.505 31.8041 17.2415 32.2482 17.0526C32.6924 16.8603 33.2011 16.7642 33.7745 16.7642C34.5998 16.7642 35.2494 16.9432 35.7234 17.3011C36.1973 17.6558 36.5105 18.1314 36.663 18.728ZM40.6689 24.669C40.185 24.669 39.7475 24.5795 39.3564 24.4006C38.9653 24.2183 38.6554 23.9548 38.4267 23.6101C38.2013 23.2654 38.0886 22.8428 38.0886 22.3423C38.0886 21.9115 38.1715 21.5568 38.3372 21.2784C38.5029 21 38.7266 20.7796 39.0083 20.6172C39.2901 20.4548 39.6049 20.3321 39.9529 20.2493C40.301 20.1664 40.6556 20.1035 41.0169 20.0604C41.4743 20.0073 41.8455 19.9643 42.1305 19.9311C42.4155 19.8946 42.6227 19.8366 42.752 19.7571C42.8812 19.6776 42.9458 19.5483 42.9458 19.3693V19.3345C42.9458 18.9003 42.8232 18.5639 42.5779 18.3253C42.336 18.0866 41.9747 17.9673 41.4941 17.9673C40.9937 17.9673 40.5993 18.0784 40.3109 18.3004C40.0259 18.5192 39.8287 18.7628 39.7193 19.0312L38.3223 18.7131C38.488 18.2491 38.7299 17.8745 39.0481 17.5895C39.3696 17.3011 39.7392 17.0923 40.1568 16.9631C40.5744 16.8305 41.0136 16.7642 41.4743 16.7642C41.7792 16.7642 42.1023 16.8007 42.4437 16.8736C42.7884 16.9432 43.1099 17.0724 43.4082 17.2614C43.7098 17.4503 43.9567 17.7204 44.149 18.0717C44.3412 18.4197 44.4373 18.8722 44.4373 19.429V24.5H42.9856V23.456H42.926C42.8298 23.6482 42.6857 23.8371 42.4934 24.0227C42.3012 24.2083 42.0543 24.3625 41.7527 24.4851C41.4511 24.6077 41.0898 24.669 40.6689 24.669ZM40.992 23.4759C41.403 23.4759 41.7543 23.3946 42.046 23.2322C42.341 23.0698 42.5647 22.8577 42.7172 22.5959C42.8729 22.3307 42.9508 22.0473 42.9508 21.7457V20.7614C42.8978 20.8144 42.795 20.8641 42.6426 20.9105C42.4934 20.9536 42.3227 20.9917 42.1305 21.0249C41.9383 21.0547 41.751 21.0829 41.5687 21.1094C41.3864 21.1326 41.234 21.1525 41.1113 21.169C40.823 21.2055 40.5595 21.2668 40.3208 21.353C40.0855 21.4392 39.8966 21.5634 39.7541 21.7259C39.6149 21.8849 39.5453 22.0971 39.5453 22.3622C39.5453 22.7301 39.6812 23.0085 39.9529 23.1974C40.2247 23.383 40.5711 23.4759 40.992 23.4759ZM46.5366 24.5V14.3182H48.0231V18.1016H48.1126C48.1987 17.9425 48.323 17.7585 48.4854 17.5497C48.6478 17.3409 48.8732 17.1586 49.1616 17.0028C49.4499 16.8438 49.8311 16.7642 50.305 16.7642C50.9215 16.7642 51.4717 16.92 51.9556 17.2315C52.4395 17.5431 52.819 17.9922 53.0941 18.5788C53.3725 19.1655 53.5117 19.8714 53.5117 20.6967C53.5117 21.522 53.3742 22.2296 53.0991 22.8196C52.824 23.4062 52.4461 23.8587 51.9656 24.1768C51.485 24.4917 50.9364 24.6491 50.32 24.6491C49.8559 24.6491 49.4764 24.5713 49.1815 24.4155C48.8898 24.2597 48.6611 24.0774 48.4954 23.8686C48.3297 23.6598 48.2021 23.4742 48.1126 23.3118H47.9883V24.5H46.5366ZM47.9933 20.6818C47.9933 21.2187 48.0711 21.6894 48.2269 22.0938C48.3827 22.4981 48.6081 22.8146 48.9031 23.0433C49.198 23.2687 49.5593 23.3814 49.9869 23.3814C50.431 23.3814 50.8022 23.2637 51.1005 23.0284C51.3988 22.7898 51.6242 22.4666 51.7766 22.0589C51.9324 21.6513 52.0103 21.1922 52.0103 20.6818C52.0103 20.178 51.9341 19.7256 51.7816 19.3246C51.6325 18.9235 51.4071 18.607 51.1055 18.375C50.8072 18.143 50.4343 18.027 49.9869 18.027C49.556 18.027 49.1914 18.138 48.8931 18.3601C48.5981 18.5821 48.3744 18.892 48.2219 19.2898C48.0695 19.6875 47.9933 20.1515 47.9933 20.6818ZM56.6674 14.3182V24.5H55.1809V14.3182H56.6674ZM61.9585 24.6541C61.2061 24.6541 60.5581 24.4934 60.0146 24.1719C59.4743 23.8471 59.0567 23.3913 58.7617 22.8047C58.4701 22.2147 58.3242 21.5237 58.3242 20.7315C58.3242 19.9493 58.4701 19.2599 58.7617 18.6634C59.0567 18.0668 59.4677 17.6011 59.9947 17.2663C60.525 16.9316 61.1448 16.7642 61.854 16.7642C62.2849 16.7642 62.7025 16.8355 63.1069 16.978C63.5112 17.1205 63.8742 17.3442 64.1957 17.6491C64.5172 17.9541 64.7707 18.3501 64.9563 18.8374C65.1419 19.3213 65.2347 19.9096 65.2347 20.6023V21.1293H59.1644V20.0156H63.7781C63.7781 19.6245 63.6985 19.2782 63.5394 18.9766C63.3803 18.6716 63.1566 18.4313 62.8683 18.2557C62.5832 18.08 62.2485 17.9922 61.864 17.9922C61.4464 17.9922 61.0818 18.0949 60.7702 18.3004C60.462 18.5026 60.2234 18.7678 60.0543 19.0959C59.8886 19.4207 59.8058 19.7737 59.8058 20.1548V21.0249C59.8058 21.5353 59.8952 21.9695 60.0742 22.3274C60.2565 22.6854 60.5101 22.9588 60.8349 23.1477C61.1597 23.3333 61.5392 23.4261 61.9734 23.4261C62.2551 23.4261 62.512 23.3864 62.744 23.3068C62.976 23.224 63.1765 23.1013 63.3455 22.9389C63.5146 22.7765 63.6438 22.576 63.7333 22.3374L65.1403 22.5909C65.0276 23.0052 64.8254 23.3681 64.5337 23.6797C64.2454 23.9879 63.8825 24.2282 63.445 24.4006C63.0108 24.5696 62.5153 24.6541 61.9585 24.6541ZM69.7427 24.6491C69.1262 24.6491 68.5761 24.4917 68.0922 24.1768C67.6116 23.8587 67.2337 23.4062 66.9586 22.8196C66.6868 22.2296 66.551 21.522 66.551 20.6967C66.551 19.8714 66.6885 19.1655 66.9636 18.5788C67.242 17.9922 67.6232 17.5431 68.1071 17.2315C68.591 16.92 69.1395 16.7642 69.7527 16.7642C70.2266 16.7642 70.6078 16.8438 70.8961 17.0028C71.1878 17.1586 71.4132 17.3409 71.5723 17.5497C71.7347 17.7585 71.8606 17.9425 71.9501 18.1016H72.0396V14.3182H73.5261V24.5H72.0744V23.3118H71.9501C71.8606 23.4742 71.7314 23.6598 71.5623 23.8686C71.3966 24.0774 71.1679 24.2597 70.8762 24.4155C70.5846 24.5713 70.2067 24.6491 69.7427 24.6491ZM70.0708 23.3814C70.4984 23.3814 70.8597 23.2687 71.1547 23.0433C71.4529 22.8146 71.6783 22.4981 71.8308 22.0938C71.9866 21.6894 72.0645 21.2187 72.0645 20.6818C72.0645 20.1515 71.9882 19.6875 71.8358 19.2898C71.6833 18.892 71.4596 18.5821 71.1646 18.3601C70.8696 18.138 70.505 18.027 70.0708 18.027C69.6234 18.027 69.2505 18.143 68.9522 18.375C68.6539 18.607 68.4286 18.9235 68.2761 19.3246C68.127 19.7256 68.0524 20.178 68.0524 20.6818C68.0524 21.1922 68.1286 21.6513 68.2811 22.0589C68.4335 22.4666 68.6589 22.7898 68.9572 23.0284C69.2588 23.2637 69.63 23.3814 70.0708 23.3814Z" fill="#8E9192"/> +</g> +</svg> diff --git a/assets/svg/dollar-sign-circle.svg b/assets/svg/dollar-sign-circle.svg new file mode 100644 index 000000000..03aacffea --- /dev/null +++ b/assets/svg/dollar-sign-circle.svg @@ -0,0 +1,4 @@ +<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="48" height="48" rx="24" fill="#E0E3E3"/> +<path d="M23.659 10.0005C24.8164 10.0005 25.7475 10.9367 25.7475 12.1005V13.483C26.8701 13.623 27.9926 13.973 29.089 14.2792C30.2029 14.5855 30.8555 15.7405 30.5423 16.8605C30.2377 17.9805 29.089 18.6367 27.9752 18.3217C27.1485 18.0942 26.3218 17.8492 25.4777 17.683C24.2073 17.438 22.7279 17.5517 21.5358 18.0767C20.4654 18.5405 19.5865 19.6605 20.7961 20.4392C21.9448 21.183 23.3893 21.4805 24.6772 21.8567C26.1913 22.2855 28.1144 22.8367 29.5589 23.8255C31.4386 25.1205 32.3175 27.2205 31.8998 29.478C31.5082 31.6567 29.994 33.0917 28.184 33.8267C27.4357 34.133 26.609 34.2467 25.7475 34.4217V35.9005C25.7475 37.0642 24.8164 38.0005 23.659 38.0005C22.5017 38.0005 21.5706 37.0642 21.5706 35.9005L21.4923 34.2117C20.1696 33.8967 18.7947 33.4417 17.4372 32.9955C16.3407 32.628 15.749 31.438 16.1058 30.3442C16.4713 29.2417 17.5764 28.6467 18.7425 29.0055C20.0738 29.443 21.4313 29.968 22.8063 30.178C24.4509 30.423 25.7649 30.2742 26.6264 29.9242C27.7838 29.4605 28.332 28.078 27.2007 27.2905C26.0347 26.4942 24.5379 26.1792 23.2065 25.803C21.7446 25.383 19.9259 24.8667 18.551 23.983C16.6627 22.7667 15.7055 20.7367 16.1145 18.5055C16.4974 16.3792 18.142 14.9705 19.8737 14.218C20.4045 13.9905 20.9788 13.8067 21.4923 13.6667V12.1005C21.4923 10.9367 22.5017 10.0005 23.5807 10.0005H23.659Z" fill="#232323"/> +</svg> diff --git a/assets/svg/language-circle.svg b/assets/svg/language-circle.svg new file mode 100644 index 000000000..700ffede4 --- /dev/null +++ b/assets/svg/language-circle.svg @@ -0,0 +1,11 @@ +<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="48" height="48" rx="24" fill="#E0E3E3"/> +<g clip-path="url(#clip0_5784_28907)"> +<path d="M29.602 20.0474C30.0832 20.0474 30.477 20.3954 30.477 20.9067V21.0786H33.102C33.5832 21.0786 33.977 21.4267 33.977 21.938C33.977 22.4106 33.5832 22.7974 33.102 22.7974H33.0145L32.9445 22.9907C32.5551 24.0048 31.9601 24.9931 31.2076 25.8009C31.247 25.8224 31.2863 25.8095 31.3257 25.8696L32.1526 26.3552C32.5682 26.6001 32.6995 27.1286 32.4501 27.5368C32.2051 27.945 31.667 28.0739 31.2513 27.829L30.4245 27.3435C30.232 27.2274 30.0001 27.1071 29.8513 26.9782C29.392 27.3005 28.8932 27.5798 28.3682 27.8118L28.2063 27.8806C27.7645 28.0739 27.2482 27.8763 27.0513 27.4423C26.8545 27.0083 27.0557 26.5013 27.4976 26.3079L27.6551 26.2392C27.9351 26.1146 28.2063 25.9384 28.4645 25.8181L27.9351 25.2938C27.5895 24.9587 27.5895 24.4173 27.9351 24.0821C28.2763 23.7427 28.8276 23.7427 29.1688 24.0821L29.8076 24.7052L29.8338 24.6923C30.3763 24.1681 30.8182 23.5149 31.1376 22.7587H26.452C25.9313 22.7587 25.577 22.4106 25.577 21.8993C25.577 21.4267 25.9313 21.0399 26.452 21.0399H28.727V20.8681C28.727 20.3954 29.0813 20.0087 29.602 20.0087V20.0474ZM17.002 23.0208L17.8332 24.8599H16.1313L17.002 23.0208ZM10.002 18.5005C10.002 16.9815 11.2554 15.7505 12.802 15.7505H35.202C36.7463 15.7505 38.002 16.9815 38.002 18.5005V29.5005C38.002 31.0173 36.7463 32.2505 35.202 32.2505H12.802C11.2554 32.2505 10.002 31.0173 10.002 29.5005V18.5005ZM24.002 29.5005H35.202V18.5005H24.002V29.5005ZM17.8026 20.5587C17.6626 20.2493 17.3476 20.0474 17.002 20.0474C16.6563 20.0474 16.3413 20.2493 16.2013 20.5587L13.4022 26.7462C13.2062 27.1415 13.4048 27.6872 13.8467 27.8806C14.2881 28.0739 14.8057 27.8763 15.0026 27.4423L15.392 26.5399H18.612L19.0013 27.4423C19.1982 27.8763 19.7145 28.0739 20.1563 27.8806C20.5982 27.6872 20.7995 27.1415 20.6026 26.7462L17.8026 20.5587Z" fill="#232323"/> +</g> +<defs> +<clipPath id="clip0_5784_28907"> +<rect width="28" height="22" fill="white" transform="translate(10.002 13.0005)"/> +</clipPath> +</defs> +</svg> diff --git a/assets/svg/lock-circle.svg b/assets/svg/lock-circle.svg new file mode 100644 index 000000000..f8fd71831 --- /dev/null +++ b/assets/svg/lock-circle.svg @@ -0,0 +1,11 @@ +<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="48" height="48" rx="24" fill="#E0E3E3"/> +<g clip-path="url(#clip0_5764_28774)"> +<path d="M24 10.9995C28.2589 10.9995 31.7143 14.254 31.7143 18.2687V20.6918H32.5714C34.4625 20.6918 36 22.1406 36 23.9226V33.6149C36 35.3969 34.4625 36.8457 32.5714 36.8457H15.4286C13.5348 36.8457 12 35.3969 12 33.6149V23.9226C12 22.1406 13.5348 20.6918 15.4286 20.6918H16.2857V18.2687C16.2857 14.254 19.7411 10.9995 24 10.9995ZM24 14.2303C21.6321 14.2303 19.7143 16.0385 19.7143 18.2687V20.6918H28.2857V18.2687C28.2857 16.0385 26.3679 14.2303 24 14.2303ZM25.7143 27.1534C25.7143 26.2598 24.9482 25.538 24 25.538C23.0518 25.538 22.2857 26.2598 22.2857 27.1534V30.3841C22.2857 31.2776 23.0518 31.9995 24 31.9995C24.9482 31.9995 25.7143 31.2776 25.7143 30.3841V27.1534Z" fill="#232323"/> +</g> +<defs> +<clipPath id="clip0_5764_28774"> +<rect width="24" height="25.8462" fill="white" transform="translate(12 10.9995)"/> +</clipPath> +</defs> +</svg> diff --git a/assets/svg/rotate-circle.svg b/assets/svg/rotate-circle.svg new file mode 100644 index 000000000..1940da5f5 --- /dev/null +++ b/assets/svg/rotate-circle.svg @@ -0,0 +1,4 @@ +<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="48" height="48" rx="24" fill="#E0E3E3"/> +<path d="M32.25 17.5031V15.75C32.25 14.9217 32.9203 14.25 33.75 14.25C34.5797 14.25 35.25 14.9217 35.25 15.75V21C35.25 21.8297 34.5797 22.5 33.75 22.5H32.5219C32.4984 22.5 32.475 22.5 32.4516 22.5H28.5C27.6703 22.5 27 21.8297 27 21C27 20.1703 27.6703 19.5 28.5 19.5H30C28.6313 17.6766 26.4516 16.5 23.9578 16.5C20.7375 16.5 17.9578 18.5859 16.9266 21.5016C16.6505 22.2797 15.7931 22.6922 15.0122 22.4156C14.2313 22.1391 13.8216 21.2391 14.0977 20.4984C15.5386 16.4241 19.425 13.5 23.9578 13.5C27.3469 13.5 30.2859 15.0666 32.25 17.5031ZM14.25 33.75C13.4217 33.75 12.75 33.0797 12.75 32.25V27C12.75 26.1703 13.4217 25.5 14.25 25.5H19.5C20.3297 25.5 21 26.1703 21 27C21 27.8297 20.3297 28.5 19.5 28.5H17.9578C19.3687 30.3234 21.5484 31.5 24 31.5C27.2625 31.5 30.0422 29.4141 31.0734 26.4984C31.35 25.7203 32.2078 25.3078 32.9859 25.5844C33.7688 25.8609 34.1766 26.7188 33.9 27.5016C32.4609 31.575 28.575 34.5 24 34.5C20.6531 34.5 17.6719 32.9344 15.75 30.4969V32.25C15.75 33.0797 15.0783 33.75 14.25 33.75Z" fill="#232323"/> +</svg> diff --git a/assets/svg/sun-circle.svg b/assets/svg/sun-circle.svg new file mode 100644 index 000000000..eba7d031d --- /dev/null +++ b/assets/svg/sun-circle.svg @@ -0,0 +1,11 @@ +<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="48" height="48" rx="24" fill="#E0E3E3"/> +<g clip-path="url(#clip0_5813_29015)"> +<path d="M16.5734 18.4328C16.8289 18.6892 17.1657 18.8173 17.5015 18.8173C17.8373 18.8173 18.1758 18.6898 18.4328 18.4328C18.9455 17.9201 18.9455 17.0897 18.4328 16.5773L15.9555 14.0999C15.4445 13.5872 14.6123 13.5872 14.0994 14.0999C13.5864 14.6126 13.5867 15.443 14.0994 15.956L16.5734 18.4328ZM24 16.125C24.7246 16.125 25.3125 15.5371 25.3125 14.8125V11.3125C25.3125 10.5879 24.7273 10 24 10C23.2727 10 22.6875 10.5879 22.6875 11.3125V14.8125C22.6875 15.5398 23.2781 16.125 24 16.125ZM16.125 24C16.125 23.2754 15.5371 22.6875 14.8125 22.6875H11.3125C10.5879 22.6875 10 23.2781 10 24C10 24.7219 10.5879 25.3125 11.3125 25.3125H14.8125C15.5398 25.3125 16.125 24.7273 16.125 24ZM30.4969 18.8156C30.8327 18.8156 31.1695 18.6874 31.4249 18.4311L33.8995 15.9549C34.4122 15.4422 34.4122 14.6117 33.8995 14.0988C33.3868 13.5858 32.5548 13.5861 32.0434 14.0988L29.5688 16.575C29.0561 17.0877 29.0561 17.9181 29.5688 18.4306C29.8242 18.6898 30.1633 18.8156 30.4969 18.8156ZM24 31.875C23.2754 31.875 22.6875 32.4629 22.6875 33.1875V36.6875C22.6875 37.4148 23.2781 38 24 38C24.7219 38 25.3125 37.4121 25.3125 36.6875V33.1875C25.3125 32.4656 24.7273 31.875 24 31.875ZM16.5734 29.5672L14.0988 32.0434C13.5861 32.5561 13.5861 33.3866 14.0988 33.8995C14.3552 34.1559 14.6911 34.284 15.0269 34.284C15.3627 34.284 15.6995 34.1559 15.9549 33.8995L18.4295 31.4233C18.9422 30.9106 18.9422 30.0802 18.4295 29.5677C17.9168 29.0553 17.0875 29.0531 16.5734 29.5672ZM36.6875 22.6875H33.1875C32.4629 22.6875 31.875 23.2754 31.875 24C31.875 24.7246 32.4629 25.3125 33.1875 25.3125H36.6875C37.4148 25.3125 38 24.7273 38 24C38 23.2727 37.4148 22.6875 36.6875 22.6875ZM31.4266 29.5672C30.9156 29.0545 30.0834 29.0547 29.5705 29.5674C29.0575 30.0801 29.0578 30.9105 29.5705 31.4229L32.0451 33.8992C32.3006 34.1555 32.6373 34.2837 32.9731 34.2837C33.3089 34.2837 33.6447 34.1555 33.9012 33.8992C34.4139 33.3865 34.4139 32.556 33.9012 32.0431L31.4266 29.5672ZM24 17.875C20.6148 17.875 17.875 20.6148 17.875 24C17.875 27.383 20.617 30.125 24 30.125C27.383 30.125 30.125 27.383 30.125 24C30.125 20.6148 27.3852 17.875 24 17.875Z" fill="#232323"/> +</g> +<defs> +<clipPath id="clip0_5813_29015"> +<rect width="28" height="28" fill="white" transform="translate(10 10)"/> +</clipPath> +</defs> +</svg> diff --git a/lib/pages_desktop_specific/home/settings_menu/advanced_settings.dart b/lib/pages_desktop_specific/home/settings_menu/advanced_settings.dart new file mode 100644 index 000000000..e470a73aa --- /dev/null +++ b/lib/pages_desktop_specific/home/settings_menu/advanced_settings.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter_riverpod/flutter_riverpod.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/rounded_white_container.dart'; + +class AdvancedSettings extends ConsumerStatefulWidget { + const AdvancedSettings({Key? key}) : super(key: key); + + static const String routeName = "/settingsMenuAdvanced"; + + @override + ConsumerState<AdvancedSettings> createState() => _AdvancedSettings(); +} + +class _AdvancedSettings extends ConsumerState<AdvancedSettings> { + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + return Column( + children: [ + Padding( + padding: const EdgeInsets.only( + right: 30, + ), + child: RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset( + Assets.svg.circleLanguage, + width: 48, + height: 48, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.start, + text: TextSpan( + children: [ + TextSpan( + text: "Advanced", + style: STextStyles.desktopTextSmall(context), + ), + TextSpan( + text: + "\n\nConfigurate these settings only if you know what you are doing!", + style: STextStyles.desktopTextExtraExtraSmall( + context), + ), + ], + ), + ), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Padding( + padding: EdgeInsets.all( + 10, + ), + child: ShowLogsButton(), + ), + ], + ), + ], + ), + ), + ), + ], + ); + } +} + +class ShowLogsButton extends ConsumerWidget { + const ShowLogsButton({ + Key? key, + }) : super(key: key); + @override + Widget build(BuildContext context, WidgetRef ref) { + return SizedBox( + width: 200, + height: 48, + child: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + onPressed: () {}, + child: Text( + "Show logs", + style: STextStyles.button(context), + ), + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart b/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart new file mode 100644 index 000000000..c0b2c0ca5 --- /dev/null +++ b/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class AppearanceOptionSettings extends ConsumerStatefulWidget { + const AppearanceOptionSettings({Key? key}) : super(key: key); + + static const String routeName = "/settingsMenuAppearance"; + + @override + ConsumerState<AppearanceOptionSettings> createState() => + _AppearanceOptionSettings(); +} + +class _AppearanceOptionSettings + extends ConsumerState<AppearanceOptionSettings> { + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + return Column( + children: [ + Padding( + padding: const EdgeInsets.only( + right: 30, + ), + child: RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset( + Assets.svg.circleSun, + width: 48, + height: 48, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.left, + text: TextSpan( + children: [ + TextSpan( + text: "Appearances", + style: STextStyles.desktopTextSmall(context), + ), + TextSpan( + text: + "\n\nCustomize how your Stack Wallet looks according to your preferences.", + style: STextStyles.desktopTextExtraExtraSmall( + context), + ), + ], + ), + ), + ), + ], + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore_settings.dart similarity index 93% rename from lib/pages_desktop_specific/home/settings_menu/backup_and_restore.dart rename to lib/pages_desktop_specific/home/settings_menu/backup_and_restore_settings.dart index 37b27bb97..04c23ee83 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore_settings.dart @@ -8,16 +8,17 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:url_launcher/url_launcher.dart'; -class BackupRestore extends ConsumerStatefulWidget { - const BackupRestore({Key? key}) : super(key: key); +class BackupRestoreSettings extends ConsumerStatefulWidget { + const BackupRestoreSettings({Key? key}) : super(key: key); - static const String routeName = "/backupRestore"; + static const String routeName = "/settingsMenuBackupRestore"; @override - ConsumerState<BackupRestore> createState() => _BackupRestore(); + ConsumerState<BackupRestoreSettings> createState() => + _BackupRestoreSettings(); } -class _BackupRestore extends ConsumerState<BackupRestore> { +class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); @@ -37,7 +38,6 @@ class _BackupRestore extends ConsumerState<BackupRestore> { Assets.svg.backupAuto, width: 48, height: 48, - alignment: Alignment.topLeft, ), Center( child: Padding( @@ -52,7 +52,7 @@ class _BackupRestore extends ConsumerState<BackupRestore> { ), TextSpan( text: - "\n\nAuto Backup is a custom Stack Wallet feature that offers a convenient backup of your data." + "\n\nAuto backup is a custom Stack Wallet feature that offers a convenient backup of your data." "To ensure maximum security, we recommend using a unique password that you haven't used anywhere " "else on the internet before. Your password is not stored.", style: @@ -126,7 +126,7 @@ class _BackupRestore extends ConsumerState<BackupRestore> { ), TextSpan( text: - "\n\nCreate Manual backup to easily transfer your data between devices. " + "\n\nCreate manual backup to easily transfer your data between devices. " "You will create a backup file that can be later used in the Restore option. " "Use a strong password to encrypt your data.", style: @@ -227,7 +227,7 @@ class AutoBackupButton extends ConsumerWidget { .getPrimaryEnabledButtonColor(context), onPressed: () {}, child: Text( - "Enable Auto Backup", + "Enable auto backup", style: STextStyles.button(context), ), ), @@ -250,7 +250,7 @@ class ManualBackupButton extends ConsumerWidget { .getPrimaryEnabledButtonColor(context), onPressed: () {}, child: Text( - "Create Manual Backup", + "Create manual backup", style: STextStyles.button(context), ), ), @@ -273,7 +273,7 @@ class RestoreBackupButton extends ConsumerWidget { .getPrimaryEnabledButtonColor(context), onPressed: () {}, child: Text( - "Restore Backup", + "Restore backup", style: STextStyles.button(context), ), ), diff --git a/lib/pages_desktop_specific/home/settings_menu/currency_settings.dart b/lib/pages_desktop_specific/home/settings_menu/currency_settings.dart new file mode 100644 index 000000000..4327b320b --- /dev/null +++ b/lib/pages_desktop_specific/home/settings_menu/currency_settings.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter_riverpod/flutter_riverpod.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/rounded_white_container.dart'; + +class CurrencySettings extends ConsumerStatefulWidget { + const CurrencySettings({Key? key}) : super(key: key); + + static const String routeName = "/settingsMenuCurrency"; + + @override + ConsumerState<CurrencySettings> createState() => _CurrencySettings(); +} + +class _CurrencySettings extends ConsumerState<CurrencySettings> { + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + return Column( + children: [ + Padding( + padding: const EdgeInsets.only( + right: 30, + ), + child: RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset( + Assets.svg.circleDollarSign, + width: 48, + height: 48, + ), + Center( + child: Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.start, + text: TextSpan( + children: [ + TextSpan( + text: "Currency", + style: STextStyles.desktopTextSmall(context), + ), + TextSpan( + text: + "\n\nProtect your Stack Wallet with a strong password. Stack Wallet does not store " + "your password, and is therefore NOT able to restore it. Keep your password safe and secure.", + style: + STextStyles.desktopTextExtraExtraSmall(context), + ), + ], + ), + ), + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Padding( + padding: EdgeInsets.all( + 10, + ), + child: NewPasswordButton(), + ), + ], + ), + ], + ), + ), + ), + ], + ); + } +} + +class NewPasswordButton extends ConsumerWidget { + const NewPasswordButton({ + Key? key, + }) : super(key: key); + @override + Widget build(BuildContext context, WidgetRef ref) { + return SizedBox( + width: 200, + height: 48, + child: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + onPressed: () {}, + child: Text( + "Set up new password", + style: STextStyles.button(context), + ), + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/settings_menu/language_settings.dart b/lib/pages_desktop_specific/home/settings_menu/language_settings.dart new file mode 100644 index 000000000..7655188e1 --- /dev/null +++ b/lib/pages_desktop_specific/home/settings_menu/language_settings.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter_riverpod/flutter_riverpod.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/rounded_white_container.dart'; + +class LanguageOptionSettings extends ConsumerStatefulWidget { + const LanguageOptionSettings({Key? key}) : super(key: key); + + static const String routeName = "/settingsMenuLanguage"; + + @override + ConsumerState<LanguageOptionSettings> createState() => + _LanguageOptionSettings(); +} + +class _LanguageOptionSettings extends ConsumerState<LanguageOptionSettings> { + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + return Column( + children: [ + Padding( + padding: const EdgeInsets.only( + right: 30, + ), + child: RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset( + Assets.svg.circleLanguage, + width: 48, + height: 48, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.start, + text: TextSpan( + children: [ + TextSpan( + text: "Language", + style: STextStyles.desktopTextSmall(context), + ), + TextSpan( + text: + "\n\nSelect the language of your wallet. We use your system language by default.", + style: STextStyles.desktopTextExtraExtraSmall( + context), + ), + ], + ), + ), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Padding( + padding: EdgeInsets.all( + 10, + ), + child: ChangeLanguageButton(), + ), + ], + ), + ], + ), + ), + ), + ], + ); + } +} + +class ChangeLanguageButton extends ConsumerWidget { + const ChangeLanguageButton({ + Key? key, + }) : super(key: key); + @override + Widget build(BuildContext context, WidgetRef ref) { + return SizedBox( + width: 200, + height: 48, + child: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + onPressed: () {}, + child: Text( + "Change language", + style: STextStyles.button(context), + ), + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/settings_menu/security_settings.dart b/lib/pages_desktop_specific/home/settings_menu/security_settings.dart new file mode 100644 index 000000000..febb1dc1b --- /dev/null +++ b/lib/pages_desktop_specific/home/settings_menu/security_settings.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter_riverpod/flutter_riverpod.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/rounded_white_container.dart'; + +class SecuritySettings extends ConsumerStatefulWidget { + const SecuritySettings({Key? key}) : super(key: key); + + static const String routeName = "/settingsMenuSecurity"; + + @override + ConsumerState<SecuritySettings> createState() => _SecuritySettings(); +} + +class _SecuritySettings extends ConsumerState<SecuritySettings> { + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + return Column( + children: [ + Padding( + padding: const EdgeInsets.only( + right: 30, + ), + child: RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset( + Assets.svg.circleLock, + width: 48, + height: 48, + ), + Center( + child: Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.start, + text: TextSpan( + children: [ + TextSpan( + text: "Change Password", + style: STextStyles.desktopTextSmall(context), + ), + TextSpan( + text: + "\n\nProtect your Stack Wallet with a strong password. Stack Wallet does not store " + "your password, and is therefore NOT able to restore it. Keep your password safe and secure.", + style: + STextStyles.desktopTextExtraExtraSmall(context), + ), + ], + ), + ), + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Padding( + padding: EdgeInsets.all( + 10, + ), + child: NewPasswordButton(), + ), + ], + ), + ], + ), + ), + ), + ], + ); + } +} + +class NewPasswordButton extends ConsumerWidget { + const NewPasswordButton({ + Key? key, + }) : super(key: key); + @override + Widget build(BuildContext context, WidgetRef ref) { + return SizedBox( + width: 200, + height: 48, + child: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + onPressed: () {}, + child: Text( + "Set up new password", + style: STextStyles.button(context), + ), + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart b/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart new file mode 100644 index 000000000..9b6c6c85c --- /dev/null +++ b/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter_riverpod/flutter_riverpod.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/rounded_white_container.dart'; + +class SyncingPreferencesSettings extends ConsumerStatefulWidget { + const SyncingPreferencesSettings({Key? key}) : super(key: key); + + static const String routeName = "/settingsMenuSyncingPref"; + + @override + ConsumerState<SyncingPreferencesSettings> createState() => + _SyncingPreferencesSettings(); +} + +class _SyncingPreferencesSettings + extends ConsumerState<SyncingPreferencesSettings> { + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + return Column( + children: [ + Padding( + padding: const EdgeInsets.only( + right: 30, + ), + child: RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset( + Assets.svg.circleArrowRotate, + width: 48, + height: 48, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.start, + text: TextSpan( + children: [ + TextSpan( + text: "Syncing Preferences", + style: STextStyles.desktopTextSmall(context), + ), + TextSpan( + text: + "\nSet up your syncing preferences for all wallets in your Stack.", + style: STextStyles.desktopTextExtraExtraSmall( + context), + ), + ], + ), + ), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Padding( + padding: EdgeInsets.all( + 10, + ), + child: ChangePrefButton(), + ), + ], + ), + ], + ), + ), + ), + ], + ); + } +} + +class ChangePrefButton extends ConsumerWidget { + const ChangePrefButton({ + Key? key, + }) : super(key: key); + @override + Widget build(BuildContext context, WidgetRef ref) { + return SizedBox( + width: 200, + height: 48, + child: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + onPressed: () {}, + child: Text( + "Change preferences", + style: STextStyles.button(context), + ), + ), + ); + } +} diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 4a7f54e8e..e2d91a08e 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -87,8 +87,14 @@ import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_settings_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart'; -import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/advanced_settings.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/appearance_settings.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore_settings.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/currency_settings.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/language_settings.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/security_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/settings_menu.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; @@ -959,11 +965,11 @@ class RouteGenerator { builder: (_) => const DesktopHomeView(), settings: RouteSettings(name: settings.name)); - // case DesktopSettingsView.routeName: - // return getRoute( - // shouldUseMaterialRoute: useMaterialPageRoute, - // builder: (_) => const DesktopSettingsView(), - // settings: RouteSettings(name: settings.name)); + case DesktopSettingsView.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const DesktopSettingsView(), + settings: RouteSettings(name: settings.name)); case MyStackView.routeName: return getRoute( @@ -993,10 +999,46 @@ class RouteGenerator { ), settings: RouteSettings(name: settings.name)); - case BackupRestore.routeName: + case BackupRestoreSettings.routeName: return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => const BackupRestore(), + builder: (_) => const BackupRestoreSettings(), + settings: RouteSettings(name: settings.name)); + + case SecuritySettings.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const SecuritySettings(), + settings: RouteSettings(name: settings.name)); + + case CurrencySettings.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const CurrencySettings(), + settings: RouteSettings(name: settings.name)); + + case LanguageOptionSettings.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const LanguageOptionSettings(), + settings: RouteSettings(name: settings.name)); + + case SyncingPreferencesSettings.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const SyncingPreferencesSettings(), + settings: RouteSettings(name: settings.name)); + + case AppearanceOptionSettings.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const AppearanceOptionSettings(), + settings: RouteSettings(name: settings.name)); + + case AdvancedSettings.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const AdvancedSettings(), settings: RouteSettings(name: settings.name)); // == End of desktop specific routes ===================================== diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 1410d6442..5ceaabcb4 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -59,6 +59,12 @@ class _SVG { String txExchangeFailed(BuildContext context) => "assets/svg/${Theme.of(context).extension<StackColors>()!.themeType.name}/tx-exchange-icon-failed.svg"; + String get circleSun => "assets/svg/sun-circle.svg"; + String get circleArrowRotate => "assets/svg/rotate-circle.svg"; + String get circleLanguage => "assets/svg/language-circle.svg"; + String get circleDollarSign => "assets/svg/dollar-sign-circle.svg"; + String get circleLock => "assets/svg/lock-circle.svg"; + String get disableButton => "assets/svg/Button.svg"; String get polygon => "assets/svg/Polygon.svg"; String get personaIncognito => "assets/svg/persona-incognito-1.svg"; String get personaEasy => "assets/svg/persona-easy-1.svg"; From 82cc2209552150803d67bc309b59aa1ca4cd777a Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Wed, 19 Oct 2022 16:12:25 -0600 Subject: [PATCH 023/426] settings option routes were added --- .../home/desktop_settings_view.dart | 46 +++++++++++++------ 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/lib/pages_desktop_specific/home/desktop_settings_view.dart b/lib/pages_desktop_specific/home/desktop_settings_view.dart index cd11eff3d..f7927a01a 100644 --- a/lib/pages_desktop_specific/home/desktop_settings_view.dart +++ b/lib/pages_desktop_specific/home/desktop_settings_view.dart @@ -1,7 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/advanced_settings.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/appearance_settings.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore_settings.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/currency_settings.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/language_settings.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/security_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/settings_menu.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -24,28 +30,40 @@ class _DesktopSettingsViewState extends ConsumerState<DesktopSettingsView> { const Navigator( key: Key("settingsBackupRestoreDesktopKey"), onGenerateRoute: RouteGenerator.generateRoute, - initialRoute: BackupRestore.routeName, + initialRoute: BackupRestoreSettings.routeName, ), //b+r - Container( - color: Colors.green, + const Navigator( + key: Key("settingsSecurityDesktopKey"), + onGenerateRoute: RouteGenerator.generateRoute, + initialRoute: SecuritySettings.routeName, ), //security - Container( - color: Colors.red, + const Navigator( + key: Key("settingsCurrencyDesktopKey"), + onGenerateRoute: RouteGenerator.generateRoute, + initialRoute: CurrencySettings.routeName, ), //currency - Container( - color: Colors.orange, + const Navigator( + key: Key("settingsCurrencyDesktopKey"), + onGenerateRoute: RouteGenerator.generateRoute, + initialRoute: LanguageOptionSettings.routeName, ), //language Container( color: Colors.yellow, ), //nodes - Container( - color: Colors.blue, + const Navigator( + key: Key("settingsSyncingPreferencesDesktopKey"), + onGenerateRoute: RouteGenerator.generateRoute, + initialRoute: SyncingPreferencesSettings.routeName, ), //syncing prefs - Container( - color: Colors.pink, + const Navigator( + key: Key("settingsAppearanceDesktopKey"), + onGenerateRoute: RouteGenerator.generateRoute, + initialRoute: AppearanceOptionSettings.routeName, ), //appearance - Container( - color: Colors.purple, + const Navigator( + key: Key("settingsAdvancedDesktopKey"), + onGenerateRoute: RouteGenerator.generateRoute, + initialRoute: AdvancedSettings.routeName, ), //advanced ]; From 6ae941e2616b1ef17f5f7d00f1f293cafcd12bce Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Wed, 19 Oct 2022 16:13:27 -0600 Subject: [PATCH 024/426] settings svgs were added --- pubspec.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pubspec.yaml b/pubspec.yaml index d4d354625..a8b11c101 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -292,6 +292,12 @@ flutter: - assets/svg/Polygon.svg - assets/svg/persona-easy-1.svg - assets/svg/persona-incognito-1.svg + - assets/svg/Button.svg + - assets/svg/lock-circle.svg + - assets/svg/dollar-sign-circle.svg + - assets/svg/language-circle.svg + - assets/svg/rotate-circle.svg + - assets/svg/sun-circle.svg # coin icons - assets/svg/coin_icons/Bitcoin.svg - assets/svg/coin_icons/Bitcoincash.svg From b2cb194c61598a7b708bfbd74bc6234a24d4a848 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Wed, 19 Oct 2022 16:22:09 -0600 Subject: [PATCH 025/426] Marco's version updates --- pubspec.lock | 62 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 2405aef27..1a564066d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -42,7 +42,7 @@ packages: name: archive url: "https://pub.dartlang.org" source: hosted - version: "3.3.0" + version: "3.1.11" args: dependency: transitive description: @@ -63,7 +63,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.9.0" + version: "2.8.2" barcode_scan2: dependency: "direct main" description: @@ -190,7 +190,14 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "1.2.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" checked_yaml: dependency: transitive description: @@ -204,7 +211,7 @@ packages: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" + version: "1.1.0" code_builder: dependency: transitive description: @@ -274,7 +281,7 @@ packages: name: coverage url: "https://pub.dartlang.org" source: hosted - version: "1.5.0" + version: "1.2.0" cross_file: dependency: transitive description: @@ -414,7 +421,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.3.1" + version: "1.3.0" ffi: dependency: "direct main" description: @@ -529,7 +536,7 @@ packages: name: flutter_native_splash url: "https://pub.dartlang.org" source: hosted - version: "2.2.11" + version: "2.2.9" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -802,6 +809,13 @@ packages: relative: true source: path version: "0.0.1" + lint: + dependency: transitive + description: + name: lint + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0" lints: dependency: transitive description: @@ -829,28 +843,28 @@ packages: name: lottie url: "https://pub.dartlang.org" source: hosted - version: "1.4.3" + version: "1.4.2" matcher: dependency: transitive description: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.12" + version: "0.12.11" material_color_utilities: dependency: transitive description: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.5" + version: "0.1.4" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.7.0" mime: dependency: transitive description: @@ -962,7 +976,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.2" + version: "1.8.1" path_drawing: dependency: transitive description: @@ -1338,7 +1352,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.9.0" + version: "1.8.2" stack_trace: dependency: transitive description: @@ -1382,49 +1396,49 @@ packages: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" + version: "1.1.0" sync_http: dependency: transitive description: name: sync_http url: "https://pub.dartlang.org" source: hosted - version: "0.3.1" + version: "0.3.0" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "1.2.0" test: dependency: transitive description: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.21.4" + version: "1.21.1" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.12" + version: "0.4.9" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.4.16" + version: "0.4.13" time: dependency: transitive description: name: time url: "https://pub.dartlang.org" source: hosted - version: "2.1.3" + version: "2.1.2" timezone: dependency: transitive description: @@ -1466,7 +1480,7 @@ packages: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.3.1" + version: "1.3.0" universal_io: dependency: transitive description: @@ -1550,7 +1564,7 @@ packages: name: vm_service url: "https://pub.dartlang.org" source: hosted - version: "9.0.0" + version: "8.2.2" wakelock: dependency: "direct main" description: @@ -1666,5 +1680,5 @@ packages: source: hosted version: "1.0.0" sdks: - dart: ">=2.18.0 <3.0.0" - flutter: ">=3.3.0" + dart: ">=2.17.5 <3.0.0" + flutter: ">=3.0.1" From 3e34f18f9c16627fddcd706079d3695186cc24d5 Mon Sep 17 00:00:00 2001 From: rehrar <diego@cypherstack.com> Date: Thu, 20 Oct 2022 11:36:49 -0600 Subject: [PATCH 026/426] make m1 mac (ipad mode) work in desktop mode --- lib/main.dart | 3 +++ lib/utilities/util.dart | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/lib/main.dart b/lib/main.dart index ad1ef9b7f..e1297cc5a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -68,6 +68,9 @@ final openedFromSWBFileStringStateProvider = void main() async { WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); GoogleFonts.config.allowRuntimeFetching = false; + if(Platform.isIOS){ + Util.libraryPath = await getLibraryDirectory(); + } if (Util.isDesktop) { setWindowTitle('Stack Wallet'); diff --git a/lib/utilities/util.dart b/lib/utilities/util.dart index 8a98787f2..5963bfee9 100644 --- a/lib/utilities/util.dart +++ b/lib/utilities/util.dart @@ -1,9 +1,14 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; abstract class Util { + static Directory? libraryPath; static bool get isDesktop { + if(Platform.isIOS && libraryPath != null && !libraryPath!.path.contains("/var/mobile/")){ + return true; + } return Platform.isLinux || Platform.isMacOS || Platform.isWindows; } From fba7fbf1cc842976973f1cc5ad7dd372b7db9908 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Thu, 20 Oct 2022 11:54:24 -0600 Subject: [PATCH 027/426] added desktop node settings --- assets/svg/node-circle.svg | 4 + .../home/desktop_settings_view.dart | 9 +- .../home/settings_menu/nodes_settings.dart | 165 ++++++++++++++++++ lib/route_generator.dart | 7 + lib/utilities/assets.dart | 1 + pubspec.yaml | 1 + 6 files changed, 184 insertions(+), 3 deletions(-) create mode 100644 assets/svg/node-circle.svg create mode 100644 lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart diff --git a/assets/svg/node-circle.svg b/assets/svg/node-circle.svg new file mode 100644 index 000000000..bd9353a2b --- /dev/null +++ b/assets/svg/node-circle.svg @@ -0,0 +1,4 @@ +<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="48" height="48" rx="24" fill="#E0E3E3"/> +<path d="M34.5 25.5H13.5C12.6741 25.5 12 26.1741 12 27V33C12 33.8259 12.6741 34.5 13.5 34.5H34.5C35.3259 34.5 36 33.8259 36 33V27C36 26.175 35.325 25.5 34.5 25.5ZM28.5 31.125C27.8789 31.125 27.375 30.6211 27.375 30C27.375 29.3789 27.8789 28.875 28.5 28.875C29.1211 28.875 29.625 29.3789 29.625 30C29.625 30.6211 29.1234 31.125 28.5 31.125ZM31.5 31.125C30.8789 31.125 30.375 30.6211 30.375 30C30.375 29.3789 30.8789 28.875 31.5 28.875C32.1211 28.875 32.625 29.3789 32.625 30C32.625 30.6211 32.1234 31.125 31.5 31.125ZM34.5 13.5H13.5C12.6741 13.5 12 14.1741 12 15V21C12 21.8259 12.6741 22.5 13.5 22.5H34.5C35.3259 22.5 36 21.8259 36 21V15C36 14.1741 35.325 13.5 34.5 13.5ZM28.5 19.125C27.8789 19.125 27.375 18.6211 27.375 18C27.375 17.3789 27.8813 16.875 28.5 16.875C29.1187 16.875 29.625 17.3812 29.625 18C29.625 18.6188 29.1234 19.125 28.5 19.125ZM31.5 19.125C30.8789 19.125 30.375 18.6211 30.375 18C30.375 17.3789 30.8813 16.875 31.5 16.875C32.1187 16.875 32.625 17.3812 32.625 18C32.625 18.6188 32.1234 19.125 31.5 19.125Z" fill="#232323"/> +</svg> diff --git a/lib/pages_desktop_specific/home/desktop_settings_view.dart b/lib/pages_desktop_specific/home/desktop_settings_view.dart index f7927a01a..7aff94b66 100644 --- a/lib/pages_desktop_specific/home/desktop_settings_view.dart +++ b/lib/pages_desktop_specific/home/desktop_settings_view.dart @@ -5,6 +5,7 @@ import 'package:stackwallet/pages_desktop_specific/home/settings_menu/appearance import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/currency_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/language_settings.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/nodes_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/security_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/settings_menu.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart'; @@ -43,12 +44,14 @@ class _DesktopSettingsViewState extends ConsumerState<DesktopSettingsView> { initialRoute: CurrencySettings.routeName, ), //currency const Navigator( - key: Key("settingsCurrencyDesktopKey"), + key: Key("settingsLanguageDesktopKey"), onGenerateRoute: RouteGenerator.generateRoute, initialRoute: LanguageOptionSettings.routeName, ), //language - Container( - color: Colors.yellow, + const Navigator( + key: Key("settingsNodesDesktopKey"), + onGenerateRoute: RouteGenerator.generateRoute, + initialRoute: NodesSettings.routeName, ), //nodes const Navigator( key: Key("settingsSyncingPreferencesDesktopKey"), diff --git a/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart b/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart new file mode 100644 index 000000000..4227b537f --- /dev/null +++ b/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart @@ -0,0 +1,165 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +import '../../../providers/global/node_service_provider.dart'; +import '../../../providers/global/prefs_provider.dart'; +import '../../../utilities/constants.dart'; +import '../../../utilities/enums/coin_enum.dart'; + +class NodesSettings extends ConsumerStatefulWidget { + const NodesSettings({Key? key}) : super(key: key); + + static const String routeName = "/settingsMenuNodes"; + + @override + ConsumerState<NodesSettings> createState() => _NodesSettings(); +} + +class _NodesSettings extends ConsumerState<NodesSettings> { + List<Coin> _coins = [...Coin.values]; + + @override + void initState() { + _coins = _coins.toList(); + _coins.remove(Coin.firoTestNet); + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + bool showTestNet = ref.watch( + prefsChangeNotifierProvider.select((value) => value.showTestNetCoins), + ); + + List<Coin> coins = showTestNet + ? _coins + : _coins.sublist(0, _coins.length - kTestNetCoinCount); + + debugPrint("BUILD: $runtimeType"); + return Column( + children: [ + Padding( + padding: const EdgeInsets.only( + right: 30, + ), + child: RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset( + Assets.svg.circleNode, + width: 48, + height: 48, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.start, + text: TextSpan( + children: [ + TextSpan( + text: "Nodes", + style: STextStyles.desktopTextSmall(context), + ), + TextSpan( + text: "\n\nSelect a coin to see nodes", + style: STextStyles.desktopTextExtraExtraSmall( + context), + ), + ], + ), + ), + ), + ], + ), + SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ...coins.map( + (coin) { + final count = ref + .watch(nodeServiceChangeNotifierProvider + .select((value) => value.getNodesFor(coin))) + .length; + + return Padding( + padding: const EdgeInsets.all(4), + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + onPressed: () { + // Navigator.of(context).pushNamed( + // CoinNodesView.routeName, + // arguments: coin, + // ); + }, + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.iconFor(coin: coin), + width: 24, + height: 24, + ), + const SizedBox( + width: 12, + ), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "${coin.prettyName} nodes", + style: STextStyles.titleBold12( + context), + ), + Text( + count > 1 + ? "$count nodes" + : "Default", + style: STextStyles.label(context), + ), + ], + ) + ], + ), + ), + ), + ), + ); + }, + ), + ], + ), + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/route_generator.dart b/lib/route_generator.dart index e2d91a08e..f915e7837 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -92,6 +92,7 @@ import 'package:stackwallet/pages_desktop_specific/home/settings_menu/appearance import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/currency_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/language_settings.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/nodes_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/security_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/settings_menu.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart'; @@ -1023,6 +1024,12 @@ class RouteGenerator { builder: (_) => const LanguageOptionSettings(), settings: RouteSettings(name: settings.name)); + case NodesSettings.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const NodesSettings(), + settings: RouteSettings(name: settings.name)); + case SyncingPreferencesSettings.routeName: return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 5ceaabcb4..5136f1a37 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -59,6 +59,7 @@ class _SVG { String txExchangeFailed(BuildContext context) => "assets/svg/${Theme.of(context).extension<StackColors>()!.themeType.name}/tx-exchange-icon-failed.svg"; + String get circleNode => "assets/svg/node-circle.svg"; String get circleSun => "assets/svg/sun-circle.svg"; String get circleArrowRotate => "assets/svg/rotate-circle.svg"; String get circleLanguage => "assets/svg/language-circle.svg"; diff --git a/pubspec.yaml b/pubspec.yaml index a8b11c101..64fe730f6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -298,6 +298,7 @@ flutter: - assets/svg/language-circle.svg - assets/svg/rotate-circle.svg - assets/svg/sun-circle.svg + - assets/svg/node-circle.svg # coin icons - assets/svg/coin_icons/Bitcoin.svg - assets/svg/coin_icons/Bitcoincash.svg From c231758902025582ac47ec036544b05409e1fca0 Mon Sep 17 00:00:00 2001 From: rehrar <diego@cypherstack.com> Date: Thu, 20 Oct 2022 14:19:50 -0600 Subject: [PATCH 028/426] remove suggestions and autocomplete for desktop --- .../add_wallet_view/add_wallet_view.dart | 2 ++ .../name_your_wallet_view.dart | 2 ++ .../mobile_mnemonic_length_selector.dart | 6 +++++- .../sub_widgets/restore_from_date_picker.dart | 4 ++++ .../address_book_views/address_book_view.dart | 4 ++++ .../subviews/add_address_book_entry_view.dart | 4 ++++ .../subviews/edit_contact_name_emoji_view.dart | 4 ++++ .../subviews/new_contact_address_entry_form.dart | 8 +++++++- lib/pages/exchange_view/edit_trade_note_view.dart | 4 ++++ .../fixed_rate_pair_coin_selection_view.dart | 4 ++++ .../floating_rate_currency_selection_view.dart | 4 ++++ .../generate_receiving_uri_qr_code_view.dart | 5 +++++ lib/pages/send_view/send_view.dart | 15 ++++++++++++++- .../advanced_views/debug_view.dart | 4 ++++ .../global_settings_view/currency_view.dart | 4 ++++ .../global_settings_view/language_view.dart | 4 ++++ .../manage_nodes_views/add_edit_node_view.dart | 12 ++++++++++++ .../stack_backup_views/auto_backup_view.dart | 4 ++++ .../create_auto_backup_view.dart | 8 +++++++- .../stack_backup_views/create_backup_view.dart | 3 +++ .../stack_backup_views/edit_auto_backup_view.dart | 8 +++++++- .../restore_from_file_view.dart | 4 ++++ .../wallet_settings_view.dart | 6 ++++++ .../rename_wallet_view.dart | 4 ++++ .../transaction_views/all_transactions_view.dart | 4 ++++ .../transaction_views/edit_note_view.dart | 4 ++++ .../transaction_search_filter_view.dart | 4 ++++ 27 files changed, 134 insertions(+), 5 deletions(-) diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index ae72e1846..df5c44d18 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -90,6 +90,8 @@ class _AddWalletViewState extends State<AddWalletView> { Constants.size.circularBorderRadius, ), child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, controller: _searchFieldController, focusNode: _searchFocusNode, onChanged: (value) { diff --git a/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart b/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart index f7dbe3d33..8bc01b124 100644 --- a/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart +++ b/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart @@ -194,6 +194,8 @@ class _NameYourWalletViewState extends ConsumerState<NameYourWalletView> { Constants.size.circularBorderRadius, ), child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, onChanged: (string) { if (string.isEmpty) { if (_nextEnabled) { diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/sub_widgets/mobile_mnemonic_length_selector.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/sub_widgets/mobile_mnemonic_length_selector.dart index 49896e107..4f5b76fab 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/sub_widgets/mobile_mnemonic_length_selector.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/sub_widgets/mobile_mnemonic_length_selector.dart @@ -7,6 +7,8 @@ import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; + class MobileMnemonicLengthSelector extends ConsumerWidget { const MobileMnemonicLengthSelector({ Key? key, @@ -19,7 +21,9 @@ class MobileMnemonicLengthSelector extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { return Stack( children: [ - const TextField( + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, // controller: _lengthController, readOnly: true, textInputAction: TextInputAction.none, diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/sub_widgets/restore_from_date_picker.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/sub_widgets/restore_from_date_picker.dart index 8a24e95bb..112d50428 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/sub_widgets/restore_from_date_picker.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/sub_widgets/restore_from_date_picker.dart @@ -4,6 +4,8 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; + class RestoreFromDatePicker extends StatefulWidget { const RestoreFromDatePicker({Key? key, required this.onTap}) : super(key: key); @@ -37,6 +39,8 @@ class _RestoreFromDatePickerState extends State<RestoreFromDatePicker> { return Container( color: Colors.transparent, child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, onTap: onTap, controller: _dateController, style: STextStyles.field(context), diff --git a/lib/pages/address_book_views/address_book_view.dart b/lib/pages/address_book_views/address_book_view.dart index b70ef19ed..c9dd72d72 100644 --- a/lib/pages/address_book_views/address_book_view.dart +++ b/lib/pages/address_book_views/address_book_view.dart @@ -21,6 +21,8 @@ import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; +import 'package:stackwallet/utilities/util.dart'; + class AddressBookView extends ConsumerStatefulWidget { const AddressBookView({Key? key, this.coin}) : super(key: key); @@ -198,6 +200,8 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> { Constants.size.circularBorderRadius, ), child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, controller: _searchController, focusNode: _searchFocusNode, onChanged: (value) { diff --git a/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart b/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart index 4fa89908c..74f3dfde8 100644 --- a/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart +++ b/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart @@ -22,6 +22,8 @@ import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; +import 'package:stackwallet/utilities/util.dart'; + class AddAddressBookEntryView extends ConsumerStatefulWidget { const AddAddressBookEntryView({ Key? key, @@ -279,6 +281,8 @@ class _AddAddressBookEntryViewState Constants.size.circularBorderRadius, ), child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, controller: nameController, focusNode: nameFocusNode, style: STextStyles.field(context), diff --git a/lib/pages/address_book_views/subviews/edit_contact_name_emoji_view.dart b/lib/pages/address_book_views/subviews/edit_contact_name_emoji_view.dart index 45c23b13c..fff01eee3 100644 --- a/lib/pages/address_book_views/subviews/edit_contact_name_emoji_view.dart +++ b/lib/pages/address_book_views/subviews/edit_contact_name_emoji_view.dart @@ -13,6 +13,8 @@ import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; +import 'package:stackwallet/utilities/util.dart'; + class EditContactNameEmojiView extends ConsumerStatefulWidget { const EditContactNameEmojiView({ Key? key, @@ -200,6 +202,8 @@ class _EditContactNameEmojiViewState Constants.size.circularBorderRadius, ), child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, controller: nameController, focusNode: nameFocusNode, style: STextStyles.field(context), diff --git a/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart b/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart index 73de8b0aa..ce98cee10 100644 --- a/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart +++ b/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart @@ -20,6 +20,8 @@ import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; +import 'package:stackwallet/utilities/util.dart'; + class NewContactAddressEntryForm extends ConsumerStatefulWidget { const NewContactAddressEntryForm({ Key? key, @@ -71,6 +73,8 @@ class _NewContactAddressEntryFormState return Column( children: [ TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, readOnly: true, style: STextStyles.field(context), decoration: InputDecoration( @@ -154,6 +158,8 @@ class _NewContactAddressEntryFormState Constants.size.circularBorderRadius, ), child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, focusNode: addressLabelFocusNode, controller: addressLabelController, style: STextStyles.field(context), @@ -197,6 +203,7 @@ class _NewContactAddressEntryFormState Constants.size.circularBorderRadius, ), child: TextField( + enableSuggestions: Util.isDesktop ? false : true, focusNode: addressFocusNode, controller: addressController, style: STextStyles.field(context), @@ -324,7 +331,6 @@ class _NewContactAddressEntryFormState key: const Key("addAddressBookEntryViewAddressField"), readOnly: false, autocorrect: false, - enableSuggestions: false, // inputFormatters: <TextInputFormatter>[ // FilteringTextInputFormatter.allow(RegExp("[a-zA-Z0-9]{34}")), // ], diff --git a/lib/pages/exchange_view/edit_trade_note_view.dart b/lib/pages/exchange_view/edit_trade_note_view.dart index 5e1571b73..e2a72d1b4 100644 --- a/lib/pages/exchange_view/edit_trade_note_view.dart +++ b/lib/pages/exchange_view/edit_trade_note_view.dart @@ -9,6 +9,8 @@ import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; +import 'package:stackwallet/utilities/util.dart'; + class EditTradeNoteView extends ConsumerStatefulWidget { const EditTradeNoteView({ Key? key, @@ -85,6 +87,8 @@ class _EditNoteViewState extends ConsumerState<EditTradeNoteView> { Constants.size.circularBorderRadius, ), child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, controller: _noteController, style: STextStyles.field(context), focusNode: noteFieldFocusNode, diff --git a/lib/pages/exchange_view/exchange_coin_selection/fixed_rate_pair_coin_selection_view.dart b/lib/pages/exchange_view/exchange_coin_selection/fixed_rate_pair_coin_selection_view.dart index d7577e960..80bdcda62 100644 --- a/lib/pages/exchange_view/exchange_coin_selection/fixed_rate_pair_coin_selection_view.dart +++ b/lib/pages/exchange_view/exchange_coin_selection/fixed_rate_pair_coin_selection_view.dart @@ -16,6 +16,8 @@ import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; import 'package:tuple/tuple.dart'; +import 'package:stackwallet/utilities/util.dart'; + class FixedRateMarketPairCoinSelectionView extends ConsumerStatefulWidget { const FixedRateMarketPairCoinSelectionView({ Key? key, @@ -152,6 +154,8 @@ class _FixedRateMarketPairCoinSelectionViewState Constants.size.circularBorderRadius, ), child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, controller: _searchController, focusNode: _searchFocusNode, onChanged: filter, diff --git a/lib/pages/exchange_view/exchange_coin_selection/floating_rate_currency_selection_view.dart b/lib/pages/exchange_view/exchange_coin_selection/floating_rate_currency_selection_view.dart index 7c3b935b7..e1c1addd2 100644 --- a/lib/pages/exchange_view/exchange_coin_selection/floating_rate_currency_selection_view.dart +++ b/lib/pages/exchange_view/exchange_coin_selection/floating_rate_currency_selection_view.dart @@ -13,6 +13,8 @@ import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; +import 'package:stackwallet/utilities/util.dart'; + class FloatingRateCurrencySelectionView extends StatefulWidget { const FloatingRateCurrencySelectionView({ Key? key, @@ -108,6 +110,8 @@ class _FloatingRateCurrencySelectionViewState Constants.size.circularBorderRadius, ), child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, controller: _searchController, focusNode: _searchFocusNode, onChanged: filter, diff --git a/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart b/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart index 744d8b53c..3e29612d1 100644 --- a/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart +++ b/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart @@ -19,6 +19,7 @@ import 'package:stackwallet/utilities/enums/flush_bar_type.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/utilities/util.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -160,6 +161,8 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> { Constants.size.circularBorderRadius, ), child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, controller: amountController, focusNode: _amountFocusNode, style: STextStyles.field(context), @@ -209,6 +212,8 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> { Constants.size.circularBorderRadius, ), child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, controller: noteController, focusNode: _noteFocusNode, style: STextStyles.field(context), diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 5e689a580..d91b7a3ea 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -41,6 +41,8 @@ import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; +import 'package:stackwallet/utilities/util.dart'; + class SendView extends ConsumerStatefulWidget { const SendView({ Key? key, @@ -885,7 +887,10 @@ class _SendViewState extends ConsumerState<SendView> { if (coin == Coin.firo) Stack( children: [ - const TextField( + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: + Util.isDesktop ? false : true, readOnly: true, textInputAction: TextInputAction.none, ), @@ -1061,6 +1066,8 @@ class _SendViewState extends ConsumerState<SendView> { height: 8, ), TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, style: STextStyles.smallMed14(context).copyWith( color: Theme.of(context) .extension<StackColors>()! @@ -1114,6 +1121,8 @@ class _SendViewState extends ConsumerState<SendView> { ), if (Prefs.instance.externalCalls) TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, style: STextStyles.smallMed14(context).copyWith( color: Theme.of(context) .extension<StackColors>()! @@ -1238,6 +1247,8 @@ class _SendViewState extends ConsumerState<SendView> { Constants.size.circularBorderRadius, ), child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, controller: noteController, focusNode: _noteFocusNode, style: STextStyles.field(context), @@ -1283,6 +1294,8 @@ class _SendViewState extends ConsumerState<SendView> { Stack( children: [ TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, controller: feeController, readOnly: true, textInputAction: TextInputAction.none, diff --git a/lib/pages/settings_views/global_settings_view/advanced_views/debug_view.dart b/lib/pages/settings_views/global_settings_view/advanced_views/debug_view.dart index 29baad1c6..e5c442173 100644 --- a/lib/pages/settings_views/global_settings_view/advanced_views/debug_view.dart +++ b/lib/pages/settings_views/global_settings_view/advanced_views/debug_view.dart @@ -25,6 +25,8 @@ import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; +import 'package:stackwallet/utilities/util.dart'; + class DebugView extends ConsumerStatefulWidget { const DebugView({Key? key}) : super(key: key); @@ -217,6 +219,8 @@ class _DebugViewState extends ConsumerState<DebugView> { Constants.size.circularBorderRadius, ), child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, controller: _searchController, focusNode: _searchFocusNode, onChanged: (newString) { diff --git a/lib/pages/settings_views/global_settings_view/currency_view.dart b/lib/pages/settings_views/global_settings_view/currency_view.dart index cae947caa..e884393bd 100644 --- a/lib/pages/settings_views/global_settings_view/currency_view.dart +++ b/lib/pages/settings_views/global_settings_view/currency_view.dart @@ -13,6 +13,8 @@ import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; +import 'package:stackwallet/utilities/util.dart'; + class BaseCurrencySettingsView extends ConsumerStatefulWidget { const BaseCurrencySettingsView({Key? key}) : super(key: key); @@ -140,6 +142,8 @@ class _CurrencyViewState extends ConsumerState<BaseCurrencySettingsView> { Constants.size.circularBorderRadius, ), child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, controller: _searchController, focusNode: _searchFocusNode, onChanged: (newString) { diff --git a/lib/pages/settings_views/global_settings_view/language_view.dart b/lib/pages/settings_views/global_settings_view/language_view.dart index 75a2751a2..b617546e4 100644 --- a/lib/pages/settings_views/global_settings_view/language_view.dart +++ b/lib/pages/settings_views/global_settings_view/language_view.dart @@ -13,6 +13,8 @@ import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; +import 'package:stackwallet/utilities/util.dart'; + class LanguageSettingsView extends ConsumerStatefulWidget { const LanguageSettingsView({Key? key}) : super(key: key); @@ -138,6 +140,8 @@ class _LanguageViewState extends ConsumerState<LanguageSettingsView> { Constants.size.circularBorderRadius, ), child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, controller: _searchController, focusNode: _searchFocusNode, onChanged: (newString) { diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index 143b1e84d..100c03e1b 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -27,6 +27,8 @@ import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; import 'package:uuid/uuid.dart'; +import 'package:stackwallet/utilities/util.dart'; + enum AddEditNodeViewType { add, edit } class AddEditNodeView extends ConsumerStatefulWidget { @@ -648,6 +650,8 @@ class _NodeFormState extends ConsumerState<NodeForm> { Constants.size.circularBorderRadius, ), child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, key: const Key("addCustomNodeNodeNameFieldKey"), readOnly: widget.readOnly, enabled: enableField(_nameController), @@ -695,6 +699,8 @@ class _NodeFormState extends ConsumerState<NodeForm> { Constants.size.circularBorderRadius, ), child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, key: const Key("addCustomNodeNodeAddressFieldKey"), readOnly: widget.readOnly, enabled: enableField(_hostController), @@ -746,6 +752,8 @@ class _NodeFormState extends ConsumerState<NodeForm> { Constants.size.circularBorderRadius, ), child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, key: const Key("addCustomNodeNodePortFieldKey"), readOnly: widget.readOnly, enabled: enableField(_portController), @@ -797,6 +805,8 @@ class _NodeFormState extends ConsumerState<NodeForm> { Constants.size.circularBorderRadius, ), child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, controller: _usernameController, readOnly: widget.readOnly, enabled: enableField(_usernameController), @@ -844,6 +854,8 @@ class _NodeFormState extends ConsumerState<NodeForm> { Constants.size.circularBorderRadius, ), child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, controller: _passwordController, readOnly: widget.readOnly, enabled: enableField(_passwordController), diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.dart index 3f832a4af..a94375742 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.dart @@ -19,6 +19,8 @@ import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:stackwallet/utilities/util.dart'; + class AutoBackupView extends ConsumerStatefulWidget { const AutoBackupView({Key? key}) : super(key: key); @@ -423,6 +425,8 @@ class _AutoBackupViewState extends ConsumerState<AutoBackupView> { height: 10, ), TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, key: const Key("backupFrequencyFieldKey"), controller: frequencyController, enabled: false, diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart index b44a473b4..3b5dbd0b0 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart @@ -27,6 +27,8 @@ import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:zxcvbn/zxcvbn.dart'; +import 'package:stackwallet/utilities/util.dart'; + class CreateAutoBackupView extends ConsumerStatefulWidget { const CreateAutoBackupView({ Key? key, @@ -146,6 +148,8 @@ class _EnableAutoBackupViewState extends ConsumerState<CreateAutoBackupView> { ), if (!Platform.isAndroid) TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, onTap: Platform.isAndroid ? null : () async { @@ -411,7 +415,9 @@ class _EnableAutoBackupViewState extends ConsumerState<CreateAutoBackupView> { ), Stack( children: [ - const TextField( + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, readOnly: true, textInputAction: TextInputAction.none, ), diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart index 8dfc7588c..9242c0482 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart @@ -14,6 +14,7 @@ import 'package:stackwallet/utilities/enums/flush_bar_type.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/utilities/util.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/progress_bar.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; @@ -129,6 +130,8 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { return Container( color: Colors.transparent, child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, onTap: Platform.isAndroid ? null : () async { diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart index 9368d3b77..105146aa0 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart @@ -27,6 +27,8 @@ import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:zxcvbn/zxcvbn.dart'; +import '../../../../utilities/util.dart'; + class EditAutoBackupView extends ConsumerStatefulWidget { const EditAutoBackupView({ Key? key, @@ -148,6 +150,8 @@ class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> { ), if (!Platform.isAndroid) TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, onTap: Platform.isAndroid ? null : () async { @@ -413,7 +417,9 @@ class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> { ), Stack( children: [ - const TextField( + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, readOnly: true, textInputAction: TextInputAction.none, ), 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 cec114023..232be9028 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 @@ -20,6 +20,8 @@ import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:tuple/tuple.dart'; +import 'package:stackwallet/utilities/util.dart'; + class RestoreFromFileView extends ConsumerStatefulWidget { const RestoreFromFileView({Key? key}) : super(key: key); @@ -96,6 +98,8 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, onTap: () async { try { await stackFileSystem.prepareStorage(); diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart index 6e5cdd5ed..2d8909245 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart @@ -30,6 +30,8 @@ import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:tuple/tuple.dart'; +import 'package:stackwallet/utilities/util.dart'; + /// [eventBus] should only be set during testing class WalletSettingsView extends StatefulWidget { const WalletSettingsView({ @@ -374,6 +376,8 @@ class _EpiBoxInfoFormState extends ConsumerState<EpicBoxInfoForm> { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, controller: hostController, decoration: const InputDecoration(hintText: "Host"), ), @@ -381,6 +385,8 @@ class _EpiBoxInfoFormState extends ConsumerState<EpicBoxInfoForm> { height: 8, ), TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, controller: portController, decoration: const InputDecoration(hintText: "Port"), keyboardType: const TextInputType.numberWithOptions(), diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rename_wallet_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rename_wallet_view.dart index b876216e0..e9eb14868 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rename_wallet_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rename_wallet_view.dart @@ -11,6 +11,8 @@ import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; +import 'package:stackwallet/utilities/util.dart'; + class RenameWalletView extends ConsumerStatefulWidget { const RenameWalletView({ Key? key, @@ -74,6 +76,8 @@ class _RenameWalletViewState extends ConsumerState<RenameWalletView> { Constants.size.circularBorderRadius, ), child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, controller: _controller, focusNode: _focusNode, style: STextStyles.field(context), diff --git a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart index 78f24ba6a..4194a7307 100644 --- a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart +++ b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart @@ -21,6 +21,8 @@ import 'package:stackwallet/widgets/textfield_icon_button.dart'; import 'package:stackwallet/widgets/transaction_card.dart'; import 'package:tuple/tuple.dart'; +import 'package:stackwallet/utilities/util.dart'; + class AllTransactionsView extends ConsumerStatefulWidget { const AllTransactionsView({ Key? key, @@ -234,6 +236,8 @@ class _TransactionDetailsViewState extends ConsumerState<AllTransactionsView> { Constants.size.circularBorderRadius, ), child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, controller: _searchController, focusNode: searchFieldFocusNode, onChanged: (value) { diff --git a/lib/pages/wallet_view/transaction_views/edit_note_view.dart b/lib/pages/wallet_view/transaction_views/edit_note_view.dart index aa085429b..b811dc62d 100644 --- a/lib/pages/wallet_view/transaction_views/edit_note_view.dart +++ b/lib/pages/wallet_view/transaction_views/edit_note_view.dart @@ -9,6 +9,8 @@ import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; +import 'package:stackwallet/utilities/util.dart'; + class EditNoteView extends ConsumerStatefulWidget { const EditNoteView({ Key? key, @@ -87,6 +89,8 @@ class _EditNoteViewState extends ConsumerState<EditNoteView> { Constants.size.circularBorderRadius, ), child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, controller: _noteController, style: STextStyles.field(context), focusNode: noteFieldFocusNode, diff --git a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart index 8175597f6..f9932c672 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart @@ -569,6 +569,8 @@ class _TransactionSearchViewState Constants.size.circularBorderRadius, ), child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, key: const Key("transactionSearchViewAmountFieldKey"), controller: _amountTextEditingController, focusNode: amountTextFieldFocusNode, @@ -636,6 +638,8 @@ class _TransactionSearchViewState Constants.size.circularBorderRadius, ), child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, key: const Key("transactionSearchViewKeywordFieldKey"), controller: _keywordTextEditingController, From 44790dd2df675803e44f6eefb6f467bdbd80df17 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Fri, 21 Oct 2022 13:05:39 -0600 Subject: [PATCH 029/426] added nodes and dark theme icon --- assets/svg/dark/dark-theme.svg | 24 ++++ .../home/settings_menu/nodes_settings.dart | 117 ++++++++++-------- lib/utilities/assets.dart | 2 + pubspec.yaml | 1 + 4 files changed, 91 insertions(+), 53 deletions(-) create mode 100644 assets/svg/dark/dark-theme.svg diff --git a/assets/svg/dark/dark-theme.svg b/assets/svg/dark/dark-theme.svg new file mode 100644 index 000000000..47b5e2d5e --- /dev/null +++ b/assets/svg/dark/dark-theme.svg @@ -0,0 +1,24 @@ +<svg width="200" height="162" viewBox="0 0 200 162" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0_5863_29353)"> +<rect width="200" height="162" rx="8" fill="#2A2D34"/> +<rect x="10" y="10" width="180" height="20" rx="2" fill="#444953"/> +<rect x="16" y="16" width="106" height="8" rx="1" fill="#7E8692"/> +<rect x="10" y="40" width="180" height="20" rx="2" fill="#333942"/> +<rect x="16" y="46" width="106" height="8" rx="1" fill="#575C63"/> +<rect x="10" y="62" width="180" height="20" rx="2" fill="#333942"/> +<rect x="16" y="68" width="106" height="8" rx="1" fill="#575C63"/> +<rect x="10" y="84" width="180" height="20" rx="2" fill="#333942"/> +<rect x="16" y="90" width="106" height="8" rx="1" fill="#575C63"/> +<rect x="10" y="106" width="180" height="20" rx="2" fill="#333942"/> +<rect x="16" y="112" width="106" height="8" rx="1" fill="#575C63"/> +<rect x="10" y="128" width="180" height="20" rx="2" fill="#333942"/> +<rect x="16" y="134" width="106" height="8" rx="1" fill="#575C63"/> +<rect x="10" y="150" width="180" height="20" rx="2" fill="#333942"/> +<rect x="16" y="156" width="106" height="8" rx="1" fill="#575C63"/> +</g> +<defs> +<clipPath id="clip0_5863_29353"> +<rect width="200" height="162" rx="8" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart b/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart index 4227b537f..1cc6edaaa 100644 --- a/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart @@ -2,15 +2,15 @@ import 'package:flutter/material.dart'; import 'package:flutter/src/widgets/framework.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/providers/global/node_service_provider.dart'; +import 'package:stackwallet/providers/global/prefs_provider.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/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; -import '../../../providers/global/node_service_provider.dart'; -import '../../../providers/global/prefs_provider.dart'; -import '../../../utilities/constants.dart'; -import '../../../utilities/enums/coin_enum.dart'; - class NodesSettings extends ConsumerStatefulWidget { const NodesSettings({Key? key}) : super(key: key); @@ -37,6 +37,9 @@ class _NodesSettings extends ConsumerState<NodesSettings> { @override Widget build(BuildContext context) { + double deviceWidth(BuildContext context) => + MediaQuery.of(context).size.width; + bool showTestNet = ref.watch( prefsChangeNotifierProvider.select((value) => value.showTestNetCoins), ); @@ -85,6 +88,7 @@ class _NodesSettings extends ConsumerState<NodesSettings> { ), ], ), + //TODO: add search bar SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -97,55 +101,62 @@ class _NodesSettings extends ConsumerState<NodesSettings> { .length; return Padding( - padding: const EdgeInsets.all(4), - child: RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - onPressed: () { - // Navigator.of(context).pushNamed( - // CoinNodesView.routeName, - // arguments: coin, - // ); - }, - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.iconFor(coin: coin), - width: 24, - height: 24, - ), - const SizedBox( - width: 12, - ), - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "${coin.prettyName} nodes", - style: STextStyles.titleBold12( - context), - ), - Text( - count > 1 - ? "$count nodes" - : "Default", - style: STextStyles.label(context), - ), - ], - ) - ], - ), + side: BorderSide( + color: Theme.of(context) + .extension<StackColors>()! + .shadow), + ), + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + onPressed: () { + // Navigator.of(context).pushNamed( + // CoinNodesView.routeName, + // arguments: coin, + // ); + }, + child: Padding( + padding: const EdgeInsets.all( + 12.0, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.iconFor(coin: coin), + width: 24, + height: 24, + ), + const SizedBox( + width: 12, + ), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "${coin.prettyName} nodes", + style: + STextStyles.titleBold12(context), + ), + Text( + count > 1 + ? "$count nodes" + : "Default", + style: STextStyles.label(context), + ), + ], + ), + // SvgPicture.asset( + // Assets.svg.chevronRight, + // alignment: Alignment.centerRight, + // ), + ], ), ), ), diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 5136f1a37..407332cf4 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -59,6 +59,8 @@ class _SVG { String txExchangeFailed(BuildContext context) => "assets/svg/${Theme.of(context).extension<StackColors>()!.themeType.name}/tx-exchange-icon-failed.svg"; + // String get themeLight => "assets/svg/light/light-theme.svg"; + String get themeDark => "assets/svg/dark/dark-theme.svg"; String get circleNode => "assets/svg/node-circle.svg"; String get circleSun => "assets/svg/sun-circle.svg"; String get circleArrowRotate => "assets/svg/rotate-circle.svg"; diff --git a/pubspec.yaml b/pubspec.yaml index 64fe730f6..75a0e8d3f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -299,6 +299,7 @@ flutter: - assets/svg/rotate-circle.svg - assets/svg/sun-circle.svg - assets/svg/node-circle.svg + - assets/svg/dark/dark-theme.svg # coin icons - assets/svg/coin_icons/Bitcoin.svg - assets/svg/coin_icons/Bitcoincash.svg From ab5190562d07b8ae04837c70830e34688e22e9d3 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Tue, 25 Oct 2022 09:19:45 -0600 Subject: [PATCH 030/426] nodes settings ui and light-theme icon added --- assets/svg/light/light-mode.svg | 24 +++++++++ .../home/settings_menu/nodes_settings.dart | 54 ++++++++++--------- lib/utilities/assets.dart | 2 +- pubspec.yaml | 1 + 4 files changed, 56 insertions(+), 25 deletions(-) create mode 100644 assets/svg/light/light-mode.svg diff --git a/assets/svg/light/light-mode.svg b/assets/svg/light/light-mode.svg new file mode 100644 index 000000000..4ff9e2696 --- /dev/null +++ b/assets/svg/light/light-mode.svg @@ -0,0 +1,24 @@ +<svg width="200" height="162" viewBox="0 0 200 162" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0_5887_94222)"> +<rect width="200" height="162" rx="8" fill="#E8EAEC"/> +<rect x="10" y="10" width="180" height="20" rx="2" fill="#DBDDE1"/> +<rect x="16" y="16" width="106" height="8" rx="1" fill="#C4C8CC"/> +<rect x="10" y="40" width="180" height="20" rx="2" fill="#FEFEFE"/> +<rect x="16" y="46" width="106" height="8" rx="1" fill="#C4C8CC"/> +<rect x="10" y="62" width="180" height="20" rx="2" fill="#FEFEFE"/> +<rect x="16" y="68" width="106" height="8" rx="1" fill="#C4C8CC"/> +<rect x="10" y="84" width="180" height="20" rx="2" fill="#FEFEFE"/> +<rect x="16" y="90" width="106" height="8" rx="1" fill="#C4C8CC"/> +<rect x="10" y="106" width="180" height="20" rx="2" fill="#FEFEFE"/> +<rect x="16" y="112" width="106" height="8" rx="1" fill="#C4C8CC"/> +<rect x="10" y="128" width="180" height="20" rx="2" fill="#FEFEFE"/> +<rect x="16" y="134" width="106" height="8" rx="1" fill="#C4C8CC"/> +<rect x="10" y="150" width="180" height="20" rx="2" fill="#FEFEFE"/> +<rect x="16" y="156" width="106" height="8" rx="1" fill="#C4C8CC"/> +</g> +<defs> +<clipPath id="clip0_5887_94222"> +<rect width="200" height="162" rx="8" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart b/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart index 1cc6edaaa..8608e79af 100644 --- a/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart @@ -127,35 +127,41 @@ class _NodesSettings extends ConsumerState<NodesSettings> { ), child: Row( children: [ - SvgPicture.asset( - Assets.svg.iconFor(coin: coin), - width: 24, - height: 24, - ), - const SizedBox( - width: 12, - ), - Column( - crossAxisAlignment: - CrossAxisAlignment.start, + Row( children: [ - Text( - "${coin.prettyName} nodes", - style: - STextStyles.titleBold12(context), + SvgPicture.asset( + Assets.svg.iconFor(coin: coin), + width: 24, + height: 24, ), - Text( - count > 1 - ? "$count nodes" - : "Default", - style: STextStyles.label(context), + const SizedBox( + width: 12, + ), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "${coin.prettyName} nodes", + style: STextStyles.titleBold12( + context), + ), + Text( + count > 1 + ? "$count nodes" + : "Default", + style: STextStyles.label(context), + ), + ], ), ], ), - // SvgPicture.asset( - // Assets.svg.chevronRight, - // alignment: Alignment.centerRight, - // ), + Expanded( + child: SvgPicture.asset( + Assets.svg.chevronRight, + alignment: Alignment.centerRight, + ), + ), ], ), ), diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 407332cf4..2ada80a5d 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -59,7 +59,7 @@ class _SVG { String txExchangeFailed(BuildContext context) => "assets/svg/${Theme.of(context).extension<StackColors>()!.themeType.name}/tx-exchange-icon-failed.svg"; - // String get themeLight => "assets/svg/light/light-theme.svg"; + String get themeLight => "assets/svg/light/light-mode.svg"; String get themeDark => "assets/svg/dark/dark-theme.svg"; String get circleNode => "assets/svg/node-circle.svg"; String get circleSun => "assets/svg/sun-circle.svg"; diff --git a/pubspec.yaml b/pubspec.yaml index 75a0e8d3f..35fbecf3f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -300,6 +300,7 @@ flutter: - assets/svg/sun-circle.svg - assets/svg/node-circle.svg - assets/svg/dark/dark-theme.svg + - assets/svg/light/light-mode.svg # coin icons - assets/svg/coin_icons/Bitcoin.svg - assets/svg/coin_icons/Bitcoincash.svg From aa8b7221746637a375c2081998221b3151c97713 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 26 Oct 2022 11:59:54 -0600 Subject: [PATCH 031/426] basic desktop send layout --- .../wallet_view/desktop_wallet_view.dart | 58 +- .../wallet_view/receive/desktop_receive.dart | 17 + .../wallet_view/send/desktop_send.dart | 1171 +++++++++++++++++ 3 files changed, 1208 insertions(+), 38 deletions(-) create mode 100644 lib/pages_desktop_specific/home/my_stack_view/wallet_view/receive/desktop_receive.dart create mode 100644 lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index f8cc7e2dc..959f9c052 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/receive/desktop_receive.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -171,11 +173,13 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { ), Expanded( child: Row( - children: const [ + children: [ Expanded( - child: MyWallet(), + child: MyWallet( + walletId: walletId, + ), ), - SizedBox( + const SizedBox( width: 16, ), Expanded( @@ -192,7 +196,12 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { } class MyWallet extends StatefulWidget { - const MyWallet({Key? key}) : super(key: key); + const MyWallet({ + Key? key, + required this.walletId, + }) : super(key: key); + + final String walletId; @override State<MyWallet> createState() => _MyWalletState(); @@ -246,10 +255,15 @@ class _MyWalletState extends State<MyWallet> { Tab(text: "Receive"), ], ), - const Expanded( + Expanded( child: TabBarView( children: [ - DesktopSend(), + Padding( + padding: const EdgeInsets.all(20), + child: DesktopSend( + walletId: widget.walletId, + ), + ), DesktopReceive(), ], ), @@ -264,38 +278,6 @@ class _MyWalletState extends State<MyWallet> { } } -class DesktopReceive extends StatefulWidget { - const DesktopReceive({Key? key}) : super(key: key); - - @override - State<DesktopReceive> createState() => _DesktopReceiveState(); -} - -class _DesktopReceiveState extends State<DesktopReceive> { - @override - Widget build(BuildContext context) { - return Container( - color: Colors.green, - ); - } -} - -class DesktopSend extends StatefulWidget { - const DesktopSend({Key? key}) : super(key: key); - - @override - State<DesktopSend> createState() => _DesktopSendState(); -} - -class _DesktopSendState extends State<DesktopSend> { - @override - Widget build(BuildContext context) { - return Container( - color: Colors.red, - ); - } -} - class RecentDesktopTransactions extends StatefulWidget { const RecentDesktopTransactions({Key? key}) : super(key: key); diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/receive/desktop_receive.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/receive/desktop_receive.dart new file mode 100644 index 000000000..319076dfe --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/receive/desktop_receive.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class DesktopReceive extends StatefulWidget { + const DesktopReceive({Key? key}) : super(key: key); + + @override + State<DesktopReceive> createState() => _DesktopReceiveState(); +} + +class _DesktopReceiveState extends State<DesktopReceive> { + @override + Widget build(BuildContext context) { + return Container( + color: Colors.green, + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart new file mode 100644 index 000000000..8f45b70c4 --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart @@ -0,0 +1,1171 @@ +import 'dart:async'; + +import 'package:decimal/decimal.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/models/send_view_auto_fill_data.dart'; +import 'package:stackwallet/pages/address_book_views/address_book_view.dart'; +import 'package:stackwallet/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart'; +import 'package:stackwallet/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/providers/ui/fee_rate_type_state_provider.dart'; +import 'package:stackwallet/providers/ui/preview_tx_button_state_provider.dart'; +import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart'; +import 'package:stackwallet/route_generator.dart'; +import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; +import 'package:stackwallet/services/coins/manager.dart'; +import 'package:stackwallet/utilities/address_utils.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/barcode_scanner_interface.dart'; +import 'package:stackwallet/utilities/clipboard_interface.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; +import 'package:stackwallet/utilities/format.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/prefs.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/animated_text.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/icon_widgets/addressbook_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; + +import '../../../../../pages/send_view/confirm_transaction_view.dart'; +import '../../../../../pages/send_view/sub_widgets/building_transaction_dialog.dart'; + +class DesktopSend extends ConsumerStatefulWidget { + const DesktopSend({ + Key? key, + required this.walletId, + this.autoFillData, + this.clipboard = const ClipboardWrapper(), + this.barcodeScanner = const BarcodeScannerWrapper(), + }) : super(key: key); + + final String walletId; + final SendViewAutoFillData? autoFillData; + final ClipboardInterface clipboard; + final BarcodeScannerInterface barcodeScanner; + + @override + ConsumerState<DesktopSend> createState() => _DesktopSendState(); +} + +class _DesktopSendState extends ConsumerState<DesktopSend> { + late final String walletId; + late final Coin coin; + late final ClipboardInterface clipboard; + late final BarcodeScannerInterface scanner; + + late TextEditingController sendToController; + late TextEditingController cryptoAmountController; + late TextEditingController baseAmountController; + late TextEditingController noteController; + late TextEditingController feeController; + + late final SendViewAutoFillData? _data; + + final _addressFocusNode = FocusNode(); + final _noteFocusNode = FocusNode(); + final _cryptoFocus = FocusNode(); + final _baseFocus = FocusNode(); + + Decimal? _amountToSend; + Decimal? _cachedAmountToSend; + String? _address; + + String? _privateBalanceString; + String? _publicBalanceString; + + bool _addressToggleFlag = false; + + bool _cryptoAmountChangeLock = false; + late VoidCallback onCryptoAmountChanged; + + Decimal? _cachedBalance; + + Future<void> previewSend() async { + // wait for keyboard to disappear + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 100), + ); + final manager = + ref.read(walletsChangeNotifierProvider).getManager(walletId); + + // TODO: remove the need for this!! + final bool isOwnAddress = await manager.isOwnAddress(_address!); + if (isOwnAddress) { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return StackDialog( + title: "Transaction failed", + message: "Sending to self is currently disabled", + rightButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Ok", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ); + }, + ); + return; + } + + final amount = Format.decimalAmountToSatoshis(_amountToSend!); + int availableBalance; + if ((coin == Coin.firo || coin == Coin.firoTestNet)) { + if (ref.read(publicPrivateBalanceStateProvider.state).state == + "Private") { + availableBalance = Format.decimalAmountToSatoshis( + await (manager.wallet as FiroWallet).availablePrivateBalance()); + } else { + availableBalance = Format.decimalAmountToSatoshis( + await (manager.wallet as FiroWallet).availablePublicBalance()); + } + } else { + availableBalance = + Format.decimalAmountToSatoshis(await manager.availableBalance); + } + + // confirm send all + if (amount == availableBalance) { + final bool? shouldSendAll = await showDialog<bool>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return StackDialog( + title: "Confirm send all", + message: + "You are about to send your entire balance. Would you like to continue?", + leftButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Cancel", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + rightButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + child: Text( + "Yes", + style: STextStyles.button(context), + ), + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ); + }, + ); + + if (shouldSendAll == null || shouldSendAll == false) { + // cancel preview + return; + } + } + + try { + bool wasCancelled = false; + + unawaited(showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return BuildingTransactionDialog( + onCancel: () { + wasCancelled = true; + + Navigator.of(context).pop(); + }, + ); + }, + )); + + Map<String, dynamic> txData; + + if ((coin == Coin.firo || coin == Coin.firoTestNet) && + ref.read(publicPrivateBalanceStateProvider.state).state != + "Private") { + txData = await (manager.wallet as FiroWallet).prepareSendPublic( + address: _address!, + satoshiAmount: amount, + args: {"feeRate": ref.read(feeRateTypeStateProvider)}, + ); + } else { + txData = await manager.prepareSend( + address: _address!, + satoshiAmount: amount, + args: {"feeRate": ref.read(feeRateTypeStateProvider)}, + ); + } + + if (!wasCancelled && mounted) { + // pop building dialog + Navigator.of(context).pop(); + txData["note"] = noteController.text; + txData["address"] = _address; + + unawaited(Navigator.of(context).push( + RouteGenerator.getRoute( + shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, + builder: (_) => ConfirmTransactionView( + transactionInfo: txData, + walletId: walletId, + ), + settings: const RouteSettings( + name: ConfirmTransactionView.routeName, + ), + ), + )); + } + } catch (e) { + if (mounted) { + // pop building dialog + Navigator.of(context).pop(); + + unawaited(showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return StackDialog( + title: "Transaction failed", + message: e.toString(), + rightButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Ok", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ); + }, + )); + } + } + } + + void _cryptoAmountChanged() async { + if (!_cryptoAmountChangeLock) { + final String cryptoAmount = cryptoAmountController.text; + if (cryptoAmount.isNotEmpty && + cryptoAmount != "." && + cryptoAmount != ",") { + _amountToSend = cryptoAmount.contains(",") + ? Decimal.parse(cryptoAmount.replaceFirst(",", ".")) + : Decimal.parse(cryptoAmount); + if (_cachedAmountToSend != null && + _cachedAmountToSend == _amountToSend) { + return; + } + _cachedAmountToSend = _amountToSend; + Logging.instance.log("it changed $_amountToSend $_cachedAmountToSend", + level: LogLevel.Info); + + final price = + ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1; + + if (price > Decimal.zero) { + final String fiatAmountString = Format.localizedStringAsFixed( + value: _amountToSend! * price, + locale: ref.read(localeServiceChangeNotifierProvider).locale, + decimalPlaces: 2, + ); + + baseAmountController.text = fiatAmountString; + } + } else { + _amountToSend = null; + baseAmountController.text = ""; + } + + _updatePreviewButtonState(_address, _amountToSend); + } + } + + String? _updateInvalidAddressText(String address, Manager manager) { + if (_data != null && _data!.contactLabel == address) { + return null; + } + if (address.isNotEmpty && !manager.validateAddress(address)) { + return "Invalid address"; + } + return null; + } + + void _updatePreviewButtonState(String? address, Decimal? amount) { + final isValidAddress = ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .validateAddress(address ?? ""); + ref.read(previewTxButtonStateProvider.state).state = + (isValidAddress && amount != null && amount > Decimal.zero); + } + + late Future<String> _calculateFeesFuture; + + Map<int, String> cachedFees = {}; + Map<int, String> cachedFiroPrivateFees = {}; + Map<int, String> cachedFiroPublicFees = {}; + + Future<String> calculateFees(int amount) async { + if (amount <= 0) { + return "0"; + } + + if (coin == Coin.firo || coin == Coin.firoTestNet) { + if (ref.read(publicPrivateBalanceStateProvider.state).state == + "Private") { + if (cachedFiroPrivateFees[amount] != null) { + return cachedFiroPrivateFees[amount]!; + } + } else { + if (cachedFiroPublicFees[amount] != null) { + return cachedFiroPublicFees[amount]!; + } + } + } else if (cachedFees[amount] != null) { + return cachedFees[amount]!; + } + + final manager = + ref.read(walletsChangeNotifierProvider).getManager(walletId); + final feeObject = await manager.fees; + + late final int feeRate; + + switch (ref.read(feeRateTypeStateProvider.state).state) { + case FeeRateType.fast: + feeRate = feeObject.fast; + break; + case FeeRateType.average: + feeRate = feeObject.medium; + break; + case FeeRateType.slow: + feeRate = feeObject.slow; + break; + } + + int fee; + + if (coin == Coin.firo || coin == Coin.firoTestNet) { + if (ref.read(publicPrivateBalanceStateProvider.state).state == + "Private") { + fee = await manager.estimateFeeFor(amount, feeRate); + + cachedFiroPrivateFees[amount] = Format.satoshisToAmount(fee) + .toStringAsFixed(Constants.decimalPlaces); + + return cachedFiroPrivateFees[amount]!; + } else { + fee = await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate); + + cachedFiroPublicFees[amount] = Format.satoshisToAmount(fee) + .toStringAsFixed(Constants.decimalPlaces); + + return cachedFiroPublicFees[amount]!; + } + } else { + fee = await manager.estimateFeeFor(amount, feeRate); + cachedFees[amount] = + Format.satoshisToAmount(fee).toStringAsFixed(Constants.decimalPlaces); + + return cachedFees[amount]!; + } + } + + Future<String?> _firoBalanceFuture( + ChangeNotifierProvider<Manager> provider, String locale) async { + final wallet = ref.read(provider).wallet as FiroWallet?; + + if (wallet != null) { + Decimal? balance; + if (ref.read(publicPrivateBalanceStateProvider.state).state == + "Private") { + balance = await wallet.availablePrivateBalance(); + } else { + balance = await wallet.availablePublicBalance(); + } + + return Format.localizedStringAsFixed( + value: balance, locale: locale, decimalPlaces: 8); + } + + return null; + } + + @override + void initState() { + ref.refresh(feeSheetSessionCacheProvider); + + _calculateFeesFuture = calculateFees(0); + _data = widget.autoFillData; + walletId = widget.walletId; + coin = ref.read(walletsChangeNotifierProvider).getManager(walletId).coin; + clipboard = widget.clipboard; + scanner = widget.barcodeScanner; + + sendToController = TextEditingController(); + cryptoAmountController = TextEditingController(); + baseAmountController = TextEditingController(); + noteController = TextEditingController(); + feeController = TextEditingController(); + + onCryptoAmountChanged = _cryptoAmountChanged; + cryptoAmountController.addListener(onCryptoAmountChanged); + + if (_data != null) { + if (_data!.amount != null) { + cryptoAmountController.text = _data!.amount!.toString(); + } + sendToController.text = _data!.contactLabel; + _address = _data!.address; + _addressToggleFlag = true; + } + + _cryptoFocus.addListener(() { + if (!_cryptoFocus.hasFocus && !_baseFocus.hasFocus) { + if (_amountToSend == null) { + setState(() { + _calculateFeesFuture = calculateFees(0); + }); + } else { + setState(() { + _calculateFeesFuture = + calculateFees(Format.decimalAmountToSatoshis(_amountToSend!)); + }); + } + } + }); + + _baseFocus.addListener(() { + if (!_cryptoFocus.hasFocus && !_baseFocus.hasFocus) { + if (_amountToSend == null) { + setState(() { + _calculateFeesFuture = calculateFees(0); + }); + } else { + setState(() { + _calculateFeesFuture = + calculateFees(Format.decimalAmountToSatoshis(_amountToSend!)); + }); + } + } + }); + + super.initState(); + } + + @override + void dispose() { + cryptoAmountController.removeListener(onCryptoAmountChanged); + + sendToController.dispose(); + cryptoAmountController.dispose(); + baseAmountController.dispose(); + noteController.dispose(); + feeController.dispose(); + + _noteFocusNode.dispose(); + _addressFocusNode.dispose(); + _cryptoFocus.dispose(); + _baseFocus.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + final provider = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManagerProvider(walletId))); + final String locale = ref.watch( + localeServiceChangeNotifierProvider.select((value) => value.locale)); + + if (coin == Coin.firo || coin == Coin.firoTestNet) { + ref.listen(publicPrivateBalanceStateProvider, (previous, next) { + if (_amountToSend == null) { + setState(() { + _calculateFeesFuture = calculateFees(0); + }); + } else { + setState(() { + _calculateFeesFuture = + calculateFees(Format.decimalAmountToSatoshis(_amountToSend!)); + }); + } + }); + } + + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 4, + ), + if (coin == Coin.firo) + Text( + "Send from", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + if (coin == Coin.firo) + const SizedBox( + height: 10, + ), + if (coin == Coin.firo) + Stack( + children: [ + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + readOnly: true, + textInputAction: TextInputAction.none, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + ), + child: RawMaterialButton( + splashColor: + Theme.of(context).extension<StackColors>()!.highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + showModalBottomSheet<dynamic>( + backgroundColor: Colors.transparent, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + builder: (_) => FiroBalanceSelectionSheet( + walletId: walletId, + ), + ); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + "${ref.watch(publicPrivateBalanceStateProvider.state).state} balance", + style: STextStyles.itemSubtitle12(context), + ), + const SizedBox( + width: 10, + ), + FutureBuilder( + future: _firoBalanceFuture(provider, locale), + builder: + (context, AsyncSnapshot<String?> snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + if (ref + .read( + publicPrivateBalanceStateProvider + .state) + .state == + "Private") { + _privateBalanceString = snapshot.data!; + } else { + _publicBalanceString = snapshot.data!; + } + } + if (ref + .read( + publicPrivateBalanceStateProvider + .state) + .state == + "Private" && + _privateBalanceString != null) { + return Text( + "$_privateBalanceString ${coin.ticker}", + style: STextStyles.itemSubtitle(context), + ); + } else if (ref + .read( + publicPrivateBalanceStateProvider + .state) + .state == + "Public" && + _publicBalanceString != null) { + return Text( + "$_publicBalanceString ${coin.ticker}", + style: STextStyles.itemSubtitle(context), + ); + } else { + return AnimatedText( + stringsToLoopThrough: const [ + "Loading balance", + "Loading balance.", + "Loading balance..", + "Loading balance...", + ], + style: STextStyles.itemSubtitle(context), + ); + } + }, + ), + ], + ), + SvgPicture.asset( + Assets.svg.chevronDown, + width: 8, + height: 4, + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle2, + ), + ], + ), + ), + ) + ], + ), + if (coin == Coin.firo) + const SizedBox( + height: 20, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Amount", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + BlueTextButton( + text: "Send all ${coin.ticker}", + onTap: () async { + if (coin == Coin.firo || coin == Coin.firoTestNet) { + final firoWallet = ref.read(provider).wallet as FiroWallet; + if (ref + .read(publicPrivateBalanceStateProvider.state) + .state == + "Private") { + cryptoAmountController.text = + (await firoWallet.availablePrivateBalance()) + .toStringAsFixed(Constants.decimalPlaces); + } else { + cryptoAmountController.text = + (await firoWallet.availablePublicBalance()) + .toStringAsFixed(Constants.decimalPlaces); + } + } else { + cryptoAmountController.text = + (await ref.read(provider).availableBalance) + .toStringAsFixed(Constants.decimalPlaces); + } + }, + ), + ], + ), + const SizedBox( + height: 10, + ), + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark, + ), + key: const Key("amountInputFieldCryptoTextFieldKey"), + controller: cryptoAmountController, + focusNode: _cryptoFocus, + keyboardType: const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), + textAlign: TextAlign.right, + inputFormatters: [ + // regex to validate a crypto amount with 8 decimal places + TextInputFormatter.withFunction((oldValue, newValue) => + RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') + .hasMatch(newValue.text) + ? newValue + : oldValue), + ], + decoration: InputDecoration( + contentPadding: const EdgeInsets.only( + top: 12, + right: 12, + ), + hintText: "0", + hintStyle: STextStyles.fieldLabel(context).copyWith( + fontSize: 14, + ), + prefixIcon: FittedBox( + fit: BoxFit.scaleDown, + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + coin.ticker, + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + ), + ), + ), + if (Prefs.instance.externalCalls) + const SizedBox( + height: 10, + ), + if (Prefs.instance.externalCalls) + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark, + ), + key: const Key("amountInputFieldFiatTextFieldKey"), + controller: baseAmountController, + focusNode: _baseFocus, + keyboardType: const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), + textAlign: TextAlign.right, + inputFormatters: [ + // regex to validate a fiat amount with 2 decimal places + TextInputFormatter.withFunction((oldValue, newValue) => + RegExp(r'^([0-9]*[,.]?[0-9]{0,2}|[,.][0-9]{0,2})$') + .hasMatch(newValue.text) + ? newValue + : oldValue), + ], + onChanged: (baseAmountString) { + if (baseAmountString.isNotEmpty && + baseAmountString != "." && + baseAmountString != ",") { + final baseAmount = baseAmountString.contains(",") + ? Decimal.parse(baseAmountString.replaceFirst(",", ".")) + : Decimal.parse(baseAmountString); + + var _price = ref + .read(priceAnd24hChangeNotifierProvider) + .getPrice(coin) + .item1; + + if (_price == Decimal.zero) { + _amountToSend = Decimal.zero; + } else { + _amountToSend = baseAmount <= Decimal.zero + ? Decimal.zero + : (baseAmount / _price).toDecimal( + scaleOnInfinitePrecision: Constants.decimalPlaces); + } + if (_cachedAmountToSend != null && + _cachedAmountToSend == _amountToSend) { + return; + } + _cachedAmountToSend = _amountToSend; + Logging.instance.log( + "it changed $_amountToSend $_cachedAmountToSend", + level: LogLevel.Info); + + final amountString = Format.localizedStringAsFixed( + value: _amountToSend!, + locale: + ref.read(localeServiceChangeNotifierProvider).locale, + decimalPlaces: Constants.decimalPlaces, + ); + + _cryptoAmountChangeLock = true; + cryptoAmountController.text = amountString; + _cryptoAmountChangeLock = false; + } else { + _amountToSend = Decimal.zero; + _cryptoAmountChangeLock = true; + cryptoAmountController.text = ""; + _cryptoAmountChangeLock = false; + } + // setState(() { + // _calculateFeesFuture = calculateFees( + // Format.decimalAmountToSatoshis( + // _amountToSend!)); + // }); + _updatePreviewButtonState(_address, _amountToSend); + }, + decoration: InputDecoration( + contentPadding: const EdgeInsets.only( + top: 12, + right: 12, + ), + hintText: "0", + hintStyle: STextStyles.fieldLabel(context).copyWith( + fontSize: 14, + ), + prefixIcon: FittedBox( + fit: BoxFit.scaleDown, + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + ref.watch(prefsChangeNotifierProvider + .select((value) => value.currency)), + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + ), + ), + ), + const SizedBox( + height: 20, + ), + Text( + "Send to", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + const SizedBox( + height: 10, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("sendViewAddressFieldKey"), + controller: sendToController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + // inputFormatters: <TextInputFormatter>[ + // FilteringTextInputFormatter.allow( + // RegExp("[a-zA-Z0-9]{34}")), + // ], + toolbarOptions: const ToolbarOptions( + copy: false, + cut: false, + paste: true, + selectAll: false, + ), + onChanged: (newValue) { + _address = newValue; + _updatePreviewButtonState(_address, _amountToSend); + + setState(() { + _addressToggleFlag = newValue.isNotEmpty; + }); + }, + focusNode: _addressFocusNode, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Enter ${coin.ticker} address", + _addressFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: sendToController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _addressToggleFlag + ? TextFieldIconButton( + key: const Key( + "sendViewClearAddressFieldButtonKey"), + onTap: () { + sendToController.text = ""; + _address = ""; + _updatePreviewButtonState( + _address, _amountToSend); + setState(() { + _addressToggleFlag = false; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "sendViewPasteAddressFieldButtonKey"), + onTap: () async { + final ClipboardData? data = await clipboard + .getData(Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + String content = data.text!.trim(); + if (content.contains("\n")) { + content = content.substring( + 0, content.indexOf("\n")); + } + + sendToController.text = content; + _address = content; + + _updatePreviewButtonState( + _address, _amountToSend); + setState(() { + _addressToggleFlag = + sendToController.text.isNotEmpty; + }); + } + }, + child: sendToController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (sendToController.text.isEmpty) + TextFieldIconButton( + key: const Key("sendViewAddressBookButtonKey"), + onTap: () { + Navigator.of(context).pushNamed( + AddressBookView.routeName, + arguments: coin, + ); + }, + child: const AddressBookIcon(), + ), + if (sendToController.text.isEmpty) + TextFieldIconButton( + key: const Key("sendViewScanQrButtonKey"), + onTap: () async { + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75)); + } + + final qrResult = await scanner.scan(); + + Logging.instance.log( + "qrResult content: ${qrResult.rawContent}", + level: LogLevel.Info); + + final results = + AddressUtils.parseUri(qrResult.rawContent); + + Logging.instance.log( + "qrResult parsed: $results", + level: LogLevel.Info); + + if (results.isNotEmpty && + results["scheme"] == coin.uriScheme) { + // auto fill address + _address = results["address"] ?? ""; + sendToController.text = _address!; + + // autofill notes field + if (results["message"] != null) { + noteController.text = results["message"]!; + } else if (results["label"] != null) { + noteController.text = results["label"]!; + } + + // autofill amount field + if (results["amount"] != null) { + final amount = + Decimal.parse(results["amount"]!); + cryptoAmountController.text = + Format.localizedStringAsFixed( + value: amount, + locale: ref + .read( + localeServiceChangeNotifierProvider) + .locale, + decimalPlaces: Constants.decimalPlaces, + ); + amount.toString(); + _amountToSend = amount; + } + + _updatePreviewButtonState( + _address, _amountToSend); + setState(() { + _addressToggleFlag = + sendToController.text.isNotEmpty; + }); + + // now check for non standard encoded basic address + } else if (ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .validateAddress(qrResult.rawContent)) { + _address = qrResult.rawContent; + sendToController.text = _address ?? ""; + + _updatePreviewButtonState( + _address, _amountToSend); + setState(() { + _addressToggleFlag = + sendToController.text.isNotEmpty; + }); + } + } on PlatformException catch (e, s) { + // here we ignore the exception caused by not giving permission + // to use the camera to scan a qr code + Logging.instance.log( + "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", + level: LogLevel.Warning); + } + }, + child: const QrCodeIcon(), + ) + ], + ), + ), + ), + ), + ), + ), + Builder( + builder: (_) { + final error = _updateInvalidAddressText( + _address ?? "", + ref.read(walletsChangeNotifierProvider).getManager(walletId), + ); + + if (error == null || error.isEmpty) { + return Container(); + } else { + return Align( + alignment: Alignment.topLeft, + child: Padding( + padding: const EdgeInsets.only( + left: 12.0, + top: 4.0, + ), + child: Text( + error, + textAlign: TextAlign.left, + style: STextStyles.label(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textError, + ), + ), + ), + ); + } + }, + ), + const SizedBox( + height: 20, + ), + Text( + "Note (optional)", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + const SizedBox( + height: 10, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: noteController, + focusNode: _noteFocusNode, + style: STextStyles.field(context), + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Type something...", + _noteFocusNode, + context, + ).copyWith( + suffixIcon: noteController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + noteController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + const SizedBox( + height: 36, + ), + PrimaryButton( + label: "Preview send", + enabled: ref.watch(previewTxButtonStateProvider.state).state, + onPressed: ref.watch(previewTxButtonStateProvider.state).state + ? previewSend + : null, + ) + ], + ), + ); + } +} From 77354123039ce24d3089fe26156f3ad5220a01bb Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 26 Oct 2022 13:43:22 -0600 Subject: [PATCH 032/426] basic desktop receive layout --- .../wallet_view/desktop_wallet_view.dart | 7 +- .../wallet_view/receive/desktop_receive.dart | 237 +++++++++++++++++- .../wallet_view/send/desktop_send.dart | 1 + 3 files changed, 237 insertions(+), 8 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index 959f9c052..510c4bc7c 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -264,7 +264,12 @@ class _MyWalletState extends State<MyWallet> { walletId: widget.walletId, ), ), - DesktopReceive(), + Padding( + padding: const EdgeInsets.all(20), + child: DesktopReceive( + walletId: widget.walletId, + ), + ), ], ), ), diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/receive/desktop_receive.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/receive/desktop_receive.dart index 319076dfe..2efbcd84f 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/receive/desktop_receive.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/receive/desktop_receive.dart @@ -1,17 +1,240 @@ -import 'package:flutter/material.dart'; +import 'dart:async'; -class DesktopReceive extends StatefulWidget { - const DesktopReceive({Key? key}) : super(key: key); +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/receive_view/generate_receiving_uri_qr_code_view.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/route_generator.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/clipboard_interface.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/custom_loading_overlay.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class DesktopReceive extends ConsumerStatefulWidget { + const DesktopReceive({ + Key? key, + required this.walletId, + this.clipboard = const ClipboardWrapper(), + }) : super(key: key); + + final String walletId; + final ClipboardInterface clipboard; @override - State<DesktopReceive> createState() => _DesktopReceiveState(); + ConsumerState<DesktopReceive> createState() => _DesktopReceiveState(); } -class _DesktopReceiveState extends State<DesktopReceive> { +class _DesktopReceiveState extends ConsumerState<DesktopReceive> { + late final Coin coin; + late final String walletId; + late final ClipboardInterface clipboard; + + Future<void> generateNewAddress() async { + bool shouldPop = false; + unawaited( + showDialog( + context: context, + builder: (_) { + return WillPopScope( + onWillPop: () async => shouldPop, + child: Container( + color: Theme.of(context) + .extension<StackColors>()! + .overlay + .withOpacity(0.5), + child: const CustomLoadingOverlay( + message: "Generating address", + eventBus: null, + ), + ), + ); + }, + ), + ); + + await ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .generateNewAddress(); + + shouldPop = true; + + if (mounted) { + Navigator.of(context, rootNavigator: true).pop(); + } + } + + String receivingAddress = ""; + + @override + void initState() { + walletId = widget.walletId; + coin = ref.read(walletsChangeNotifierProvider).getManager(walletId).coin; + clipboard = widget.clipboard; + + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + final address = await ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .currentReceivingAddress; + setState(() { + receivingAddress = address; + }); + }); + + super.initState(); + } + @override Widget build(BuildContext context) { - return Container( - color: Colors.green, + debugPrint("BUILD: $runtimeType"); + + ref.listen( + ref + .read(walletsChangeNotifierProvider) + .getManagerProvider(walletId) + .select((value) => value.currentReceivingAddress), + (previous, next) { + if (next is Future<String>) { + next.then((value) => setState(() => receivingAddress = value)); + } + }); + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + GestureDetector( + onTap: () { + clipboard.setData( + ClipboardData(text: receivingAddress), + ); + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ); + }, + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).extension<StackColors>()!.background, + width: 2, + ), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: RoundedWhiteContainer( + child: Column( + children: [ + Row( + children: [ + Text( + "Your ${coin.ticker} address", + style: STextStyles.itemSubtitle(context), + ), + const Spacer(), + Row( + children: [ + SvgPicture.asset( + Assets.svg.copy, + width: 10, + height: 10, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + ), + const SizedBox( + width: 4, + ), + Text( + "Copy", + style: STextStyles.link2(context), + ), + ], + ), + ], + ), + const SizedBox( + height: 4, + ), + Row( + children: [ + Expanded( + child: Text( + receivingAddress, + style: STextStyles.itemSubtitle12(context), + ), + ), + ], + ), + ], + ), + ), + ), + ), + if (coin != Coin.epicCash) + const SizedBox( + height: 20, + ), + if (coin != Coin.epicCash) + SecondaryButton( + height: 56, + onPressed: generateNewAddress, + label: "Generate new address", + ), + const SizedBox( + height: 32, + ), + Center( + child: SizedBox( + width: 200, + height: 200, + child: QrImage( + data: "${coin.uriScheme}:$receivingAddress", + size: MediaQuery.of(context).size.width / 2, + foregroundColor: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + const SizedBox( + height: 32, + ), + Center( + child: BlueTextButton( + text: "Create new QR code", + onTap: () async { + unawaited( + Navigator.of(context).push( + RouteGenerator.getRoute( + shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, + builder: (_) => GenerateUriQrCodeView( + coin: coin, + receivingAddress: receivingAddress, + ), + settings: const RouteSettings( + name: GenerateUriQrCodeView.routeName, + ), + ), + ), + ); + }, + ), + ), + ], ); } } diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart index 8f45b70c4..866f1ab56 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart @@ -1158,6 +1158,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { height: 36, ), PrimaryButton( + height: 56, label: "Preview send", enabled: ref.watch(previewTxButtonStateProvider.state).state, onPressed: ref.watch(previewTxButtonStateProvider.state).state From 992debc86ae98a2635af18551c20fd31cf0a10f7 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 26 Oct 2022 15:04:04 -0600 Subject: [PATCH 033/426] desktop menu icons update and exit functionality implemented --- assets/svg/about-desktop.svg | 3 + assets/svg/address-book-desktop.svg | 3 + assets/svg/exchange-desktop.svg | 3 + assets/svg/exit-desktop.svg | 3 + assets/svg/wallet-desktop.svg | 3 + .../home/desktop_home_view.dart | 3 - .../home/desktop_menu.dart | 303 +++++++++++------- lib/utilities/assets.dart | 5 + pubspec.yaml | 5 + 9 files changed, 208 insertions(+), 123 deletions(-) create mode 100644 assets/svg/about-desktop.svg create mode 100644 assets/svg/address-book-desktop.svg create mode 100644 assets/svg/exchange-desktop.svg create mode 100644 assets/svg/exit-desktop.svg create mode 100644 assets/svg/wallet-desktop.svg diff --git a/assets/svg/about-desktop.svg b/assets/svg/about-desktop.svg new file mode 100644 index 000000000..a80067d9c --- /dev/null +++ b/assets/svg/about-desktop.svg @@ -0,0 +1,3 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M3.44444 2.21973C2.09618 2.21973 1 3.31591 1 4.66417V15.6642C1 17.0124 2.09618 18.1086 3.44444 18.1086H10.1667L9.75799 19.3308H7.11111C6.43507 19.3308 5.88889 19.877 5.88889 20.5531C5.88889 21.2291 6.43507 21.7753 7.11111 21.7753H16.8889C17.5649 21.7753 18.1111 21.2291 18.1111 20.5531C18.1111 19.877 17.5649 19.3308 16.8889 19.3308H14.242L13.8333 18.1086H20.5556C21.9038 18.1086 23 17.0124 23 15.6642V4.66417C23 3.31591 21.9038 2.21973 20.5556 2.21973H3.44444ZM20.5556 4.66417V13.2197H3.44444V4.66417H20.5556Z" fill="#232323"/> +</svg> diff --git a/assets/svg/address-book-desktop.svg b/assets/svg/address-book-desktop.svg new file mode 100644 index 000000000..fb85e3e11 --- /dev/null +++ b/assets/svg/address-book-desktop.svg @@ -0,0 +1,3 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M16.8242 1H4.44922C2.93027 1 1.69922 2.23105 1.69922 3.75V20.25C1.69922 21.7689 2.93027 23 4.44922 23H16.8242C18.3432 23 19.5742 21.7689 19.5742 20.25V3.75C19.5742 2.23105 18.341 1 16.8242 1ZM10.6367 6.5C12.1557 6.5 13.3867 7.73105 13.3867 9.25C13.3867 10.7689 12.1557 12 10.6367 12C9.1182 12 7.88672 10.7689 7.88672 9.25C7.88672 7.73105 9.11992 6.5 10.6367 6.5ZM14.7617 17.5H6.51172C6.13359 17.5 5.82422 17.1906 5.82422 16.8125C5.82422 14.9133 7.3625 13.375 9.26172 13.375H12.0117C13.9101 13.375 15.4492 14.9141 15.4492 16.8125C15.4492 17.1906 15.1398 17.5 14.7617 17.5ZM21.6367 3.75H20.9492V7.875H21.6367C22.0148 7.875 22.3242 7.56563 22.3242 7.1875V4.4375C22.3242 4.05766 22.0148 3.75 21.6367 3.75ZM21.6367 9.25H20.9492V13.375H21.6367C22.0148 13.375 22.3242 13.0656 22.3242 12.6875V9.9375C22.3242 9.55937 22.0148 9.25 21.6367 9.25ZM21.6367 14.75H20.9492V18.875H21.6367C22.0164 18.875 22.3242 18.5672 22.3242 18.1875V15.4375C22.3242 15.0594 22.0148 14.75 21.6367 14.75Z" fill="#232323"/> +</svg> diff --git a/assets/svg/exchange-desktop.svg b/assets/svg/exchange-desktop.svg new file mode 100644 index 000000000..8eacfa84e --- /dev/null +++ b/assets/svg/exchange-desktop.svg @@ -0,0 +1,3 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M6.69844 6.70024C9.61406 3.78461 14.325 3.77055 17.2594 6.65336L15.3281 8.57993C15.0047 8.90336 14.9109 9.38618 15.0844 9.80805C15.2578 10.2299 15.6703 10.5018 16.125 10.5018H21.7266H22.125C22.7484 10.5018 23.25 10.0002 23.25 9.3768V3.3768C23.25 2.92211 22.9781 2.50961 22.5563 2.33618C22.1344 2.16274 21.6516 2.25649 21.3281 2.57993L19.3781 4.52993C15.2719 0.475238 8.65781 0.489301 4.575 4.5768C3.43125 5.72055 2.60625 7.06586 2.1 8.50492C1.82344 9.28774 2.23594 10.1409 3.01406 10.4174C3.79219 10.694 4.65 10.2815 4.92656 9.50336C5.2875 8.48149 5.87344 7.52055 6.69844 6.70024ZM0.75 14.6268V14.9831V15.0159V20.6268C0.75 21.0815 1.02187 21.494 1.44375 21.6674C1.86562 21.8409 2.34844 21.7471 2.67188 21.4237L4.62187 19.4737C8.72812 23.5284 15.3422 23.5143 19.425 19.4268C20.5688 18.2831 21.3984 16.9377 21.9047 15.5034C22.1812 14.7206 21.7687 13.8674 20.9906 13.5909C20.2125 13.3143 19.3547 13.7268 19.0781 14.5049C18.7172 15.5268 18.1313 16.4877 17.3063 17.3081C14.3906 20.2237 9.67969 20.2377 6.74531 17.3549L8.67188 15.4237C8.99531 15.1002 9.08906 14.6174 8.91562 14.1956C8.74219 13.7737 8.32969 13.5018 7.875 13.5018H2.26875H2.23594H1.875C1.25156 13.5018 0.75 14.0034 0.75 14.6268Z" fill="#232323"/> +</svg> diff --git a/assets/svg/exit-desktop.svg b/assets/svg/exit-desktop.svg new file mode 100644 index 000000000..abba264cd --- /dev/null +++ b/assets/svg/exit-desktop.svg @@ -0,0 +1,3 @@ +<svg width="22" height="20" viewBox="0 0 22 20" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M6.875 3.12012C7.63555 3.12012 8.25 2.50566 8.25 1.74512C8.25 0.98457 7.63555 0.370117 6.875 0.370117H4.125C1.84766 0.370117 0 2.21777 0 4.49512V15.4951C0 17.7725 1.84766 19.6201 4.125 19.6201H6.875C7.63555 19.6201 8.25 19.0057 8.25 18.2451C8.25 17.4846 7.63555 16.8701 6.875 16.8701H4.125C3.36445 16.8701 2.75 16.2557 2.75 15.4951V4.49512C2.75 3.73457 3.36445 3.12012 4.125 3.12012H6.875ZM21.6777 10.7428C21.884 10.5494 22 10.2787 22 9.99512C22 9.71152 21.884 9.44082 21.6777 9.24746L15.4902 3.40371C15.1895 3.12012 14.7512 3.04277 14.373 3.20605C13.9949 3.36934 13.75 3.74316 13.75 4.15137V7.24512H8.25C7.48945 7.24512 6.875 7.85957 6.875 8.62012V11.3701C6.875 12.1307 7.48945 12.7451 8.25 12.7451H13.75V15.8389C13.75 16.2514 13.9949 16.6209 14.373 16.7842C14.7512 16.9475 15.1895 16.8701 15.4902 16.5865L21.6777 10.7428Z" fill="#232323"/> +</svg> diff --git a/assets/svg/wallet-desktop.svg b/assets/svg/wallet-desktop.svg new file mode 100644 index 000000000..0b0acdae3 --- /dev/null +++ b/assets/svg/wallet-desktop.svg @@ -0,0 +1,3 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M20.25 2.83008C21.0105 2.83008 21.625 3.4165 21.625 4.1396C21.625 4.8627 21.0105 5.44913 20.25 5.44913H4.4375C4.05766 5.44913 3.75 5.74377 3.75 6.10389C3.75 6.46401 4.05766 6.75865 4.4375 6.75865H20.25C21.7668 6.75865 23 7.93313 23 9.3777V18.5444C23 19.9889 21.7668 21.1634 20.25 21.1634H3.75C2.23105 21.1634 1 19.9889 1 18.5444V5.44913C1 4.00251 2.23105 2.83008 3.75 2.83008H20.25ZM18.875 15.2706C19.6355 15.2706 20.25 14.6854 20.25 13.961C20.25 13.2367 19.6355 12.6515 18.875 12.6515C18.1145 12.6515 17.5 13.2367 17.5 13.961C17.5 14.6854 18.1145 15.2706 18.875 15.2706Z" fill="#232323"/> +</svg> diff --git a/lib/pages_desktop_specific/home/desktop_home_view.dart b/lib/pages_desktop_specific/home/desktop_home_view.dart index 41f4b5041..6aa104081 100644 --- a/lib/pages_desktop_specific/home/desktop_home_view.dart +++ b/lib/pages_desktop_specific/home/desktop_home_view.dart @@ -43,9 +43,6 @@ class _DesktopHomeViewState extends ConsumerState<DesktopHomeView> { Container( color: Colors.pink, ), - Container( - color: Colors.purple, - ), ]; void onMenuSelectionChanged(int newIndex) { diff --git a/lib/pages_desktop_specific/home/desktop_menu.dart b/lib/pages_desktop_specific/home/desktop_menu.dart index b71c20f6e..7409a4156 100644 --- a/lib/pages_desktop_specific/home/desktop_menu.dart +++ b/lib/pages_desktop_specific/home/desktop_menu.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -70,136 +72,197 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { const SizedBox( height: 60, ), - SizedBox( - width: _width == expandedWidth - ? _width - 32 // 16 padding on either side - : _width - 16, // 8 padding on either side - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - DesktopMenuItem( - icon: SvgPicture.asset( - Assets.svg.walletFa, - width: 20, - height: 20, + Expanded( + child: SizedBox( + width: _width == expandedWidth + ? _width - 32 // 16 padding on either side + : _width - 16, // 8 padding on either side + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + DesktopMenuItem( + icon: SvgPicture.asset( + Assets.svg.walletDesktop, + width: 20, + height: 20, + color: 0 == selectedMenuItem + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textDark + .withOpacity(0.8), + ), + label: "My Stack", + value: 0, + group: selectedMenuItem, + onChanged: updateSelectedMenuItem, + iconOnly: _width == minimizedWidth, ), - label: "My Stack", - value: 0, - group: selectedMenuItem, - onChanged: updateSelectedMenuItem, - iconOnly: _width == minimizedWidth, - ), - const SizedBox( - height: 2, - ), - DesktopMenuItem( - icon: SvgPicture.asset( - Assets.svg.exchange3, - width: 20, - height: 20, + const SizedBox( + height: 2, ), - label: "Exchange", - value: 1, - group: selectedMenuItem, - onChanged: updateSelectedMenuItem, - iconOnly: _width == minimizedWidth, - ), - const SizedBox( - height: 2, - ), - DesktopMenuItem( - icon: SvgPicture.asset( - Assets.svg.bell, - width: 20, - height: 20, + DesktopMenuItem( + icon: SvgPicture.asset( + Assets.svg.exchangeDesktop, + width: 20, + height: 20, + color: 1 == selectedMenuItem + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textDark + .withOpacity(0.8), + ), + label: "Exchange", + value: 1, + group: selectedMenuItem, + onChanged: updateSelectedMenuItem, + iconOnly: _width == minimizedWidth, ), - label: "Notifications", - value: 2, - group: selectedMenuItem, - onChanged: updateSelectedMenuItem, - iconOnly: _width == minimizedWidth, - ), - const SizedBox( - height: 2, - ), - DesktopMenuItem( - icon: SvgPicture.asset( - Assets.svg.addressBook2, - width: 20, - height: 20, + const SizedBox( + height: 2, ), - label: "Address Book", - value: 3, - group: selectedMenuItem, - onChanged: updateSelectedMenuItem, - iconOnly: _width == minimizedWidth, - ), - const SizedBox( - height: 2, - ), - DesktopMenuItem( - icon: SvgPicture.asset( - Assets.svg.gear, - width: 20, - height: 20, + DesktopMenuItem( + icon: SvgPicture.asset( + Assets.svg.bell, + width: 20, + height: 20, + color: 2 == selectedMenuItem + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textDark + .withOpacity(0.8), + ), + label: "Notifications", + value: 2, + group: selectedMenuItem, + onChanged: updateSelectedMenuItem, + iconOnly: _width == minimizedWidth, ), - label: "Settings", - value: 4, - group: selectedMenuItem, - onChanged: updateSelectedMenuItem, - iconOnly: _width == minimizedWidth, - ), - const SizedBox( - height: 2, - ), - DesktopMenuItem( - icon: SvgPicture.asset( - Assets.svg.messageQuestion, - width: 20, - height: 20, + const SizedBox( + height: 2, ), - label: "Support", - value: 5, - group: selectedMenuItem, - onChanged: updateSelectedMenuItem, - iconOnly: _width == minimizedWidth, - ), - const SizedBox( - height: 2, - ), - DesktopMenuItem( - icon: SvgPicture.asset( - Assets.svg.messageQuestion, - width: 20, - height: 20, + DesktopMenuItem( + icon: SvgPicture.asset( + Assets.svg.addressBookDesktop, + width: 20, + height: 20, + color: 3 == selectedMenuItem + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textDark + .withOpacity(0.8), + ), + label: "Address Book", + value: 3, + group: selectedMenuItem, + onChanged: updateSelectedMenuItem, + iconOnly: _width == minimizedWidth, ), - label: "About", - value: 6, - group: selectedMenuItem, - onChanged: updateSelectedMenuItem, - iconOnly: _width == minimizedWidth, - ), - const SizedBox( - height: 2, - ), - DesktopMenuItem( - icon: SvgPicture.asset( - Assets.svg.messageQuestion, - width: 20, - height: 20, + const SizedBox( + height: 2, ), - label: "Exit", - value: 7, - group: selectedMenuItem, - onChanged: updateSelectedMenuItem, - iconOnly: _width == minimizedWidth, - ), - ], + DesktopMenuItem( + icon: SvgPicture.asset( + Assets.svg.gear, + width: 20, + height: 20, + color: 4 == selectedMenuItem + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textDark + .withOpacity(0.8), + ), + label: "Settings", + value: 4, + group: selectedMenuItem, + onChanged: updateSelectedMenuItem, + iconOnly: _width == minimizedWidth, + ), + const SizedBox( + height: 2, + ), + DesktopMenuItem( + icon: SvgPicture.asset( + Assets.svg.messageQuestion, + width: 20, + height: 20, + color: 5 == selectedMenuItem + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textDark + .withOpacity(0.8), + ), + label: "Support", + value: 5, + group: selectedMenuItem, + onChanged: updateSelectedMenuItem, + iconOnly: _width == minimizedWidth, + ), + const SizedBox( + height: 2, + ), + DesktopMenuItem( + icon: SvgPicture.asset( + Assets.svg.aboutDesktop, + width: 20, + height: 20, + color: 6 == selectedMenuItem + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textDark + .withOpacity(0.8), + ), + label: "About", + value: 6, + group: selectedMenuItem, + onChanged: updateSelectedMenuItem, + iconOnly: _width == minimizedWidth, + ), + const Spacer(), + DesktopMenuItem( + icon: SvgPicture.asset( + Assets.svg.exitDesktop, + width: 20, + height: 20, + color: Theme.of(context) + .extension<StackColors>()! + .textDark + .withOpacity(0.8), + ), + label: "Exit", + value: 7, + group: selectedMenuItem, + onChanged: (_) { + // todo: save stuff/ notify before exit? + exit(0); + }, + iconOnly: _width == minimizedWidth, + ), + ], + ), ), ), - const Spacer(), Row( - mainAxisAlignment: MainAxisAlignment.end, children: [ const Spacer(), IconButton( @@ -212,7 +275,7 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { ), ), ], - ) + ), ], ), ), diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 2ada80a5d..ebe2d9848 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -139,6 +139,11 @@ class _SVG { String get anonymize => "assets/svg/tx-icon-anonymize.svg"; String get anonymizePending => "assets/svg/tx-icon-anonymize-pending.svg"; String get anonymizeFailed => "assets/svg/tx-icon-anonymize-failed.svg"; + String get addressBookDesktop => "assets/svg/address-book-desktop.svg"; + String get exchangeDesktop => "assets/svg/exchange-desktop.svg"; + String get aboutDesktop => "assets/svg/about-desktop.svg"; + String get walletDesktop => "assets/svg/wallet-desktop.svg"; + String get exitDesktop => "assets/svg/exit-desktop.svg"; String get ellipse1 => "assets/svg/Ellipse-43.svg"; String get ellipse2 => "assets/svg/Ellipse-42.svg"; diff --git a/pubspec.yaml b/pubspec.yaml index 35fbecf3f..aabf3f3de 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -301,6 +301,11 @@ flutter: - assets/svg/node-circle.svg - assets/svg/dark/dark-theme.svg - assets/svg/light/light-mode.svg + - assets/svg/address-book-desktop.svg + - assets/svg/about-desktop.svg + - assets/svg/exchange-desktop.svg + - assets/svg/wallet-desktop.svg + - assets/svg/exit-desktop.svg # coin icons - assets/svg/coin_icons/Bitcoin.svg - assets/svg/coin_icons/Bitcoincash.svg From 480266ef5f236078443df76786c222b549e598f6 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 26 Oct 2022 15:09:58 -0600 Subject: [PATCH 034/426] lock send/receive width --- .../home/my_stack_view/wallet_view/desktop_wallet_view.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index 510c4bc7c..8a5c35d8d 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -174,7 +174,8 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { Expanded( child: Row( children: [ - Expanded( + SizedBox( + width: 450, child: MyWallet( walletId: walletId, ), From b4e7e219a4a465f21d9cb86213e4a0e0b57bd76c Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Wed, 26 Oct 2022 16:07:37 -0600 Subject: [PATCH 035/426] started nodes dialog popup --- .../manage_nodes_views/coin_nodes_view.dart | 187 ++++++++++++------ .../settings_menu/enable_backup_dialog.dart | 0 .../home/settings_menu/nodes_settings.dart | 24 ++- 3 files changed, 141 insertions(+), 70 deletions(-) create mode 100644 lib/pages_desktop_specific/home/settings_menu/enable_backup_dialog.dart diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/coin_nodes_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/coin_nodes_view.dart index 12573042e..e3743d54e 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/coin_nodes_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/coin_nodes_view.dart @@ -7,7 +7,10 @@ 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/utilities/util.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:tuple/tuple.dart'; class CoinNodesView extends ConsumerStatefulWidget { @@ -37,69 +40,139 @@ class _CoinNodesViewState extends ConsumerState<CoinNodesView> { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "${widget.coin.prettyName} nodes", - style: STextStyles.navBarTitle(context), - ), - actions: [ - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("manageNodesAddNewNodeButtonKey"), - size: 36, - shadows: const [], - color: Theme.of(context).extension<StackColors>()!.background, - icon: SvgPicture.asset( - Assets.svg.plus, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - width: 20, - height: 20, + if (Util.isDesktop) { + return DesktopDialog( + child: Column( + children: [ + Row( + children: [ + const SizedBox( + width: 32, ), - onPressed: () { - Navigator.of(context).pushNamed( - AddEditNodeView.routeName, - arguments: Tuple4( - AddEditNodeViewType.add, - widget.coin, - null, - CoinNodesView.routeName, + SvgPicture.asset( + Assets.svg.iconFor(coin: widget.coin), + width: 24, + height: 24, + ), + const SizedBox( + width: 12, + ), + Text( + "${widget.coin.prettyName} nodes", + style: STextStyles.desktopH3(context), + textAlign: TextAlign.center, + ), + Expanded( + child: const DesktopDialogCloseButton(), + ), + ], + ), + Padding( + padding: EdgeInsets.only( + left: 32, + right: 32, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "${widget.coin.prettyName} nodes", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of(context).extension<StackColors>()!.textDark3, ), - ); - }, + textAlign: TextAlign.left, + ), + RichText( + text: TextSpan( + text: 'Add new nodes', + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: Colors.blueAccent, + ), + ), + ), + ], ), ), - ), - ], - ), - body: Padding( - padding: const EdgeInsets.only( - top: 12, - left: 12, - right: 12, + const SizedBox( + width: 12, + ), + Padding( + padding: const EdgeInsets.all(20), + child: NodesList( + coin: widget.coin, + popBackToRoute: CoinNodesView.routeName, + ), + ), + ], ), - child: SingleChildScrollView( - child: NodesList( - coin: widget.coin, - popBackToRoute: CoinNodesView.routeName, + ); + } else { + return Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "${widget.coin.prettyName} nodes", + style: STextStyles.navBarTitle(context), + ), + actions: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("manageNodesAddNewNodeButtonKey"), + size: 36, + shadows: const [], + color: Theme.of(context).extension<StackColors>()!.background, + icon: SvgPicture.asset( + Assets.svg.plus, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + width: 20, + height: 20, + ), + onPressed: () { + Navigator.of(context).pushNamed( + AddEditNodeView.routeName, + arguments: Tuple4( + AddEditNodeViewType.add, + widget.coin, + null, + CoinNodesView.routeName, + ), + ); + }, + ), + ), + ), + ], + ), + body: Padding( + padding: const EdgeInsets.only( + top: 12, + left: 12, + right: 12, + ), + child: SingleChildScrollView( + child: NodesList( + coin: widget.coin, + popBackToRoute: CoinNodesView.routeName, + ), ), ), - ), - ); + ); + } } } diff --git a/lib/pages_desktop_specific/home/settings_menu/enable_backup_dialog.dart b/lib/pages_desktop_specific/home/settings_menu/enable_backup_dialog.dart new file mode 100644 index 000000000..e69de29bb diff --git a/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart b/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart index 8608e79af..f354927c4 100644 --- a/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart @@ -5,12 +5,13 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/providers/global/node_service_provider.dart'; import 'package:stackwallet/providers/global/prefs_provider.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/text_styles.dart'; -import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; +import '../../../pages/settings_views/global_settings_view/manage_nodes_views/coin_nodes_view.dart'; +import '../../../utilities/constants.dart'; + class NodesSettings extends ConsumerStatefulWidget { const NodesSettings({Key? key}) : super(key: key); @@ -37,9 +38,6 @@ class _NodesSettings extends ConsumerState<NodesSettings> { @override Widget build(BuildContext context) { - double deviceWidth(BuildContext context) => - MediaQuery.of(context).size.width; - bool showTestNet = ref.watch( prefsChangeNotifierProvider.select((value) => value.showTestNetCoins), ); @@ -108,18 +106,18 @@ class _NodesSettings extends ConsumerState<NodesSettings> { borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), - side: BorderSide( - color: Theme.of(context) - .extension<StackColors>()! - .shadow), + // side: BorderSide( + // color: Theme.of(context) + // .extension<StackColors>()! + // .shadow), ), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, onPressed: () { - // Navigator.of(context).pushNamed( - // CoinNodesView.routeName, - // arguments: coin, - // ); + Navigator.of(context).pushNamed( + CoinNodesView.routeName, + arguments: coin, + ); }, child: Padding( padding: const EdgeInsets.all( From da408860126e6f4989f8dd5c6fd91517abb674de Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Wed, 26 Oct 2022 16:17:39 -0600 Subject: [PATCH 036/426] auto backup dialog started --- .../settings_menu/enable_backup_dialog.dart | 73 +++++++++++++++++++ .../home/settings_menu/security_settings.dart | 43 ++++++++++- 2 files changed, 114 insertions(+), 2 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/enable_backup_dialog.dart b/lib/pages_desktop_specific/home/settings_menu/enable_backup_dialog.dart index e69de29bb..4925177c3 100644 --- a/lib/pages_desktop_specific/home/settings_menu/enable_backup_dialog.dart +++ b/lib/pages_desktop_specific/home/settings_menu/enable_backup_dialog.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.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'; + +class EnableBackupDialog extends StatelessWidget { + const EnableBackupDialog({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return DesktopDialog( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(32), + child: Text( + "Enable Auto Backup", + style: STextStyles.desktopH3(context), + textAlign: TextAlign.center, + ), + ), + const DesktopDialogCloseButton(), + ], + ), + const SizedBox( + height: 30, + ), + Text( + "To enable Auto Backup, you need to create a backup file.", + style: STextStyles.desktopTextSmall(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark3, + ), + textAlign: TextAlign.center, + ), + const Spacer(), + Padding( + padding: const EdgeInsets.all(32), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Continue", + onPressed: () { + // Navigator.of(context).pop(); + // onConfirm.call(); + }, + ), + ) + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/settings_menu/security_settings.dart b/lib/pages_desktop_specific/home/settings_menu/security_settings.dart index febb1dc1b..7c36be8cd 100644 --- a/lib/pages_desktop_specific/home/settings_menu/security_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/security_settings.dart @@ -4,9 +4,11 @@ import 'package:flutter_riverpod/flutter_riverpod.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/rounded_white_container.dart'; +import '../../../utilities/theme/stack_colors.dart'; +import 'enable_backup_dialog.dart'; + class SecuritySettings extends ConsumerStatefulWidget { const SecuritySettings({Key? key}) : super(key: key); @@ -17,6 +19,23 @@ class SecuritySettings extends ConsumerStatefulWidget { } class _SecuritySettings extends ConsumerState<SecuritySettings> { + Future<void> enableAutoBackup() async { + // wait for keyboard to disappear + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 100), + ); + + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return EnableBackupDialog(); + }, + ); + } + @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); @@ -82,8 +101,26 @@ class NewPasswordButton extends ConsumerWidget { const NewPasswordButton({ Key? key, }) : super(key: key); + @override Widget build(BuildContext context, WidgetRef ref) { + Future<void> enableAutoBackup() async { + // wait for keyboard to disappear + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 100), + ); + + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return EnableBackupDialog(); + }, + ); + } + return SizedBox( width: 200, height: 48, @@ -91,7 +128,9 @@ class NewPasswordButton extends ConsumerWidget { style: Theme.of(context) .extension<StackColors>()! .getPrimaryEnabledButtonColor(context), - onPressed: () {}, + onPressed: () { + enableAutoBackup(); + }, child: Text( "Set up new password", style: STextStyles.button(context), From 2a27776acfe287061d567bcedb508c832ab435cc Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 26 Oct 2022 16:00:54 -0600 Subject: [PATCH 037/426] lock send/receive height to fit --- .../wallet_view/desktop_wallet_view.dart | 206 ++- .../wallet_view/receive/desktop_receive.dart | 14 +- .../wallet_view/send/desktop_send.dart | 1114 ++++++++--------- 3 files changed, 721 insertions(+), 613 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index 8a5c35d8d..4c2b510de 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -209,6 +209,8 @@ class MyWallet extends StatefulWidget { } class _MyWalletState extends State<MyWallet> { + int _selectedIndex = 0; + @override Widget build(BuildContext context) { return Column( @@ -225,53 +227,171 @@ class _MyWalletState extends State<MyWallet> { const SizedBox( height: 16, ), + Container( + decoration: BoxDecoration( + color: Theme.of(context).extension<StackColors>()!.popupBG, + borderRadius: BorderRadius.vertical( + top: Radius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + child: SendReceiveTabMenu( + onChanged: (index) { + setState(() { + _selectedIndex = index; + }); + }, + ), + ), + Container( + decoration: BoxDecoration( + color: Theme.of(context).extension<StackColors>()!.popupBG, + borderRadius: BorderRadius.vertical( + bottom: Radius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + child: IndexedStack( + index: _selectedIndex, + children: [ + Padding( + key: const Key("desktopSendViewPortKey"), + padding: const EdgeInsets.all(20), + child: DesktopSend( + walletId: widget.walletId, + ), + ), + Padding( + key: const Key("desktopReceiveViewPortKey"), + padding: const EdgeInsets.all(20), + child: DesktopReceive( + walletId: widget.walletId, + ), + ), + ], + ), + ), + const Spacer(), + ], + ); + } +} + +class SendReceiveTabMenu extends StatefulWidget { + const SendReceiveTabMenu({ + Key? key, + this.initialIndex = 0, + this.onChanged, + }) : super(key: key); + + final int initialIndex; + final void Function(int)? onChanged; + + @override + State<SendReceiveTabMenu> createState() => _SendReceiveTabMenuState(); +} + +class _SendReceiveTabMenuState extends State<SendReceiveTabMenu> { + late int _selectedIndex; + + void _onChanged(int newIndex) { + if (_selectedIndex != newIndex) { + setState(() { + _selectedIndex = newIndex; + }); + widget.onChanged?.call(_selectedIndex); + } + } + + @override + void initState() { + _selectedIndex = widget.initialIndex; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Row( + children: [ Expanded( - child: RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: DefaultTabController( - length: 2, + child: GestureDetector( + onTap: () => _onChanged(0), + child: Container( + color: Colors.transparent, child: Column( children: [ - TabBar( - indicatorColor: Theme.of(context) - .extension<StackColors>()! - .accentColorBlue, - labelStyle: STextStyles.desktopTextExtraSmall(context), - labelColor: Theme.of(context) - .extension<StackColors>()! - .accentColorBlue, - unselectedLabelColor: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - labelPadding: const EdgeInsets.symmetric( - vertical: 6, - ), - splashBorderRadius: BorderRadius.vertical( - top: Radius.circular( - Constants.size.circularBorderRadius, - ), - ), - tabs: const [ - Tab(text: "Send"), - Tab(text: "Receive"), - ], + const SizedBox( + height: 16, ), - Expanded( - child: TabBarView( - children: [ - Padding( - padding: const EdgeInsets.all(20), - child: DesktopSend( - walletId: widget.walletId, - ), - ), - Padding( - padding: const EdgeInsets.all(20), - child: DesktopReceive( - walletId: widget.walletId, - ), - ), - ], + Text( + "Send", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: _selectedIndex == 0 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ), + const SizedBox( + height: 19, + ), + Container( + height: 2, + decoration: BoxDecoration( + color: _selectedIndex == 0 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Theme.of(context) + .extension<StackColors>()! + .background, + ), + ), + ], + ), + ), + ), + ), + Expanded( + child: GestureDetector( + onTap: () => _onChanged(1), + child: Container( + color: Colors.transparent, + child: Column( + children: [ + const SizedBox( + height: 16, + ), + Text( + "Receive", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: _selectedIndex == 1 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ), + const SizedBox( + height: 19, + ), + Container( + height: 2, + decoration: BoxDecoration( + color: _selectedIndex == 1 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Theme.of(context) + .extension<StackColors>()! + .background, ), ), ], diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/receive/desktop_receive.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/receive/desktop_receive.dart index 2efbcd84f..79e3b81cf 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/receive/desktop_receive.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/receive/desktop_receive.dart @@ -199,15 +199,11 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> { height: 32, ), Center( - child: SizedBox( - width: 200, - height: 200, - child: QrImage( - data: "${coin.uriScheme}:$receivingAddress", - size: MediaQuery.of(context).size.width / 2, - foregroundColor: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), + child: QrImage( + data: "${coin.uriScheme}:$receivingAddress", + size: 200, + foregroundColor: + Theme.of(context).extension<StackColors>()!.accentColorDark, ), ), const SizedBox( diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart index 866f1ab56..1579b2367 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart @@ -541,202 +541,301 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { }); } - return SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 4, + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 4, + ), + if (coin == Coin.firo) + Text( + "Send from", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, ), - if (coin == Coin.firo) - Text( - "Send from", - style: STextStyles.smallMed12(context), - textAlign: TextAlign.left, - ), - if (coin == Coin.firo) - const SizedBox( - height: 10, - ), - if (coin == Coin.firo) - Stack( - children: [ - TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - readOnly: true, - textInputAction: TextInputAction.none, + if (coin == Coin.firo) + const SizedBox( + height: 10, + ), + if (coin == Coin.firo) + Stack( + children: [ + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + readOnly: true, + textInputAction: TextInputAction.none, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - ), - child: RawMaterialButton( - splashColor: - Theme.of(context).extension<StackColors>()!.highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), + child: RawMaterialButton( + splashColor: + Theme.of(context).extension<StackColors>()!.highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - onPressed: () { - showModalBottomSheet<dynamic>( - backgroundColor: Colors.transparent, - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), + ), + onPressed: () { + showModalBottomSheet<dynamic>( + backgroundColor: Colors.transparent, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + builder: (_) => FiroBalanceSelectionSheet( + walletId: walletId, + ), + ); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + "${ref.watch(publicPrivateBalanceStateProvider.state).state} balance", + style: STextStyles.itemSubtitle12(context), ), - ), - builder: (_) => FiroBalanceSelectionSheet( - walletId: walletId, - ), - ); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Text( - "${ref.watch(publicPrivateBalanceStateProvider.state).state} balance", - style: STextStyles.itemSubtitle12(context), - ), - const SizedBox( - width: 10, - ), - FutureBuilder( - future: _firoBalanceFuture(provider, locale), - builder: - (context, AsyncSnapshot<String?> snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - if (ref + const SizedBox( + width: 10, + ), + FutureBuilder( + future: _firoBalanceFuture(provider, locale), + builder: + (context, AsyncSnapshot<String?> snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + if (ref + .read(publicPrivateBalanceStateProvider + .state) + .state == + "Private") { + _privateBalanceString = snapshot.data!; + } else { + _publicBalanceString = snapshot.data!; + } + } + if (ref .read( publicPrivateBalanceStateProvider .state) .state == - "Private") { - _privateBalanceString = snapshot.data!; - } else { - _publicBalanceString = snapshot.data!; - } - } - if (ref - .read( - publicPrivateBalanceStateProvider - .state) - .state == - "Private" && - _privateBalanceString != null) { - return Text( - "$_privateBalanceString ${coin.ticker}", - style: STextStyles.itemSubtitle(context), - ); - } else if (ref - .read( - publicPrivateBalanceStateProvider - .state) - .state == - "Public" && - _publicBalanceString != null) { - return Text( - "$_publicBalanceString ${coin.ticker}", - style: STextStyles.itemSubtitle(context), - ); - } else { - return AnimatedText( - stringsToLoopThrough: const [ - "Loading balance", - "Loading balance.", - "Loading balance..", - "Loading balance...", - ], - style: STextStyles.itemSubtitle(context), - ); - } - }, - ), - ], - ), - SvgPicture.asset( - Assets.svg.chevronDown, - width: 8, - height: 4, - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle2, - ), - ], - ), + "Private" && + _privateBalanceString != null) { + return Text( + "$_privateBalanceString ${coin.ticker}", + style: STextStyles.itemSubtitle(context), + ); + } else if (ref + .read( + publicPrivateBalanceStateProvider + .state) + .state == + "Public" && + _publicBalanceString != null) { + return Text( + "$_publicBalanceString ${coin.ticker}", + style: STextStyles.itemSubtitle(context), + ); + } else { + return AnimatedText( + stringsToLoopThrough: const [ + "Loading balance", + "Loading balance.", + "Loading balance..", + "Loading balance...", + ], + style: STextStyles.itemSubtitle(context), + ); + } + }, + ), + ], + ), + SvgPicture.asset( + Assets.svg.chevronDown, + width: 8, + height: 4, + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle2, + ), + ], ), - ) - ], - ), - if (coin == Coin.firo) - const SizedBox( - height: 20, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Amount", - style: STextStyles.smallMed12(context), - textAlign: TextAlign.left, - ), - BlueTextButton( - text: "Send all ${coin.ticker}", - onTap: () async { - if (coin == Coin.firo || coin == Coin.firoTestNet) { - final firoWallet = ref.read(provider).wallet as FiroWallet; - if (ref - .read(publicPrivateBalanceStateProvider.state) - .state == - "Private") { - cryptoAmountController.text = - (await firoWallet.availablePrivateBalance()) - .toStringAsFixed(Constants.decimalPlaces); - } else { - cryptoAmountController.text = - (await firoWallet.availablePublicBalance()) - .toStringAsFixed(Constants.decimalPlaces); - } - } else { - cryptoAmountController.text = - (await ref.read(provider).availableBalance) - .toStringAsFixed(Constants.decimalPlaces); - } - }, - ), + ), + ) ], ), + if (coin == Coin.firo) + const SizedBox( + height: 20, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Amount", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + BlueTextButton( + text: "Send all ${coin.ticker}", + onTap: () async { + if (coin == Coin.firo || coin == Coin.firoTestNet) { + final firoWallet = ref.read(provider).wallet as FiroWallet; + if (ref.read(publicPrivateBalanceStateProvider.state).state == + "Private") { + cryptoAmountController.text = + (await firoWallet.availablePrivateBalance()) + .toStringAsFixed(Constants.decimalPlaces); + } else { + cryptoAmountController.text = + (await firoWallet.availablePublicBalance()) + .toStringAsFixed(Constants.decimalPlaces); + } + } else { + cryptoAmountController.text = + (await ref.read(provider).availableBalance) + .toStringAsFixed(Constants.decimalPlaces); + } + }, + ), + ], + ), + const SizedBox( + height: 10, + ), + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark, + ), + key: const Key("amountInputFieldCryptoTextFieldKey"), + controller: cryptoAmountController, + focusNode: _cryptoFocus, + keyboardType: const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), + textAlign: TextAlign.right, + inputFormatters: [ + // regex to validate a crypto amount with 8 decimal places + TextInputFormatter.withFunction((oldValue, newValue) => + RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') + .hasMatch(newValue.text) + ? newValue + : oldValue), + ], + decoration: InputDecoration( + contentPadding: const EdgeInsets.only( + top: 12, + right: 12, + ), + hintText: "0", + hintStyle: STextStyles.fieldLabel(context).copyWith( + fontSize: 14, + ), + prefixIcon: FittedBox( + fit: BoxFit.scaleDown, + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + coin.ticker, + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + ), + ), + ), + if (Prefs.instance.externalCalls) const SizedBox( height: 10, ), + if (Prefs.instance.externalCalls) TextField( autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, style: STextStyles.smallMed14(context).copyWith( color: Theme.of(context).extension<StackColors>()!.textDark, ), - key: const Key("amountInputFieldCryptoTextFieldKey"), - controller: cryptoAmountController, - focusNode: _cryptoFocus, + key: const Key("amountInputFieldFiatTextFieldKey"), + controller: baseAmountController, + focusNode: _baseFocus, keyboardType: const TextInputType.numberWithOptions( signed: false, decimal: true, ), textAlign: TextAlign.right, inputFormatters: [ - // regex to validate a crypto amount with 8 decimal places + // regex to validate a fiat amount with 2 decimal places TextInputFormatter.withFunction((oldValue, newValue) => - RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') + RegExp(r'^([0-9]*[,.]?[0-9]{0,2}|[,.][0-9]{0,2})$') .hasMatch(newValue.text) ? newValue : oldValue), ], + onChanged: (baseAmountString) { + if (baseAmountString.isNotEmpty && + baseAmountString != "." && + baseAmountString != ",") { + final baseAmount = baseAmountString.contains(",") + ? Decimal.parse(baseAmountString.replaceFirst(",", ".")) + : Decimal.parse(baseAmountString); + + var _price = ref + .read(priceAnd24hChangeNotifierProvider) + .getPrice(coin) + .item1; + + if (_price == Decimal.zero) { + _amountToSend = Decimal.zero; + } else { + _amountToSend = baseAmount <= Decimal.zero + ? Decimal.zero + : (baseAmount / _price).toDecimal( + scaleOnInfinitePrecision: Constants.decimalPlaces); + } + if (_cachedAmountToSend != null && + _cachedAmountToSend == _amountToSend) { + return; + } + _cachedAmountToSend = _amountToSend; + Logging.instance.log( + "it changed $_amountToSend $_cachedAmountToSend", + level: LogLevel.Info); + + final amountString = Format.localizedStringAsFixed( + value: _amountToSend!, + locale: ref.read(localeServiceChangeNotifierProvider).locale, + decimalPlaces: Constants.decimalPlaces, + ); + + _cryptoAmountChangeLock = true; + cryptoAmountController.text = amountString; + _cryptoAmountChangeLock = false; + } else { + _amountToSend = Decimal.zero; + _cryptoAmountChangeLock = true; + cryptoAmountController.text = ""; + _cryptoAmountChangeLock = false; + } + // setState(() { + // _calculateFeesFuture = calculateFees( + // Format.decimalAmountToSatoshis( + // _amountToSend!)); + // }); + _updatePreviewButtonState(_address, _amountToSend); + }, decoration: InputDecoration( contentPadding: const EdgeInsets.only( top: 12, @@ -751,7 +850,8 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { child: Padding( padding: const EdgeInsets.all(12), child: Text( - coin.ticker, + ref.watch(prefsChangeNotifierProvider + .select((value) => value.currency)), style: STextStyles.smallMed14(context).copyWith( color: Theme.of(context) .extension<StackColors>()! @@ -761,412 +861,304 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { ), ), ), - if (Prefs.instance.externalCalls) - const SizedBox( - height: 10, - ), - if (Prefs.instance.externalCalls) - TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context).extension<StackColors>()!.textDark, - ), - key: const Key("amountInputFieldFiatTextFieldKey"), - controller: baseAmountController, - focusNode: _baseFocus, - keyboardType: const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), - textAlign: TextAlign.right, - inputFormatters: [ - // regex to validate a fiat amount with 2 decimal places - TextInputFormatter.withFunction((oldValue, newValue) => - RegExp(r'^([0-9]*[,.]?[0-9]{0,2}|[,.][0-9]{0,2})$') - .hasMatch(newValue.text) - ? newValue - : oldValue), - ], - onChanged: (baseAmountString) { - if (baseAmountString.isNotEmpty && - baseAmountString != "." && - baseAmountString != ",") { - final baseAmount = baseAmountString.contains(",") - ? Decimal.parse(baseAmountString.replaceFirst(",", ".")) - : Decimal.parse(baseAmountString); - - var _price = ref - .read(priceAnd24hChangeNotifierProvider) - .getPrice(coin) - .item1; - - if (_price == Decimal.zero) { - _amountToSend = Decimal.zero; - } else { - _amountToSend = baseAmount <= Decimal.zero - ? Decimal.zero - : (baseAmount / _price).toDecimal( - scaleOnInfinitePrecision: Constants.decimalPlaces); - } - if (_cachedAmountToSend != null && - _cachedAmountToSend == _amountToSend) { - return; - } - _cachedAmountToSend = _amountToSend; - Logging.instance.log( - "it changed $_amountToSend $_cachedAmountToSend", - level: LogLevel.Info); - - final amountString = Format.localizedStringAsFixed( - value: _amountToSend!, - locale: - ref.read(localeServiceChangeNotifierProvider).locale, - decimalPlaces: Constants.decimalPlaces, - ); - - _cryptoAmountChangeLock = true; - cryptoAmountController.text = amountString; - _cryptoAmountChangeLock = false; - } else { - _amountToSend = Decimal.zero; - _cryptoAmountChangeLock = true; - cryptoAmountController.text = ""; - _cryptoAmountChangeLock = false; - } - // setState(() { - // _calculateFeesFuture = calculateFees( - // Format.decimalAmountToSatoshis( - // _amountToSend!)); - // }); - _updatePreviewButtonState(_address, _amountToSend); - }, - decoration: InputDecoration( - contentPadding: const EdgeInsets.only( - top: 12, - right: 12, - ), - hintText: "0", - hintStyle: STextStyles.fieldLabel(context).copyWith( - fontSize: 14, - ), - prefixIcon: FittedBox( - fit: BoxFit.scaleDown, - child: Padding( - padding: const EdgeInsets.all(12), - child: Text( - ref.watch(prefsChangeNotifierProvider - .select((value) => value.currency)), - style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - ), - ), - ), - ), - ), - const SizedBox( - height: 20, + const SizedBox( + height: 20, + ), + Text( + "Send to", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + const SizedBox( + height: 10, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - Text( - "Send to", - style: STextStyles.smallMed12(context), - textAlign: TextAlign.left, - ), - const SizedBox( - height: 10, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + child: TextField( + key: const Key("sendViewAddressFieldKey"), + controller: sendToController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + // inputFormatters: <TextInputFormatter>[ + // FilteringTextInputFormatter.allow( + // RegExp("[a-zA-Z0-9]{34}")), + // ], + toolbarOptions: const ToolbarOptions( + copy: false, + cut: false, + paste: true, + selectAll: false, ), - child: TextField( - key: const Key("sendViewAddressFieldKey"), - controller: sendToController, - readOnly: false, - autocorrect: false, - enableSuggestions: false, - // inputFormatters: <TextInputFormatter>[ - // FilteringTextInputFormatter.allow( - // RegExp("[a-zA-Z0-9]{34}")), - // ], - toolbarOptions: const ToolbarOptions( - copy: false, - cut: false, - paste: true, - selectAll: false, - ), - onChanged: (newValue) { - _address = newValue; - _updatePreviewButtonState(_address, _amountToSend); + onChanged: (newValue) { + _address = newValue; + _updatePreviewButtonState(_address, _amountToSend); - setState(() { - _addressToggleFlag = newValue.isNotEmpty; - }); - }, - focusNode: _addressFocusNode, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter ${coin.ticker} address", - _addressFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: sendToController.text.isEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - _addressToggleFlag - ? TextFieldIconButton( - key: const Key( - "sendViewClearAddressFieldButtonKey"), - onTap: () { - sendToController.text = ""; - _address = ""; - _updatePreviewButtonState( - _address, _amountToSend); - setState(() { - _addressToggleFlag = false; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - key: const Key( - "sendViewPasteAddressFieldButtonKey"), - onTap: () async { - final ClipboardData? data = await clipboard - .getData(Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - String content = data.text!.trim(); - if (content.contains("\n")) { - content = content.substring( - 0, content.indexOf("\n")); - } - - sendToController.text = content; - _address = content; - - _updatePreviewButtonState( - _address, _amountToSend); - setState(() { - _addressToggleFlag = - sendToController.text.isNotEmpty; - }); - } - }, - child: sendToController.text.isEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - if (sendToController.text.isEmpty) - TextFieldIconButton( - key: const Key("sendViewAddressBookButtonKey"), - onTap: () { - Navigator.of(context).pushNamed( - AddressBookView.routeName, - arguments: coin, - ); - }, - child: const AddressBookIcon(), - ), - if (sendToController.text.isEmpty) - TextFieldIconButton( - key: const Key("sendViewScanQrButtonKey"), - onTap: () async { - try { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration(milliseconds: 75)); - } - - final qrResult = await scanner.scan(); - - Logging.instance.log( - "qrResult content: ${qrResult.rawContent}", - level: LogLevel.Info); - - final results = - AddressUtils.parseUri(qrResult.rawContent); - - Logging.instance.log( - "qrResult parsed: $results", - level: LogLevel.Info); - - if (results.isNotEmpty && - results["scheme"] == coin.uriScheme) { - // auto fill address - _address = results["address"] ?? ""; - sendToController.text = _address!; - - // autofill notes field - if (results["message"] != null) { - noteController.text = results["message"]!; - } else if (results["label"] != null) { - noteController.text = results["label"]!; - } - - // autofill amount field - if (results["amount"] != null) { - final amount = - Decimal.parse(results["amount"]!); - cryptoAmountController.text = - Format.localizedStringAsFixed( - value: amount, - locale: ref - .read( - localeServiceChangeNotifierProvider) - .locale, - decimalPlaces: Constants.decimalPlaces, - ); - amount.toString(); - _amountToSend = amount; - } - - _updatePreviewButtonState( - _address, _amountToSend); - setState(() { - _addressToggleFlag = - sendToController.text.isNotEmpty; - }); - - // now check for non standard encoded basic address - } else if (ref - .read(walletsChangeNotifierProvider) - .getManager(walletId) - .validateAddress(qrResult.rawContent)) { - _address = qrResult.rawContent; - sendToController.text = _address ?? ""; - - _updatePreviewButtonState( - _address, _amountToSend); - setState(() { - _addressToggleFlag = - sendToController.text.isNotEmpty; - }); - } - } on PlatformException catch (e, s) { - // here we ignore the exception caused by not giving permission - // to use the camera to scan a qr code - Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", - level: LogLevel.Warning); - } - }, - child: const QrCodeIcon(), - ) - ], - ), - ), - ), - ), - ), - ), - Builder( - builder: (_) { - final error = _updateInvalidAddressText( - _address ?? "", - ref.read(walletsChangeNotifierProvider).getManager(walletId), - ); - - if (error == null || error.isEmpty) { - return Container(); - } else { - return Align( - alignment: Alignment.topLeft, - child: Padding( - padding: const EdgeInsets.only( - left: 12.0, - top: 4.0, - ), - child: Text( - error, - textAlign: TextAlign.left, - style: STextStyles.label(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textError, - ), - ), - ), - ); - } + setState(() { + _addressToggleFlag = newValue.isNotEmpty; + }); }, - ), - const SizedBox( - height: 20, - ), - Text( - "Note (optional)", - style: STextStyles.smallMed12(context), - textAlign: TextAlign.left, - ), - const SizedBox( - height: 10, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: noteController, - focusNode: _noteFocusNode, - style: STextStyles.field(context), - onChanged: (_) => setState(() {}), - decoration: standardInputDecoration( - "Type something...", - _noteFocusNode, - context, - ).copyWith( - suffixIcon: noteController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { + focusNode: _addressFocusNode, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Enter ${coin.ticker} address", + _addressFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: sendToController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _addressToggleFlag + ? TextFieldIconButton( + key: const Key( + "sendViewClearAddressFieldButtonKey"), + onTap: () { + sendToController.text = ""; + _address = ""; + _updatePreviewButtonState( + _address, _amountToSend); + setState(() { + _addressToggleFlag = false; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "sendViewPasteAddressFieldButtonKey"), + onTap: () async { + final ClipboardData? data = await clipboard + .getData(Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + String content = data.text!.trim(); + if (content.contains("\n")) { + content = content.substring( + 0, content.indexOf("\n")); + } + + sendToController.text = content; + _address = content; + + _updatePreviewButtonState( + _address, _amountToSend); setState(() { - noteController.text = ""; + _addressToggleFlag = + sendToController.text.isNotEmpty; }); - }, - ), - ], - ), + } + }, + child: sendToController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (sendToController.text.isEmpty) + TextFieldIconButton( + key: const Key("sendViewAddressBookButtonKey"), + onTap: () { + Navigator.of(context).pushNamed( + AddressBookView.routeName, + arguments: coin, + ); + }, + child: const AddressBookIcon(), ), - ) - : null, + if (sendToController.text.isEmpty) + TextFieldIconButton( + key: const Key("sendViewScanQrButtonKey"), + onTap: () async { + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75)); + } + + final qrResult = await scanner.scan(); + + Logging.instance.log( + "qrResult content: ${qrResult.rawContent}", + level: LogLevel.Info); + + final results = + AddressUtils.parseUri(qrResult.rawContent); + + Logging.instance.log("qrResult parsed: $results", + level: LogLevel.Info); + + if (results.isNotEmpty && + results["scheme"] == coin.uriScheme) { + // auto fill address + _address = results["address"] ?? ""; + sendToController.text = _address!; + + // autofill notes field + if (results["message"] != null) { + noteController.text = results["message"]!; + } else if (results["label"] != null) { + noteController.text = results["label"]!; + } + + // autofill amount field + if (results["amount"] != null) { + final amount = + Decimal.parse(results["amount"]!); + cryptoAmountController.text = + Format.localizedStringAsFixed( + value: amount, + locale: ref + .read( + localeServiceChangeNotifierProvider) + .locale, + decimalPlaces: Constants.decimalPlaces, + ); + amount.toString(); + _amountToSend = amount; + } + + _updatePreviewButtonState( + _address, _amountToSend); + setState(() { + _addressToggleFlag = + sendToController.text.isNotEmpty; + }); + + // now check for non standard encoded basic address + } else if (ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .validateAddress(qrResult.rawContent)) { + _address = qrResult.rawContent; + sendToController.text = _address ?? ""; + + _updatePreviewButtonState( + _address, _amountToSend); + setState(() { + _addressToggleFlag = + sendToController.text.isNotEmpty; + }); + } + } on PlatformException catch (e, s) { + // here we ignore the exception caused by not giving permission + // to use the camera to scan a qr code + Logging.instance.log( + "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", + level: LogLevel.Warning); + } + }, + child: const QrCodeIcon(), + ) + ], + ), + ), ), ), ), - const SizedBox( - height: 36, + ), + Builder( + builder: (_) { + final error = _updateInvalidAddressText( + _address ?? "", + ref.read(walletsChangeNotifierProvider).getManager(walletId), + ); + + if (error == null || error.isEmpty) { + return Container(); + } else { + return Align( + alignment: Alignment.topLeft, + child: Padding( + padding: const EdgeInsets.only( + left: 12.0, + top: 4.0, + ), + child: Text( + error, + textAlign: TextAlign.left, + style: STextStyles.label(context).copyWith( + color: + Theme.of(context).extension<StackColors>()!.textError, + ), + ), + ), + ); + } + }, + ), + const SizedBox( + height: 20, + ), + Text( + "Note (optional)", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + const SizedBox( + height: 10, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - PrimaryButton( - height: 56, - label: "Preview send", - enabled: ref.watch(previewTxButtonStateProvider.state).state, - onPressed: ref.watch(previewTxButtonStateProvider.state).state - ? previewSend - : null, - ) - ], - ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: noteController, + focusNode: _noteFocusNode, + style: STextStyles.field(context), + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Type something...", + _noteFocusNode, + context, + ).copyWith( + suffixIcon: noteController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + noteController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + const SizedBox( + height: 36, + ), + PrimaryButton( + height: 56, + label: "Preview send", + enabled: ref.watch(previewTxButtonStateProvider.state).state, + onPressed: ref.watch(previewTxButtonStateProvider.state).state + ? previewSend + : null, + ) + ], ); } } From 5bec4962e4490efbc456f96fc5a0dfb9011a4252 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 26 Oct 2022 16:27:45 -0600 Subject: [PATCH 038/426] WIP: desktop wallet balance display --- .../wallet_view/desktop_wallet_summary.dart | 283 ++++++++++++++++++ .../wallet_view/desktop_wallet_view.dart | 72 +++-- 2 files changed, 325 insertions(+), 30 deletions(-) create mode 100644 lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_summary.dart diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_summary.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_summary.dart new file mode 100644 index 000000000..fe2cfa7b2 --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_summary.dart @@ -0,0 +1,283 @@ +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/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart'; +import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_refresh_button.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/providers/wallet/wallet_balance_toggle_state_provider.dart'; +import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; +import 'package:stackwallet/services/coins/manager.dart'; +import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart'; +import 'package:stackwallet/utilities/format.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/animated_text.dart'; + +class DesktopWalletSummary extends StatefulWidget { + const DesktopWalletSummary({ + Key? key, + required this.walletId, + required this.managerProvider, + required this.initialSyncStatus, + }) : super(key: key); + + final String walletId; + final ChangeNotifierProvider<Manager> managerProvider; + final WalletSyncStatus initialSyncStatus; + + @override + State<DesktopWalletSummary> createState() => _WDesktopWalletSummaryState(); +} + +class _WDesktopWalletSummaryState extends State<DesktopWalletSummary> { + late final String walletId; + late final ChangeNotifierProvider<Manager> managerProvider; + + void showSheet() { + showModalBottomSheet<dynamic>( + backgroundColor: Colors.transparent, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + builder: (_) => WalletBalanceToggleSheet(walletId: walletId), + ); + } + + Decimal? _balanceTotalCached; + Decimal? _balanceCached; + + @override + void initState() { + walletId = widget.walletId; + managerProvider = widget.managerProvider; + super.initState(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + children: [ + Consumer( + builder: (_, ref, __) { + final Coin coin = + ref.watch(managerProvider.select((value) => value.coin)); + final externalCalls = ref.watch(prefsChangeNotifierProvider + .select((value) => value.externalCalls)); + + Future<Decimal>? totalBalanceFuture; + Future<Decimal>? availableBalanceFuture; + if (coin == Coin.firo || coin == Coin.firoTestNet) { + final firoWallet = + ref.watch(managerProvider.select((value) => value.wallet)) + as FiroWallet; + totalBalanceFuture = firoWallet.availablePublicBalance(); + availableBalanceFuture = firoWallet.availablePrivateBalance(); + } else { + totalBalanceFuture = ref.watch( + managerProvider.select((value) => value.totalBalance)); + + availableBalanceFuture = ref.watch(managerProvider + .select((value) => value.availableBalance)); + } + + final locale = ref.watch(localeServiceChangeNotifierProvider + .select((value) => value.locale)); + + final baseCurrency = ref.watch(prefsChangeNotifierProvider + .select((value) => value.currency)); + + final priceTuple = ref.watch(priceAnd24hChangeNotifierProvider + .select((value) => value.getPrice(coin))); + + final _showAvailable = + ref.watch(walletBalanceToggleStateProvider.state).state == + WalletBalanceToggleState.available; + + return FutureBuilder( + future: _showAvailable + ? availableBalanceFuture + : totalBalanceFuture, + builder: (fbContext, AsyncSnapshot<Decimal> snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData && + snapshot.data != null) { + if (_showAvailable) { + _balanceCached = snapshot.data!; + } else { + _balanceTotalCached = snapshot.data!; + } + } + Decimal? balanceToShow = + _showAvailable ? _balanceCached : _balanceTotalCached; + + if (balanceToShow != null) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: showSheet, + child: Row( + children: [ + if (coin == Coin.firo || + coin == Coin.firoTestNet) + Text( + "${_showAvailable ? "Private" : "Public"} Balance", + style: STextStyles.subtitle500(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFavoriteCard, + ), + ), + if (coin != Coin.firo && + coin != Coin.firoTestNet) + Text( + "${_showAvailable ? "Available" : "Full"} Balance", + style: STextStyles.subtitle500(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFavoriteCard, + ), + ), + const SizedBox( + width: 4, + ), + SvgPicture.asset( + Assets.svg.chevronDown, + color: Theme.of(context) + .extension<StackColors>()! + .textFavoriteCard, + width: 8, + height: 4, + ), + ], + ), + ), + FittedBox( + fit: BoxFit.scaleDown, + child: Text( + "${Format.localizedStringAsFixed( + value: balanceToShow, + locale: locale, + decimalPlaces: 8, + )} ${coin.ticker}", + style: STextStyles.desktopH3(context), + ), + ), + if (externalCalls) + Text( + "${Format.localizedStringAsFixed( + value: priceTuple.item1 * balanceToShow, + locale: locale, + decimalPlaces: 2, + )} $baseCurrency", + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ), + ], + ); + } else { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: showSheet, + child: Row( + children: [ + if (coin == Coin.firo || + coin == Coin.firoTestNet) + Text( + "${_showAvailable ? "Private" : "Public"} Balance", + style: STextStyles.subtitle500(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFavoriteCard, + ), + ), + if (coin != Coin.firo && + coin != Coin.firoTestNet) + Text( + "${_showAvailable ? "Available" : "Full"} Balance", + style: STextStyles.subtitle500(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFavoriteCard, + ), + ), + const SizedBox( + width: 4, + ), + SvgPicture.asset( + Assets.svg.chevronDown, + width: 8, + height: 4, + color: Theme.of(context) + .extension<StackColors>()! + .textFavoriteCard, + ), + ], + ), + ), + AnimatedText( + stringsToLoopThrough: const [ + "Loading balance ", + "Loading balance. ", + "Loading balance.. ", + "Loading balance..." + ], + style: STextStyles.desktopH3(context).copyWith( + fontSize: 24, + color: Theme.of(context) + .extension<StackColors>()! + .textFavoriteCard, + ), + ), + AnimatedText( + stringsToLoopThrough: const [ + "Loading balance ", + "Loading balance. ", + "Loading balance.. ", + "Loading balance..." + ], + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ), + ], + ); + } + }, + ); + }, + ), + ], + ), + WalletRefreshButton( + walletId: walletId, + initialSyncStatus: widget.initialSyncStatus, + ) + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index 4c2b510de..7f13c7fd8 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_summary.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/receive/desktop_receive.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -48,6 +50,8 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { final manager = ref.watch(walletsChangeNotifierProvider .select((value) => value.getManager(walletId))); final coin = manager.coin; + final managerProvider = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManagerProvider(walletId))); return DesktopScaffold( appBar: DesktopAppBar( @@ -120,37 +124,45 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { const SizedBox( width: 10, ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Text( - "TODO: balance", - style: STextStyles.desktopH3(context), - ), - const SizedBox( - width: 8, - ), - Container( - color: Colors.red, - width: 20, - height: 20, - ), - ], - ), - Text( - "todo: fiat balance", - style: - STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ) - ], + DesktopWalletSummary( + walletId: walletId, + managerProvider: managerProvider, + initialSyncStatus: ref.watch(managerProvider + .select((value) => value.isRefreshing)) + ? WalletSyncStatus.syncing + : WalletSyncStatus.synced, ), + // Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // Row( + // children: [ + // Text( + // "TODO: balance", + // style: STextStyles.desktopH3(context), + // ), + // const SizedBox( + // width: 8, + // ), + // Container( + // color: Colors.red, + // width: 20, + // height: 20, + // ), + // ], + // ), + // Text( + // "todo: fiat balance", + // style: + // STextStyles.desktopTextExtraSmall(context).copyWith( + // color: Theme.of(context) + // .extension<StackColors>()! + // .textSubtitle1, + // ), + // ) + // ], + // ), const Spacer(), SecondaryButton( width: 180, From fa48ab30f531d966a6a6bbcf24fe5b852807c1eb Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Wed, 26 Oct 2022 16:29:51 -0600 Subject: [PATCH 039/426] working on theme toggles --- .../settings_menu/appearance_settings.dart | 244 ++++++++++++++++++ 1 file changed, 244 insertions(+) diff --git a/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart b/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart index c0b2c0ca5..d524453e2 100644 --- a/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart @@ -4,8 +4,13 @@ import 'package:flutter_riverpod/flutter_riverpod.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/rounded_white_container.dart'; +import '../../../providers/global/prefs_provider.dart'; +import '../../../utilities/constants.dart'; +import '../../../widgets/custom_buttons/draggable_switch_button.dart'; + class AppearanceOptionSettings extends ConsumerStatefulWidget { const AppearanceOptionSettings({Key? key}) : super(key: key); @@ -61,6 +66,74 @@ class _AppearanceOptionSettings ), ], ), + const Padding( + padding: EdgeInsets.all(10.0), + child: Divider( + thickness: 0.5, + ), + ), + Padding( + padding: const EdgeInsets.all(10.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Display favorite wallets", + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: ref.watch( + prefsChangeNotifierProvider + .select((value) => value.showFavoriteWallets), + ), + onValueChanged: (newValue) { + ref + .read(prefsChangeNotifierProvider) + .showFavoriteWallets = newValue; + }, + ), + ) + ], + ), + ), + const Padding( + padding: EdgeInsets.all(10.0), + child: Divider( + thickness: 0.5, + ), + ), + Padding( + padding: const EdgeInsets.all(10.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Choose theme", + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark), + textAlign: TextAlign.left, + ), + ], + ), + ), + const Padding( + padding: EdgeInsets.only( + left: 10, + right: 10, + ), + child: ThemeToggle(), + ), ], ), ), @@ -69,3 +142,174 @@ class _AppearanceOptionSettings ); } } + +class ThemeToggle extends StatefulWidget { + const ThemeToggle({ + Key? key, + }) : super(key: key); + + // final bool externalCallsEnabled; + // final void Function(bool)? onChanged; + + @override + State<StatefulWidget> createState() => _ThemeToggle(); +} + +class _ThemeToggle extends State<ThemeToggle> { + // late bool externalCallsEnabled; + + @override + Widget build(BuildContext context) { + return Row( + // mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: RawMaterialButton( + elevation: 0, + hoverColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius * 2, + ), + ), + onPressed: () {}, //onPressed + child: Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 24, + ), + child: SvgPicture.asset( + Assets.svg.themeLight, + ), + ), + Padding( + padding: const EdgeInsets.only( + left: 50, + top: 12, + ), + child: Text( + "Light", + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + ) + ], + ), + // if (externalCallsEnabled) + Positioned( + bottom: 0, + left: 6, + child: SvgPicture.asset( + Assets.svg.checkCircle, + width: 20, + height: 20, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + ), + ), + // if (!externalCallsEnabled) + // Positioned( + // top: 4, + // right: 4, + // child: Container( + // width: 20, + // height: 20, + // decoration: BoxDecoration( + // borderRadius: BorderRadius.circular(1000), + // color: Theme.of(context) + // .extension<StackColors>()! + // .textFieldDefaultBG, + // ), + // ), + // ), + ], + ), + ), + ), + const SizedBox( + width: 1, + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: RawMaterialButton( + elevation: 0, + hoverColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius * 2, + ), + ), + onPressed: () {}, //onPressed + child: Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset( + Assets.svg.themeDark, + ), + Padding( + padding: const EdgeInsets.only( + left: 50, + top: 12, + ), + child: Text( + "Dark", + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + ), + ], + ), + // if (externalCallsEnabled) + Positioned( + bottom: 0, + left: 0, + child: SvgPicture.asset( + Assets.svg.checkCircle, + width: 20, + height: 20, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + ), + ), + // if (!externalCallsEnabled) + // Positioned( + // top: 4, + // right: 4, + // child: Container( + // width: 20, + // height: 20, + // decoration: BoxDecoration( + // borderRadius: BorderRadius.circular(1000), + // color: Theme.of(context) + // .extension<StackColors>()! + // .textFieldDefaultBG, + // ), + // ), + // ), + ], + ), + ), + ), + ), + ], + ); + } +} From df7871fa6ba06261957dc88ba393994d35993290 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 26 Oct 2022 16:49:38 -0600 Subject: [PATCH 040/426] WIP: keys + sync status --- .../wallet_view/desktop_wallet_view.dart | 51 +++++++++++++++++-- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index 7f13c7fd8..f86eba750 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -98,7 +98,7 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { children: const [ NetworkInfoButton(), SizedBox( - width: 12, + width: 32, ), WalletKeysButton(), SizedBox( @@ -467,8 +467,28 @@ class NetworkInfoButton extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - child: Text("todo: sync status"), + return GestureDetector( + child: Container( + color: Colors.transparent, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.network, + width: 24, + height: 24, + color: + Theme.of(context).extension<StackColors>()!.accentColorGreen, + ), + const SizedBox( + width: 6, + ), + Text( + "Synchronised", + style: STextStyles.desktopMenuItemSelected(context), + ) + ], + ), + ), ); } } @@ -478,8 +498,29 @@ class WalletKeysButton extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - child: Text("todo: wallet keys"), + return GestureDetector( + child: Container( + color: Colors.transparent, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.key, + width: 20, + height: 20, + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, + ), + const SizedBox( + width: 6, + ), + Text( + "Wallet keys", + style: STextStyles.desktopMenuItemSelected(context), + ) + ], + ), + ), ); } } From a736c9a5032344a4f99533b254c74115397cb53c Mon Sep 17 00:00:00 2001 From: Marco <marco@cypherstack.com> Date: Wed, 26 Oct 2022 18:26:40 -0600 Subject: [PATCH 041/426] fix price not showing --- lib/pages/stack_privacy_calls.dart | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lib/pages/stack_privacy_calls.dart b/lib/pages/stack_privacy_calls.dart index 2aa2a5c8a..7ca21c494 100644 --- a/lib/pages/stack_privacy_calls.dart +++ b/lib/pages/stack_privacy_calls.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; @@ -6,12 +8,17 @@ import 'package:stackwallet/pages_desktop_specific/create_password/create_passwo import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; +import '../hive/db.dart'; +import '../providers/global/price_provider.dart'; +import '../services/exchange/exchange_data_loading_service.dart'; + class StackPrivacyCalls extends ConsumerStatefulWidget { const StackPrivacyCalls({ Key? key, @@ -160,6 +167,21 @@ class _StackPrivacyCalls extends ConsumerState<StackPrivacyCalls> { onPressed: () { ref.read(prefsChangeNotifierProvider).externalCalls = isEasy; + + DB.instance + .put<dynamic>( + boxName: DB.boxNamePrefs, + key: "externalCalls", + value: isEasy) + .then((_) { + if (isEasy) { + unawaited( + ExchangeDataLoadingService().loadAll(ref)); + ref + .read(priceAnd24hChangeNotifierProvider) + .start(true); + } + }); if (!widget.isSettings) { if (isDesktop) { Navigator.of(context).pushNamed( From 89e301df45f8202962d7c67c0d5ea1642e13f172 Mon Sep 17 00:00:00 2001 From: Marco <marco@cypherstack.com> Date: Wed, 26 Oct 2022 18:26:40 -0600 Subject: [PATCH 042/426] fix price not showing (cherry picked from commit a736c9a5032344a4f99533b254c74115397cb53c) --- lib/pages/stack_privacy_calls.dart | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lib/pages/stack_privacy_calls.dart b/lib/pages/stack_privacy_calls.dart index 2aa2a5c8a..7ca21c494 100644 --- a/lib/pages/stack_privacy_calls.dart +++ b/lib/pages/stack_privacy_calls.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; @@ -6,12 +8,17 @@ import 'package:stackwallet/pages_desktop_specific/create_password/create_passwo import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; +import '../hive/db.dart'; +import '../providers/global/price_provider.dart'; +import '../services/exchange/exchange_data_loading_service.dart'; + class StackPrivacyCalls extends ConsumerStatefulWidget { const StackPrivacyCalls({ Key? key, @@ -160,6 +167,21 @@ class _StackPrivacyCalls extends ConsumerState<StackPrivacyCalls> { onPressed: () { ref.read(prefsChangeNotifierProvider).externalCalls = isEasy; + + DB.instance + .put<dynamic>( + boxName: DB.boxNamePrefs, + key: "externalCalls", + value: isEasy) + .then((_) { + if (isEasy) { + unawaited( + ExchangeDataLoadingService().loadAll(ref)); + ref + .read(priceAnd24hChangeNotifierProvider) + .start(true); + } + }); if (!widget.isSettings) { if (isDesktop) { Navigator.of(context).pushNamed( From 49c3c15a7d2eccff80c40f66ff736149f1dd951e Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 27 Oct 2022 08:31:25 -0600 Subject: [PATCH 043/426] disable fee preview calculation for desktop send form --- .../wallet_view/send/desktop_send.dart | 242 +++++++++--------- pubspec.lock | 2 +- 2 files changed, 120 insertions(+), 124 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart index 1579b2367..0ffe3b962 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart @@ -7,6 +7,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/models/send_view_auto_fill_data.dart'; import 'package:stackwallet/pages/address_book_views/address_book_view.dart'; +import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart'; +import 'package:stackwallet/pages/send_view/sub_widgets/building_transaction_dialog.dart'; import 'package:stackwallet/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart'; import 'package:stackwallet/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart'; import 'package:stackwallet/providers/providers.dart'; @@ -22,7 +24,6 @@ import 'package:stackwallet/utilities/barcode_scanner_interface.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; @@ -40,9 +41,6 @@ import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; -import '../../../../../pages/send_view/confirm_transaction_view.dart'; -import '../../../../../pages/send_view/sub_widgets/building_transaction_dialog.dart'; - class DesktopSend extends ConsumerStatefulWidget { const DesktopSend({ Key? key, @@ -71,7 +69,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { late TextEditingController cryptoAmountController; late TextEditingController baseAmountController; late TextEditingController noteController; - late TextEditingController feeController; + // late TextEditingController feeController; late final SendViewAutoFillData? _data; @@ -92,8 +90,6 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { bool _cryptoAmountChangeLock = false; late VoidCallback onCryptoAmountChanged; - Decimal? _cachedBalance; - Future<void> previewSend() async { // wait for keyboard to disappear FocusScope.of(context).unfocus(); @@ -346,78 +342,78 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { (isValidAddress && amount != null && amount > Decimal.zero); } - late Future<String> _calculateFeesFuture; + // late Future<String> _calculateFeesFuture; - Map<int, String> cachedFees = {}; - Map<int, String> cachedFiroPrivateFees = {}; - Map<int, String> cachedFiroPublicFees = {}; + // Map<int, String> cachedFees = {}; + // Map<int, String> cachedFiroPrivateFees = {}; + // Map<int, String> cachedFiroPublicFees = {}; - Future<String> calculateFees(int amount) async { - if (amount <= 0) { - return "0"; - } - - if (coin == Coin.firo || coin == Coin.firoTestNet) { - if (ref.read(publicPrivateBalanceStateProvider.state).state == - "Private") { - if (cachedFiroPrivateFees[amount] != null) { - return cachedFiroPrivateFees[amount]!; - } - } else { - if (cachedFiroPublicFees[amount] != null) { - return cachedFiroPublicFees[amount]!; - } - } - } else if (cachedFees[amount] != null) { - return cachedFees[amount]!; - } - - final manager = - ref.read(walletsChangeNotifierProvider).getManager(walletId); - final feeObject = await manager.fees; - - late final int feeRate; - - switch (ref.read(feeRateTypeStateProvider.state).state) { - case FeeRateType.fast: - feeRate = feeObject.fast; - break; - case FeeRateType.average: - feeRate = feeObject.medium; - break; - case FeeRateType.slow: - feeRate = feeObject.slow; - break; - } - - int fee; - - if (coin == Coin.firo || coin == Coin.firoTestNet) { - if (ref.read(publicPrivateBalanceStateProvider.state).state == - "Private") { - fee = await manager.estimateFeeFor(amount, feeRate); - - cachedFiroPrivateFees[amount] = Format.satoshisToAmount(fee) - .toStringAsFixed(Constants.decimalPlaces); - - return cachedFiroPrivateFees[amount]!; - } else { - fee = await (manager.wallet as FiroWallet) - .estimateFeeForPublic(amount, feeRate); - - cachedFiroPublicFees[amount] = Format.satoshisToAmount(fee) - .toStringAsFixed(Constants.decimalPlaces); - - return cachedFiroPublicFees[amount]!; - } - } else { - fee = await manager.estimateFeeFor(amount, feeRate); - cachedFees[amount] = - Format.satoshisToAmount(fee).toStringAsFixed(Constants.decimalPlaces); - - return cachedFees[amount]!; - } - } + // Future<String> calculateFees(int amount) async { + // if (amount <= 0) { + // return "0"; + // } + // + // if (coin == Coin.firo || coin == Coin.firoTestNet) { + // if (ref.read(publicPrivateBalanceStateProvider.state).state == + // "Private") { + // if (cachedFiroPrivateFees[amount] != null) { + // return cachedFiroPrivateFees[amount]!; + // } + // } else { + // if (cachedFiroPublicFees[amount] != null) { + // return cachedFiroPublicFees[amount]!; + // } + // } + // } else if (cachedFees[amount] != null) { + // return cachedFees[amount]!; + // } + // + // final manager = + // ref.read(walletsChangeNotifierProvider).getManager(walletId); + // final feeObject = await manager.fees; + // + // late final int feeRate; + // + // switch (ref.read(feeRateTypeStateProvider.state).state) { + // case FeeRateType.fast: + // feeRate = feeObject.fast; + // break; + // case FeeRateType.average: + // feeRate = feeObject.medium; + // break; + // case FeeRateType.slow: + // feeRate = feeObject.slow; + // break; + // } + // + // int fee; + // + // if (coin == Coin.firo || coin == Coin.firoTestNet) { + // if (ref.read(publicPrivateBalanceStateProvider.state).state == + // "Private") { + // fee = await manager.estimateFeeFor(amount, feeRate); + // + // cachedFiroPrivateFees[amount] = Format.satoshisToAmount(fee) + // .toStringAsFixed(Constants.decimalPlaces); + // + // return cachedFiroPrivateFees[amount]!; + // } else { + // fee = await (manager.wallet as FiroWallet) + // .estimateFeeForPublic(amount, feeRate); + // + // cachedFiroPublicFees[amount] = Format.satoshisToAmount(fee) + // .toStringAsFixed(Constants.decimalPlaces); + // + // return cachedFiroPublicFees[amount]!; + // } + // } else { + // fee = await manager.estimateFeeFor(amount, feeRate); + // cachedFees[amount] = + // Format.satoshisToAmount(fee).toStringAsFixed(Constants.decimalPlaces); + // + // return cachedFees[amount]!; + // } + // } Future<String?> _firoBalanceFuture( ChangeNotifierProvider<Manager> provider, String locale) async { @@ -443,7 +439,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { void initState() { ref.refresh(feeSheetSessionCacheProvider); - _calculateFeesFuture = calculateFees(0); + // _calculateFeesFuture = calculateFees(0); _data = widget.autoFillData; walletId = widget.walletId; coin = ref.read(walletsChangeNotifierProvider).getManager(walletId).coin; @@ -454,7 +450,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { cryptoAmountController = TextEditingController(); baseAmountController = TextEditingController(); noteController = TextEditingController(); - feeController = TextEditingController(); + // feeController = TextEditingController(); onCryptoAmountChanged = _cryptoAmountChanged; cryptoAmountController.addListener(onCryptoAmountChanged); @@ -468,35 +464,35 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { _addressToggleFlag = true; } - _cryptoFocus.addListener(() { - if (!_cryptoFocus.hasFocus && !_baseFocus.hasFocus) { - if (_amountToSend == null) { - setState(() { - _calculateFeesFuture = calculateFees(0); - }); - } else { - setState(() { - _calculateFeesFuture = - calculateFees(Format.decimalAmountToSatoshis(_amountToSend!)); - }); - } - } - }); - - _baseFocus.addListener(() { - if (!_cryptoFocus.hasFocus && !_baseFocus.hasFocus) { - if (_amountToSend == null) { - setState(() { - _calculateFeesFuture = calculateFees(0); - }); - } else { - setState(() { - _calculateFeesFuture = - calculateFees(Format.decimalAmountToSatoshis(_amountToSend!)); - }); - } - } - }); + // _cryptoFocus.addListener(() { + // if (!_cryptoFocus.hasFocus && !_baseFocus.hasFocus) { + // if (_amountToSend == null) { + // setState(() { + // _calculateFeesFuture = calculateFees(0); + // }); + // } else { + // setState(() { + // _calculateFeesFuture = + // calculateFees(Format.decimalAmountToSatoshis(_amountToSend!)); + // }); + // } + // } + // }); + // + // _baseFocus.addListener(() { + // if (!_cryptoFocus.hasFocus && !_baseFocus.hasFocus) { + // if (_amountToSend == null) { + // setState(() { + // _calculateFeesFuture = calculateFees(0); + // }); + // } else { + // setState(() { + // _calculateFeesFuture = + // calculateFees(Format.decimalAmountToSatoshis(_amountToSend!)); + // }); + // } + // } + // }); super.initState(); } @@ -509,7 +505,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { cryptoAmountController.dispose(); baseAmountController.dispose(); noteController.dispose(); - feeController.dispose(); + // feeController.dispose(); _noteFocusNode.dispose(); _addressFocusNode.dispose(); @@ -526,20 +522,20 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { final String locale = ref.watch( localeServiceChangeNotifierProvider.select((value) => value.locale)); - if (coin == Coin.firo || coin == Coin.firoTestNet) { - ref.listen(publicPrivateBalanceStateProvider, (previous, next) { - if (_amountToSend == null) { - setState(() { - _calculateFeesFuture = calculateFees(0); - }); - } else { - setState(() { - _calculateFeesFuture = - calculateFees(Format.decimalAmountToSatoshis(_amountToSend!)); - }); - } - }); - } + // if (coin == Coin.firo || coin == Coin.firoTestNet) { + // ref.listen(publicPrivateBalanceStateProvider, (previous, next) { + // if (_amountToSend == null) { + // setState(() { + // _calculateFeesFuture = calculateFees(0); + // }); + // } else { + // setState(() { + // _calculateFeesFuture = + // calculateFees(Format.decimalAmountToSatoshis(_amountToSend!)); + // }); + // } + // }); + // } return Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/pubspec.lock b/pubspec.lock index 394d1ec8e..20992d827 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1410,7 +1410,7 @@ packages: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" + version: "1.1.0" string_validator: dependency: "direct main" description: From d263bf1b5ea1599a47cdbea4a42e5a848231187c Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 27 Oct 2022 08:39:52 -0600 Subject: [PATCH 044/426] set maxLines in desktop send address and note fields to 5 --- .../home/my_stack_view/wallet_view/send/desktop_send.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart index 0ffe3b962..0d458e03b 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart @@ -873,6 +873,8 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { Constants.size.circularBorderRadius, ), child: TextField( + minLines: 1, + maxLines: 5, key: const Key("sendViewAddressFieldKey"), controller: sendToController, readOnly: false, @@ -1110,6 +1112,8 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { Constants.size.circularBorderRadius, ), child: TextField( + minLines: 1, + maxLines: 5, autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, controller: noteController, From 3eff509c3295ad334810b013cbbbd22342730128 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 27 Oct 2022 08:50:50 -0600 Subject: [PATCH 045/426] refactor longer bits of logic out of the build method --- .../wallet_view/send/desktop_send.dart | 428 +++++++++--------- 1 file changed, 205 insertions(+), 223 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart index 0d458e03b..c353caa5e 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart @@ -416,7 +416,9 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { // } Future<String?> _firoBalanceFuture( - ChangeNotifierProvider<Manager> provider, String locale) async { + ChangeNotifierProvider<Manager> provider, + String locale, + ) async { final wallet = ref.read(provider).wallet as FiroWallet?; if (wallet != null) { @@ -435,6 +437,203 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { return null; } + Widget firoBalanceFutureBuilder( + BuildContext context, + AsyncSnapshot<String?> snapshot, + ) { + if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { + if (ref.read(publicPrivateBalanceStateProvider.state).state == + "Private") { + _privateBalanceString = snapshot.data!; + } else { + _publicBalanceString = snapshot.data!; + } + } + if (ref.read(publicPrivateBalanceStateProvider.state).state == "Private" && + _privateBalanceString != null) { + return Text( + "$_privateBalanceString ${coin.ticker}", + style: STextStyles.itemSubtitle(context), + ); + } else if (ref.read(publicPrivateBalanceStateProvider.state).state == + "Public" && + _publicBalanceString != null) { + return Text( + "$_publicBalanceString ${coin.ticker}", + style: STextStyles.itemSubtitle(context), + ); + } else { + return AnimatedText( + stringsToLoopThrough: const [ + "Loading balance", + "Loading balance.", + "Loading balance..", + "Loading balance...", + ], + style: STextStyles.itemSubtitle(context), + ); + } + } + + Future<void> scanQr() async { + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 75)); + } + + final qrResult = await scanner.scan(); + + Logging.instance.log("qrResult content: ${qrResult.rawContent}", + level: LogLevel.Info); + + final results = AddressUtils.parseUri(qrResult.rawContent); + + Logging.instance.log("qrResult parsed: $results", level: LogLevel.Info); + + if (results.isNotEmpty && results["scheme"] == coin.uriScheme) { + // auto fill address + _address = results["address"] ?? ""; + sendToController.text = _address!; + + // autofill notes field + if (results["message"] != null) { + noteController.text = results["message"]!; + } else if (results["label"] != null) { + noteController.text = results["label"]!; + } + + // autofill amount field + if (results["amount"] != null) { + final amount = Decimal.parse(results["amount"]!); + cryptoAmountController.text = Format.localizedStringAsFixed( + value: amount, + locale: ref.read(localeServiceChangeNotifierProvider).locale, + decimalPlaces: Constants.decimalPlaces, + ); + amount.toString(); + _amountToSend = amount; + } + + _updatePreviewButtonState(_address, _amountToSend); + setState(() { + _addressToggleFlag = sendToController.text.isNotEmpty; + }); + + // now check for non standard encoded basic address + } else if (ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .validateAddress(qrResult.rawContent)) { + _address = qrResult.rawContent; + sendToController.text = _address ?? ""; + + _updatePreviewButtonState(_address, _amountToSend); + setState(() { + _addressToggleFlag = sendToController.text.isNotEmpty; + }); + } + } on PlatformException catch (e, s) { + // here we ignore the exception caused by not giving permission + // to use the camera to scan a qr code + Logging.instance.log( + "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", + level: LogLevel.Warning); + } + } + + Future<void> pasteAddress() async { + final ClipboardData? data = await clipboard.getData(Clipboard.kTextPlain); + if (data?.text != null && data!.text!.isNotEmpty) { + String content = data.text!.trim(); + if (content.contains("\n")) { + content = content.substring(0, content.indexOf("\n")); + } + + sendToController.text = content; + _address = content; + + _updatePreviewButtonState(_address, _amountToSend); + setState(() { + _addressToggleFlag = sendToController.text.isNotEmpty; + }); + } + } + + void fiatTextFieldOnChanged(String baseAmountString) { + if (baseAmountString.isNotEmpty && + baseAmountString != "." && + baseAmountString != ",") { + final baseAmount = baseAmountString.contains(",") + ? Decimal.parse(baseAmountString.replaceFirst(",", ".")) + : Decimal.parse(baseAmountString); + + var _price = + ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1; + + if (_price == Decimal.zero) { + _amountToSend = Decimal.zero; + } else { + _amountToSend = baseAmount <= Decimal.zero + ? Decimal.zero + : (baseAmount / _price) + .toDecimal(scaleOnInfinitePrecision: Constants.decimalPlaces); + } + if (_cachedAmountToSend != null && _cachedAmountToSend == _amountToSend) { + return; + } + _cachedAmountToSend = _amountToSend; + Logging.instance.log("it changed $_amountToSend $_cachedAmountToSend", + level: LogLevel.Info); + + final amountString = Format.localizedStringAsFixed( + value: _amountToSend!, + locale: ref.read(localeServiceChangeNotifierProvider).locale, + decimalPlaces: Constants.decimalPlaces, + ); + + _cryptoAmountChangeLock = true; + cryptoAmountController.text = amountString; + _cryptoAmountChangeLock = false; + } else { + _amountToSend = Decimal.zero; + _cryptoAmountChangeLock = true; + cryptoAmountController.text = ""; + _cryptoAmountChangeLock = false; + } + // setState(() { + // _calculateFeesFuture = calculateFees( + // Format.decimalAmountToSatoshis( + // _amountToSend!)); + // }); + _updatePreviewButtonState(_address, _amountToSend); + } + + Future<void> sendAllTapped() async { + if (coin == Coin.firo || coin == Coin.firoTestNet) { + final firoWallet = ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .wallet as FiroWallet; + if (ref.read(publicPrivateBalanceStateProvider.state).state == + "Private") { + cryptoAmountController.text = + (await firoWallet.availablePrivateBalance()) + .toStringAsFixed(Constants.decimalPlaces); + } else { + cryptoAmountController.text = + (await firoWallet.availablePublicBalance()) + .toStringAsFixed(Constants.decimalPlaces); + } + } else { + cryptoAmountController.text = (await ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .availableBalance) + .toStringAsFixed(Constants.decimalPlaces); + } + } + @override void initState() { ref.refresh(feeSheetSessionCacheProvider); @@ -602,55 +801,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { ), FutureBuilder( future: _firoBalanceFuture(provider, locale), - builder: - (context, AsyncSnapshot<String?> snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - if (ref - .read(publicPrivateBalanceStateProvider - .state) - .state == - "Private") { - _privateBalanceString = snapshot.data!; - } else { - _publicBalanceString = snapshot.data!; - } - } - if (ref - .read( - publicPrivateBalanceStateProvider - .state) - .state == - "Private" && - _privateBalanceString != null) { - return Text( - "$_privateBalanceString ${coin.ticker}", - style: STextStyles.itemSubtitle(context), - ); - } else if (ref - .read( - publicPrivateBalanceStateProvider - .state) - .state == - "Public" && - _publicBalanceString != null) { - return Text( - "$_publicBalanceString ${coin.ticker}", - style: STextStyles.itemSubtitle(context), - ); - } else { - return AnimatedText( - stringsToLoopThrough: const [ - "Loading balance", - "Loading balance.", - "Loading balance..", - "Loading balance...", - ], - style: STextStyles.itemSubtitle(context), - ); - } - }, + builder: firoBalanceFutureBuilder, ), ], ), @@ -682,25 +833,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { ), BlueTextButton( text: "Send all ${coin.ticker}", - onTap: () async { - if (coin == Coin.firo || coin == Coin.firoTestNet) { - final firoWallet = ref.read(provider).wallet as FiroWallet; - if (ref.read(publicPrivateBalanceStateProvider.state).state == - "Private") { - cryptoAmountController.text = - (await firoWallet.availablePrivateBalance()) - .toStringAsFixed(Constants.decimalPlaces); - } else { - cryptoAmountController.text = - (await firoWallet.availablePublicBalance()) - .toStringAsFixed(Constants.decimalPlaces); - } - } else { - cryptoAmountController.text = - (await ref.read(provider).availableBalance) - .toStringAsFixed(Constants.decimalPlaces); - } - }, + onTap: sendAllTapped, ), ], ), @@ -780,58 +913,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { ? newValue : oldValue), ], - onChanged: (baseAmountString) { - if (baseAmountString.isNotEmpty && - baseAmountString != "." && - baseAmountString != ",") { - final baseAmount = baseAmountString.contains(",") - ? Decimal.parse(baseAmountString.replaceFirst(",", ".")) - : Decimal.parse(baseAmountString); - - var _price = ref - .read(priceAnd24hChangeNotifierProvider) - .getPrice(coin) - .item1; - - if (_price == Decimal.zero) { - _amountToSend = Decimal.zero; - } else { - _amountToSend = baseAmount <= Decimal.zero - ? Decimal.zero - : (baseAmount / _price).toDecimal( - scaleOnInfinitePrecision: Constants.decimalPlaces); - } - if (_cachedAmountToSend != null && - _cachedAmountToSend == _amountToSend) { - return; - } - _cachedAmountToSend = _amountToSend; - Logging.instance.log( - "it changed $_amountToSend $_cachedAmountToSend", - level: LogLevel.Info); - - final amountString = Format.localizedStringAsFixed( - value: _amountToSend!, - locale: ref.read(localeServiceChangeNotifierProvider).locale, - decimalPlaces: Constants.decimalPlaces, - ); - - _cryptoAmountChangeLock = true; - cryptoAmountController.text = amountString; - _cryptoAmountChangeLock = false; - } else { - _amountToSend = Decimal.zero; - _cryptoAmountChangeLock = true; - cryptoAmountController.text = ""; - _cryptoAmountChangeLock = false; - } - // setState(() { - // _calculateFeesFuture = calculateFees( - // Format.decimalAmountToSatoshis( - // _amountToSend!)); - // }); - _updatePreviewButtonState(_address, _amountToSend); - }, + onChanged: fiatTextFieldOnChanged, decoration: InputDecoration( contentPadding: const EdgeInsets.only( top: 12, @@ -937,28 +1019,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { : TextFieldIconButton( key: const Key( "sendViewPasteAddressFieldButtonKey"), - onTap: () async { - final ClipboardData? data = await clipboard - .getData(Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - String content = data.text!.trim(); - if (content.contains("\n")) { - content = content.substring( - 0, content.indexOf("\n")); - } - - sendToController.text = content; - _address = content; - - _updatePreviewButtonState( - _address, _amountToSend); - setState(() { - _addressToggleFlag = - sendToController.text.isNotEmpty; - }); - } - }, + onTap: pasteAddress, child: sendToController.text.isEmpty ? const ClipboardIcon() : const XIcon(), @@ -977,86 +1038,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { if (sendToController.text.isEmpty) TextFieldIconButton( key: const Key("sendViewScanQrButtonKey"), - onTap: () async { - try { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration(milliseconds: 75)); - } - - final qrResult = await scanner.scan(); - - Logging.instance.log( - "qrResult content: ${qrResult.rawContent}", - level: LogLevel.Info); - - final results = - AddressUtils.parseUri(qrResult.rawContent); - - Logging.instance.log("qrResult parsed: $results", - level: LogLevel.Info); - - if (results.isNotEmpty && - results["scheme"] == coin.uriScheme) { - // auto fill address - _address = results["address"] ?? ""; - sendToController.text = _address!; - - // autofill notes field - if (results["message"] != null) { - noteController.text = results["message"]!; - } else if (results["label"] != null) { - noteController.text = results["label"]!; - } - - // autofill amount field - if (results["amount"] != null) { - final amount = - Decimal.parse(results["amount"]!); - cryptoAmountController.text = - Format.localizedStringAsFixed( - value: amount, - locale: ref - .read( - localeServiceChangeNotifierProvider) - .locale, - decimalPlaces: Constants.decimalPlaces, - ); - amount.toString(); - _amountToSend = amount; - } - - _updatePreviewButtonState( - _address, _amountToSend); - setState(() { - _addressToggleFlag = - sendToController.text.isNotEmpty; - }); - - // now check for non standard encoded basic address - } else if (ref - .read(walletsChangeNotifierProvider) - .getManager(walletId) - .validateAddress(qrResult.rawContent)) { - _address = qrResult.rawContent; - sendToController.text = _address ?? ""; - - _updatePreviewButtonState( - _address, _amountToSend); - setState(() { - _addressToggleFlag = - sendToController.text.isNotEmpty; - }); - } - } on PlatformException catch (e, s) { - // here we ignore the exception caused by not giving permission - // to use the camera to scan a qr code - Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", - level: LogLevel.Warning); - } - }, + onTap: scanQr, child: const QrCodeIcon(), ) ], From d3656145332e352fca5a6957c338960dc5c10ca1 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 27 Oct 2022 10:51:56 -0600 Subject: [PATCH 046/426] desktop send fields restyle --- .../wallet_view/send/desktop_send.dart | 68 +++++++++++++++---- lib/widgets/stack_text_field.dart | 20 +++++- 2 files changed, 71 insertions(+), 17 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart index c353caa5e..b084603b6 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart @@ -745,7 +745,11 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { if (coin == Coin.firo) Text( "Send from", - style: STextStyles.smallMed12(context), + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveSearchIconRight, + ), textAlign: TextAlign.left, ), if (coin == Coin.firo) @@ -828,7 +832,11 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { children: [ Text( "Amount", - style: STextStyles.smallMed12(context), + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveSearchIconRight, + ), textAlign: TextAlign.left, ), BlueTextButton( @@ -864,12 +872,15 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { ], decoration: InputDecoration( contentPadding: const EdgeInsets.only( - top: 12, + top: 22, right: 12, + bottom: 22, ), hintText: "0", - hintStyle: STextStyles.fieldLabel(context).copyWith( - fontSize: 14, + hintStyle: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultText, ), prefixIcon: FittedBox( fit: BoxFit.scaleDown, @@ -916,12 +927,15 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { onChanged: fiatTextFieldOnChanged, decoration: InputDecoration( contentPadding: const EdgeInsets.only( - top: 12, + top: 22, right: 12, + bottom: 22, ), hintText: "0", - hintStyle: STextStyles.fieldLabel(context).copyWith( - fontSize: 14, + hintStyle: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultText, ), prefixIcon: FittedBox( fit: BoxFit.scaleDown, @@ -944,7 +958,11 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { ), Text( "Send to", - style: STextStyles.smallMed12(context), + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveSearchIconRight, + ), textAlign: TextAlign.left, ), const SizedBox( @@ -981,16 +999,22 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { }); }, focusNode: _addressFocusNode, - style: STextStyles.field(context), + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveText, + height: 1.8, + ), decoration: standardInputDecoration( "Enter ${coin.ticker} address", _addressFocusNode, context, + desktopMed: true, ).copyWith( contentPadding: const EdgeInsets.only( left: 16, - top: 6, - bottom: 8, + top: 11, + bottom: 12, right: 5, ), suffixIcon: Padding( @@ -1083,7 +1107,11 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { ), Text( "Note (optional)", - style: STextStyles.smallMed12(context), + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveSearchIconRight, + ), textAlign: TextAlign.left, ), const SizedBox( @@ -1100,13 +1128,25 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { enableSuggestions: Util.isDesktop ? false : true, controller: noteController, focusNode: _noteFocusNode, - style: STextStyles.field(context), + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveText, + height: 1.8, + ), onChanged: (_) => setState(() {}), decoration: standardInputDecoration( "Type something...", _noteFocusNode, context, + desktopMed: true, ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 11, + bottom: 12, + right: 5, + ), suffixIcon: noteController.text.isNotEmpty ? Padding( padding: const EdgeInsets.only(right: 0), diff --git a/lib/widgets/stack_text_field.dart b/lib/widgets/stack_text_field.dart index 9858c18db..1f1e9f8de 100644 --- a/lib/widgets/stack_text_field.dart +++ b/lib/widgets/stack_text_field.dart @@ -4,7 +4,11 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; InputDecoration standardInputDecoration( - String? labelText, FocusNode textFieldFocusNode, BuildContext context) { + String? labelText, + FocusNode textFieldFocusNode, + BuildContext context, { + bool desktopMed = false, +}) { final isDesktop = Util.isDesktop; return InputDecoration( @@ -13,10 +17,20 @@ InputDecoration standardInputDecoration( ? Theme.of(context).extension<StackColors>()!.textFieldActiveBG : Theme.of(context).extension<StackColors>()!.textFieldDefaultBG, labelStyle: isDesktop - ? STextStyles.desktopTextFieldLabel(context) + ? desktopMed + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultText) + : STextStyles.desktopTextFieldLabel(context) : STextStyles.fieldLabel(context), hintStyle: isDesktop - ? STextStyles.desktopTextFieldLabel(context) + ? desktopMed + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultText) + : STextStyles.desktopTextFieldLabel(context) : STextStyles.fieldLabel(context), enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, From 51b6a953903b0dc1ca18447c5e6c47ddcb08e469 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 27 Oct 2022 11:05:23 -0600 Subject: [PATCH 047/426] medium desktop buttons --- .../wallet_view/receive/desktop_receive.dart | 2 +- .../wallet_view/send/desktop_send.dart | 2 +- lib/widgets/desktop/primary_button.dart | 44 +++++++++++++------ lib/widgets/desktop/secondary_button.dart | 44 +++++++++++++------ 4 files changed, 62 insertions(+), 30 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/receive/desktop_receive.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/receive/desktop_receive.dart index 79e3b81cf..71f708e35 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/receive/desktop_receive.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/receive/desktop_receive.dart @@ -191,7 +191,7 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> { ), if (coin != Coin.epicCash) SecondaryButton( - height: 56, + desktopMed: true, onPressed: generateNewAddress, label: "Generate new address", ), diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart index b084603b6..116262d23 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart @@ -1173,7 +1173,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { height: 36, ), PrimaryButton( - height: 56, + desktopMed: true, label: "Preview send", enabled: ref.watch(previewTxButtonStateProvider.state).state, onPressed: ref.watch(previewTxButtonStateProvider.state).state diff --git a/lib/widgets/desktop/primary_button.dart b/lib/widgets/desktop/primary_button.dart index 6034cc08b..f3c900c34 100644 --- a/lib/widgets/desktop/primary_button.dart +++ b/lib/widgets/desktop/primary_button.dart @@ -13,6 +13,7 @@ class PrimaryButton extends StatelessWidget { this.icon, this.onPressed, this.enabled = true, + this.desktopMed = false, }) : super(key: key); final double? width; @@ -21,13 +22,40 @@ class PrimaryButton extends StatelessWidget { final VoidCallback? onPressed; final bool enabled; final Widget? icon; + final bool desktopMed; + + TextStyle getStyle(bool isDesktop, BuildContext context) { + if (isDesktop) { + if (desktopMed) { + return STextStyles.desktopTextExtraSmall(context).copyWith( + color: enabled + ? Theme.of(context).extension<StackColors>()!.buttonTextPrimary + : Theme.of(context) + .extension<StackColors>()! + .buttonTextPrimaryDisabled, + ); + } else { + return enabled + ? STextStyles.desktopButtonEnabled(context) + : STextStyles.desktopButtonDisabled(context); + } + } else { + return STextStyles.button(context).copyWith( + color: enabled + ? Theme.of(context).extension<StackColors>()!.buttonTextPrimary + : Theme.of(context) + .extension<StackColors>()! + .buttonTextPrimaryDisabled, + ); + } + } @override Widget build(BuildContext context) { final isDesktop = Util.isDesktop; return CustomTextButtonBase( - height: height, + height: desktopMed ? 56 : height, width: width, textButton: TextButton( onPressed: enabled ? onPressed : null, @@ -49,19 +77,7 @@ class PrimaryButton extends StatelessWidget { if (label != null) Text( label!, - style: isDesktop - ? enabled - ? STextStyles.desktopButtonEnabled(context) - : STextStyles.desktopButtonDisabled(context) - : STextStyles.button(context).copyWith( - color: enabled - ? Theme.of(context) - .extension<StackColors>()! - .buttonTextPrimary - : Theme.of(context) - .extension<StackColors>()! - .buttonTextPrimaryDisabled, - ), + style: getStyle(isDesktop, context), ), ], ), diff --git a/lib/widgets/desktop/secondary_button.dart b/lib/widgets/desktop/secondary_button.dart index 2a88e548d..8d5eae0ce 100644 --- a/lib/widgets/desktop/secondary_button.dart +++ b/lib/widgets/desktop/secondary_button.dart @@ -13,6 +13,7 @@ class SecondaryButton extends StatelessWidget { this.icon, this.onPressed, this.enabled = true, + this.desktopMed = false, }) : super(key: key); final double? width; @@ -21,13 +22,40 @@ class SecondaryButton extends StatelessWidget { final VoidCallback? onPressed; final bool enabled; final Widget? icon; + final bool desktopMed; + + TextStyle getStyle(bool isDesktop, BuildContext context) { + if (isDesktop) { + if (desktopMed) { + return STextStyles.desktopTextExtraSmall(context).copyWith( + color: enabled + ? Theme.of(context).extension<StackColors>()!.buttonTextSecondary + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondaryDisabled, + ); + } else { + return enabled + ? STextStyles.desktopButtonSecondaryEnabled(context) + : STextStyles.desktopButtonSecondaryDisabled(context); + } + } else { + return STextStyles.button(context).copyWith( + color: enabled + ? Theme.of(context).extension<StackColors>()!.buttonTextSecondary + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondaryDisabled, + ); + } + } @override Widget build(BuildContext context) { final isDesktop = Util.isDesktop; return CustomTextButtonBase( - height: height, + height: desktopMed ? 56 : height, width: width, textButton: TextButton( onPressed: enabled ? onPressed : null, @@ -49,19 +77,7 @@ class SecondaryButton extends StatelessWidget { if (label != null) Text( label!, - style: isDesktop - ? enabled - ? STextStyles.desktopButtonSecondaryEnabled(context) - : STextStyles.desktopButtonSecondaryDisabled(context) - : STextStyles.button(context).copyWith( - color: enabled - ? Theme.of(context) - .extension<StackColors>()! - .buttonTextSecondary - : Theme.of(context) - .extension<StackColors>()! - .buttonTextSecondaryDisabled, - ), + style: getStyle(isDesktop, context), ), ], ), From 00e80196398432465dfc7ca0dbe4b1679afeea40 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 27 Oct 2022 11:21:22 -0600 Subject: [PATCH 048/426] desktop wallet balance info style fixes --- .../sub_widgets/wallet_refresh_button.dart | 25 ++- .../wallet_view/desktop_wallet_summary.dart | 200 +++++++++--------- 2 files changed, 119 insertions(+), 106 deletions(-) diff --git a/lib/pages/wallet_view/sub_widgets/wallet_refresh_button.dart b/lib/pages/wallet_view/sub_widgets/wallet_refresh_button.dart index 7faae106f..603b72338 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_refresh_button.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_refresh_button.dart @@ -10,6 +10,7 @@ import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; /// [eventBus] should only be set during testing class WalletRefreshButton extends ConsumerStatefulWidget { @@ -70,7 +71,7 @@ class _RefreshButtonState extends ConsumerState<WalletRefreshButton> _spinController?.stop(); break; case WalletSyncStatus.syncing: - _spinController?.repeat(); + unawaited(_spinController?.repeat()); break; } } @@ -92,10 +93,15 @@ class _RefreshButtonState extends ConsumerState<WalletRefreshButton> @override Widget build(BuildContext context) { + final isDesktop = Util.isDesktop; + return SizedBox( - height: 36, - width: 36, + height: isDesktop ? 22 : 36, + width: isDesktop ? 22 : 36, child: MaterialButton( + color: isDesktop + ? Theme.of(context).extension<StackColors>()!.buttonBackSecondary + : null, splashColor: Theme.of(context).extension<StackColors>()!.highlight, onPressed: () { final managerProvider = ref @@ -110,6 +116,9 @@ class _RefreshButtonState extends ConsumerState<WalletRefreshButton> .then((_) => _spinController?.stop()); } }, + elevation: 0, + highlightElevation: 0, + hoverElevation: 0, padding: EdgeInsets.zero, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, shape: RoundedRectangleBorder( @@ -121,9 +130,13 @@ class _RefreshButtonState extends ConsumerState<WalletRefreshButton> turns: _spinAnimation, child: SvgPicture.asset( Assets.svg.arrowRotate, - width: 24, - height: 24, - color: Theme.of(context).extension<StackColors>()!.textFavoriteCard, + width: isDesktop ? 12 : 24, + height: isDesktop ? 12 : 24, + color: isDesktop + ? Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultSearchIconRight + : Theme.of(context).extension<StackColors>()!.textFavoriteCard, ), ), ), diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_summary.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_summary.dart index fe2cfa7b2..7a9e93467 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_summary.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_summary.dart @@ -1,17 +1,13 @@ 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/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_refresh_button.dart'; import 'package:stackwallet/providers/providers.dart'; -import 'package:stackwallet/providers/wallet/wallet_balance_toggle_state_provider.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; -import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -100,9 +96,9 @@ class _WDesktopWalletSummaryState extends State<DesktopWalletSummary> { final priceTuple = ref.watch(priceAnd24hChangeNotifierProvider .select((value) => value.getPrice(coin))); - final _showAvailable = - ref.watch(walletBalanceToggleStateProvider.state).state == - WalletBalanceToggleState.available; + final _showAvailable = false; + // ref.watch(walletBalanceToggleStateProvider.state).state == + // WalletBalanceToggleState.available; return FutureBuilder( future: _showAvailable @@ -125,46 +121,46 @@ class _WDesktopWalletSummaryState extends State<DesktopWalletSummary> { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - GestureDetector( - onTap: showSheet, - child: Row( - children: [ - if (coin == Coin.firo || - coin == Coin.firoTestNet) - Text( - "${_showAvailable ? "Private" : "Public"} Balance", - style: STextStyles.subtitle500(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textFavoriteCard, - ), - ), - if (coin != Coin.firo && - coin != Coin.firoTestNet) - Text( - "${_showAvailable ? "Available" : "Full"} Balance", - style: STextStyles.subtitle500(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textFavoriteCard, - ), - ), - const SizedBox( - width: 4, - ), - SvgPicture.asset( - Assets.svg.chevronDown, - color: Theme.of(context) - .extension<StackColors>()! - .textFavoriteCard, - width: 8, - height: 4, - ), - ], - ), - ), + // GestureDetector( + // onTap: showSheet, + // child: Row( + // children: [ + // if (coin == Coin.firo || + // coin == Coin.firoTestNet) + // Text( + // "${_showAvailable ? "Private" : "Public"} Balance", + // style: STextStyles.subtitle500(context) + // .copyWith( + // color: Theme.of(context) + // .extension<StackColors>()! + // .textFavoriteCard, + // ), + // ), + // if (coin != Coin.firo && + // coin != Coin.firoTestNet) + // Text( + // "${_showAvailable ? "Available" : "Full"} Balance", + // style: STextStyles.subtitle500(context) + // .copyWith( + // color: Theme.of(context) + // .extension<StackColors>()! + // .textFavoriteCard, + // ), + // ), + // const SizedBox( + // width: 4, + // ), + // SvgPicture.asset( + // Assets.svg.chevronDown, + // color: Theme.of(context) + // .extension<StackColors>()! + // .textFavoriteCard, + // width: 8, + // height: 4, + // ), + // ], + // ), + // ), FittedBox( fit: BoxFit.scaleDown, child: Text( @@ -196,46 +192,46 @@ class _WDesktopWalletSummaryState extends State<DesktopWalletSummary> { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - GestureDetector( - onTap: showSheet, - child: Row( - children: [ - if (coin == Coin.firo || - coin == Coin.firoTestNet) - Text( - "${_showAvailable ? "Private" : "Public"} Balance", - style: STextStyles.subtitle500(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textFavoriteCard, - ), - ), - if (coin != Coin.firo && - coin != Coin.firoTestNet) - Text( - "${_showAvailable ? "Available" : "Full"} Balance", - style: STextStyles.subtitle500(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textFavoriteCard, - ), - ), - const SizedBox( - width: 4, - ), - SvgPicture.asset( - Assets.svg.chevronDown, - width: 8, - height: 4, - color: Theme.of(context) - .extension<StackColors>()! - .textFavoriteCard, - ), - ], - ), - ), + // GestureDetector( + // onTap: showSheet, + // child: Row( + // children: [ + // if (coin == Coin.firo || + // coin == Coin.firoTestNet) + // Text( + // "${_showAvailable ? "Private" : "Public"} Balance", + // style: STextStyles.subtitle500(context) + // .copyWith( + // color: Theme.of(context) + // .extension<StackColors>()! + // .textFavoriteCard, + // ), + // ), + // if (coin != Coin.firo && + // coin != Coin.firoTestNet) + // Text( + // "${_showAvailable ? "Available" : "Full"} Balance", + // style: STextStyles.subtitle500(context) + // .copyWith( + // color: Theme.of(context) + // .extension<StackColors>()! + // .textFavoriteCard, + // ), + // ), + // const SizedBox( + // width: 4, + // ), + // SvgPicture.asset( + // Assets.svg.chevronDown, + // width: 8, + // height: 4, + // color: Theme.of(context) + // .extension<StackColors>()! + // .textFavoriteCard, + // ), + // ], + // ), + // ), AnimatedText( stringsToLoopThrough: const [ "Loading balance ", @@ -250,20 +246,21 @@ class _WDesktopWalletSummaryState extends State<DesktopWalletSummary> { .textFavoriteCard, ), ), - AnimatedText( - stringsToLoopThrough: const [ - "Loading balance ", - "Loading balance. ", - "Loading balance.. ", - "Loading balance..." - ], - style: STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, + if (externalCalls) + AnimatedText( + stringsToLoopThrough: const [ + "Loading balance ", + "Loading balance. ", + "Loading balance.. ", + "Loading balance..." + ], + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), ), - ), ], ); } @@ -273,6 +270,9 @@ class _WDesktopWalletSummaryState extends State<DesktopWalletSummary> { ), ], ), + const SizedBox( + width: 8, + ), WalletRefreshButton( walletId: walletId, initialSyncStatus: widget.initialSyncStatus, From 6ca5b557144984429bdce391c44933daea318a41 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 27 Oct 2022 11:34:46 -0600 Subject: [PATCH 049/426] desktop receive style fixes --- .../wallet_view/receive/desktop_receive.dart | 75 +++++++++++++------ 1 file changed, 54 insertions(+), 21 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/receive/desktop_receive.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/receive/desktop_receive.dart index 71f708e35..46b4cfcfc 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/receive/desktop_receive.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/receive/desktop_receive.dart @@ -16,7 +16,6 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; -import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/custom_loading_overlay.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -150,8 +149,8 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> { children: [ SvgPicture.asset( Assets.svg.copy, - width: 10, - height: 10, + width: 15, + height: 15, color: Theme.of(context) .extension<StackColors>()! .infoItemIcons, @@ -168,14 +167,19 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> { ], ), const SizedBox( - height: 4, + height: 8, ), Row( children: [ Expanded( child: Text( receivingAddress, - style: STextStyles.itemSubtitle12(context), + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), ), ), ], @@ -209,25 +213,54 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> { const SizedBox( height: 32, ), - Center( - child: BlueTextButton( - text: "Create new QR code", - onTap: () async { - unawaited( - Navigator.of(context).push( - RouteGenerator.getRoute( - shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: (_) => GenerateUriQrCodeView( - coin: coin, - receivingAddress: receivingAddress, - ), - settings: const RouteSettings( - name: GenerateUriQrCodeView.routeName, + // TODO: create transparent button class to account for hover + GestureDetector( + onTap: () async { + unawaited( + Navigator.of(context).push( + RouteGenerator.getRoute( + shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, + builder: (_) => GenerateUriQrCodeView( + coin: coin, + receivingAddress: receivingAddress, + ), + settings: const RouteSettings( + name: GenerateUriQrCodeView.routeName, + ), + ), + ), + ); + }, + child: Container( + color: Colors.transparent, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SvgPicture.asset( + Assets.svg.qrcode, + width: 14, + height: 16, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorBlue, + ), + const SizedBox( + width: 8, + ), + Padding( + padding: const EdgeInsets.only(bottom: 2), + child: Text( + "Create new QR code", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorBlue, ), ), ), - ); - }, + ], + ), ), ), ], From 8b136b003c820687acf8d1ba9066379c3e0702d0 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 27 Oct 2022 11:39:06 -0600 Subject: [PATCH 050/426] overflow fix --- .../home/my_stack_view/wallet_view/desktop_wallet_view.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index f86eba750..e411def59 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -225,8 +225,7 @@ class _MyWalletState extends State<MyWallet> { @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, + return ListView( children: [ Text( "My wallet", From 7203f09eb036e7655e93e2aa6758c9a7dabe1b48 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 27 Oct 2022 12:47:30 -0600 Subject: [PATCH 051/426] desktop sync status styling and basic functionality --- .../wallet_view/desktop_wallet_view.dart | 276 ++++++++++++++---- 1 file changed, 223 insertions(+), 53 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index e411def59..2bcb9eeb4 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -1,13 +1,24 @@ +import 'dart:async'; + +import 'package:event_bus/event_bus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_summary.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/receive/desktop_receive.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart'; +import 'package:stackwallet/providers/global/auto_swb_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/providers/ui/transaction_filter_provider.dart'; +import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; +import 'package:stackwallet/services/event_bus/global_event_bus.dart'; +import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/backup_frequency_type.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_buttons/app_bar_icon_button.dart'; @@ -16,16 +27,20 @@ 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'; import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:tuple/tuple.dart'; +/// [eventBus] should only be set during testing class DesktopWalletView extends ConsumerStatefulWidget { const DesktopWalletView({ Key? key, required this.walletId, + this.eventBus, }) : super(key: key); static const String routeName = "/desktopWalletView"; final String walletId; + final EventBus? eventBus; @override ConsumerState<DesktopWalletView> createState() => _DesktopWalletViewState(); @@ -33,15 +48,69 @@ class DesktopWalletView extends ConsumerStatefulWidget { class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { late final String walletId; + late final EventBus eventBus; + + late final bool _shouldDisableAutoSyncOnLogOut; + + final _cnLoadingService = ExchangeDataLoadingService(); Future<void> onBackPressed() async { - // TODO log out and close wallet before popping back - Navigator.of(context).pop(); + await _logout(); + if (mounted) { + Navigator.of(context).pop(); + } + } + + Future<void> _logout() async { + final managerProvider = + ref.read(walletsChangeNotifierProvider).getManagerProvider(walletId); + if (_shouldDisableAutoSyncOnLogOut) { + // disable auto sync if it was enabled only when loading wallet + ref.read(managerProvider).shouldAutoSync = false; + } + ref.read(managerProvider.notifier).isActiveWallet = false; + ref.read(transactionFilterProvider.state).state = null; + if (ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled && + ref.read(prefsChangeNotifierProvider).backupFrequencyType == + BackupFrequencyType.afterClosingAWallet) { + unawaited(ref.read(autoSWBServiceProvider).doBackup()); + } + } + + void _loadCNData() { + // unawaited future + if (ref.read(prefsChangeNotifierProvider).externalCalls) { + _cnLoadingService.loadAll(ref, + coin: ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .coin); + } else { + Logging.instance.log("User does not want to use external calls", + level: LogLevel.Info); + } } @override void initState() { walletId = widget.walletId; + final managerProvider = + ref.read(walletsChangeNotifierProvider).getManagerProvider(walletId); + + eventBus = + widget.eventBus != null ? widget.eventBus! : GlobalEventBus.instance; + + ref.read(managerProvider).isActiveWallet = true; + if (!ref.read(managerProvider).shouldAutoSync) { + // enable auto sync if it wasn't enabled when loading wallet + ref.read(managerProvider).shouldAutoSync = true; + _shouldDisableAutoSyncOnLogOut = true; + } else { + _shouldDisableAutoSyncOnLogOut = false; + } + + ref.read(managerProvider).refresh(); + super.initState(); } @@ -95,13 +164,16 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { ], ), trailing: Row( - children: const [ - NetworkInfoButton(), - SizedBox( + children: [ + NetworkInfoButton( + walletId: walletId, + eventBus: eventBus, + ), + const SizedBox( width: 32, ), - WalletKeysButton(), - SizedBox( + const WalletKeysButton(), + const SizedBox( width: 32, ), ], @@ -132,37 +204,6 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { ? WalletSyncStatus.syncing : WalletSyncStatus.synced, ), - // Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // Row( - // children: [ - // Text( - // "TODO: balance", - // style: STextStyles.desktopH3(context), - // ), - // const SizedBox( - // width: 8, - // ), - // Container( - // color: Colors.red, - // width: 20, - // height: 20, - // ), - // ], - // ), - // Text( - // "todo: fiat balance", - // style: - // STextStyles.desktopTextExtraSmall(context).copyWith( - // color: Theme.of(context) - // .extension<StackColors>()! - // .textSubtitle1, - // ), - // ) - // ], - // ), const Spacer(), SecondaryButton( width: 180, @@ -195,7 +236,7 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { const SizedBox( width: 16, ), - Expanded( + const Expanded( child: RecentDesktopTransactions(), ), ], @@ -284,7 +325,6 @@ class _MyWalletState extends State<MyWallet> { ], ), ), - const Spacer(), ], ); } @@ -461,30 +501,160 @@ class _RecentDesktopTransactionsState extends State<RecentDesktopTransactions> { } } -class NetworkInfoButton extends StatelessWidget { - const NetworkInfoButton({Key? key}) : super(key: key); +class NetworkInfoButton extends ConsumerStatefulWidget { + const NetworkInfoButton({ + Key? key, + required this.walletId, + this.eventBus, + }) : super(key: key); + + final String walletId; + final EventBus? eventBus; + + @override + ConsumerState<NetworkInfoButton> createState() => _NetworkInfoButtonState(); +} + +class _NetworkInfoButtonState extends ConsumerState<NetworkInfoButton> { + late final String walletId; + late final EventBus eventBus; + + late WalletSyncStatus _currentSyncStatus; + late NodeConnectionStatus _currentNodeStatus; + + late StreamSubscription<dynamic> _syncStatusSubscription; + late StreamSubscription<dynamic> _nodeStatusSubscription; + + @override + void initState() { + walletId = widget.walletId; + final managerProvider = + ref.read(walletsChangeNotifierProvider).getManagerProvider(walletId); + + eventBus = + widget.eventBus != null ? widget.eventBus! : GlobalEventBus.instance; + + if (ref.read(managerProvider).isRefreshing) { + _currentSyncStatus = WalletSyncStatus.syncing; + _currentNodeStatus = NodeConnectionStatus.connected; + } else { + _currentSyncStatus = WalletSyncStatus.synced; + if (ref.read(managerProvider).isConnected) { + _currentNodeStatus = NodeConnectionStatus.connected; + } else { + _currentNodeStatus = NodeConnectionStatus.disconnected; + _currentSyncStatus = WalletSyncStatus.unableToSync; + } + } + + _syncStatusSubscription = + eventBus.on<WalletSyncStatusChangedEvent>().listen( + (event) async { + if (event.walletId == widget.walletId) { + setState(() { + _currentSyncStatus = event.newStatus; + }); + } + }, + ); + + _nodeStatusSubscription = + eventBus.on<NodeConnectionStatusChangedEvent>().listen( + (event) async { + if (event.walletId == widget.walletId) { + setState(() { + _currentNodeStatus = event.newStatus; + }); + } + }, + ); + + super.initState(); + } + + @override + void dispose() { + _nodeStatusSubscription.cancel(); + _syncStatusSubscription.cancel(); + super.dispose(); + } + + Widget _buildNetworkIcon(WalletSyncStatus status, BuildContext context) { + const size = 24.0; + switch (status) { + case WalletSyncStatus.unableToSync: + return SvgPicture.asset( + Assets.svg.radioProblem, + color: Theme.of(context).extension<StackColors>()!.accentColorRed, + width: size, + height: size, + ); + case WalletSyncStatus.synced: + return SvgPicture.asset( + Assets.svg.radio, + color: Theme.of(context).extension<StackColors>()!.accentColorGreen, + width: size, + height: size, + ); + case WalletSyncStatus.syncing: + return SvgPicture.asset( + Assets.svg.radioSyncing, + color: Theme.of(context).extension<StackColors>()!.accentColorYellow, + width: size, + height: size, + ); + } + } + + Widget _buildText(WalletSyncStatus status, BuildContext context) { + String label; + Color color; + + switch (status) { + case WalletSyncStatus.unableToSync: + label = "Unable to sync"; + color = Theme.of(context).extension<StackColors>()!.accentColorRed; + break; + case WalletSyncStatus.synced: + label = "Synchronised"; + color = Theme.of(context).extension<StackColors>()!.accentColorGreen; + break; + case WalletSyncStatus.syncing: + label = "Synchronising"; + color = Theme.of(context).extension<StackColors>()!.accentColorYellow; + break; + } + + return Text( + label, + style: STextStyles.desktopMenuItemSelected(context).copyWith( + color: color, + ), + ); + } @override Widget build(BuildContext context) { return GestureDetector( + onTap: () { + Navigator.of(context).pushNamed( + WalletNetworkSettingsView.routeName, + arguments: Tuple3( + walletId, + _currentSyncStatus, + _currentNodeStatus, + ), + ); + }, child: Container( color: Colors.transparent, child: Row( children: [ - SvgPicture.asset( - Assets.svg.network, - width: 24, - height: 24, - color: - Theme.of(context).extension<StackColors>()!.accentColorGreen, - ), + _buildNetworkIcon(_currentSyncStatus, context), const SizedBox( width: 6, ), - Text( - "Synchronised", - style: STextStyles.desktopMenuItemSelected(context), - ) + _buildText(_currentSyncStatus, context), ], ), ), From a92e4e24c0a121c9ccf0014549478283d1250cec Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 27 Oct 2022 13:01:05 -0600 Subject: [PATCH 052/426] desktop in wallet exchange button --- .../wallet_view/desktop_wallet_view.dart | 100 ++++++++++++++++-- 1 file changed, 94 insertions(+), 6 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index 2bcb9eeb4..76650e2c0 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -4,6 +4,8 @@ import 'package:event_bus/event_bus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart'; +import 'package:stackwallet/pages/exchange_view/wallet_initiated_exchange_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_summary.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/receive/desktop_receive.dart'; @@ -14,10 +16,12 @@ import 'package:stackwallet/providers/ui/transaction_filter_provider.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; +import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart'; import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -27,6 +31,7 @@ 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'; import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:tuple/tuple.dart'; /// [eventBus] should only be set during testing @@ -91,6 +96,73 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { } } + void _onExchangePressed(BuildContext context) async { + final managerProvider = + ref.read(walletsChangeNotifierProvider).getManagerProvider(walletId); + unawaited(_cnLoadingService.loadAll(ref)); + + final coin = ref.read(managerProvider).coin; + + if (coin == Coin.epicCash) { + await showDialog<void>( + context: context, + builder: (_) => const StackOkDialog( + title: "Exchange not available for Epic Cash", + ), + ); + } else if (coin.name.endsWith("TestNet")) { + await showDialog<void>( + context: context, + builder: (_) => const StackOkDialog( + title: "Exchange not available for test net coins", + ), + ); + } else { + ref.read(currentExchangeNameStateProvider.state).state = + ChangeNowExchange.exchangeName; + final walletId = ref.read(managerProvider).walletId; + ref.read(prefsChangeNotifierProvider).exchangeRateType = + ExchangeRateType.estimated; + + ref.read(exchangeFormStateProvider).exchange = ref.read(exchangeProvider); + ref.read(exchangeFormStateProvider).exchangeType = + ExchangeRateType.estimated; + + final currencies = ref + .read(availableChangeNowCurrenciesProvider) + .currencies + .where((element) => + element.ticker.toLowerCase() == coin.ticker.toLowerCase()); + + if (currencies.isNotEmpty) { + ref.read(exchangeFormStateProvider).setCurrencies( + currencies.first, + ref + .read(availableChangeNowCurrenciesProvider) + .currencies + .firstWhere( + (element) => + element.ticker.toLowerCase() != + coin.ticker.toLowerCase(), + ), + ); + } + + if (mounted) { + unawaited( + Navigator.of(context).pushNamed( + WalletInitiatedExchangeView.routeName, + arguments: Tuple3( + walletId, + coin, + _loadCNData, + ), + ), + ); + } + } + } + @override void initState() { walletId = widget.walletId; @@ -207,17 +279,33 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { const Spacer(), SecondaryButton( width: 180, - height: 56, + desktopMed: true, onPressed: () { - // todo: go to wallet initiated exchange + _onExchangePressed(context); }, label: "Exchange", icon: Container( - color: Colors.red, - width: 20, - height: 20, + width: 24, + height: 24, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + color: Theme.of(context) + .extension<StackColors>()! + .buttonBackPrimary + .withOpacity(0.2), + ), + child: Center( + child: SvgPicture.asset( + Assets.svg.arrowRotate2, + width: 14, + height: 14, + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, + ), + ), ), - ) + ), ], ), ), From 934cdcc917b032af961e78d49012f3f784221244 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 27 Oct 2022 13:26:55 -0600 Subject: [PATCH 053/426] WIP: desktop transactions list --- .../sub_widgets/transactions_list.dart | 167 ++++++++++-------- .../wallet_view/desktop_wallet_view.dart | 28 ++- lib/widgets/trade_card.dart | 3 +- lib/widgets/transaction_card.dart | 32 +++- 4 files changed, 140 insertions(+), 90 deletions(-) diff --git a/lib/pages/wallet_view/sub_widgets/transactions_list.dart b/lib/pages/wallet_view/sub_widgets/transactions_list.dart index 2246882ab..d23d3082f 100644 --- a/lib/pages/wallet_view/sub_widgets/transactions_list.dart +++ b/lib/pages/wallet_view/sub_widgets/transactions_list.dart @@ -10,6 +10,7 @@ import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/trade_card.dart'; import 'package:stackwallet/widgets/transaction_card.dart'; @@ -67,6 +68,65 @@ class _TransactionsListState extends ConsumerState<TransactionsList> { ); } + Widget itemBuilder( + BuildContext context, Transaction tx, BorderRadius? radius) { + final matchingTrades = ref + .read(tradesServiceProvider) + .trades + .where((e) => e.payInTxid == tx.txid || e.payOutTxid == tx.txid); + if (tx.txType == "Sent" && matchingTrades.isNotEmpty) { + final trade = matchingTrades.first; + return Container( + decoration: BoxDecoration( + color: Theme.of(context).extension<StackColors>()!.popupBG, + borderRadius: radius, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TransactionCard( + // this may mess with combined firo transactions + key: Key(tx.toString()), // + transaction: tx, + walletId: widget.walletId, + ), + TradeCard( + // this may mess with combined firo transactions + key: Key(tx.toString() + trade.uuid), // + trade: trade, + onTap: () { + unawaited( + Navigator.of(context).pushNamed( + TradeDetailsView.routeName, + arguments: Tuple4( + trade.tradeId, + tx, + widget.walletId, + ref.read(managerProvider).walletName, + ), + ), + ); + }, + ) + ], + ), + ); + } else { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).extension<StackColors>()!.popupBG, + borderRadius: radius, + ), + child: TransactionCard( + // this may mess with combined firo transactions + key: Key(tx.toString()), // + transaction: tx, + walletId: widget.walletId, + ), + ); + } + } + @override void initState() { managerProvider = widget.managerProvider; @@ -119,77 +179,42 @@ class _TransactionsListState extends ConsumerState<TransactionsList> { unawaited(ref.read(managerProvider).refresh()); } }, - child: ListView.builder( - itemCount: list.length, - itemBuilder: (context, index) { - BorderRadius? radius; - if (index == list.length - 1) { - radius = _borderRadiusLast; - } else if (index == 0) { - radius = _borderRadiusFirst; - } - final tx = list[index]; - - final matchingTrades = ref - .read(tradesServiceProvider) - .trades - .where((e) => - e.payInTxid == tx.txid || e.payOutTxid == tx.txid); - if (tx.txType == "Sent" && matchingTrades.isNotEmpty) { - final trade = matchingTrades.first; - return Container( - decoration: BoxDecoration( - color: - Theme.of(context).extension<StackColors>()!.popupBG, - borderRadius: radius, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TransactionCard( - // this may mess with combined firo transactions - key: Key(tx.toString()), // - transaction: tx, - walletId: widget.walletId, - ), - TradeCard( - // this may mess with combined firo transactions - key: Key(tx.toString() + trade.uuid), // - trade: trade, - onTap: () { - unawaited( - Navigator.of(context).pushNamed( - TradeDetailsView.routeName, - arguments: Tuple4( - trade.tradeId, - tx, - widget.walletId, - ref.read(managerProvider).walletName, - ), - ), - ); - }, - ) - ], - ), - ); - } else { - return Container( - decoration: BoxDecoration( - color: - Theme.of(context).extension<StackColors>()!.popupBG, - borderRadius: radius, - ), - child: TransactionCard( - // this may mess with combined firo transactions - key: Key(tx.toString()), // - transaction: tx, - walletId: widget.walletId, - ), - ); - } - }, - ), + child: Util.isDesktop + ? ListView.separated( + itemBuilder: (context, index) { + BorderRadius? radius; + if (index == list.length - 1) { + radius = _borderRadiusLast; + } else if (index == 0) { + radius = _borderRadiusFirst; + } + final tx = list[index]; + return itemBuilder(context, tx, radius); + }, + separatorBuilder: (context, index) { + return Container( + width: double.infinity, + height: 2, + color: Theme.of(context) + .extension<StackColors>()! + .background, + ); + }, + itemCount: list.length, + ) + : ListView.builder( + itemCount: list.length, + itemBuilder: (context, index) { + BorderRadius? radius; + if (index == list.length - 1) { + radius = _borderRadiusLast; + } else if (index == 0) { + radius = _borderRadiusFirst; + } + final tx = list[index]; + return itemBuilder(context, tx, radius); + }, + ), ); } }, diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index 76650e2c0..baa91b3f8 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -7,6 +7,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart'; import 'package:stackwallet/pages/exchange_view/wallet_initiated_exchange_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart'; +import 'package:stackwallet/pages/wallet_view/sub_widgets/transactions_list.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_summary.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/receive/desktop_receive.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart'; @@ -324,8 +325,10 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { const SizedBox( width: 16, ), - const Expanded( - child: RecentDesktopTransactions(), + Expanded( + child: RecentDesktopTransactions( + walletId: walletId, + ), ), ], ), @@ -543,15 +546,21 @@ class _SendReceiveTabMenuState extends State<SendReceiveTabMenu> { } } -class RecentDesktopTransactions extends StatefulWidget { - const RecentDesktopTransactions({Key? key}) : super(key: key); +class RecentDesktopTransactions extends ConsumerStatefulWidget { + const RecentDesktopTransactions({ + Key? key, + required this.walletId, + }) : super(key: key); + + final String walletId; @override - State<RecentDesktopTransactions> createState() => + ConsumerState<RecentDesktopTransactions> createState() => _RecentDesktopTransactionsState(); } -class _RecentDesktopTransactionsState extends State<RecentDesktopTransactions> { +class _RecentDesktopTransactionsState + extends ConsumerState<RecentDesktopTransactions> { @override Widget build(BuildContext context) { return Column( @@ -579,9 +588,10 @@ class _RecentDesktopTransactionsState extends State<RecentDesktopTransactions> { height: 16, ), Expanded( - child: RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: Container(), + child: TransactionsList( + managerProvider: ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManagerProvider(widget.walletId))), + walletId: widget.walletId, ), ), ], diff --git a/lib/widgets/trade_card.dart b/lib/widgets/trade_card.dart index ba07b9576..0ac8e9346 100644 --- a/lib/widgets/trade_card.dart +++ b/lib/widgets/trade_card.dart @@ -7,6 +7,7 @@ import 'package:stackwallet/models/exchange/response_objects/trade.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class TradeCard extends ConsumerWidget { @@ -84,7 +85,7 @@ class TradeCard extends ConsumerWidget { style: STextStyles.itemSubtitle12(context), ), Text( - "${Decimal.tryParse(trade.payInAmount) ?? "..."} ${trade.payInCurrency.toUpperCase()}", + "${Util.isDesktop ? "-" : ""}${Decimal.tryParse(trade.payInAmount) ?? "..."} ${trade.payInCurrency.toUpperCase()}", style: STextStyles.itemSubtitle12(context), ), ], diff --git a/lib/widgets/transaction_card.dart b/lib/widgets/transaction_card.dart index cb737ae08..de0447684 100644 --- a/lib/widgets/transaction_card.dart +++ b/lib/widgets/transaction_card.dart @@ -13,6 +13,7 @@ import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; import 'package:tuple/tuple.dart'; class TransactionCard extends ConsumerStatefulWidget { @@ -100,6 +101,17 @@ class _TransactionCardState extends ConsumerState<TransactionCard> { .select((value) => value.getPrice(coin))) .item1; + late final String prefix; + if (Util.isDesktop) { + if (_transaction.txType == "Sent") { + prefix = "-"; + } else if (_transaction.txType == "Received") { + prefix = "+"; + } + } else { + prefix = ""; + } + return Material( color: Theme.of(context).extension<StackColors>()!.popupBG, elevation: 0, @@ -126,14 +138,16 @@ class _TransactionCardState extends ConsumerState<TransactionCard> { )); return; } - unawaited(Navigator.of(context).pushNamed( - TransactionDetailsView.routeName, - arguments: Tuple3( - _transaction, - coin, - walletId, + unawaited( + Navigator.of(context).pushNamed( + TransactionDetailsView.routeName, + arguments: Tuple3( + _transaction, + coin, + walletId, + ), ), - )); + ); }, child: Padding( padding: const EdgeInsets.all(8), @@ -176,7 +190,7 @@ class _TransactionCardState extends ConsumerState<TransactionCard> { ? (_transaction.amount ~/ 1000) : _transaction.amount; return Text( - "${Format.satoshiAmountToPrettyString(amount, locale)} ${coin.ticker}", + "$prefix${Format.satoshiAmountToPrettyString(amount, locale)} ${coin.ticker}", style: STextStyles.itemSubtitle12_600(context), ); @@ -223,7 +237,7 @@ class _TransactionCardState extends ConsumerState<TransactionCard> { } return Text( - "${Format.localizedStringAsFixed( + "$prefix${Format.localizedStringAsFixed( value: Format.satoshisToAmount(value) * price, locale: locale, From be81625d2b5ecf1d7c2c8d93681c49538a7dc74c Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 27 Oct 2022 14:23:55 -0600 Subject: [PATCH 054/426] WIP: desktop transactions filter --- .../all_transactions_view.dart | 297 +++++++++++------- .../wallet_view/desktop_wallet_view.dart | 6 +- 2 files changed, 196 insertions(+), 107 deletions(-) diff --git a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart index 4194a7307..e3d7bf0b6 100644 --- a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart +++ b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart @@ -12,7 +12,11 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.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'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -21,8 +25,6 @@ import 'package:stackwallet/widgets/textfield_icon_button.dart'; import 'package:stackwallet/widgets/transaction_card.dart'; import 'package:tuple/tuple.dart'; -import 'package:stackwallet/utilities/util.dart'; - class AllTransactionsView extends ConsumerStatefulWidget { const AllTransactionsView({ Key? key, @@ -166,123 +168,206 @@ class _TransactionDetailsViewState extends ConsumerState<AllTransactionsView> { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "Transactions", - style: STextStyles.navBarTitle(context), - ), - actions: [ - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 20, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("transactionSearchFilterViewButton"), - size: 36, - shadows: const [], - color: Theme.of(context).extension<StackColors>()!.background, - icon: SvgPicture.asset( - Assets.svg.filter, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - width: 20, - height: 20, - ), - onPressed: () { - Navigator.of(context).pushNamed( - TransactionSearchFilterView.routeName, - arguments: ref - .read(walletsChangeNotifierProvider) - .getManager(walletId) - .coin, - ); + final isDesktop = Util.isDesktop; + + return MasterScaffold( + background: Theme.of(context).extension<StackColors>()!.background, + isDesktop: isDesktop, + appBar: isDesktop + ? DesktopAppBar( + isCompactHeight: true, + background: Theme.of(context).extension<StackColors>()!.popupBG, + leading: Row( + children: [ + const SizedBox( + width: 32, + ), + AppBarIconButton( + size: 32, + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + shadows: const [], + icon: SvgPicture.asset( + Assets.svg.arrowLeft, + width: 18, + height: 18, + color: Theme.of(context) + .extension<StackColors>()! + .topNavIconPrimary, + ), + onPressed: Navigator.of(context).pop, + ), + const SizedBox( + width: 12, + ), + Text( + "Transactions", + style: STextStyles.desktopH3(context), + ), + ], + ), + ) + : AppBar( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } }, ), + title: Text( + "Transactions", + style: STextStyles.navBarTitle(context), + ), + actions: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 20, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("transactionSearchFilterViewButton"), + size: 36, + shadows: const [], + color: Theme.of(context) + .extension<StackColors>()! + .background, + icon: SvgPicture.asset( + Assets.svg.filter, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + width: 20, + height: 20, + ), + onPressed: () { + Navigator.of(context).pushNamed( + TransactionSearchFilterView.routeName, + arguments: ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .coin, + ); + }, + ), + ), + ), + ], ), - ), - ], - ), body: Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, + padding: EdgeInsets.only( + left: isDesktop ? 20 : 12, + top: isDesktop ? 20 : 12, + right: isDesktop ? 20 : 12, ), child: Column( children: [ Padding( padding: const EdgeInsets.all(4), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: _searchController, - focusNode: searchFieldFocusNode, - onChanged: (value) { - setState(() { - _searchString = value; - }); - }, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - searchFieldFocusNode, - context, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, + child: Row( + children: [ + SizedBox( + width: isDesktop ? 570 : null, + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, + child: TextField( + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: _searchController, + focusNode: searchFieldFocusNode, + onChanged: (value) { + setState(() { + _searchString = value; + }); + }, + style: isDesktop + ? STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveText, + height: 1.8, + ) + : STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + searchFieldFocusNode, + context, + desktopMed: true, + ).copyWith( + prefixIcon: Padding( + padding: EdgeInsets.symmetric( + horizontal: isDesktop ? 12 : 10, + vertical: isDesktop ? 18 : 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: isDesktop ? 20 : 16, + height: isDesktop ? 20 : 16, + ), + ), + suffixIcon: _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), ), ), - suffixIcon: _searchController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchController.text = ""; - }); - }, - ), - ], - ), - ), - ) - : null, ), - ), + const SizedBox( + width: 20, + ), + SecondaryButton( + desktopMed: true, + width: 200, + label: "Filter", + icon: SvgPicture.asset( + Assets.svg.filter, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + width: 20, + height: 20, + ), + onPressed: () { + Navigator.of(context).pushNamed( + TransactionSearchFilterView.routeName, + arguments: ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .coin, + ); + }, + ), + ], ), ), const SizedBox( diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index baa91b3f8..9d0d9ed64 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -8,6 +8,7 @@ import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet. import 'package:stackwallet/pages/exchange_view/wallet_initiated_exchange_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/transactions_list.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_summary.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/receive/desktop_receive.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart'; @@ -579,7 +580,10 @@ class _RecentDesktopTransactionsState BlueTextButton( text: "See all", onTap: () { - // todo: show all txns + Navigator.of(context).pushNamed( + AllTransactionsView.routeName, + arguments: widget.walletId, + ); }, ), ], From ec7840419fa727f45fb98d0b63dfb4d5fc70b59d Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 27 Oct 2022 15:23:31 -0600 Subject: [PATCH 055/426] desktop dialog mod to allow setting max size --- lib/widgets/desktop/desktop_dialog.dart | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/widgets/desktop/desktop_dialog.dart b/lib/widgets/desktop/desktop_dialog.dart index 5ada3a545..d11124ba6 100644 --- a/lib/widgets/desktop/desktop_dialog.dart +++ b/lib/widgets/desktop/desktop_dialog.dart @@ -2,9 +2,16 @@ import 'package:flutter/material.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; class DesktopDialog extends StatelessWidget { - const DesktopDialog({Key? key, this.child}) : super(key: key); + const DesktopDialog({ + Key? key, + this.child, + this.maxWidth = 641, + this.maxHeight = 474, + }) : super(key: key); final Widget? child; + final double maxWidth; + final double maxHeight; @override Widget build(BuildContext context) { @@ -13,9 +20,9 @@ class DesktopDialog extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 641, - maxHeight: 474, + constraints: BoxConstraints( + maxWidth: maxWidth, + maxHeight: maxHeight, ), child: Material( borderRadius: BorderRadius.circular( From cc0770b2a27fd9e1250c05652f6bf792ecf68464 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 27 Oct 2022 16:04:13 -0600 Subject: [PATCH 056/426] desktop tx search filter + various bugfixes --- .../all_transactions_view.dart | 60 +- .../transaction_search_filter_view.dart | 1228 ++++++++++------- .../wallet_view/desktop_wallet_view.dart | 1 + lib/utilities/text_styles.dart | 19 + 4 files changed, 772 insertions(+), 536 deletions(-) diff --git a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart index e3d7bf0b6..7122f5379 100644 --- a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart +++ b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart @@ -306,7 +306,7 @@ class _TransactionDetailsViewState extends ConsumerState<AllTransactionsView> { "Search", searchFieldFocusNode, context, - desktopMed: true, + desktopMed: isDesktop, ).copyWith( prefixIcon: Padding( padding: EdgeInsets.symmetric( @@ -330,6 +330,7 @@ class _TransactionDetailsViewState extends ConsumerState<AllTransactionsView> { onTap: () async { setState(() { _searchController.text = ""; + _searchString = ""; }); }, ), @@ -342,31 +343,45 @@ class _TransactionDetailsViewState extends ConsumerState<AllTransactionsView> { ), ), ), - const SizedBox( - width: 20, - ), - SecondaryButton( - desktopMed: true, - width: 200, - label: "Filter", - icon: SvgPicture.asset( - Assets.svg.filter, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, + if (isDesktop) + const SizedBox( width: 20, - height: 20, ), - onPressed: () { - Navigator.of(context).pushNamed( - TransactionSearchFilterView.routeName, - arguments: ref + if (isDesktop) + SecondaryButton( + desktopMed: isDesktop, + width: 200, + label: "Filter", + icon: SvgPicture.asset( + Assets.svg.filter, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + width: 20, + height: 20, + ), + onPressed: () { + final coin = ref .read(walletsChangeNotifierProvider) .getManager(walletId) - .coin, - ); - }, - ), + .coin; + if (isDesktop) { + showDialog<void>( + context: context, + builder: (context) { + return TransactionSearchFilterView( + coin: coin, + ); + }, + ); + } else { + Navigator.of(context).pushNamed( + TransactionSearchFilterView.routeName, + arguments: coin, + ); + } + }, + ), ], ), ), @@ -402,6 +417,7 @@ class _TransactionDetailsViewState extends ConsumerState<AllTransactionsView> { final monthlyList = groupTransactionsByMonth(searched); return ListView.builder( + primary: isDesktop ? false : null, itemCount: monthlyList.length, itemBuilder: (_, index) { final month = monthlyList[index]; diff --git a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart index f9932c672..7d18d4428 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart @@ -16,6 +16,10 @@ import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.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/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; @@ -43,6 +47,7 @@ class _TransactionSearchViewState bool _isActiveReceivedCheckbox = false; bool _isActiveSentCheckbox = false; + bool _isActiveTradeCheckbox = false; String _fromDateString = ""; String _toDateString = ""; @@ -63,6 +68,9 @@ class _TransactionSearchViewState _selectedFromDate = filterState.from; _keywordTextEditingController.text = filterState.keyword; + _fromDateString = Format.formatDate(_selectedFromDate); + _toDateString = Format.formatDate(_selectedToDate); + // TODO: Fix XMR (modify Format.funcs to take optional Coin parameter) // final amt = Format.satoshisToAmount(widget.coin == Coin.monero ? ) String amount = ""; @@ -166,104 +174,113 @@ class _TransactionSearchViewState Widget _buildDateRangePicker() { const middleSeparatorPadding = 2.0; const middleSeparatorWidth = 12.0; - final width = (MediaQuery.of(context).size.width - - (middleSeparatorWidth + - (2 * middleSeparatorPadding) + - (2 * Constants.size.standardPadding))) / - 2; + final isDesktop = Util.isDesktop; + + final width = isDesktop + ? null + : (MediaQuery.of(context).size.width - + (middleSeparatorWidth + + (2 * middleSeparatorPadding) + + (2 * Constants.size.standardPadding))) / + 2; return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - GestureDetector( - key: const Key("transactionSearchViewFromDatePickerKey"), - onTap: () async { - final color = - Theme.of(context).extension<StackColors>()!.accentColorDark; - final height = MediaQuery.of(context).size.height; - // check and hide keyboard - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 125)); - } - - final date = await showRoundedDatePicker( - // This doesn't change statusbar color... - // background: CFColors.starryNight.withOpacity(0.8), - context: context, - initialDate: DateTime.now(), - height: height * 0.5, - theme: ThemeData( - primarySwatch: Util.createMaterialColor( - color, - ), - ), - //TODO pick a better initial date - // 2007 chosen as that is just before bitcoin launched - firstDate: DateTime(2007), - lastDate: DateTime.now(), - borderRadius: Constants.size.circularBorderRadius * 2, - - textPositiveButton: "SELECT", - - styleDatePicker: _buildDatePickerStyle(), - styleYearPicker: _buildYearPickerStyle(), - ); - if (date != null) { - _selectedFromDate = date; - - // flag to adjust date so from date is always before to date - final flag = !_selectedFromDate.isBefore(_selectedToDate); - if (flag) { - _selectedToDate = DateTime.fromMillisecondsSinceEpoch( - _selectedFromDate.millisecondsSinceEpoch); + Expanded( + child: GestureDetector( + key: const Key("transactionSearchViewFromDatePickerKey"), + onTap: () async { + final color = + Theme.of(context).extension<StackColors>()!.accentColorDark; + final height = MediaQuery.of(context).size.height; + // check and hide keyboard + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 125)); } - setState(() { + final date = await showRoundedDatePicker( + // This doesn't change statusbar color... + // background: CFColors.starryNight.withOpacity(0.8), + context: context, + initialDate: DateTime.now(), + height: height * 0.5, + theme: ThemeData( + primarySwatch: Util.createMaterialColor( + color, + ), + ), + //TODO pick a better initial date + // 2007 chosen as that is just before bitcoin launched + firstDate: DateTime(2007), + lastDate: DateTime.now(), + borderRadius: Constants.size.circularBorderRadius * 2, + + textPositiveButton: "SELECT", + + styleDatePicker: _buildDatePickerStyle(), + styleYearPicker: _buildYearPickerStyle(), + ); + if (date != null) { + _selectedFromDate = date; + + // flag to adjust date so from date is always before to date + final flag = !_selectedFromDate.isBefore(_selectedToDate); if (flag) { - _toDateString = Format.formatDate(_selectedToDate); + _selectedToDate = DateTime.fromMillisecondsSinceEpoch( + _selectedFromDate.millisecondsSinceEpoch); } - _fromDateString = Format.formatDate(_selectedFromDate); - }); - } - }, - child: Container( - width: width, - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - borderRadius: - BorderRadius.circular(Constants.size.circularBorderRadius), - border: Border.all( + + setState(() { + if (flag) { + _toDateString = Format.formatDate(_selectedToDate); + } + _fromDateString = Format.formatDate(_selectedFromDate); + }); + } + }, + child: Container( + width: width, + decoration: BoxDecoration( color: Theme.of(context) .extension<StackColors>()! .textFieldDefaultBG, - width: 1, + borderRadius: + BorderRadius.circular(Constants.size.circularBorderRadius), + border: Border.all( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + width: 1, + ), ), - ), - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.calendar, - height: 20, - width: 20, - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle2, - ), - const SizedBox( - width: 10, - ), - Align( - alignment: Alignment.centerLeft, - child: FittedBox( - child: _dateFromText, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 12, + vertical: isDesktop ? 17 : 12, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.calendar, + height: 20, + width: 20, + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle2, ), - ) - ], + const SizedBox( + width: 10, + ), + Align( + alignment: Alignment.centerLeft, + child: FittedBox( + child: _dateFromText, + ), + ) + ], + ), ), ), ), @@ -277,470 +294,653 @@ class _TransactionSearchViewState // color: CFColors.smoke, ), ), - GestureDetector( - key: const Key("transactionSearchViewToDatePickerKey"), - onTap: () async { - final color = - Theme.of(context).extension<StackColors>()!.accentColorDark; - final height = MediaQuery.of(context).size.height; - // check and hide keyboard - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 125)); - } - - final date = await showRoundedDatePicker( - // This doesn't change statusbar color... - // background: CFColors.starryNight.withOpacity(0.8), - context: context, - height: height * 0.5, - theme: ThemeData( - primarySwatch: Util.createMaterialColor( - color, - ), - ), - //TODO pick a better initial date - // 2007 chosen as that is just before bitcoin launched - initialDate: DateTime.now(), - firstDate: DateTime(2007), - lastDate: DateTime.now(), - borderRadius: Constants.size.circularBorderRadius * 2, - - textPositiveButton: "SELECT", - - styleDatePicker: _buildDatePickerStyle(), - styleYearPicker: _buildYearPickerStyle(), - ); - if (date != null) { - _selectedToDate = date; - - // flag to adjust date so from date is always before to date - final flag = !_selectedToDate.isAfter(_selectedFromDate); - if (flag) { - _selectedFromDate = DateTime.fromMillisecondsSinceEpoch( - _selectedToDate.millisecondsSinceEpoch); + Expanded( + child: GestureDetector( + key: const Key("transactionSearchViewToDatePickerKey"), + onTap: () async { + final color = + Theme.of(context).extension<StackColors>()!.accentColorDark; + final height = MediaQuery.of(context).size.height; + // check and hide keyboard + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 125)); } - setState(() { + final date = await showRoundedDatePicker( + // This doesn't change statusbar color... + // background: CFColors.starryNight.withOpacity(0.8), + context: context, + height: height * 0.5, + theme: ThemeData( + primarySwatch: Util.createMaterialColor( + color, + ), + ), + //TODO pick a better initial date + // 2007 chosen as that is just before bitcoin launched + initialDate: DateTime.now(), + firstDate: DateTime(2007), + lastDate: DateTime.now(), + borderRadius: Constants.size.circularBorderRadius * 2, + + textPositiveButton: "SELECT", + + styleDatePicker: _buildDatePickerStyle(), + styleYearPicker: _buildYearPickerStyle(), + ); + if (date != null) { + _selectedToDate = date; + + // flag to adjust date so from date is always before to date + final flag = !_selectedToDate.isAfter(_selectedFromDate); if (flag) { - _fromDateString = Format.formatDate(_selectedFromDate); + _selectedFromDate = DateTime.fromMillisecondsSinceEpoch( + _selectedToDate.millisecondsSinceEpoch); } - _toDateString = Format.formatDate(_selectedToDate); - }); - } - }, - child: Container( - width: width, - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - borderRadius: - BorderRadius.circular(Constants.size.circularBorderRadius), - border: Border.all( + + setState(() { + if (flag) { + _fromDateString = Format.formatDate(_selectedFromDate); + } + _toDateString = Format.formatDate(_selectedToDate); + }); + } + }, + child: Container( + width: width, + decoration: BoxDecoration( color: Theme.of(context) .extension<StackColors>()! .textFieldDefaultBG, - width: 1, + borderRadius: + BorderRadius.circular(Constants.size.circularBorderRadius), + border: Border.all( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + width: 1, + ), ), - ), - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.calendar, - height: 20, - width: 20, - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle2, - ), - const SizedBox( - width: 10, - ), - Align( - alignment: Alignment.centerLeft, - child: FittedBox( - child: _dateToText, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 12, + vertical: isDesktop ? 17 : 12, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.calendar, + height: 20, + width: 20, + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle2, ), - ) - ], + const SizedBox( + width: 10, + ), + Align( + alignment: Alignment.centerLeft, + child: FittedBox( + child: _dateToText, + ), + ) + ], + ), ), ), ), ), + if (isDesktop) + const SizedBox( + width: 24, + ), ], ); } @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( + if (Util.isDesktop) { + return DesktopDialog( + maxWidth: 576, + maxHeight: double.infinity, + child: Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 32, + ), + child: _buildContent(context), + ), + ); + } else { + return Scaffold( backgroundColor: Theme.of(context).extension<StackColors>()!.background, - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Transactions filter", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Transactions filter", - style: STextStyles.navBarTitle(context), + body: Padding( + padding: EdgeInsets.symmetric( + horizontal: Constants.size.standardPadding, + ), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: constraints.maxHeight), + child: IntrinsicHeight( + child: _buildContent(context), + ), + ), + ); + }, + ), ), - ), - body: Padding( - padding: EdgeInsets.symmetric( - horizontal: Constants.size.standardPadding, + ); + } + } + + Widget _buildContent(BuildContext context) { + final isDesktop = Util.isDesktop; + + return Column( + children: [ + if (isDesktop) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transaction filter", + style: STextStyles.desktopH3(context), + textAlign: TextAlign.center, + ), + const DesktopDialogCloseButton(), + ], + ), + SizedBox( + height: isDesktop ? 14 : 10, ), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints(minHeight: constraints.maxHeight), - child: IntrinsicHeight( - child: Column( - children: [ - const SizedBox( - height: 10, - ), - Align( - alignment: Alignment.centerLeft, - child: FittedBox( - child: Text( - "Transactions", - style: STextStyles.smallMed12(context), - ), - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - GestureDetector( - onTap: () { - setState(() { - _isActiveSentCheckbox = - !_isActiveSentCheckbox; - }); - }, - child: Container( - color: Colors.transparent, - child: Row( - children: [ - SizedBox( - height: 20, - width: 20, - child: Checkbox( - key: const Key( - "transactionSearchViewSentCheckboxKey"), - materialTapTargetSize: - MaterialTapTargetSize - .shrinkWrap, - value: _isActiveSentCheckbox, - onChanged: (newValue) { - setState(() { - _isActiveSentCheckbox = - newValue!; - }); - }, - ), - ), - const SizedBox( - width: 14, - ), - Align( - alignment: Alignment.centerLeft, - child: FittedBox( - child: Text( - "Sent", - style: STextStyles.itemSubtitle12( - context), - ), - ), - ) - ], - ), - ), - ), - ], - ), - const SizedBox( - height: 10, - ), - Row( - children: [ - GestureDetector( - onTap: () { - setState(() { - _isActiveReceivedCheckbox = - !_isActiveReceivedCheckbox; - }); - }, - child: Container( - color: Colors.transparent, - child: Row( - children: [ - SizedBox( - height: 20, - width: 20, - child: Checkbox( - key: const Key( - "transactionSearchViewReceivedCheckboxKey"), - materialTapTargetSize: - MaterialTapTargetSize - .shrinkWrap, - value: _isActiveReceivedCheckbox, - onChanged: (newValue) { - setState(() { - _isActiveReceivedCheckbox = - newValue!; - }); - }, - ), - ), - const SizedBox( - width: 14, - ), - Align( - alignment: Alignment.centerLeft, - child: FittedBox( - child: Text( - "Received", - style: STextStyles.itemSubtitle12( - context), - ), - ), - ) - ], - ), - ), - ), - ], - ), - ], - ), - ), - const SizedBox( - height: 24, - ), - Align( - alignment: Alignment.centerLeft, - child: FittedBox( - child: Text( - "Date", - style: STextStyles.smallMed12(context), - ), - ), - ), - const SizedBox( - height: 8, - ), - _buildDateRangePicker(), - const SizedBox( - height: 24, - ), - Align( - alignment: Alignment.centerLeft, - child: FittedBox( - child: Text( - "Amount", - style: STextStyles.smallMed12(context), - ), - ), - ), - const SizedBox( - height: 8, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - key: const Key("transactionSearchViewAmountFieldKey"), - controller: _amountTextEditingController, - focusNode: amountTextFieldFocusNode, - onChanged: (_) => setState(() {}), - keyboardType: const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), - inputFormatters: [ - // regex to validate a crypto amount with 8 decimal places - TextInputFormatter.withFunction((oldValue, - newValue) => - RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') - .hasMatch(newValue.text) - ? newValue - : oldValue), - ], - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter ${widget.coin.ticker} amount...", - keywordTextFieldFocusNode, - context, - ).copyWith( - suffixIcon: _amountTextEditingController - .text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _amountTextEditingController - .text = ""; - }); - }, - ), - ], - ), - ), - ) - : null, - ), - ), - ), - const SizedBox( - height: 24, - ), - Align( - alignment: Alignment.centerLeft, - child: FittedBox( - child: Text( - "Keyword", - style: STextStyles.smallMed12(context), - ), - ), - ), - const SizedBox( - height: 8, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - key: - const Key("transactionSearchViewKeywordFieldKey"), - controller: _keywordTextEditingController, - focusNode: keywordTextFieldFocusNode, - style: STextStyles.field(context), - onChanged: (_) => setState(() {}), - decoration: standardInputDecoration( - "Type keyword...", - keywordTextFieldFocusNode, - context, - ).copyWith( - suffixIcon: _keywordTextEditingController - .text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _keywordTextEditingController - .text = ""; - }); - }, - ), - ], - ), - ), - ) - : null, - ), - ), - ), - const Spacer(), - const SizedBox( - height: 20, - ), - Row( + if (!isDesktop) + Align( + alignment: Alignment.centerLeft, + child: FittedBox( + child: Text( + "Transactions", + style: STextStyles.smallMed12(context), + ), + ), + ), + if (!isDesktop) + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: EdgeInsets.all(isDesktop ? 0 : 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + GestureDetector( + onTap: () { + setState(() { + _isActiveSentCheckbox = !_isActiveSentCheckbox; + }); + }, + child: Container( + color: Colors.transparent, + child: Row( children: [ - Expanded( - child: SizedBox( - height: 48, - child: TextButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - ), - ), + SizedBox( + height: 20, + width: 20, + child: Checkbox( + key: const Key( + "transactionSearchViewSentCheckboxKey"), + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + value: _isActiveSentCheckbox, + onChanged: (newValue) { + setState(() { + _isActiveSentCheckbox = newValue!; + }); + }, ), ), const SizedBox( - width: 16, + width: 14, ), - Expanded( - child: SizedBox( - height: 48, - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () async { - _onApplyPressed(); - }, - child: Text( - "Save", - style: STextStyles.button(context), - ), + Align( + alignment: Alignment.centerLeft, + child: FittedBox( + child: Column( + children: [ + Text( + "Sent", + style: isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.itemSubtitle12(context), + ), + if (isDesktop) + const SizedBox( + height: 4, + ), + ], ), ), - ), + ) ], ), - const SizedBox( - height: 20, - ), - ], + ), ), - ), + ], ), - ); - }, + SizedBox( + height: isDesktop ? 4 : 10, + ), + Row( + children: [ + GestureDetector( + onTap: () { + setState(() { + _isActiveReceivedCheckbox = !_isActiveReceivedCheckbox; + }); + }, + child: Container( + color: Colors.transparent, + child: Row( + children: [ + SizedBox( + height: 20, + width: 20, + child: Checkbox( + key: const Key( + "transactionSearchViewReceivedCheckboxKey"), + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + value: _isActiveReceivedCheckbox, + onChanged: (newValue) { + setState(() { + _isActiveReceivedCheckbox = newValue!; + }); + }, + ), + ), + const SizedBox( + width: 14, + ), + Align( + alignment: Alignment.centerLeft, + child: FittedBox( + child: Column( + children: [ + Text( + "Received", + style: isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.itemSubtitle12(context), + ), + if (isDesktop) + const SizedBox( + height: 4, + ), + ], + ), + ), + ) + ], + ), + ), + ), + ], + ), + SizedBox( + height: isDesktop ? 4 : 10, + ), + Row( + children: [ + GestureDetector( + onTap: () { + setState(() { + _isActiveTradeCheckbox = !_isActiveTradeCheckbox; + }); + }, + child: Container( + color: Colors.transparent, + child: Row( + children: [ + SizedBox( + height: 20, + width: 20, + child: Checkbox( + key: const Key( + "transactionSearchViewSentCheckboxKey"), + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + value: _isActiveTradeCheckbox, + onChanged: (newValue) { + setState(() { + _isActiveTradeCheckbox = newValue!; + }); + }, + ), + ), + const SizedBox( + width: 14, + ), + Align( + alignment: Alignment.centerLeft, + child: FittedBox( + child: Column( + children: [ + Text( + "Trades", + style: isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.itemSubtitle12(context), + ), + if (isDesktop) + const SizedBox( + height: 4, + ), + ], + ), + ), + ) + ], + ), + ), + ), + ], + ), + ], + ), ), - ), + SizedBox( + height: isDesktop ? 32 : 24, + ), + Align( + alignment: Alignment.centerLeft, + child: FittedBox( + child: Text( + "Date", + style: isDesktop + ? STextStyles.labelExtraExtraSmall(context) + : STextStyles.smallMed12(context), + ), + ), + ), + SizedBox( + height: isDesktop ? 10 : 8, + ), + _buildDateRangePicker(), + SizedBox( + height: isDesktop ? 32 : 24, + ), + Align( + alignment: Alignment.centerLeft, + child: FittedBox( + child: Text( + "Amount", + style: isDesktop + ? STextStyles.labelExtraExtraSmall(context) + : STextStyles.smallMed12(context), + ), + ), + ), + SizedBox( + height: isDesktop ? 10 : 8, + ), + Padding( + padding: EdgeInsets.only(right: isDesktop ? 32 : 0), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + key: const Key("transactionSearchViewAmountFieldKey"), + controller: _amountTextEditingController, + focusNode: amountTextFieldFocusNode, + onChanged: (_) => setState(() {}), + keyboardType: const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), + inputFormatters: [ + // regex to validate a crypto amount with 8 decimal places + TextInputFormatter.withFunction((oldValue, newValue) => + RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') + .hasMatch(newValue.text) + ? newValue + : oldValue), + ], + style: isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of(context).extension<StackColors>()!.textDark, + height: 1.8, + ) + : STextStyles.field(context), + decoration: standardInputDecoration( + "Enter ${widget.coin.ticker} amount...", + keywordTextFieldFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + contentPadding: isDesktop + ? const EdgeInsets.symmetric( + vertical: 10, + horizontal: 16, + ) + : null, + suffixIcon: _amountTextEditingController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _amountTextEditingController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + ), + SizedBox( + height: isDesktop ? 32 : 24, + ), + Align( + alignment: Alignment.centerLeft, + child: FittedBox( + child: Text( + "Keyword", + style: isDesktop + ? STextStyles.labelExtraExtraSmall(context) + : STextStyles.smallMed12(context), + ), + ), + ), + SizedBox( + height: isDesktop ? 10 : 8, + ), + Padding( + padding: EdgeInsets.only(right: isDesktop ? 32 : 0), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + key: const Key("transactionSearchViewKeywordFieldKey"), + controller: _keywordTextEditingController, + focusNode: keywordTextFieldFocusNode, + style: isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of(context).extension<StackColors>()!.textDark, + height: 1.8, + ) + : STextStyles.field(context), + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Type keyword...", + keywordTextFieldFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + contentPadding: isDesktop + ? const EdgeInsets.symmetric( + vertical: 10, + horizontal: 16, + ) + : null, + suffixIcon: _keywordTextEditingController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _keywordTextEditingController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + ), + if (!isDesktop) const Spacer(), + SizedBox( + height: isDesktop ? 32 : 20, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + desktopMed: isDesktop, + onPressed: () async { + if (!isDesktop) { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration( + milliseconds: 75, + ), + ); + } + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + ), + // Expanded( + // child: SizedBox( + // height: 48, + // child: TextButton( + // onPressed: () async { + // if (FocusScope.of(context).hasFocus) { + // FocusScope.of(context).unfocus(); + // await Future<void>.delayed( + // const Duration(milliseconds: 75)); + // } + // if (mounted) { + // Navigator.of(context).pop(); + // } + // }, + // style: Theme.of(context) + // .extension<StackColors>()! + // .getSecondaryEnabledButtonColor(context), + // child: Text( + // "Cancel", + // style: STextStyles.button(context).copyWith( + // color: Theme.of(context) + // .extension<StackColors>()! + // .accentColorDark), + // ), + // ), + // ), + // ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + desktopMed: isDesktop, + onPressed: () async { + await _onApplyPressed(); + }, + label: "Save", + ), + ), + // Expanded( + // child: SizedBox( + // height: 48, + // child: TextButton( + // style: Theme.of(context) + // .extension<StackColors>()! + // .getPrimaryEnabledButtonColor(context), + // onPressed: () async { + // await _onApplyPressed(); + // }, + // child: Text( + // "Save", + // style: STextStyles.button(context), + // ), + // ), + // ), + // ), + if (isDesktop) + const SizedBox( + width: 32, + ), + ], + ), + if (!isDesktop) + const SizedBox( + height: 20, + ), + ], ); } diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index 9d0d9ed64..35b63174c 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -359,6 +359,7 @@ class _MyWalletState extends State<MyWallet> { @override Widget build(BuildContext context) { return ListView( + primary: false, children: [ Text( "My wallet", diff --git a/lib/utilities/text_styles.dart b/lib/utilities/text_styles.dart index 82e2ad349..b583dba98 100644 --- a/lib/utilities/text_styles.dart +++ b/lib/utilities/text_styles.dart @@ -228,6 +228,25 @@ class STextStyles { } } + static TextStyle labelExtraExtraSmall(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textFieldActiveSearchIconRight, + fontWeight: FontWeight.w500, + fontSize: 14, + height: 14 / 14, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textFieldActiveSearchIconRight, + fontWeight: FontWeight.w500, + fontSize: 14, + height: 14 / 14, + ); + } + } + static TextStyle label700(BuildContext context) { switch (_theme(context).themeType) { case ThemeType.light: From 4c4df1b618f03584c1101e3e7816d4d4fea262a4 Mon Sep 17 00:00:00 2001 From: Marco <marco@cypherstack.com> Date: Thu, 27 Oct 2022 17:24:14 -0600 Subject: [PATCH 057/426] before changes for litecoin --- .../coins/litecoin/litecoin_wallet.dart | 3796 +++++++++++++++++ 1 file changed, 3796 insertions(+) create mode 100644 lib/services/coins/litecoin/litecoin_wallet.dart diff --git a/lib/services/coins/litecoin/litecoin_wallet.dart b/lib/services/coins/litecoin/litecoin_wallet.dart new file mode 100644 index 000000000..da3bdfed0 --- /dev/null +++ b/lib/services/coins/litecoin/litecoin_wallet.dart @@ -0,0 +1,3796 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:bech32/bech32.dart'; +import 'package:bip32/bip32.dart' as bip32; +import 'package:bip39/bip39.dart' as bip39; +import 'package:bitcoindart/bitcoindart.dart'; +import 'package:bs58check/bs58check.dart' as bs58check; +import 'package:crypto/crypto.dart'; +import 'package:decimal/decimal.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:http/http.dart'; +import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; +import 'package:stackwallet/electrumx_rpc/electrumx.dart'; +import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/models/models.dart' as models; +import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/models/paymint/transactions_model.dart'; +import 'package:stackwallet/models/paymint/utxo_model.dart'; +import 'package:stackwallet/services/coins/coin_service.dart'; +import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; +import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart'; +import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; +import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; +import 'package:stackwallet/services/event_bus/global_event_bus.dart'; +import 'package:stackwallet/services/node_service.dart'; +import 'package:stackwallet/services/notifications_api.dart'; +import 'package:stackwallet/services/price.dart'; +import 'package:stackwallet/services/transaction_notification_tracker.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/default_nodes.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:stackwallet/utilities/format.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/prefs.dart'; +import 'package:tuple/tuple.dart'; +import 'package:uuid/uuid.dart'; + +const int MINIMUM_CONFIRMATIONS = 1; +const int DUST_LIMIT = 294; + +const String GENESIS_HASH_MAINNET = + "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"; +const String GENESIS_HASH_TESTNET = + "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"; + +enum DerivePathType { bip44, bip49, bip84 } + +bip32.BIP32 getBip32Node( + int chain, + int index, + String mnemonic, + NetworkType network, + DerivePathType derivePathType, +) { + final root = getBip32Root(mnemonic, network); + + final node = getBip32NodeFromRoot(chain, index, root, derivePathType); + return node; +} + +/// wrapper for compute() +bip32.BIP32 getBip32NodeWrapper( + Tuple5<int, int, String, NetworkType, DerivePathType> args, +) { + return getBip32Node( + args.item1, + args.item2, + args.item3, + args.item4, + args.item5, + ); +} + +bip32.BIP32 getBip32NodeFromRoot( + int chain, + int index, + bip32.BIP32 root, + DerivePathType derivePathType, +) { + String coinType; + switch (root.network.wif) { + case 0x80: // btc mainnet wif + coinType = "0"; // btc mainnet + break; + case 0xef: // btc testnet wif + coinType = "1"; // btc testnet + break; + default: + throw Exception("Invalid Bitcoin network type used!"); + } + switch (derivePathType) { + case DerivePathType.bip44: + return root.derivePath("m/44'/$coinType'/0'/$chain/$index"); + case DerivePathType.bip49: + return root.derivePath("m/49'/$coinType'/0'/$chain/$index"); + case DerivePathType.bip84: + return root.derivePath("m/84'/$coinType'/0'/$chain/$index"); + default: + throw Exception("DerivePathType must not be null."); + } +} + +/// wrapper for compute() +bip32.BIP32 getBip32NodeFromRootWrapper( + Tuple4<int, int, bip32.BIP32, DerivePathType> args, +) { + return getBip32NodeFromRoot( + args.item1, + args.item2, + args.item3, + args.item4, + ); +} + +bip32.BIP32 getBip32Root(String mnemonic, NetworkType network) { + final seed = bip39.mnemonicToSeed(mnemonic); + final networkType = bip32.NetworkType( + wif: network.wif, + bip32: bip32.Bip32Type( + public: network.bip32.public, + private: network.bip32.private, + ), + ); + + final root = bip32.BIP32.fromSeed(seed, networkType); + return root; +} + +/// wrapper for compute() +bip32.BIP32 getBip32RootWrapper(Tuple2<String, NetworkType> args) { + return getBip32Root(args.item1, args.item2); +} + +class BitcoinWallet extends CoinServiceAPI { + static const integrationTestFlag = + bool.fromEnvironment("IS_INTEGRATION_TEST"); + + final _prefs = Prefs.instance; + + Timer? timer; + late Coin _coin; + + late final TransactionNotificationTracker txTracker; + + NetworkType get _network { + switch (coin) { + case Coin.bitcoin: + return bitcoin; + case Coin.bitcoinTestNet: + return testnet; + default: + throw Exception("Invalid network type!"); + } + } + + List<UtxoObject> outputsList = []; + + @override + set isFavorite(bool markFavorite) { + DB.instance.put<dynamic>( + boxName: walletId, key: "isFavorite", value: markFavorite); + } + + @override + bool get isFavorite { + try { + return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite") + as bool; + } catch (e, s) { + Logging.instance + .log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error); + rethrow; + } + } + + @override + Coin get coin => _coin; + + @override + Future<List<String>> get allOwnAddresses => + _allOwnAddresses ??= _fetchAllOwnAddresses(); + Future<List<String>>? _allOwnAddresses; + + Future<UtxoData>? _utxoData; + Future<UtxoData> get utxoData => _utxoData ??= _fetchUtxoData(); + + @override + Future<List<UtxoObject>> get unspentOutputs async => + (await utxoData).unspentOutputArray; + + @override + Future<Decimal> get availableBalance async { + final data = await utxoData; + return Format.satoshisToAmount( + data.satoshiBalance - data.satoshiBalanceUnconfirmed); + } + + @override + Future<Decimal> get pendingBalance async { + final data = await utxoData; + return Format.satoshisToAmount(data.satoshiBalanceUnconfirmed); + } + + @override + Future<Decimal> get balanceMinusMaxFee async => + (await availableBalance) - + (Decimal.fromInt((await maxFee)) / Decimal.fromInt(Constants.satsPerCoin)) + .toDecimal(); + + @override + Future<Decimal> get totalBalance async { + if (!isActive) { + final totalBalance = DB.instance + .get<dynamic>(boxName: walletId, key: 'totalBalance') as int?; + if (totalBalance == null) { + final data = await utxoData; + return Format.satoshisToAmount(data.satoshiBalance); + } else { + return Format.satoshisToAmount(totalBalance); + } + } + final data = await utxoData; + return Format.satoshisToAmount(data.satoshiBalance); + } + + @override + Future<String> get currentReceivingAddress => _currentReceivingAddress ??= + _getCurrentAddressForChain(0, DerivePathType.bip84); + Future<String>? _currentReceivingAddress; + + Future<String> get currentLegacyReceivingAddress => + _currentReceivingAddressP2PKH ??= + _getCurrentAddressForChain(0, DerivePathType.bip44); + Future<String>? _currentReceivingAddressP2PKH; + + Future<String> get currentReceivingAddressP2SH => + _currentReceivingAddressP2SH ??= + _getCurrentAddressForChain(0, DerivePathType.bip49); + Future<String>? _currentReceivingAddressP2SH; + + @override + Future<void> exit() async { + _hasCalledExit = true; + timer?.cancel(); + timer = null; + stopNetworkAlivePinging(); + } + + bool _hasCalledExit = false; + + @override + bool get hasCalledExit => _hasCalledExit; + + @override + Future<FeeObject> get fees => _feeObject ??= _getFees(); + Future<FeeObject>? _feeObject; + + @override + Future<int> get maxFee async { + final fee = (await fees).fast as String; + final satsFee = Decimal.parse(fee) * Decimal.fromInt(Constants.satsPerCoin); + return satsFee.floor().toBigInt().toInt(); + } + + @override + Future<List<String>> get mnemonic => _getMnemonicList(); + + Future<int> get chainHeight async { + try { + final result = await _electrumXClient.getBlockHeadTip(); + return result["height"] as int; + } catch (e, s) { + Logging.instance.log("Exception caught in chainHeight: $e\n$s", + level: LogLevel.Error); + return -1; + } + } + + int get storedChainHeight { + final storedHeight = DB.instance + .get<dynamic>(boxName: walletId, key: "storedChainHeight") as int?; + return storedHeight ?? 0; + } + + Future<void> updateStoredChainHeight({required int newHeight}) async { + await DB.instance.put<dynamic>( + boxName: walletId, key: "storedChainHeight", value: newHeight); + } + + DerivePathType addressType({required String address}) { + Uint8List? decodeBase58; + Segwit? decodeBech32; + try { + decodeBase58 = bs58check.decode(address); + } catch (err) { + // Base58check decode fail + } + if (decodeBase58 != null) { + if (decodeBase58[0] == _network.pubKeyHash) { + // P2PKH + return DerivePathType.bip44; + } + if (decodeBase58[0] == _network.scriptHash) { + // P2SH + return DerivePathType.bip49; + } + throw ArgumentError('Invalid version or Network mismatch'); + } else { + try { + decodeBech32 = segwit.decode(address); + } catch (err) { + // Bech32 decode fail + } + if (_network.bech32 != decodeBech32!.hrp) { + throw ArgumentError('Invalid prefix or Network mismatch'); + } + if (decodeBech32.version != 0) { + throw ArgumentError('Invalid address version'); + } + // P2WPKH + return DerivePathType.bip84; + } + } + + bool longMutex = false; + + @override + Future<void> recoverFromMnemonic({ + required String mnemonic, + required int maxUnusedAddressGap, + required int maxNumberOfIndexesToCheck, + required int height, + }) async { + longMutex = true; + final start = DateTime.now(); + try { + Logging.instance.log("IS_INTEGRATION_TEST: $integrationTestFlag", + level: LogLevel.Info); + if (!integrationTestFlag) { + final features = await electrumXClient.getServerFeatures(); + Logging.instance.log("features: $features", level: LogLevel.Info); + switch (coin) { + case Coin.bitcoin: + if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { + throw Exception("genesis hash does not match main net!"); + } + break; + case Coin.bitcoinTestNet: + if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { + throw Exception("genesis hash does not match test net!"); + } + break; + default: + throw Exception( + "Attempted to generate a BitcoinWallet using a non bitcoin coin type: ${coin.name}"); + } + // if (_networkType == BasicNetworkType.main) { + // if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { + // throw Exception("genesis hash does not match main net!"); + // } + // } else if (_networkType == BasicNetworkType.test) { + // if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { + // throw Exception("genesis hash does not match test net!"); + // } + // } + } + // check to make sure we aren't overwriting a mnemonic + // this should never fail + if ((await _secureStore.read(key: '${_walletId}_mnemonic')) != null) { + longMutex = false; + throw Exception("Attempted to overwrite mnemonic on restore!"); + } + await _secureStore.write( + key: '${_walletId}_mnemonic', value: mnemonic.trim()); + await _recoverWalletFromBIP32SeedPhrase( + mnemonic: mnemonic.trim(), + maxUnusedAddressGap: maxUnusedAddressGap, + maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck, + ); + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from recoverFromMnemonic(): $e\n$s", + level: LogLevel.Error); + longMutex = false; + rethrow; + } + longMutex = false; + + final end = DateTime.now(); + Logging.instance.log( + "$walletName recovery time: ${end.difference(start).inMilliseconds} millis", + level: LogLevel.Info); + } + + Future<Map<String, dynamic>> _checkGaps( + int maxNumberOfIndexesToCheck, + int maxUnusedAddressGap, + int txCountBatchSize, + bip32.BIP32 root, + DerivePathType type, + int account) async { + List<String> addressArray = []; + int returningIndex = -1; + Map<String, Map<String, String>> derivations = {}; + int gapCounter = 0; + for (int index = 0; + index < maxNumberOfIndexesToCheck && gapCounter < maxUnusedAddressGap; + index += txCountBatchSize) { + List<String> iterationsAddressArray = []; + Logging.instance.log( + "index: $index, \t GapCounter $account ${type.name}: $gapCounter", + level: LogLevel.Info); + + final _id = "k_$index"; + Map<String, String> txCountCallArgs = {}; + final Map<String, dynamic> receivingNodes = {}; + + for (int j = 0; j < txCountBatchSize; j++) { + final node = await compute( + getBip32NodeFromRootWrapper, + Tuple4( + account, + index + j, + root, + type, + ), + ); + String? address; + switch (type) { + case DerivePathType.bip44: + address = P2PKH( + data: PaymentData(pubkey: node.publicKey), + network: _network) + .data + .address!; + break; + case DerivePathType.bip49: + address = P2SH( + data: PaymentData( + redeem: P2WPKH( + data: PaymentData(pubkey: node.publicKey), + network: _network) + .data), + network: _network) + .data + .address!; + break; + case DerivePathType.bip84: + address = P2WPKH( + network: _network, + data: PaymentData(pubkey: node.publicKey)) + .data + .address!; + break; + default: + throw Exception("No Path type $type exists"); + } + receivingNodes.addAll({ + "${_id}_$j": { + "node": node, + "address": address, + } + }); + txCountCallArgs.addAll({ + "${_id}_$j": address, + }); + } + + // get address tx counts + final counts = await _getBatchTxCount(addresses: txCountCallArgs); + + // check and add appropriate addresses + for (int k = 0; k < txCountBatchSize; k++) { + int count = counts["${_id}_$k"]!; + if (count > 0) { + final node = receivingNodes["${_id}_$k"]; + // add address to array + addressArray.add(node["address"] as String); + iterationsAddressArray.add(node["address"] as String); + // set current index + returningIndex = index + k; + // reset counter + gapCounter = 0; + // add info to derivations + derivations[node["address"] as String] = { + "pubKey": Format.uint8listToString( + (node["node"] as bip32.BIP32).publicKey), + "wif": (node["node"] as bip32.BIP32).toWIF(), + }; + } + + // increase counter when no tx history found + if (count == 0) { + gapCounter++; + } + } + // cache all the transactions while waiting for the current function to finish. + unawaited(getTransactionCacheEarly(iterationsAddressArray)); + } + return { + "addressArray": addressArray, + "index": returningIndex, + "derivations": derivations + }; + } + + Future<void> getTransactionCacheEarly(List<String> allAddresses) async { + try { + final List<Map<String, dynamic>> allTxHashes = + await _fetchHistory(allAddresses); + for (final txHash in allTxHashes) { + try { + unawaited(cachedElectrumXClient.getTransaction( + txHash: txHash["tx_hash"] as String, + verbose: true, + coin: coin, + )); + } catch (e) { + continue; + } + } + } catch (e) { + // + } + } + + Future<void> _recoverWalletFromBIP32SeedPhrase({ + required String mnemonic, + int maxUnusedAddressGap = 20, + int maxNumberOfIndexesToCheck = 1000, + }) async { + longMutex = true; + + Map<String, Map<String, String>> p2pkhReceiveDerivations = {}; + Map<String, Map<String, String>> p2shReceiveDerivations = {}; + Map<String, Map<String, String>> p2wpkhReceiveDerivations = {}; + Map<String, Map<String, String>> p2pkhChangeDerivations = {}; + Map<String, Map<String, String>> p2shChangeDerivations = {}; + Map<String, Map<String, String>> p2wpkhChangeDerivations = {}; + + final root = await compute(getBip32RootWrapper, Tuple2(mnemonic, _network)); + + List<String> p2pkhReceiveAddressArray = []; + List<String> p2shReceiveAddressArray = []; + List<String> p2wpkhReceiveAddressArray = []; + int p2pkhReceiveIndex = -1; + int p2shReceiveIndex = -1; + int p2wpkhReceiveIndex = -1; + + List<String> p2pkhChangeAddressArray = []; + List<String> p2shChangeAddressArray = []; + List<String> p2wpkhChangeAddressArray = []; + int p2pkhChangeIndex = -1; + int p2shChangeIndex = -1; + int p2wpkhChangeIndex = -1; + + // actual size is 36 due to p2pkh, p2sh, and p2wpkh so 12x3 + const txCountBatchSize = 12; + + try { + // receiving addresses + Logging.instance + .log("checking receiving addresses...", level: LogLevel.Info); + final resultReceive44 = _checkGaps(maxNumberOfIndexesToCheck, + maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip44, 0); + + final resultReceive49 = _checkGaps(maxNumberOfIndexesToCheck, + maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip49, 0); + + final resultReceive84 = _checkGaps(maxNumberOfIndexesToCheck, + maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip84, 0); + + Logging.instance + .log("checking change addresses...", level: LogLevel.Info); + // change addresses + final resultChange44 = _checkGaps(maxNumberOfIndexesToCheck, + maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip44, 1); + + final resultChange49 = _checkGaps(maxNumberOfIndexesToCheck, + maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip49, 1); + + final resultChange84 = _checkGaps(maxNumberOfIndexesToCheck, + maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip84, 1); + + await Future.wait([ + resultReceive44, + resultReceive49, + resultReceive84, + resultChange44, + resultChange49, + resultChange84 + ]); + + p2pkhReceiveAddressArray = + (await resultReceive44)['addressArray'] as List<String>; + p2pkhReceiveIndex = (await resultReceive44)['index'] as int; + p2pkhReceiveDerivations = (await resultReceive44)['derivations'] + as Map<String, Map<String, String>>; + + p2shReceiveAddressArray = + (await resultReceive49)['addressArray'] as List<String>; + p2shReceiveIndex = (await resultReceive49)['index'] as int; + p2shReceiveDerivations = (await resultReceive49)['derivations'] + as Map<String, Map<String, String>>; + + p2wpkhReceiveAddressArray = + (await resultReceive84)['addressArray'] as List<String>; + p2wpkhReceiveIndex = (await resultReceive84)['index'] as int; + p2wpkhReceiveDerivations = (await resultReceive84)['derivations'] + as Map<String, Map<String, String>>; + + p2pkhChangeAddressArray = + (await resultChange44)['addressArray'] as List<String>; + p2pkhChangeIndex = (await resultChange44)['index'] as int; + p2pkhChangeDerivations = (await resultChange44)['derivations'] + as Map<String, Map<String, String>>; + + p2shChangeAddressArray = + (await resultChange49)['addressArray'] as List<String>; + p2shChangeIndex = (await resultChange49)['index'] as int; + p2shChangeDerivations = (await resultChange49)['derivations'] + as Map<String, Map<String, String>>; + + p2wpkhChangeAddressArray = + (await resultChange84)['addressArray'] as List<String>; + p2wpkhChangeIndex = (await resultChange84)['index'] as int; + p2wpkhChangeDerivations = (await resultChange84)['derivations'] + as Map<String, Map<String, String>>; + + // save the derivations (if any) + if (p2pkhReceiveDerivations.isNotEmpty) { + await addDerivations( + chain: 0, + derivePathType: DerivePathType.bip44, + derivationsToAdd: p2pkhReceiveDerivations); + } + if (p2shReceiveDerivations.isNotEmpty) { + await addDerivations( + chain: 0, + derivePathType: DerivePathType.bip49, + derivationsToAdd: p2shReceiveDerivations); + } + if (p2wpkhReceiveDerivations.isNotEmpty) { + await addDerivations( + chain: 0, + derivePathType: DerivePathType.bip84, + derivationsToAdd: p2wpkhReceiveDerivations); + } + if (p2pkhChangeDerivations.isNotEmpty) { + await addDerivations( + chain: 1, + derivePathType: DerivePathType.bip44, + derivationsToAdd: p2pkhChangeDerivations); + } + if (p2shChangeDerivations.isNotEmpty) { + await addDerivations( + chain: 1, + derivePathType: DerivePathType.bip49, + derivationsToAdd: p2shChangeDerivations); + } + if (p2wpkhChangeDerivations.isNotEmpty) { + await addDerivations( + chain: 1, + derivePathType: DerivePathType.bip84, + derivationsToAdd: p2wpkhChangeDerivations); + } + + // If restoring a wallet that never received any funds, then set receivingArray manually + // If we didn't do this, it'd store an empty array + if (p2pkhReceiveIndex == -1) { + final address = + await _generateAddressForChain(0, 0, DerivePathType.bip44); + p2pkhReceiveAddressArray.add(address); + p2pkhReceiveIndex = 0; + } + if (p2shReceiveIndex == -1) { + final address = + await _generateAddressForChain(0, 0, DerivePathType.bip49); + p2shReceiveAddressArray.add(address); + p2shReceiveIndex = 0; + } + if (p2wpkhReceiveIndex == -1) { + final address = + await _generateAddressForChain(0, 0, DerivePathType.bip84); + p2wpkhReceiveAddressArray.add(address); + p2wpkhReceiveIndex = 0; + } + + // If restoring a wallet that never sent any funds with change, then set changeArray + // manually. If we didn't do this, it'd store an empty array. + if (p2pkhChangeIndex == -1) { + final address = + await _generateAddressForChain(1, 0, DerivePathType.bip44); + p2pkhChangeAddressArray.add(address); + p2pkhChangeIndex = 0; + } + if (p2shChangeIndex == -1) { + final address = + await _generateAddressForChain(1, 0, DerivePathType.bip49); + p2shChangeAddressArray.add(address); + p2shChangeIndex = 0; + } + if (p2wpkhChangeIndex == -1) { + final address = + await _generateAddressForChain(1, 0, DerivePathType.bip84); + p2wpkhChangeAddressArray.add(address); + p2wpkhChangeIndex = 0; + } + + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingAddressesP2WPKH', + value: p2wpkhReceiveAddressArray); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeAddressesP2WPKH', + value: p2wpkhChangeAddressArray); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingAddressesP2PKH', + value: p2pkhReceiveAddressArray); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeAddressesP2PKH', + value: p2pkhChangeAddressArray); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingAddressesP2SH', + value: p2shReceiveAddressArray); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeAddressesP2SH', + value: p2shChangeAddressArray); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingIndexP2WPKH', + value: p2wpkhReceiveIndex); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeIndexP2WPKH', + value: p2wpkhChangeIndex); + await DB.instance.put<dynamic>( + boxName: walletId, key: 'changeIndexP2PKH', value: p2pkhChangeIndex); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingIndexP2PKH', + value: p2pkhReceiveIndex); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingIndexP2SH', + value: p2shReceiveIndex); + await DB.instance.put<dynamic>( + boxName: walletId, key: 'changeIndexP2SH', value: p2shChangeIndex); + await DB.instance + .put<dynamic>(boxName: walletId, key: "id", value: _walletId); + await DB.instance + .put<dynamic>(boxName: walletId, key: "isFavorite", value: false); + + longMutex = false; + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from _recoverWalletFromBIP32SeedPhrase(): $e\n$s", + level: LogLevel.Error); + + longMutex = false; + rethrow; + } + } + + Future<bool> refreshIfThereIsNewData() async { + if (longMutex) return false; + if (_hasCalledExit) return false; + Logging.instance.log("refreshIfThereIsNewData", level: LogLevel.Info); + + try { + bool needsRefresh = false; + Set<String> txnsToCheck = {}; + + for (final String txid in txTracker.pendings) { + if (!txTracker.wasNotifiedConfirmed(txid)) { + txnsToCheck.add(txid); + } + } + + for (String txid in txnsToCheck) { + final txn = await electrumXClient.getTransaction(txHash: txid); + int confirmations = txn["confirmations"] as int? ?? 0; + bool isUnconfirmed = confirmations < MINIMUM_CONFIRMATIONS; + if (!isUnconfirmed) { + // unconfirmedTxs = {}; + needsRefresh = true; + break; + } + } + if (!needsRefresh) { + var allOwnAddresses = await _fetchAllOwnAddresses(); + List<Map<String, dynamic>> allTxs = + await _fetchHistory(allOwnAddresses); + final txData = await transactionData; + for (Map<String, dynamic> transaction in allTxs) { + if (txData.findTransaction(transaction['tx_hash'] as String) == + null) { + Logging.instance.log( + " txid not found in address history already ${transaction['tx_hash']}", + level: LogLevel.Info); + needsRefresh = true; + break; + } + } + } + return needsRefresh; + } catch (e, s) { + Logging.instance.log( + "Exception caught in refreshIfThereIsNewData: $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + Future<void> getAllTxsToWatch( + TransactionData txData, + ) async { + if (_hasCalledExit) return; + List<models.Transaction> unconfirmedTxnsToNotifyPending = []; + List<models.Transaction> unconfirmedTxnsToNotifyConfirmed = []; + + for (final chunk in txData.txChunks) { + for (final tx in chunk.transactions) { + if (tx.confirmedStatus) { + // get all transactions that were notified as pending but not as confirmed + if (txTracker.wasNotifiedPending(tx.txid) && + !txTracker.wasNotifiedConfirmed(tx.txid)) { + unconfirmedTxnsToNotifyConfirmed.add(tx); + } + } else { + // get all transactions that were not notified as pending yet + if (!txTracker.wasNotifiedPending(tx.txid)) { + unconfirmedTxnsToNotifyPending.add(tx); + } + } + } + } + + // notify on unconfirmed transactions + for (final tx in unconfirmedTxnsToNotifyPending) { + if (tx.txType == "Received") { + unawaited(NotificationApi.showNotification( + title: "Incoming transaction", + body: walletName, + walletId: walletId, + iconAssetName: Assets.svg.iconFor(coin: coin), + date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), + shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS, + coinName: coin.name, + txid: tx.txid, + confirmations: tx.confirmations, + requiredConfirmations: MINIMUM_CONFIRMATIONS, + )); + await txTracker.addNotifiedPending(tx.txid); + } else if (tx.txType == "Sent") { + unawaited(NotificationApi.showNotification( + title: "Sending transaction", + body: walletName, + walletId: walletId, + iconAssetName: Assets.svg.iconFor(coin: coin), + date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), + shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS, + coinName: coin.name, + txid: tx.txid, + confirmations: tx.confirmations, + requiredConfirmations: MINIMUM_CONFIRMATIONS, + )); + await txTracker.addNotifiedPending(tx.txid); + } + } + + // notify on confirmed + for (final tx in unconfirmedTxnsToNotifyConfirmed) { + if (tx.txType == "Received") { + unawaited(NotificationApi.showNotification( + title: "Incoming transaction confirmed", + body: walletName, + walletId: walletId, + iconAssetName: Assets.svg.iconFor(coin: coin), + date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), + shouldWatchForUpdates: false, + coinName: coin.name, + )); + await txTracker.addNotifiedConfirmed(tx.txid); + } else if (tx.txType == "Sent") { + unawaited(NotificationApi.showNotification( + title: "Outgoing transaction confirmed", + body: walletName, + walletId: walletId, + iconAssetName: Assets.svg.iconFor(coin: coin), + date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), + shouldWatchForUpdates: false, + coinName: coin.name, + )); + await txTracker.addNotifiedConfirmed(tx.txid); + } + } + } + + bool _shouldAutoSync = false; + + @override + bool get shouldAutoSync => _shouldAutoSync; + + @override + set shouldAutoSync(bool shouldAutoSync) { + if (_shouldAutoSync != shouldAutoSync) { + _shouldAutoSync = shouldAutoSync; + if (!shouldAutoSync) { + timer?.cancel(); + timer = null; + stopNetworkAlivePinging(); + } else { + startNetworkAlivePinging(); + refresh(); + } + } + } + + @override + bool get isRefreshing => refreshMutex; + + bool refreshMutex = false; + + //TODO Show percentages properly/more consistently + /// Refreshes display data for the wallet + @override + Future<void> refresh() async { + if (refreshMutex) { + Logging.instance.log("$walletId $walletName refreshMutex denied", + level: LogLevel.Info); + return; + } else { + refreshMutex = true; + } + + try { + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.syncing, + walletId, + coin, + ), + ); + + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.0, walletId)); + + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.1, walletId)); + + final currentHeight = await chainHeight; + const storedHeight = 1; //await storedChainHeight; + + Logging.instance + .log("chain height: $currentHeight", level: LogLevel.Info); + Logging.instance + .log("cached height: $storedHeight", level: LogLevel.Info); + + if (currentHeight != storedHeight) { + if (currentHeight != -1) { + // -1 failed to fetch current height + unawaited(updateStoredChainHeight(newHeight: currentHeight)); + } + + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId)); + final changeAddressForTransactions = + _checkChangeAddressForTransactions(DerivePathType.bip84); + + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId)); + final currentReceivingAddressesForTransactions = + _checkCurrentReceivingAddressesForTransactions(); + + final newTxData = _fetchTransactionData(); + GlobalEventBus.instance + .fire(RefreshPercentChangedEvent(0.50, walletId)); + + final newUtxoData = _fetchUtxoData(); + final feeObj = _getFees(); + GlobalEventBus.instance + .fire(RefreshPercentChangedEvent(0.60, walletId)); + + _transactionData = Future(() => newTxData); + + GlobalEventBus.instance + .fire(RefreshPercentChangedEvent(0.70, walletId)); + _feeObject = Future(() => feeObj); + _utxoData = Future(() => newUtxoData); + GlobalEventBus.instance + .fire(RefreshPercentChangedEvent(0.80, walletId)); + + final allTxsToWatch = getAllTxsToWatch(await newTxData); + await Future.wait([ + newTxData, + changeAddressForTransactions, + currentReceivingAddressesForTransactions, + newUtxoData, + feeObj, + allTxsToWatch, + ]); + GlobalEventBus.instance + .fire(RefreshPercentChangedEvent(0.90, walletId)); + } + + refreshMutex = false; + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(1.0, walletId)); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.synced, + walletId, + coin, + ), + ); + + if (shouldAutoSync) { + timer ??= Timer.periodic(const Duration(seconds: 30), (timer) async { + Logging.instance.log( + "Periodic refresh check for $walletId $walletName in object instance: $hashCode", + level: LogLevel.Info); + // chain height check currently broken + // if ((await chainHeight) != (await storedChainHeight)) { + if (await refreshIfThereIsNewData()) { + await refresh(); + GlobalEventBus.instance.fire(UpdatedInBackgroundEvent( + "New data found in $walletId $walletName in background!", + walletId)); + } + // } + }); + } + } catch (error, strace) { + refreshMutex = false; + GlobalEventBus.instance.fire( + NodeConnectionStatusChangedEvent( + NodeConnectionStatus.disconnected, + walletId, + coin, + ), + ); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.unableToSync, + walletId, + coin, + ), + ); + Logging.instance.log( + "Caught exception in refreshWalletData(): $error\n$strace", + level: LogLevel.Error); + } + } + + @override + Future<Map<String, dynamic>> prepareSend({ + required String address, + required int satoshiAmount, + Map<String, dynamic>? args, + }) async { + try { + final feeRateType = args?["feeRate"]; + final feeRateAmount = args?["feeRateAmount"]; + if (feeRateType is FeeRateType || feeRateAmount is int) { + late final int rate; + if (feeRateType is FeeRateType) { + int fee = 0; + final feeObject = await fees; + switch (feeRateType) { + case FeeRateType.fast: + fee = feeObject.fast; + break; + case FeeRateType.average: + fee = feeObject.medium; + break; + case FeeRateType.slow: + fee = feeObject.slow; + break; + } + rate = fee; + } else { + rate = feeRateAmount as int; + } + + // check for send all + bool isSendAll = false; + final balance = Format.decimalAmountToSatoshis(await availableBalance); + if (satoshiAmount == balance) { + isSendAll = true; + } + + final txData = + await coinSelection(satoshiAmount, rate, address, isSendAll); + + Logging.instance.log("prepare send: $txData", level: LogLevel.Info); + try { + if (txData is int) { + switch (txData) { + case 1: + throw Exception("Insufficient balance!"); + case 2: + throw Exception( + "Insufficient funds to pay for transaction fee!"); + default: + throw Exception("Transaction failed with error code $txData"); + } + } else { + final hex = txData["hex"]; + + if (hex is String) { + final fee = txData["fee"] as int; + final vSize = txData["vSize"] as int; + + Logging.instance + .log("prepared txHex: $hex", level: LogLevel.Info); + Logging.instance.log("prepared fee: $fee", level: LogLevel.Info); + Logging.instance + .log("prepared vSize: $vSize", level: LogLevel.Info); + + // fee should never be less than vSize sanity check + if (fee < vSize) { + throw Exception( + "Error in fee calculation: Transaction fee cannot be less than vSize"); + } + + return txData as Map<String, dynamic>; + } else { + throw Exception("prepared hex is not a String!!!"); + } + } + } catch (e, s) { + Logging.instance.log("Exception rethrown from prepareSend(): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } else { + throw ArgumentError("Invalid fee rate argument provided!"); + } + } catch (e, s) { + Logging.instance.log("Exception rethrown from prepareSend(): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + @override + Future<String> confirmSend({required Map<String, dynamic> txData}) async { + try { + Logging.instance.log("confirmSend txData: $txData", level: LogLevel.Info); + + final hex = txData["hex"] as String; + + final txHash = await _electrumXClient.broadcastTransaction(rawTx: hex); + Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info); + + return txHash; + } catch (e, s) { + Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + @override + Future<String> send({ + required String toAddress, + required int amount, + Map<String, String> args = const {}, + }) async { + try { + final txData = await prepareSend( + address: toAddress, satoshiAmount: amount, args: args); + final txHash = await confirmSend(txData: txData); + return txHash; + } catch (e, s) { + Logging.instance + .log("Exception rethrown from send(): $e\n$s", level: LogLevel.Error); + rethrow; + } + } + + @override + Future<bool> testNetworkConnection() async { + try { + final result = await _electrumXClient.ping(); + return result; + } catch (_) { + return false; + } + } + + Timer? _networkAliveTimer; + + void startNetworkAlivePinging() { + // call once on start right away + _periodicPingCheck(); + + // then periodically check + _networkAliveTimer = Timer.periodic( + Constants.networkAliveTimerDuration, + (_) async { + _periodicPingCheck(); + }, + ); + } + + void _periodicPingCheck() async { + bool hasNetwork = await testNetworkConnection(); + _isConnected = hasNetwork; + if (_isConnected != hasNetwork) { + NodeConnectionStatus status = hasNetwork + ? NodeConnectionStatus.connected + : NodeConnectionStatus.disconnected; + GlobalEventBus.instance + .fire(NodeConnectionStatusChangedEvent(status, walletId, coin)); + } + } + + void stopNetworkAlivePinging() { + _networkAliveTimer?.cancel(); + _networkAliveTimer = null; + } + + bool _isConnected = false; + + @override + bool get isConnected => _isConnected; + + @override + Future<void> initializeNew() async { + Logging.instance + .log("Generating new ${coin.prettyName} wallet.", level: LogLevel.Info); + + if ((DB.instance.get<dynamic>(boxName: walletId, key: "id")) != null) { + throw Exception( + "Attempted to initialize a new wallet using an existing wallet ID!"); + } + + await _prefs.init(); + try { + await _generateNewWallet(); + } catch (e, s) { + Logging.instance.log("Exception rethrown from initializeNew(): $e\n$s", + level: LogLevel.Fatal); + rethrow; + } + await Future.wait([ + DB.instance.put<dynamic>(boxName: walletId, key: "id", value: walletId), + DB.instance + .put<dynamic>(boxName: walletId, key: "isFavorite", value: false), + ]); + } + + @override + Future<void> initializeExisting() async { + Logging.instance.log("Opening existing ${coin.prettyName} wallet.", + level: LogLevel.Info); + + if ((DB.instance.get<dynamic>(boxName: walletId, key: "id")) == null) { + throw Exception( + "Attempted to initialize an existing wallet using an unknown wallet ID!"); + } + await _prefs.init(); + final data = + DB.instance.get<dynamic>(boxName: walletId, key: "latest_tx_model") + as TransactionData?; + if (data != null) { + _transactionData = Future(() => data); + } + } + + @override + Future<TransactionData> get transactionData => + _transactionData ??= _fetchTransactionData(); + Future<TransactionData>? _transactionData; + + @override + bool validateAddress(String address) { + return Address.validateAddress(address, _network); + } + + @override + String get walletId => _walletId; + late String _walletId; + + @override + String get walletName => _walletName; + late String _walletName; + + // setter for updating on rename + @override + set walletName(String newName) => _walletName = newName; + + late ElectrumX _electrumXClient; + + ElectrumX get electrumXClient => _electrumXClient; + + late CachedElectrumX _cachedElectrumXClient; + + CachedElectrumX get cachedElectrumXClient => _cachedElectrumXClient; + + late FlutterSecureStorageInterface _secureStore; + + late PriceAPI _priceAPI; + + BitcoinWallet({ + required String walletId, + required String walletName, + required Coin coin, + required ElectrumX client, + required CachedElectrumX cachedClient, + required TransactionNotificationTracker tracker, + PriceAPI? priceAPI, + FlutterSecureStorageInterface? secureStore, + }) { + txTracker = tracker; + _walletId = walletId; + _walletName = walletName; + _coin = coin; + _electrumXClient = client; + _cachedElectrumXClient = cachedClient; + + _priceAPI = priceAPI ?? PriceAPI(Client()); + _secureStore = + secureStore ?? const SecureStorageWrapper(FlutterSecureStorage()); + } + + @override + Future<void> updateNode(bool shouldRefresh) async { + final failovers = NodeService() + .failoverNodesFor(coin: coin) + .map((e) => ElectrumXNode( + address: e.host, + port: e.port, + name: e.name, + id: e.id, + useSSL: e.useSSL, + )) + .toList(); + final newNode = await getCurrentNode(); + _cachedElectrumXClient = CachedElectrumX.from( + node: newNode, + prefs: _prefs, + failovers: failovers, + ); + _electrumXClient = ElectrumX.from( + node: newNode, + prefs: _prefs, + failovers: failovers, + ); + + if (shouldRefresh) { + unawaited(refresh()); + } + } + + Future<List<String>> _getMnemonicList() async { + final mnemonicString = + await _secureStore.read(key: '${_walletId}_mnemonic'); + if (mnemonicString == null) { + return []; + } + final List<String> data = mnemonicString.split(' '); + return data; + } + + Future<ElectrumXNode> getCurrentNode() async { + final node = NodeService().getPrimaryNodeFor(coin: coin) ?? + DefaultNodes.getNodeFor(coin); + + return ElectrumXNode( + address: node.host, + port: node.port, + name: node.name, + useSSL: node.useSSL, + id: node.id, + ); + } + + Future<List<String>> _fetchAllOwnAddresses() async { + final List<String> allAddresses = []; + final receivingAddresses = DB.instance.get<dynamic>( + boxName: walletId, key: 'receivingAddressesP2WPKH') as List<dynamic>; + final changeAddresses = DB.instance.get<dynamic>( + boxName: walletId, key: 'changeAddressesP2WPKH') as List<dynamic>; + final receivingAddressesP2PKH = DB.instance.get<dynamic>( + boxName: walletId, key: 'receivingAddressesP2PKH') as List<dynamic>; + final changeAddressesP2PKH = + DB.instance.get<dynamic>(boxName: walletId, key: 'changeAddressesP2PKH') + as List<dynamic>; + final receivingAddressesP2SH = DB.instance.get<dynamic>( + boxName: walletId, key: 'receivingAddressesP2SH') as List<dynamic>; + final changeAddressesP2SH = + DB.instance.get<dynamic>(boxName: walletId, key: 'changeAddressesP2SH') + as List<dynamic>; + + for (var i = 0; i < receivingAddresses.length; i++) { + if (!allAddresses.contains(receivingAddresses[i])) { + allAddresses.add(receivingAddresses[i] as String); + } + } + for (var i = 0; i < changeAddresses.length; i++) { + if (!allAddresses.contains(changeAddresses[i])) { + allAddresses.add(changeAddresses[i] as String); + } + } + for (var i = 0; i < receivingAddressesP2PKH.length; i++) { + if (!allAddresses.contains(receivingAddressesP2PKH[i])) { + allAddresses.add(receivingAddressesP2PKH[i] as String); + } + } + for (var i = 0; i < changeAddressesP2PKH.length; i++) { + if (!allAddresses.contains(changeAddressesP2PKH[i])) { + allAddresses.add(changeAddressesP2PKH[i] as String); + } + } + for (var i = 0; i < receivingAddressesP2SH.length; i++) { + if (!allAddresses.contains(receivingAddressesP2SH[i])) { + allAddresses.add(receivingAddressesP2SH[i] as String); + } + } + for (var i = 0; i < changeAddressesP2SH.length; i++) { + if (!allAddresses.contains(changeAddressesP2SH[i])) { + allAddresses.add(changeAddressesP2SH[i] as String); + } + } + return allAddresses; + } + + Future<FeeObject> _getFees() async { + try { + //TODO adjust numbers for different speeds? + const int f = 1, m = 5, s = 20; + + final fast = await electrumXClient.estimateFee(blocks: f); + final medium = await electrumXClient.estimateFee(blocks: m); + final slow = await electrumXClient.estimateFee(blocks: s); + + final feeObject = FeeObject( + numberOfBlocksFast: f, + numberOfBlocksAverage: m, + numberOfBlocksSlow: s, + fast: Format.decimalAmountToSatoshis(fast), + medium: Format.decimalAmountToSatoshis(medium), + slow: Format.decimalAmountToSatoshis(slow), + ); + + Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); + return feeObject; + } catch (e) { + Logging.instance + .log("Exception rethrown from _getFees(): $e", level: LogLevel.Error); + rethrow; + } + } + + Future<void> _generateNewWallet() async { + Logging.instance + .log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info); + if (!integrationTestFlag) { + final features = await electrumXClient.getServerFeatures(); + Logging.instance.log("features: $features", level: LogLevel.Info); + switch (coin) { + case Coin.bitcoin: + if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { + throw Exception("genesis hash does not match main net!"); + } + break; + case Coin.bitcoinTestNet: + if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { + throw Exception("genesis hash does not match test net!"); + } + break; + default: + throw Exception( + "Attempted to generate a BitcoinWallet using a non bitcoin coin type: ${coin.name}"); + } + } + + // this should never fail + if ((await _secureStore.read(key: '${_walletId}_mnemonic')) != null) { + throw Exception( + "Attempted to overwrite mnemonic on generate new wallet!"); + } + await _secureStore.write( + key: '${_walletId}_mnemonic', + value: bip39.generateMnemonic(strength: 256)); + + // Set relevant indexes + await DB.instance + .put<dynamic>(boxName: walletId, key: "receivingIndexP2WPKH", value: 0); + await DB.instance + .put<dynamic>(boxName: walletId, key: "changeIndexP2WPKH", value: 0); + await DB.instance + .put<dynamic>(boxName: walletId, key: "receivingIndexP2PKH", value: 0); + await DB.instance + .put<dynamic>(boxName: walletId, key: "changeIndexP2PKH", value: 0); + await DB.instance + .put<dynamic>(boxName: walletId, key: "receivingIndexP2SH", value: 0); + await DB.instance + .put<dynamic>(boxName: walletId, key: "changeIndexP2SH", value: 0); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'blocked_tx_hashes', + value: ["0xdefault"], + ); // A list of transaction hashes to represent frozen utxos in wallet + // initialize address book entries + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'addressBookEntries', + value: <String, String>{}); + + // Generate and add addresses to relevant arrays + await Future.wait([ + // P2WPKH + _generateAddressForChain(0, 0, DerivePathType.bip84).then( + (initialReceivingAddressP2WPKH) { + _addToAddressesArrayForChain( + initialReceivingAddressP2WPKH, 0, DerivePathType.bip84); + _currentReceivingAddress = + Future(() => initialReceivingAddressP2WPKH); + }, + ), + _generateAddressForChain(1, 0, DerivePathType.bip84).then( + (initialChangeAddressP2WPKH) => _addToAddressesArrayForChain( + initialChangeAddressP2WPKH, + 1, + DerivePathType.bip84, + ), + ), + + // P2PKH + _generateAddressForChain(0, 0, DerivePathType.bip44).then( + (initialReceivingAddressP2PKH) { + _addToAddressesArrayForChain( + initialReceivingAddressP2PKH, 0, DerivePathType.bip44); + _currentReceivingAddressP2PKH = + Future(() => initialReceivingAddressP2PKH); + }, + ), + _generateAddressForChain(1, 0, DerivePathType.bip44).then( + (initialChangeAddressP2PKH) => _addToAddressesArrayForChain( + initialChangeAddressP2PKH, + 1, + DerivePathType.bip44, + ), + ), + + // P2SH + _generateAddressForChain(0, 0, DerivePathType.bip49).then( + (initialReceivingAddressP2SH) { + _addToAddressesArrayForChain( + initialReceivingAddressP2SH, 0, DerivePathType.bip49); + _currentReceivingAddressP2SH = + Future(() => initialReceivingAddressP2SH); + }, + ), + _generateAddressForChain(1, 0, DerivePathType.bip49).then( + (initialChangeAddressP2SH) => _addToAddressesArrayForChain( + initialChangeAddressP2SH, + 1, + DerivePathType.bip49, + ), + ), + ]); + + // // P2PKH + // _generateAddressForChain(0, 0, DerivePathType.bip44).then( + // (initialReceivingAddressP2PKH) { + // _addToAddressesArrayForChain( + // initialReceivingAddressP2PKH, 0, DerivePathType.bip44); + // this._currentReceivingAddressP2PKH = + // Future(() => initialReceivingAddressP2PKH); + // }, + // ); + // _generateAddressForChain(1, 0, DerivePathType.bip44) + // .then((initialChangeAddressP2PKH) => _addToAddressesArrayForChain( + // initialChangeAddressP2PKH, + // 1, + // DerivePathType.bip44, + // )); + // + // // P2SH + // _generateAddressForChain(0, 0, DerivePathType.bip49).then( + // (initialReceivingAddressP2SH) { + // _addToAddressesArrayForChain( + // initialReceivingAddressP2SH, 0, DerivePathType.bip49); + // this._currentReceivingAddressP2SH = + // Future(() => initialReceivingAddressP2SH); + // }, + // ); + // _generateAddressForChain(1, 0, DerivePathType.bip49) + // .then((initialChangeAddressP2SH) => _addToAddressesArrayForChain( + // initialChangeAddressP2SH, + // 1, + // DerivePathType.bip49, + // )); + + Logging.instance.log("_generateNewWalletFinished", level: LogLevel.Info); + } + + /// Generates a new internal or external chain address for the wallet using a BIP84, BIP44, or BIP49 derivation path. + /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! + /// [index] - This can be any integer >= 0 + Future<String> _generateAddressForChain( + int chain, + int index, + DerivePathType derivePathType, + ) async { + final mnemonic = await _secureStore.read(key: '${_walletId}_mnemonic'); + final node = await compute( + getBip32NodeWrapper, + Tuple5( + chain, + index, + mnemonic!, + _network, + derivePathType, + ), + ); + final data = PaymentData(pubkey: node.publicKey); + String address; + + switch (derivePathType) { + case DerivePathType.bip44: + address = P2PKH(data: data, network: _network).data.address!; + break; + case DerivePathType.bip49: + address = P2SH( + data: PaymentData( + redeem: P2WPKH(data: data, network: _network).data), + network: _network) + .data + .address!; + break; + case DerivePathType.bip84: + address = P2WPKH(network: _network, data: data).data.address!; + break; + } + + // add generated address & info to derivations + await addDerivation( + chain: chain, + address: address, + pubKey: Format.uint8listToString(node.publicKey), + wif: node.toWIF(), + derivePathType: derivePathType, + ); + + return address; + } + + /// Increases the index for either the internal or external chain, depending on [chain]. + /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! + Future<void> _incrementAddressIndexForChain( + int chain, DerivePathType derivePathType) async { + // Here we assume chain == 1 if it isn't 0 + String indexKey = chain == 0 ? "receivingIndex" : "changeIndex"; + switch (derivePathType) { + case DerivePathType.bip44: + indexKey += "P2PKH"; + break; + case DerivePathType.bip49: + indexKey += "P2SH"; + break; + case DerivePathType.bip84: + indexKey += "P2WPKH"; + break; + } + + final newIndex = + (DB.instance.get<dynamic>(boxName: walletId, key: indexKey)) + 1; + await DB.instance + .put<dynamic>(boxName: walletId, key: indexKey, value: newIndex); + } + + /// Adds [address] to the relevant chain's address array, which is determined by [chain]. + /// [address] - Expects a standard native segwit address + /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! + Future<void> _addToAddressesArrayForChain( + String address, int chain, DerivePathType derivePathType) async { + String chainArray = ''; + if (chain == 0) { + chainArray = 'receivingAddresses'; + } else { + chainArray = 'changeAddresses'; + } + switch (derivePathType) { + case DerivePathType.bip44: + chainArray += "P2PKH"; + break; + case DerivePathType.bip49: + chainArray += "P2SH"; + break; + case DerivePathType.bip84: + chainArray += "P2WPKH"; + break; + } + + final addressArray = + DB.instance.get<dynamic>(boxName: walletId, key: chainArray); + if (addressArray == null) { + Logging.instance.log( + 'Attempting to add the following to $chainArray array for chain $chain:${[ + address + ]}', + level: LogLevel.Info); + await DB.instance + .put<dynamic>(boxName: walletId, key: chainArray, value: [address]); + } else { + // Make a deep copy of the existing list + final List<String> newArray = []; + addressArray + .forEach((dynamic _address) => newArray.add(_address as String)); + newArray.add(address); // Add the address passed into the method + await DB.instance + .put<dynamic>(boxName: walletId, key: chainArray, value: newArray); + } + } + + /// Returns the latest receiving/change (external/internal) address for the wallet depending on [chain] + /// and + /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! + Future<String> _getCurrentAddressForChain( + int chain, DerivePathType derivePathType) async { + // Here, we assume that chain == 1 if it isn't 0 + String arrayKey = chain == 0 ? "receivingAddresses" : "changeAddresses"; + switch (derivePathType) { + case DerivePathType.bip44: + arrayKey += "P2PKH"; + break; + case DerivePathType.bip49: + arrayKey += "P2SH"; + break; + case DerivePathType.bip84: + arrayKey += "P2WPKH"; + break; + } + final internalChainArray = + DB.instance.get<dynamic>(boxName: walletId, key: arrayKey); + return internalChainArray.last as String; + } + + String _buildDerivationStorageKey({ + required int chain, + required DerivePathType derivePathType, + }) { + String key; + String chainId = chain == 0 ? "receive" : "change"; + switch (derivePathType) { + case DerivePathType.bip44: + key = "${walletId}_${chainId}DerivationsP2PKH"; + break; + case DerivePathType.bip49: + key = "${walletId}_${chainId}DerivationsP2SH"; + break; + case DerivePathType.bip84: + key = "${walletId}_${chainId}DerivationsP2WPKH"; + break; + } + return key; + } + + Future<Map<String, dynamic>> _fetchDerivations({ + required int chain, + required DerivePathType derivePathType, + }) async { + // build lookup key + final key = _buildDerivationStorageKey( + chain: chain, derivePathType: derivePathType); + + // fetch current derivations + final derivationsString = await _secureStore.read(key: key); + return Map<String, dynamic>.from( + jsonDecode(derivationsString ?? "{}") as Map); + } + + /// Add a single derivation to the local secure storage for [chain] and + /// [derivePathType] where [chain] must either be 1 for change or 0 for receive. + /// This will overwrite a previous entry where the address of the new derivation + /// matches a derivation currently stored. + Future<void> addDerivation({ + required int chain, + required String address, + required String pubKey, + required String wif, + required DerivePathType derivePathType, + }) async { + // build lookup key + final key = _buildDerivationStorageKey( + chain: chain, derivePathType: derivePathType); + + // fetch current derivations + final derivationsString = await _secureStore.read(key: key); + final derivations = + Map<String, dynamic>.from(jsonDecode(derivationsString ?? "{}") as Map); + + // add derivation + derivations[address] = { + "pubKey": pubKey, + "wif": wif, + }; + + // save derivations + final newReceiveDerivationsString = jsonEncode(derivations); + await _secureStore.write(key: key, value: newReceiveDerivationsString); + } + + /// Add multiple derivations to the local secure storage for [chain] and + /// [derivePathType] where [chain] must either be 1 for change or 0 for receive. + /// This will overwrite any previous entries where the address of the new derivation + /// matches a derivation currently stored. + /// The [derivationsToAdd] must be in the format of: + /// { + /// addressA : { + /// "pubKey": <the pubKey string>, + /// "wif": <the wif string>, + /// }, + /// addressB : { + /// "pubKey": <the pubKey string>, + /// "wif": <the wif string>, + /// }, + /// } + Future<void> addDerivations({ + required int chain, + required DerivePathType derivePathType, + required Map<String, dynamic> derivationsToAdd, + }) async { + // build lookup key + final key = _buildDerivationStorageKey( + chain: chain, derivePathType: derivePathType); + + // fetch current derivations + final derivationsString = await _secureStore.read(key: key); + final derivations = + Map<String, dynamic>.from(jsonDecode(derivationsString ?? "{}") as Map); + + // add derivation + derivations.addAll(derivationsToAdd); + + // save derivations + final newReceiveDerivationsString = jsonEncode(derivations); + await _secureStore.write(key: key, value: newReceiveDerivationsString); + } + + Future<UtxoData> _fetchUtxoData() async { + final List<String> allAddresses = await _fetchAllOwnAddresses(); + + try { + final fetchedUtxoList = <List<Map<String, dynamic>>>[]; + + final Map<int, Map<String, List<dynamic>>> batches = {}; + const batchSizeMax = 100; + int batchNumber = 0; + for (int i = 0; i < allAddresses.length; i++) { + if (batches[batchNumber] == null) { + batches[batchNumber] = {}; + } + final scripthash = _convertToScriptHash(allAddresses[i], _network); + batches[batchNumber]!.addAll({ + scripthash: [scripthash] + }); + if (i % batchSizeMax == batchSizeMax - 1) { + batchNumber++; + } + } + + for (int i = 0; i < batches.length; i++) { + final response = + await _electrumXClient.getBatchUTXOs(args: batches[i]!); + for (final entry in response.entries) { + if (entry.value.isNotEmpty) { + fetchedUtxoList.add(entry.value); + } + } + } + final priceData = + await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; + final List<Map<String, dynamic>> outputArray = []; + int satoshiBalance = 0; + int satoshiBalancePending = 0; + + for (int i = 0; i < fetchedUtxoList.length; i++) { + for (int j = 0; j < fetchedUtxoList[i].length; j++) { + int value = fetchedUtxoList[i][j]["value"] as int; + satoshiBalance += value; + + final txn = await cachedElectrumXClient.getTransaction( + txHash: fetchedUtxoList[i][j]["tx_hash"] as String, + verbose: true, + coin: coin, + ); + + final Map<String, dynamic> utxo = {}; + final int confirmations = txn["confirmations"] as int? ?? 0; + final bool confirmed = confirmations >= MINIMUM_CONFIRMATIONS; + if (!confirmed) { + satoshiBalancePending += value; + } + + utxo["txid"] = txn["txid"]; + utxo["vout"] = fetchedUtxoList[i][j]["tx_pos"]; + utxo["value"] = value; + + utxo["status"] = <String, dynamic>{}; + utxo["status"]["confirmed"] = confirmed; + utxo["status"]["confirmations"] = confirmations; + utxo["status"]["block_height"] = fetchedUtxoList[i][j]["height"]; + utxo["status"]["block_hash"] = txn["blockhash"]; + utxo["status"]["block_time"] = txn["blocktime"]; + + final fiatValue = ((Decimal.fromInt(value) * currentPrice) / + Decimal.fromInt(Constants.satsPerCoin)) + .toDecimal(scaleOnInfinitePrecision: 2); + utxo["rawWorth"] = fiatValue; + utxo["fiatWorth"] = fiatValue.toString(); + outputArray.add(utxo); + } + } + + Decimal currencyBalanceRaw = + ((Decimal.fromInt(satoshiBalance) * currentPrice) / + Decimal.fromInt(Constants.satsPerCoin)) + .toDecimal(scaleOnInfinitePrecision: 2); + + final Map<String, dynamic> result = { + "total_user_currency": currencyBalanceRaw.toString(), + "total_sats": satoshiBalance, + "total_btc": (Decimal.fromInt(satoshiBalance) / + Decimal.fromInt(Constants.satsPerCoin)) + .toDecimal(scaleOnInfinitePrecision: Constants.decimalPlaces) + .toString(), + "outputArray": outputArray, + "unconfirmed": satoshiBalancePending, + }; + + final dataModel = UtxoData.fromJson(result); + + final List<UtxoObject> allOutputs = dataModel.unspentOutputArray; + Logging.instance + .log('Outputs fetched: $allOutputs', level: LogLevel.Info); + await _sortOutputs(allOutputs); + await DB.instance.put<dynamic>( + boxName: walletId, key: 'latest_utxo_model', value: dataModel); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'totalBalance', + value: dataModel.satoshiBalance); + return dataModel; + } catch (e, s) { + Logging.instance + .log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error); + final latestTxModel = + DB.instance.get<dynamic>(boxName: walletId, key: 'latest_utxo_model') + as models.UtxoData?; + + if (latestTxModel == null) { + final emptyModel = { + "total_user_currency": "0.00", + "total_sats": 0, + "total_btc": "0", + "outputArray": <dynamic>[] + }; + return UtxoData.fromJson(emptyModel); + } else { + Logging.instance + .log("Old output model located", level: LogLevel.Warning); + return latestTxModel; + } + } + } + + /// Takes in a list of UtxoObjects and adds a name (dependent on object index within list) + /// and checks for the txid associated with the utxo being blocked and marks it accordingly. + /// Now also checks for output labeling. + Future<void> _sortOutputs(List<UtxoObject> utxos) async { + final blockedHashArray = + DB.instance.get<dynamic>(boxName: walletId, key: 'blocked_tx_hashes') + as List<dynamic>?; + final List<String> lst = []; + if (blockedHashArray != null) { + for (var hash in blockedHashArray) { + lst.add(hash as String); + } + } + final labels = + DB.instance.get<dynamic>(boxName: walletId, key: 'labels') as Map? ?? + {}; + + outputsList = []; + + for (var i = 0; i < utxos.length; i++) { + if (labels[utxos[i].txid] != null) { + utxos[i].txName = labels[utxos[i].txid] as String? ?? ""; + } else { + utxos[i].txName = 'Output #$i'; + } + + if (utxos[i].status.confirmed == false) { + outputsList.add(utxos[i]); + } else { + if (lst.contains(utxos[i].txid)) { + utxos[i].blocked = true; + outputsList.add(utxos[i]); + } else if (!lst.contains(utxos[i].txid)) { + outputsList.add(utxos[i]); + } + } + } + } + + Future<int> getTxCount({required String address}) async { + String? scripthash; + try { + scripthash = _convertToScriptHash(address, _network); + final transactions = + await electrumXClient.getHistory(scripthash: scripthash); + return transactions.length; + } catch (e) { + Logging.instance.log( + "Exception rethrown in _getTxCount(address: $address, scripthash: $scripthash): $e", + level: LogLevel.Error); + rethrow; + } + } + + Future<Map<String, int>> _getBatchTxCount({ + required Map<String, String> addresses, + }) async { + try { + final Map<String, List<dynamic>> args = {}; + for (final entry in addresses.entries) { + args[entry.key] = [_convertToScriptHash(entry.value, _network)]; + } + final response = await electrumXClient.getBatchHistory(args: args); + + final Map<String, int> result = {}; + for (final entry in response.entries) { + result[entry.key] = entry.value.length; + } + return result; + } catch (e, s) { + Logging.instance.log( + "Exception rethrown in _getBatchTxCount(address: $addresses: $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + Future<void> _checkReceivingAddressForTransactions( + DerivePathType derivePathType) async { + try { + final String currentExternalAddr = + await _getCurrentAddressForChain(0, derivePathType); + final int txCount = await getTxCount(address: currentExternalAddr); + Logging.instance.log( + 'Number of txs for current receiving address $currentExternalAddr: $txCount', + level: LogLevel.Info); + + if (txCount >= 1) { + // First increment the receiving index + await _incrementAddressIndexForChain(0, derivePathType); + + // Check the new receiving index + String indexKey = "receivingIndex"; + switch (derivePathType) { + case DerivePathType.bip44: + indexKey += "P2PKH"; + break; + case DerivePathType.bip49: + indexKey += "P2SH"; + break; + case DerivePathType.bip84: + indexKey += "P2WPKH"; + break; + } + final newReceivingIndex = + DB.instance.get<dynamic>(boxName: walletId, key: indexKey) as int; + + // Use new index to derive a new receiving address + final newReceivingAddress = await _generateAddressForChain( + 0, newReceivingIndex, derivePathType); + + // Add that new receiving address to the array of receiving addresses + await _addToAddressesArrayForChain( + newReceivingAddress, 0, derivePathType); + + // Set the new receiving address that the service + + switch (derivePathType) { + case DerivePathType.bip44: + _currentReceivingAddressP2PKH = Future(() => newReceivingAddress); + break; + case DerivePathType.bip49: + _currentReceivingAddressP2SH = Future(() => newReceivingAddress); + break; + case DerivePathType.bip84: + _currentReceivingAddress = Future(() => newReceivingAddress); + break; + } + } + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from _checkReceivingAddressForTransactions($derivePathType): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + Future<void> _checkChangeAddressForTransactions( + DerivePathType derivePathType) async { + try { + final String currentExternalAddr = + await _getCurrentAddressForChain(1, derivePathType); + final int txCount = await getTxCount(address: currentExternalAddr); + Logging.instance.log( + 'Number of txs for current change address $currentExternalAddr: $txCount', + level: LogLevel.Info); + + if (txCount >= 1) { + // First increment the change index + await _incrementAddressIndexForChain(1, derivePathType); + + // Check the new change index + String indexKey = "changeIndex"; + switch (derivePathType) { + case DerivePathType.bip44: + indexKey += "P2PKH"; + break; + case DerivePathType.bip49: + indexKey += "P2SH"; + break; + case DerivePathType.bip84: + indexKey += "P2WPKH"; + break; + } + final newChangeIndex = + DB.instance.get<dynamic>(boxName: walletId, key: indexKey) as int; + + // Use new index to derive a new change address + final newChangeAddress = + await _generateAddressForChain(1, newChangeIndex, derivePathType); + + // Add that new receiving address to the array of change addresses + await _addToAddressesArrayForChain(newChangeAddress, 1, derivePathType); + } + } on SocketException catch (se, s) { + Logging.instance.log( + "SocketException caught in _checkReceivingAddressForTransactions($derivePathType): $se\n$s", + level: LogLevel.Error); + return; + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from _checkReceivingAddressForTransactions($derivePathType): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + Future<void> _checkCurrentReceivingAddressesForTransactions() async { + try { + for (final type in DerivePathType.values) { + await _checkReceivingAddressForTransactions(type); + } + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from _checkCurrentReceivingAddressesForTransactions(): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + /// public wrapper because dart can't test private... + Future<void> checkCurrentReceivingAddressesForTransactions() async { + if (Platform.environment["FLUTTER_TEST"] == "true") { + try { + return _checkCurrentReceivingAddressesForTransactions(); + } catch (_) { + rethrow; + } + } + } + + Future<void> _checkCurrentChangeAddressesForTransactions() async { + try { + for (final type in DerivePathType.values) { + await _checkChangeAddressForTransactions(type); + } + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from _checkCurrentChangeAddressesForTransactions(): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + /// public wrapper because dart can't test private... + Future<void> checkCurrentChangeAddressesForTransactions() async { + if (Platform.environment["FLUTTER_TEST"] == "true") { + try { + return _checkCurrentChangeAddressesForTransactions(); + } catch (_) { + rethrow; + } + } + } + + /// attempts to convert a string to a valid scripthash + /// + /// Returns the scripthash or throws an exception on invalid bitcoin address + String _convertToScriptHash(String bitcoinAddress, NetworkType network) { + try { + final output = Address.addressToOutputScript(bitcoinAddress, network); + final hash = sha256.convert(output.toList(growable: false)).toString(); + + final chars = hash.split(""); + final reversedPairs = <String>[]; + var i = chars.length - 1; + while (i > 0) { + reversedPairs.add(chars[i - 1]); + reversedPairs.add(chars[i]); + i -= 2; + } + return reversedPairs.join(""); + } catch (e) { + rethrow; + } + } + + Future<List<Map<String, dynamic>>> _fetchHistory( + List<String> allAddresses) async { + try { + List<Map<String, dynamic>> allTxHashes = []; + + final Map<int, Map<String, List<dynamic>>> batches = {}; + final Map<String, String> requestIdToAddressMap = {}; + const batchSizeMax = 100; + int batchNumber = 0; + for (int i = 0; i < allAddresses.length; i++) { + if (batches[batchNumber] == null) { + batches[batchNumber] = {}; + } + final scripthash = _convertToScriptHash(allAddresses[i], _network); + final id = Logger.isTestEnv ? "$i" : const Uuid().v1(); + requestIdToAddressMap[id] = allAddresses[i]; + batches[batchNumber]!.addAll({ + id: [scripthash] + }); + if (i % batchSizeMax == batchSizeMax - 1) { + batchNumber++; + } + } + + for (int i = 0; i < batches.length; i++) { + final response = + await _electrumXClient.getBatchHistory(args: batches[i]!); + for (final entry in response.entries) { + for (int j = 0; j < entry.value.length; j++) { + entry.value[j]["address"] = requestIdToAddressMap[entry.key]; + if (!allTxHashes.contains(entry.value[j])) { + allTxHashes.add(entry.value[j]); + } + } + } + } + + return allTxHashes; + } catch (e, s) { + Logging.instance.log("_fetchHistory: $e\n$s", level: LogLevel.Error); + rethrow; + } + } + + bool _duplicateTxCheck( + List<Map<String, dynamic>> allTransactions, String txid) { + for (int i = 0; i < allTransactions.length; i++) { + if (allTransactions[i]["txid"] == txid) { + return true; + } + } + return false; + } + + Future<List<Map<String, dynamic>>> fastFetch(List<String> allTxHashes) async { + List<Map<String, dynamic>> allTransactions = []; + + const futureLimit = 30; + List<Future<Map<String, dynamic>>> transactionFutures = []; + int currentFutureCount = 0; + for (final txHash in allTxHashes) { + Future<Map<String, dynamic>> transactionFuture = + cachedElectrumXClient.getTransaction( + txHash: txHash, + verbose: true, + coin: coin, + ); + transactionFutures.add(transactionFuture); + currentFutureCount++; + if (currentFutureCount > futureLimit) { + currentFutureCount = 0; + await Future.wait(transactionFutures); + for (final fTx in transactionFutures) { + final tx = await fTx; + + allTransactions.add(tx); + } + } + } + if (currentFutureCount != 0) { + currentFutureCount = 0; + await Future.wait(transactionFutures); + for (final fTx in transactionFutures) { + final tx = await fTx; + + allTransactions.add(tx); + } + } + return allTransactions; + } + + Future<TransactionData> _fetchTransactionData() async { + final List<String> allAddresses = await _fetchAllOwnAddresses(); + + final changeAddresses = DB.instance.get<dynamic>( + boxName: walletId, key: 'changeAddressesP2WPKH') as List<dynamic>; + final changeAddressesP2PKH = + DB.instance.get<dynamic>(boxName: walletId, key: 'changeAddressesP2PKH') + as List<dynamic>; + final changeAddressesP2SH = + DB.instance.get<dynamic>(boxName: walletId, key: 'changeAddressesP2SH') + as List<dynamic>; + + for (var i = 0; i < changeAddressesP2PKH.length; i++) { + changeAddresses.add(changeAddressesP2PKH[i] as String); + } + for (var i = 0; i < changeAddressesP2SH.length; i++) { + changeAddresses.add(changeAddressesP2SH[i] as String); + } + + final List<Map<String, dynamic>> allTxHashes = + await _fetchHistory(allAddresses); + + final cachedTransactions = + DB.instance.get<dynamic>(boxName: walletId, key: 'latest_tx_model') + as TransactionData?; + int latestTxnBlockHeight = + DB.instance.get<dynamic>(boxName: walletId, key: "storedTxnDataHeight") + as int? ?? + 0; + + final unconfirmedCachedTransactions = + cachedTransactions?.getAllTransactions() ?? {}; + unconfirmedCachedTransactions + .removeWhere((key, value) => value.confirmedStatus); + + if (cachedTransactions != null) { + for (final tx in allTxHashes.toList(growable: false)) { + final txHeight = tx["height"] as int; + if (txHeight > 0 && + txHeight < latestTxnBlockHeight - MINIMUM_CONFIRMATIONS) { + if (unconfirmedCachedTransactions[tx["tx_hash"] as String] == null) { + allTxHashes.remove(tx); + } + } + } + } + + Set<String> hashes = {}; + for (var element in allTxHashes) { + hashes.add(element['tx_hash'] as String); + } + await fastFetch(hashes.toList()); + List<Map<String, dynamic>> allTransactions = []; + + for (final txHash in allTxHashes) { + final tx = await cachedElectrumXClient.getTransaction( + txHash: txHash["tx_hash"] as String, + verbose: true, + coin: coin, + ); + + // Logging.instance.log("TRANSACTION: ${jsonEncode(tx)}"); + // TODO fix this for sent to self transactions? + if (!_duplicateTxCheck(allTransactions, tx["txid"] as String)) { + tx["address"] = txHash["address"]; + tx["height"] = txHash["height"]; + allTransactions.add(tx); + } + } + + Logging.instance.log("addAddresses: $allAddresses", level: LogLevel.Info); + Logging.instance.log("allTxHashes: $allTxHashes", level: LogLevel.Info); + + Logging.instance.log("allTransactions length: ${allTransactions.length}", + level: LogLevel.Info); + + final priceData = + await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; + final List<Map<String, dynamic>> midSortedArray = []; + + Set<String> vHashes = {}; + for (final txObject in allTransactions) { + for (int i = 0; i < (txObject["vin"] as List).length; i++) { + final input = txObject["vin"]![i] as Map; + final prevTxid = input["txid"] as String; + vHashes.add(prevTxid); + } + } + await fastFetch(vHashes.toList()); + + for (final txObject in allTransactions) { + List<String> sendersArray = []; + List<String> recipientsArray = []; + + // Usually only has value when txType = 'Send' + int inputAmtSentFromWallet = 0; + // Usually has value regardless of txType due to change addresses + int outputAmtAddressedToWallet = 0; + int fee = 0; + + Map<String, dynamic> midSortedTx = {}; + + for (int i = 0; i < (txObject["vin"] as List).length; i++) { + final input = txObject["vin"]![i] as Map; + final prevTxid = input["txid"] as String; + final prevOut = input["vout"] as int; + + final tx = await _cachedElectrumXClient.getTransaction( + txHash: prevTxid, + coin: coin, + ); + + for (final out in tx["vout"] as List) { + if (prevOut == out["n"]) { + final address = out["scriptPubKey"]["address"] as String?; + if (address != null) { + sendersArray.add(address); + } + } + } + } + + Logging.instance.log("sendersArray: $sendersArray", level: LogLevel.Info); + + for (final output in txObject["vout"] as List) { + final address = output["scriptPubKey"]["address"] as String?; + if (address != null) { + recipientsArray.add(address); + } + } + + Logging.instance + .log("recipientsArray: $recipientsArray", level: LogLevel.Info); + + final foundInSenders = + allAddresses.any((element) => sendersArray.contains(element)); + Logging.instance + .log("foundInSenders: $foundInSenders", level: LogLevel.Info); + + // If txType = Sent, then calculate inputAmtSentFromWallet + if (foundInSenders) { + int totalInput = 0; + for (int i = 0; i < (txObject["vin"] as List).length; i++) { + final input = txObject["vin"]![i] as Map; + final prevTxid = input["txid"] as String; + final prevOut = input["vout"] as int; + final tx = await _cachedElectrumXClient.getTransaction( + txHash: prevTxid, + coin: coin, + ); + + for (final out in tx["vout"] as List) { + if (prevOut == out["n"]) { + inputAmtSentFromWallet += + (Decimal.parse(out["value"]!.toString()) * + Decimal.fromInt(Constants.satsPerCoin)) + .toBigInt() + .toInt(); + } + } + } + totalInput = inputAmtSentFromWallet; + int totalOutput = 0; + + for (final output in txObject["vout"] as List) { + final String address = output["scriptPubKey"]!["address"] as String; + final value = output["value"]!; + final _value = (Decimal.parse(value.toString()) * + Decimal.fromInt(Constants.satsPerCoin)) + .toBigInt() + .toInt(); + totalOutput += _value; + if (changeAddresses.contains(address)) { + inputAmtSentFromWallet -= _value; + } else { + // change address from 'sent from' to the 'sent to' address + txObject["address"] = address; + } + } + // calculate transaction fee + fee = totalInput - totalOutput; + // subtract fee from sent to calculate correct value of sent tx + inputAmtSentFromWallet -= fee; + } else { + // counters for fee calculation + int totalOut = 0; + int totalIn = 0; + + // add up received tx value + for (final output in txObject["vout"] as List) { + final address = output["scriptPubKey"]["address"]; + if (address != null) { + final value = (Decimal.parse(output["value"].toString()) * + Decimal.fromInt(Constants.satsPerCoin)) + .toBigInt() + .toInt(); + totalOut += value; + if (allAddresses.contains(address)) { + outputAmtAddressedToWallet += value; + } + } + } + + // calculate fee for received tx + for (int i = 0; i < (txObject["vin"] as List).length; i++) { + final input = txObject["vin"][i] as Map; + final prevTxid = input["txid"] as String; + final prevOut = input["vout"] as int; + final tx = await _cachedElectrumXClient.getTransaction( + txHash: prevTxid, + coin: coin, + ); + + for (final out in tx["vout"] as List) { + if (prevOut == out["n"]) { + totalIn += (Decimal.parse(out["value"].toString()) * + Decimal.fromInt(Constants.satsPerCoin)) + .toBigInt() + .toInt(); + } + } + } + fee = totalIn - totalOut; + } + + // create final tx map + midSortedTx["txid"] = txObject["txid"]; + midSortedTx["confirmed_status"] = (txObject["confirmations"] != null) && + (txObject["confirmations"] as int >= MINIMUM_CONFIRMATIONS); + midSortedTx["confirmations"] = txObject["confirmations"] ?? 0; + midSortedTx["timestamp"] = txObject["blocktime"] ?? + (DateTime.now().millisecondsSinceEpoch ~/ 1000); + + if (foundInSenders) { + midSortedTx["txType"] = "Sent"; + midSortedTx["amount"] = inputAmtSentFromWallet; + final String worthNow = + ((currentPrice * Decimal.fromInt(inputAmtSentFromWallet)) / + Decimal.fromInt(Constants.satsPerCoin)) + .toDecimal(scaleOnInfinitePrecision: 2) + .toStringAsFixed(2); + midSortedTx["worthNow"] = worthNow; + midSortedTx["worthAtBlockTimestamp"] = worthNow; + } else { + midSortedTx["txType"] = "Received"; + midSortedTx["amount"] = outputAmtAddressedToWallet; + final worthNow = + ((currentPrice * Decimal.fromInt(outputAmtAddressedToWallet)) / + Decimal.fromInt(Constants.satsPerCoin)) + .toDecimal(scaleOnInfinitePrecision: 2) + .toStringAsFixed(2); + midSortedTx["worthNow"] = worthNow; + } + midSortedTx["aliens"] = <dynamic>[]; + midSortedTx["fees"] = fee; + midSortedTx["address"] = txObject["address"]; + midSortedTx["inputSize"] = txObject["vin"].length; + midSortedTx["outputSize"] = txObject["vout"].length; + midSortedTx["inputs"] = txObject["vin"]; + midSortedTx["outputs"] = txObject["vout"]; + + final int height = txObject["height"] as int; + midSortedTx["height"] = height; + + if (height >= latestTxnBlockHeight) { + latestTxnBlockHeight = height; + } + + midSortedArray.add(midSortedTx); + } + + // sort by date ---- //TODO not sure if needed + // shouldn't be any issues with a null timestamp but I got one at some point? + midSortedArray + .sort((a, b) => (b["timestamp"] as int) - (a["timestamp"] as int)); + // { + // final aT = a["timestamp"]; + // final bT = b["timestamp"]; + // + // if (aT == null && bT == null) { + // return 0; + // } else if (aT == null) { + // return -1; + // } else if (bT == null) { + // return 1; + // } else { + // return bT - aT; + // } + // }); + + // buildDateTimeChunks + final Map<String, dynamic> result = {"dateTimeChunks": <dynamic>[]}; + final dateArray = <dynamic>[]; + + for (int i = 0; i < midSortedArray.length; i++) { + final txObject = midSortedArray[i]; + final date = extractDateFromTimestamp(txObject["timestamp"] as int); + final txTimeArray = [txObject["timestamp"], date]; + + if (dateArray.contains(txTimeArray[1])) { + result["dateTimeChunks"].forEach((dynamic chunk) { + if (extractDateFromTimestamp(chunk["timestamp"] as int) == + txTimeArray[1]) { + if (chunk["transactions"] == null) { + chunk["transactions"] = <Map<String, dynamic>>[]; + } + chunk["transactions"].add(txObject); + } + }); + } else { + dateArray.add(txTimeArray[1]); + final chunk = { + "timestamp": txTimeArray[0], + "transactions": [txObject], + }; + result["dateTimeChunks"].add(chunk); + } + } + + final transactionsMap = cachedTransactions?.getAllTransactions() ?? {}; + transactionsMap + .addAll(TransactionData.fromJson(result).getAllTransactions()); + + final txModel = TransactionData.fromMap(transactionsMap); + + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'storedTxnDataHeight', + value: latestTxnBlockHeight); + await DB.instance.put<dynamic>( + boxName: walletId, key: 'latest_tx_model', value: txModel); + + return txModel; + } + + int estimateTxFee({required int vSize, required int feeRatePerKB}) { + return vSize * (feeRatePerKB / 1000).ceil(); + } + + /// The coinselection algorithm decides whether or not the user is eligible to make the transaction + /// with [satoshiAmountToSend] and [selectedTxFeeRate]. If so, it will call buildTrasaction() and return + /// a map containing the tx hex along with other important information. If not, then it will return + /// an integer (1 or 2) + dynamic coinSelection( + int satoshiAmountToSend, + int selectedTxFeeRate, + String _recipientAddress, + bool isSendAll, { + int additionalOutputs = 0, + List<UtxoObject>? utxos, + }) async { + Logging.instance + .log("Starting coinSelection ----------", level: LogLevel.Info); + final List<UtxoObject> availableOutputs = utxos ?? outputsList; + final List<UtxoObject> spendableOutputs = []; + int spendableSatoshiValue = 0; + + // Build list of spendable outputs and totaling their satoshi amount + for (var i = 0; i < availableOutputs.length; i++) { + if (availableOutputs[i].blocked == false && + availableOutputs[i].status.confirmed == true) { + spendableOutputs.add(availableOutputs[i]); + spendableSatoshiValue += availableOutputs[i].value; + } + } + + // sort spendable by age (oldest first) + spendableOutputs.sort( + (a, b) => b.status.confirmations.compareTo(a.status.confirmations)); + + Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}", + level: LogLevel.Info); + Logging.instance + .log("spendableOutputs: $spendableOutputs", level: LogLevel.Info); + Logging.instance.log("spendableSatoshiValue: $spendableSatoshiValue", + level: LogLevel.Info); + Logging.instance + .log("satoshiAmountToSend: $satoshiAmountToSend", level: LogLevel.Info); + // If the amount the user is trying to send is smaller than the amount that they have spendable, + // then return 1, which indicates that they have an insufficient balance. + if (spendableSatoshiValue < satoshiAmountToSend) { + return 1; + // If the amount the user wants to send is exactly equal to the amount they can spend, then return + // 2, which indicates that they are not leaving enough over to pay the transaction fee + } else if (spendableSatoshiValue == satoshiAmountToSend && !isSendAll) { + return 2; + } + // If neither of these statements pass, we assume that the user has a spendable balance greater + // than the amount they're attempting to send. Note that this value still does not account for + // the added transaction fee, which may require an extra input and will need to be checked for + // later on. + + // Possible situation right here + int satoshisBeingUsed = 0; + int inputsBeingConsumed = 0; + List<UtxoObject> utxoObjectsToUse = []; + + for (var i = 0; + satoshisBeingUsed < satoshiAmountToSend && i < spendableOutputs.length; + i++) { + utxoObjectsToUse.add(spendableOutputs[i]); + satoshisBeingUsed += spendableOutputs[i].value; + inputsBeingConsumed += 1; + } + for (int i = 0; + i < additionalOutputs && inputsBeingConsumed < spendableOutputs.length; + i++) { + utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]); + satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value; + inputsBeingConsumed += 1; + } + + Logging.instance + .log("satoshisBeingUsed: $satoshisBeingUsed", level: LogLevel.Info); + Logging.instance + .log("inputsBeingConsumed: $inputsBeingConsumed", level: LogLevel.Info); + Logging.instance + .log('utxoObjectsToUse: $utxoObjectsToUse', level: LogLevel.Info); + + // numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray + List<String> recipientsArray = [_recipientAddress]; + List<int> recipientsAmtArray = [satoshiAmountToSend]; + + // gather required signing data + final utxoSigningData = await fetchBuildTxData(utxoObjectsToUse); + + if (isSendAll) { + Logging.instance + .log("Attempting to send all $coin", level: LogLevel.Info); + + final int vSizeForOneOutput = (await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: [_recipientAddress], + satoshiAmounts: [satoshisBeingUsed - 1], + ))["vSize"] as int; + int feeForOneOutput = estimateTxFee( + vSize: vSizeForOneOutput, + feeRatePerKB: selectedTxFeeRate, + ); + + final int roughEstimate = + roughFeeEstimate(spendableOutputs.length, 1, selectedTxFeeRate); + if (feeForOneOutput < roughEstimate) { + feeForOneOutput = roughEstimate; + } + + final int amount = satoshiAmountToSend - feeForOneOutput; + dynamic txn = await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: recipientsArray, + satoshiAmounts: [amount], + ); + Map<String, dynamic> transactionObject = { + "hex": txn["hex"], + "recipient": recipientsArray[0], + "recipientAmt": amount, + "fee": feeForOneOutput, + "vSize": txn["vSize"], + }; + return transactionObject; + } + + final int vSizeForOneOutput = (await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: [_recipientAddress], + satoshiAmounts: [satoshisBeingUsed - 1], + ))["vSize"] as int; + final int vSizeForTwoOutPuts = (await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: [ + _recipientAddress, + await _getCurrentAddressForChain(1, DerivePathType.bip84), + ], + satoshiAmounts: [ + satoshiAmountToSend, + satoshisBeingUsed - satoshiAmountToSend - 1 + ], // dust limit is the minimum amount a change output should be + ))["vSize"] as int; + + // Assume 1 output, only for recipient and no change + final feeForOneOutput = estimateTxFee( + vSize: vSizeForOneOutput, + feeRatePerKB: selectedTxFeeRate, + ); + // Assume 2 outputs, one for recipient and one for change + final feeForTwoOutputs = estimateTxFee( + vSize: vSizeForTwoOutPuts, + feeRatePerKB: selectedTxFeeRate, + ); + + Logging.instance + .log("feeForTwoOutputs: $feeForTwoOutputs", level: LogLevel.Info); + Logging.instance + .log("feeForOneOutput: $feeForOneOutput", level: LogLevel.Info); + + if (satoshisBeingUsed - satoshiAmountToSend > feeForOneOutput) { + if (satoshisBeingUsed - satoshiAmountToSend > + feeForOneOutput + DUST_LIMIT) { + // Here, we know that theoretically, we may be able to include another output(change) but we first need to + // factor in the value of this output in satoshis. + int changeOutputSize = + satoshisBeingUsed - satoshiAmountToSend - feeForTwoOutputs; + // We check to see if the user can pay for the new transaction with 2 outputs instead of one. If they can and + // the second output's size > DUST_LIMIT satoshis, we perform the mechanics required to properly generate and use a new + // change address. + if (changeOutputSize > DUST_LIMIT && + satoshisBeingUsed - satoshiAmountToSend - changeOutputSize == + feeForTwoOutputs) { + // generate new change address if current change address has been used + await _checkChangeAddressForTransactions(DerivePathType.bip84); + final String newChangeAddress = + await _getCurrentAddressForChain(1, DerivePathType.bip84); + + int feeBeingPaid = + satoshisBeingUsed - satoshiAmountToSend - changeOutputSize; + + recipientsArray.add(newChangeAddress); + recipientsAmtArray.add(changeOutputSize); + // At this point, we have the outputs we're going to use, the amounts to send along with which addresses + // we intend to send these amounts to. We have enough to send instructions to build the transaction. + Logging.instance.log('2 outputs in tx', level: LogLevel.Info); + Logging.instance + .log('Input size: $satoshisBeingUsed', level: LogLevel.Info); + Logging.instance.log('Recipient output size: $satoshiAmountToSend', + level: LogLevel.Info); + Logging.instance.log('Change Output Size: $changeOutputSize', + level: LogLevel.Info); + Logging.instance.log( + 'Difference (fee being paid): $feeBeingPaid sats', + level: LogLevel.Info); + Logging.instance + .log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); + dynamic txn = await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: recipientsArray, + satoshiAmounts: recipientsAmtArray, + ); + + // make sure minimum fee is accurate if that is being used + if (txn["vSize"] - feeBeingPaid == 1) { + int changeOutputSize = + satoshisBeingUsed - satoshiAmountToSend - (txn["vSize"] as int); + feeBeingPaid = + satoshisBeingUsed - satoshiAmountToSend - changeOutputSize; + recipientsAmtArray.removeLast(); + recipientsAmtArray.add(changeOutputSize); + Logging.instance.log('Adjusted Input size: $satoshisBeingUsed', + level: LogLevel.Info); + Logging.instance.log( + 'Adjusted Recipient output size: $satoshiAmountToSend', + level: LogLevel.Info); + Logging.instance.log( + 'Adjusted Change Output Size: $changeOutputSize', + level: LogLevel.Info); + Logging.instance.log( + 'Adjusted Difference (fee being paid): $feeBeingPaid sats', + level: LogLevel.Info); + Logging.instance.log('Adjusted Estimated fee: $feeForTwoOutputs', + level: LogLevel.Info); + txn = await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: recipientsArray, + satoshiAmounts: recipientsAmtArray, + ); + } + + Map<String, dynamic> transactionObject = { + "hex": txn["hex"], + "recipient": recipientsArray[0], + "recipientAmt": recipientsAmtArray[0], + "fee": feeBeingPaid, + "vSize": txn["vSize"], + }; + return transactionObject; + } else { + // Something went wrong here. It either overshot or undershot the estimated fee amount or the changeOutputSize + // is smaller than or equal to DUST_LIMIT. Revert to single output transaction. + Logging.instance.log('1 output in tx', level: LogLevel.Info); + Logging.instance + .log('Input size: $satoshisBeingUsed', level: LogLevel.Info); + Logging.instance.log('Recipient output size: $satoshiAmountToSend', + level: LogLevel.Info); + Logging.instance.log( + 'Difference (fee being paid): ${satoshisBeingUsed - satoshiAmountToSend} sats', + level: LogLevel.Info); + Logging.instance + .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); + dynamic txn = await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: recipientsArray, + satoshiAmounts: recipientsAmtArray, + ); + Map<String, dynamic> transactionObject = { + "hex": txn["hex"], + "recipient": recipientsArray[0], + "recipientAmt": recipientsAmtArray[0], + "fee": satoshisBeingUsed - satoshiAmountToSend, + "vSize": txn["vSize"], + }; + return transactionObject; + } + } else { + // No additional outputs needed since adding one would mean that it'd be smaller than DUST_LIMIT sats + // which makes it uneconomical to add to the transaction. Here, we pass data directly to instruct + // the wallet to begin crafting the transaction that the user requested. + Logging.instance.log('1 output in tx', level: LogLevel.Info); + Logging.instance + .log('Input size: $satoshisBeingUsed', level: LogLevel.Info); + Logging.instance.log('Recipient output size: $satoshiAmountToSend', + level: LogLevel.Info); + Logging.instance.log( + 'Difference (fee being paid): ${satoshisBeingUsed - satoshiAmountToSend} sats', + level: LogLevel.Info); + Logging.instance + .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); + dynamic txn = await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: recipientsArray, + satoshiAmounts: recipientsAmtArray, + ); + Map<String, dynamic> transactionObject = { + "hex": txn["hex"], + "recipient": recipientsArray[0], + "recipientAmt": recipientsAmtArray[0], + "fee": satoshisBeingUsed - satoshiAmountToSend, + "vSize": txn["vSize"], + }; + return transactionObject; + } + } else if (satoshisBeingUsed - satoshiAmountToSend == feeForOneOutput) { + // In this scenario, no additional change output is needed since inputs - outputs equal exactly + // what we need to pay for fees. Here, we pass data directly to instruct the wallet to begin + // crafting the transaction that the user requested. + Logging.instance.log('1 output in tx', level: LogLevel.Info); + Logging.instance + .log('Input size: $satoshisBeingUsed', level: LogLevel.Info); + Logging.instance.log('Recipient output size: $satoshiAmountToSend', + level: LogLevel.Info); + Logging.instance.log( + 'Fee being paid: ${satoshisBeingUsed - satoshiAmountToSend} sats', + level: LogLevel.Info); + Logging.instance + .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); + dynamic txn = await buildTransaction( + utxosToUse: utxoObjectsToUse, + utxoSigningData: utxoSigningData, + recipients: recipientsArray, + satoshiAmounts: recipientsAmtArray, + ); + Map<String, dynamic> transactionObject = { + "hex": txn["hex"], + "recipient": recipientsArray[0], + "recipientAmt": recipientsAmtArray[0], + "fee": feeForOneOutput, + "vSize": txn["vSize"], + }; + return transactionObject; + } else { + // Remember that returning 2 indicates that the user does not have a sufficient balance to + // pay for the transaction fee. Ideally, at this stage, we should check if the user has any + // additional outputs they're able to spend and then recalculate fees. + Logging.instance.log( + 'Cannot pay tx fee - checking for more outputs and trying again', + level: LogLevel.Warning); + // try adding more outputs + if (spendableOutputs.length > inputsBeingConsumed) { + return coinSelection(satoshiAmountToSend, selectedTxFeeRate, + _recipientAddress, isSendAll, + additionalOutputs: additionalOutputs + 1, utxos: utxos); + } + return 2; + } + } + + Future<Map<String, dynamic>> fetchBuildTxData( + List<UtxoObject> utxosToUse, + ) async { + // return data + Map<String, dynamic> results = {}; + Map<String, List<String>> addressTxid = {}; + + // addresses to check + List<String> addressesP2PKH = []; + List<String> addressesP2SH = []; + List<String> addressesP2WPKH = []; + + try { + // Populating the addresses to check + for (var i = 0; i < utxosToUse.length; i++) { + final txid = utxosToUse[i].txid; + final tx = await _cachedElectrumXClient.getTransaction( + txHash: txid, + coin: coin, + ); + + for (final output in tx["vout"] as List) { + final n = output["n"]; + if (n != null && n == utxosToUse[i].vout) { + final address = output["scriptPubKey"]["address"] as String; + if (!addressTxid.containsKey(address)) { + addressTxid[address] = <String>[]; + } + (addressTxid[address] as List).add(txid); + switch (addressType(address: address)) { + case DerivePathType.bip44: + addressesP2PKH.add(address); + break; + case DerivePathType.bip49: + addressesP2SH.add(address); + break; + case DerivePathType.bip84: + addressesP2WPKH.add(address); + break; + } + } + } + } + + // p2pkh / bip44 + final p2pkhLength = addressesP2PKH.length; + if (p2pkhLength > 0) { + final receiveDerivations = await _fetchDerivations( + chain: 0, + derivePathType: DerivePathType.bip44, + ); + final changeDerivations = await _fetchDerivations( + chain: 1, + derivePathType: DerivePathType.bip44, + ); + for (int i = 0; i < p2pkhLength; i++) { + // receives + final receiveDerivation = receiveDerivations[addressesP2PKH[i]]; + // if a match exists it will not be null + if (receiveDerivation != null) { + final data = P2PKH( + data: PaymentData( + pubkey: Format.stringToUint8List( + receiveDerivation["pubKey"] as String)), + network: _network, + ).data; + + for (String tx in addressTxid[addressesP2PKH[i]]!) { + results[tx] = { + "output": data.output, + "keyPair": ECPair.fromWIF( + receiveDerivation["wif"] as String, + network: _network, + ), + }; + } + } else { + // if its not a receive, check change + final changeDerivation = changeDerivations[addressesP2PKH[i]]; + // if a match exists it will not be null + if (changeDerivation != null) { + final data = P2PKH( + data: PaymentData( + pubkey: Format.stringToUint8List( + changeDerivation["pubKey"] as String)), + network: _network, + ).data; + + for (String tx in addressTxid[addressesP2PKH[i]]!) { + results[tx] = { + "output": data.output, + "keyPair": ECPair.fromWIF( + changeDerivation["wif"] as String, + network: _network, + ), + }; + } + } + } + } + } + + // p2sh / bip49 + final p2shLength = addressesP2SH.length; + if (p2shLength > 0) { + final receiveDerivations = await _fetchDerivations( + chain: 0, + derivePathType: DerivePathType.bip49, + ); + final changeDerivations = await _fetchDerivations( + chain: 1, + derivePathType: DerivePathType.bip49, + ); + for (int i = 0; i < p2shLength; i++) { + // receives + final receiveDerivation = receiveDerivations[addressesP2SH[i]]; + // if a match exists it will not be null + if (receiveDerivation != null) { + final p2wpkh = P2WPKH( + data: PaymentData( + pubkey: Format.stringToUint8List( + receiveDerivation["pubKey"] as String)), + network: _network) + .data; + + final redeemScript = p2wpkh.output; + + final data = + P2SH(data: PaymentData(redeem: p2wpkh), network: _network).data; + + for (String tx in addressTxid[addressesP2SH[i]]!) { + results[tx] = { + "output": data.output, + "keyPair": ECPair.fromWIF( + receiveDerivation["wif"] as String, + network: _network, + ), + "redeemScript": redeemScript, + }; + } + } else { + // if its not a receive, check change + final changeDerivation = changeDerivations[addressesP2SH[i]]; + // if a match exists it will not be null + if (changeDerivation != null) { + final p2wpkh = P2WPKH( + data: PaymentData( + pubkey: Format.stringToUint8List( + changeDerivation["pubKey"] as String)), + network: _network) + .data; + + final redeemScript = p2wpkh.output; + + final data = + P2SH(data: PaymentData(redeem: p2wpkh), network: _network) + .data; + + for (String tx in addressTxid[addressesP2SH[i]]!) { + results[tx] = { + "output": data.output, + "keyPair": ECPair.fromWIF( + changeDerivation["wif"] as String, + network: _network, + ), + "redeemScript": redeemScript, + }; + } + } + } + } + } + + // p2wpkh / bip84 + final p2wpkhLength = addressesP2WPKH.length; + if (p2wpkhLength > 0) { + final receiveDerivations = await _fetchDerivations( + chain: 0, + derivePathType: DerivePathType.bip84, + ); + final changeDerivations = await _fetchDerivations( + chain: 1, + derivePathType: DerivePathType.bip84, + ); + + for (int i = 0; i < p2wpkhLength; i++) { + // receives + final receiveDerivation = receiveDerivations[addressesP2WPKH[i]]; + // if a match exists it will not be null + if (receiveDerivation != null) { + final data = P2WPKH( + data: PaymentData( + pubkey: Format.stringToUint8List( + receiveDerivation["pubKey"] as String)), + network: _network, + ).data; + + for (String tx in addressTxid[addressesP2WPKH[i]]!) { + results[tx] = { + "output": data.output, + "keyPair": ECPair.fromWIF( + receiveDerivation["wif"] as String, + network: _network, + ), + }; + } + } else { + // if its not a receive, check change + final changeDerivation = changeDerivations[addressesP2WPKH[i]]; + // if a match exists it will not be null + if (changeDerivation != null) { + final data = P2WPKH( + data: PaymentData( + pubkey: Format.stringToUint8List( + changeDerivation["pubKey"] as String)), + network: _network, + ).data; + + for (String tx in addressTxid[addressesP2WPKH[i]]!) { + results[tx] = { + "output": data.output, + "keyPair": ECPair.fromWIF( + changeDerivation["wif"] as String, + network: _network, + ), + }; + } + } + } + } + } + + return results; + } catch (e, s) { + Logging.instance + .log("fetchBuildTxData() threw: $e,\n$s", level: LogLevel.Error); + rethrow; + } + } + + /// Builds and signs a transaction + Future<Map<String, dynamic>> buildTransaction({ + required List<UtxoObject> utxosToUse, + required Map<String, dynamic> utxoSigningData, + required List<String> recipients, + required List<int> satoshiAmounts, + }) async { + Logging.instance + .log("Starting buildTransaction ----------", level: LogLevel.Info); + + final txb = TransactionBuilder(network: _network); + txb.setVersion(1); + + // Add transaction inputs + for (var i = 0; i < utxosToUse.length; i++) { + final txid = utxosToUse[i].txid; + txb.addInput(txid, utxosToUse[i].vout, null, + utxoSigningData[txid]["output"] as Uint8List); + } + + // Add transaction output + for (var i = 0; i < recipients.length; i++) { + txb.addOutput(recipients[i], satoshiAmounts[i]); + } + + try { + // Sign the transaction accordingly + for (var i = 0; i < utxosToUse.length; i++) { + final txid = utxosToUse[i].txid; + txb.sign( + vin: i, + keyPair: utxoSigningData[txid]["keyPair"] as ECPair, + witnessValue: utxosToUse[i].value, + redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?, + ); + } + } catch (e, s) { + Logging.instance.log("Caught exception while signing transaction: $e\n$s", + level: LogLevel.Error); + rethrow; + } + + final builtTx = txb.build(); + final vSize = builtTx.virtualSize(); + + return {"hex": builtTx.toHex(), "vSize": vSize}; + } + + @override + Future<void> fullRescan( + int maxUnusedAddressGap, + int maxNumberOfIndexesToCheck, + ) async { + Logging.instance.log("Starting full rescan!", level: LogLevel.Info); + longMutex = true; + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.syncing, + walletId, + coin, + ), + ); + + // clear cache + await _cachedElectrumXClient.clearSharedTransactionCache(coin: coin); + + // back up data + await _rescanBackup(); + + try { + final mnemonic = await _secureStore.read(key: '${_walletId}_mnemonic'); + await _recoverWalletFromBIP32SeedPhrase( + mnemonic: mnemonic!, + maxUnusedAddressGap: maxUnusedAddressGap, + maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck, + ); + + longMutex = false; + Logging.instance.log("Full rescan complete!", level: LogLevel.Info); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.synced, + walletId, + coin, + ), + ); + } catch (e, s) { + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.unableToSync, + walletId, + coin, + ), + ); + + // restore from backup + await _rescanRestore(); + + longMutex = false; + Logging.instance.log("Exception rethrown from fullRescan(): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + Future<void> _rescanRestore() async { + Logging.instance.log("starting rescan restore", level: LogLevel.Info); + + // restore from backup + // p2pkh + final tempReceivingAddressesP2PKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'receivingAddressesP2PKH_BACKUP'); + final tempChangeAddressesP2PKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'changeAddressesP2PKH_BACKUP'); + final tempReceivingIndexP2PKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'receivingIndexP2PKH_BACKUP'); + final tempChangeIndexP2PKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'changeIndexP2PKH_BACKUP'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingAddressesP2PKH', + value: tempReceivingAddressesP2PKH); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeAddressesP2PKH', + value: tempChangeAddressesP2PKH); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingIndexP2PKH', + value: tempReceivingIndexP2PKH); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeIndexP2PKH', + value: tempChangeIndexP2PKH); + await DB.instance.delete<dynamic>( + key: 'receivingAddressesP2PKH_BACKUP', boxName: walletId); + await DB.instance + .delete<dynamic>(key: 'changeAddressesP2PKH_BACKUP', boxName: walletId); + await DB.instance + .delete<dynamic>(key: 'receivingIndexP2PKH_BACKUP', boxName: walletId); + await DB.instance + .delete<dynamic>(key: 'changeIndexP2PKH_BACKUP', boxName: walletId); + + // p2Sh + final tempReceivingAddressesP2SH = DB.instance + .get<dynamic>(boxName: walletId, key: 'receivingAddressesP2SH_BACKUP'); + final tempChangeAddressesP2SH = DB.instance + .get<dynamic>(boxName: walletId, key: 'changeAddressesP2SH_BACKUP'); + final tempReceivingIndexP2SH = DB.instance + .get<dynamic>(boxName: walletId, key: 'receivingIndexP2SH_BACKUP'); + final tempChangeIndexP2SH = DB.instance + .get<dynamic>(boxName: walletId, key: 'changeIndexP2SH_BACKUP'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingAddressesP2SH', + value: tempReceivingAddressesP2SH); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeAddressesP2SH', + value: tempChangeAddressesP2SH); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingIndexP2SH', + value: tempReceivingIndexP2SH); + await DB.instance.put<dynamic>( + boxName: walletId, key: 'changeIndexP2SH', value: tempChangeIndexP2SH); + await DB.instance.delete<dynamic>( + key: 'receivingAddressesP2SH_BACKUP', boxName: walletId); + await DB.instance + .delete<dynamic>(key: 'changeAddressesP2SH_BACKUP', boxName: walletId); + await DB.instance + .delete<dynamic>(key: 'receivingIndexP2SH_BACKUP', boxName: walletId); + await DB.instance + .delete<dynamic>(key: 'changeIndexP2SH_BACKUP', boxName: walletId); + + // p2wpkh + final tempReceivingAddressesP2WPKH = DB.instance.get<dynamic>( + boxName: walletId, key: 'receivingAddressesP2WPKH_BACKUP'); + final tempChangeAddressesP2WPKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'changeAddressesP2WPKH_BACKUP'); + final tempReceivingIndexP2WPKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'receivingIndexP2WPKH_BACKUP'); + final tempChangeIndexP2WPKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'changeIndexP2WPKH_BACKUP'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingAddressesP2WPKH', + value: tempReceivingAddressesP2WPKH); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeAddressesP2WPKH', + value: tempChangeAddressesP2WPKH); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingIndexP2WPKH', + value: tempReceivingIndexP2WPKH); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeIndexP2WPKH', + value: tempChangeIndexP2WPKH); + await DB.instance.delete<dynamic>( + key: 'receivingAddressesP2WPKH_BACKUP', boxName: walletId); + await DB.instance.delete<dynamic>( + key: 'changeAddressesP2WPKH_BACKUP', boxName: walletId); + await DB.instance + .delete<dynamic>(key: 'receivingIndexP2WPKH_BACKUP', boxName: walletId); + await DB.instance + .delete<dynamic>(key: 'changeIndexP2WPKH_BACKUP', boxName: walletId); + + // P2PKH derivations + final p2pkhReceiveDerivationsString = await _secureStore.read( + key: "${walletId}_receiveDerivationsP2PKH_BACKUP"); + final p2pkhChangeDerivationsString = await _secureStore.read( + key: "${walletId}_changeDerivationsP2PKH_BACKUP"); + + await _secureStore.write( + key: "${walletId}_receiveDerivationsP2PKH", + value: p2pkhReceiveDerivationsString); + await _secureStore.write( + key: "${walletId}_changeDerivationsP2PKH", + value: p2pkhChangeDerivationsString); + + await _secureStore.delete( + key: "${walletId}_receiveDerivationsP2PKH_BACKUP"); + await _secureStore.delete(key: "${walletId}_changeDerivationsP2PKH_BACKUP"); + + // P2SH derivations + final p2shReceiveDerivationsString = await _secureStore.read( + key: "${walletId}_receiveDerivationsP2SH_BACKUP"); + final p2shChangeDerivationsString = await _secureStore.read( + key: "${walletId}_changeDerivationsP2SH_BACKUP"); + + await _secureStore.write( + key: "${walletId}_receiveDerivationsP2SH", + value: p2shReceiveDerivationsString); + await _secureStore.write( + key: "${walletId}_changeDerivationsP2SH", + value: p2shChangeDerivationsString); + + await _secureStore.delete(key: "${walletId}_receiveDerivationsP2SH_BACKUP"); + await _secureStore.delete(key: "${walletId}_changeDerivationsP2SH_BACKUP"); + + // P2WPKH derivations + final p2wpkhReceiveDerivationsString = await _secureStore.read( + key: "${walletId}_receiveDerivationsP2WPKH_BACKUP"); + final p2wpkhChangeDerivationsString = await _secureStore.read( + key: "${walletId}_changeDerivationsP2WPKH_BACKUP"); + + await _secureStore.write( + key: "${walletId}_receiveDerivationsP2WPKH", + value: p2wpkhReceiveDerivationsString); + await _secureStore.write( + key: "${walletId}_changeDerivationsP2WPKH", + value: p2wpkhChangeDerivationsString); + + await _secureStore.delete( + key: "${walletId}_receiveDerivationsP2WPKH_BACKUP"); + await _secureStore.delete( + key: "${walletId}_changeDerivationsP2WPKH_BACKUP"); + + // UTXOs + final utxoData = DB.instance + .get<dynamic>(boxName: walletId, key: 'latest_utxo_model_BACKUP'); + await DB.instance.put<dynamic>( + boxName: walletId, key: 'latest_utxo_model', value: utxoData); + await DB.instance + .delete<dynamic>(key: 'latest_utxo_model_BACKUP', boxName: walletId); + + Logging.instance.log("rescan restore complete", level: LogLevel.Info); + } + + Future<void> _rescanBackup() async { + Logging.instance.log("starting rescan backup", level: LogLevel.Info); + + // backup current and clear data + // p2pkh + final tempReceivingAddressesP2PKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'receivingAddressesP2PKH'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingAddressesP2PKH_BACKUP', + value: tempReceivingAddressesP2PKH); + await DB.instance + .delete<dynamic>(key: 'receivingAddressesP2PKH', boxName: walletId); + + final tempChangeAddressesP2PKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'changeAddressesP2PKH'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeAddressesP2PKH_BACKUP', + value: tempChangeAddressesP2PKH); + await DB.instance + .delete<dynamic>(key: 'changeAddressesP2PKH', boxName: walletId); + + final tempReceivingIndexP2PKH = + DB.instance.get<dynamic>(boxName: walletId, key: 'receivingIndexP2PKH'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingIndexP2PKH_BACKUP', + value: tempReceivingIndexP2PKH); + await DB.instance + .delete<dynamic>(key: 'receivingIndexP2PKH', boxName: walletId); + + final tempChangeIndexP2PKH = + DB.instance.get<dynamic>(boxName: walletId, key: 'changeIndexP2PKH'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeIndexP2PKH_BACKUP', + value: tempChangeIndexP2PKH); + await DB.instance + .delete<dynamic>(key: 'changeIndexP2PKH', boxName: walletId); + + // p2sh + final tempReceivingAddressesP2SH = DB.instance + .get<dynamic>(boxName: walletId, key: 'receivingAddressesP2SH'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingAddressesP2SH_BACKUP', + value: tempReceivingAddressesP2SH); + await DB.instance + .delete<dynamic>(key: 'receivingAddressesP2SH', boxName: walletId); + + final tempChangeAddressesP2SH = + DB.instance.get<dynamic>(boxName: walletId, key: 'changeAddressesP2SH'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeAddressesP2SH_BACKUP', + value: tempChangeAddressesP2SH); + await DB.instance + .delete<dynamic>(key: 'changeAddressesP2SH', boxName: walletId); + + final tempReceivingIndexP2SH = + DB.instance.get<dynamic>(boxName: walletId, key: 'receivingIndexP2SH'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingIndexP2SH_BACKUP', + value: tempReceivingIndexP2SH); + await DB.instance + .delete<dynamic>(key: 'receivingIndexP2SH', boxName: walletId); + + final tempChangeIndexP2SH = + DB.instance.get<dynamic>(boxName: walletId, key: 'changeIndexP2SH'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeIndexP2SH_BACKUP', + value: tempChangeIndexP2SH); + await DB.instance + .delete<dynamic>(key: 'changeIndexP2SH', boxName: walletId); + + // p2wpkh + final tempReceivingAddressesP2WPKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'receivingAddressesP2WPKH'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingAddressesP2WPKH_BACKUP', + value: tempReceivingAddressesP2WPKH); + await DB.instance + .delete<dynamic>(key: 'receivingAddressesP2WPKH', boxName: walletId); + + final tempChangeAddressesP2WPKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'changeAddressesP2WPKH'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeAddressesP2WPKH_BACKUP', + value: tempChangeAddressesP2WPKH); + await DB.instance + .delete<dynamic>(key: 'changeAddressesP2WPKH', boxName: walletId); + + final tempReceivingIndexP2WPKH = DB.instance + .get<dynamic>(boxName: walletId, key: 'receivingIndexP2WPKH'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'receivingIndexP2WPKH_BACKUP', + value: tempReceivingIndexP2WPKH); + await DB.instance + .delete<dynamic>(key: 'receivingIndexP2WPKH', boxName: walletId); + + final tempChangeIndexP2WPKH = + DB.instance.get<dynamic>(boxName: walletId, key: 'changeIndexP2WPKH'); + await DB.instance.put<dynamic>( + boxName: walletId, + key: 'changeIndexP2WPKH_BACKUP', + value: tempChangeIndexP2WPKH); + await DB.instance + .delete<dynamic>(key: 'changeIndexP2WPKH', boxName: walletId); + + // P2PKH derivations + final p2pkhReceiveDerivationsString = + await _secureStore.read(key: "${walletId}_receiveDerivationsP2PKH"); + final p2pkhChangeDerivationsString = + await _secureStore.read(key: "${walletId}_changeDerivationsP2PKH"); + + await _secureStore.write( + key: "${walletId}_receiveDerivationsP2PKH_BACKUP", + value: p2pkhReceiveDerivationsString); + await _secureStore.write( + key: "${walletId}_changeDerivationsP2PKH_BACKUP", + value: p2pkhChangeDerivationsString); + + await _secureStore.delete(key: "${walletId}_receiveDerivationsP2PKH"); + await _secureStore.delete(key: "${walletId}_changeDerivationsP2PKH"); + + // P2SH derivations + final p2shReceiveDerivationsString = + await _secureStore.read(key: "${walletId}_receiveDerivationsP2SH"); + final p2shChangeDerivationsString = + await _secureStore.read(key: "${walletId}_changeDerivationsP2SH"); + + await _secureStore.write( + key: "${walletId}_receiveDerivationsP2SH_BACKUP", + value: p2shReceiveDerivationsString); + await _secureStore.write( + key: "${walletId}_changeDerivationsP2SH_BACKUP", + value: p2shChangeDerivationsString); + + await _secureStore.delete(key: "${walletId}_receiveDerivationsP2SH"); + await _secureStore.delete(key: "${walletId}_changeDerivationsP2SH"); + + // P2WPKH derivations + final p2wpkhReceiveDerivationsString = + await _secureStore.read(key: "${walletId}_receiveDerivationsP2WPKH"); + final p2wpkhChangeDerivationsString = + await _secureStore.read(key: "${walletId}_changeDerivationsP2WPKH"); + + await _secureStore.write( + key: "${walletId}_receiveDerivationsP2WPKH_BACKUP", + value: p2wpkhReceiveDerivationsString); + await _secureStore.write( + key: "${walletId}_changeDerivationsP2WPKH_BACKUP", + value: p2wpkhChangeDerivationsString); + + await _secureStore.delete(key: "${walletId}_receiveDerivationsP2WPKH"); + await _secureStore.delete(key: "${walletId}_changeDerivationsP2WPKH"); + + // UTXOs + final utxoData = + DB.instance.get<dynamic>(boxName: walletId, key: 'latest_utxo_model'); + await DB.instance.put<dynamic>( + boxName: walletId, key: 'latest_utxo_model_BACKUP', value: utxoData); + await DB.instance + .delete<dynamic>(key: 'latest_utxo_model', boxName: walletId); + + Logging.instance.log("rescan backup complete", level: LogLevel.Info); + } + + bool isActive = false; + + @override + void Function(bool)? get onIsActiveWalletChanged => + (isActive) => this.isActive = isActive; + + @override + Future<int> estimateFeeFor(int satoshiAmount, int feeRate) async { + final available = Format.decimalAmountToSatoshis(await availableBalance); + + if (available == satoshiAmount) { + return satoshiAmount - sweepAllEstimate(feeRate); + } else if (satoshiAmount <= 0 || satoshiAmount > available) { + return roughFeeEstimate(1, 2, feeRate); + } + + int runningBalance = 0; + int inputCount = 0; + for (final output in outputsList) { + runningBalance += output.value; + inputCount++; + if (runningBalance > satoshiAmount) { + break; + } + } + + final oneOutPutFee = roughFeeEstimate(inputCount, 1, feeRate); + final twoOutPutFee = roughFeeEstimate(inputCount, 2, feeRate); + + if (runningBalance - satoshiAmount > oneOutPutFee) { + if (runningBalance - satoshiAmount > oneOutPutFee + DUST_LIMIT) { + final change = runningBalance - satoshiAmount - twoOutPutFee; + if (change > DUST_LIMIT && + runningBalance - satoshiAmount - change == twoOutPutFee) { + return runningBalance - satoshiAmount - change; + } else { + return runningBalance - satoshiAmount; + } + } else { + return runningBalance - satoshiAmount; + } + } else if (runningBalance - satoshiAmount == oneOutPutFee) { + return oneOutPutFee; + } else { + return twoOutPutFee; + } + } + + int roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + return ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * + (feeRatePerKB / 1000).ceil(); + } + + int sweepAllEstimate(int feeRate) { + int available = 0; + int inputCount = 0; + for (final output in outputsList) { + if (output.status.confirmed) { + available += output.value; + inputCount++; + } + } + + // transaction will only have 1 output minus the fee + final estimatedFee = roughFeeEstimate(inputCount, 1, feeRate); + + return available - estimatedFee; + } + + @override + Future<bool> generateNewAddress() async { + try { + await _incrementAddressIndexForChain( + 0, DerivePathType.bip84); // First increment the receiving index + final newReceivingIndex = DB.instance.get<dynamic>( + boxName: walletId, + key: 'receivingIndexP2WPKH') as int; // Check the new receiving index + final newReceivingAddress = await _generateAddressForChain( + 0, + newReceivingIndex, + DerivePathType + .bip84); // Use new index to derive a new receiving address + await _addToAddressesArrayForChain( + newReceivingAddress, + 0, + DerivePathType + .bip84); // Add that new receiving address to the array of receiving addresses + _currentReceivingAddress = Future(() => + newReceivingAddress); // Set the new receiving address that the service + + return true; + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from generateNewAddress(): $e\n$s", + level: LogLevel.Error); + return false; + } + } +} From 8549eda1ed31c8510acab4195a35ce110e36fde4 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 28 Oct 2022 09:16:35 -0600 Subject: [PATCH 058/426] desktop stack experience view layout --- lib/pages/stack_privacy_calls.dart | 458 +++++++++++++++++------------ lib/utilities/text_styles.dart | 19 ++ 2 files changed, 285 insertions(+), 192 deletions(-) diff --git a/lib/pages/stack_privacy_calls.dart b/lib/pages/stack_privacy_calls.dart index 7ca21c494..533f8344d 100644 --- a/lib/pages/stack_privacy_calls.dart +++ b/lib/pages/stack_privacy_calls.dart @@ -8,16 +8,18 @@ import 'package:stackwallet/pages_desktop_specific/create_password/create_passwo import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.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/rounded_white_container.dart'; import '../hive/db.dart'; import '../providers/global/price_provider.dart'; import '../services/exchange/exchange_data_loading_service.dart'; +import '../widgets/desktop/primary_button.dart'; class StackPrivacyCalls extends ConsumerStatefulWidget { const StackPrivacyCalls({ @@ -53,155 +55,195 @@ class _StackPrivacyCalls extends ConsumerState<StackPrivacyCalls> { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ), + return MasterScaffold( + background: Theme.of(context).extension<StackColors>()!.background, + isDesktop: isDesktop, + appBar: isDesktop + ? const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + ) + : AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), body: SafeArea( child: Padding( - padding: const EdgeInsets.fromLTRB(0, 40, 0, 0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - "Choose your Stack experience", - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 8, - ), - Text( - "You can change it later in Settings", - style: STextStyles.subtitle(context), - ), - const SizedBox( - height: 36, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, + padding: EdgeInsets.fromLTRB(0, isDesktop ? 0 : 40, 0, 0), + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: isDesktop ? 480 : double.infinity, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Choose your Stack experience", + style: isDesktop + ? STextStyles.desktopH2(context) + : STextStyles.pageTitleH1(context), ), - child: PrivacyToggle( - externalCallsEnabled: isEasy, - onChanged: (externalCalls) { - isEasy = externalCalls; - setState(() { - infoToggle = isEasy; - }); - }, + SizedBox( + height: isDesktop ? 16 : 8, ), - ), - const SizedBox( - height: 36, - ), - Padding( - padding: const EdgeInsets.all(16.0), - child: RoundedWhiteContainer( - child: Center( - child: RichText( - textAlign: TextAlign.left, - text: TextSpan( - style: - STextStyles.label(context).copyWith(fontSize: 12.0), - children: infoToggle - ? [ - const TextSpan( - text: - "Exchange data preloaded for a seamless experience."), - const TextSpan( - text: - "\n\nCoinGecko enabled: (24 hour price change shown in-app, total wallet value shown in USD or other currency)."), - TextSpan( - text: - "\n\nRecommended for most crypto users.", - style: TextStyle( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - fontWeight: FontWeight.w600, - ), + Text( + "You can change it later in Settings", + style: isDesktop + ? STextStyles.desktopSubtitleH2(context) + : STextStyles.subtitle(context), + ), + SizedBox( + height: isDesktop ? 32 : 36, + ), + Padding( + padding: EdgeInsets.symmetric( + horizontal: isDesktop ? 0 : 16, + ), + child: PrivacyToggle( + externalCallsEnabled: isEasy, + onChanged: (externalCalls) { + isEasy = externalCalls; + setState(() { + infoToggle = isEasy; + }); + }, + ), + ), + SizedBox( + height: isDesktop ? 16 : 36, + ), + Padding( + padding: isDesktop + ? const EdgeInsets.all(0) + : const EdgeInsets.all(16.0), + child: RoundedWhiteContainer( + child: Center( + child: RichText( + textAlign: TextAlign.left, + text: TextSpan( + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.label(context).copyWith( + fontSize: 12.0, ), - ] - : [ - const TextSpan( + children: infoToggle + ? [ + const TextSpan( + text: + "Exchange data preloaded for a seamless experience."), + const TextSpan( + text: + "\n\nCoinGecko enabled: (24 hour price change shown in-app, total wallet value shown in USD or other currency)."), + TextSpan( text: - "Exchange data not preloaded (slower experience)."), - const TextSpan( - text: - "\n\nCoinGecko disabled (price changes not shown, no wallet value shown in other currencies)."), - TextSpan( - text: - "\n\nRecommended for the privacy conscious.", - style: TextStyle( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - fontWeight: FontWeight.w600, + "\n\nRecommended for most crypto users.", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall600( + context) + : TextStyle( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + fontWeight: FontWeight.w600, + ), ), - ), - ], + ] + : [ + const TextSpan( + text: + "Exchange data not preloaded (slower experience)."), + const TextSpan( + text: + "\n\nCoinGecko disabled (price changes not shown, no wallet value shown in other currencies)."), + TextSpan( + text: + "\n\nRecommended for the privacy conscious.", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall600( + context) + : TextStyle( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + fontWeight: FontWeight.w600, + ), + ), + ], + ), ), ), ), ), - ), - const Spacer( - flex: 4, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 16, - ), - child: Row( - children: [ - Expanded( - child: ContinueButton( - isDesktop: isDesktop, - label: !widget.isSettings ? "Continue" : "Save changes", - onPressed: () { - ref.read(prefsChangeNotifierProvider).externalCalls = - isEasy; + if (!isDesktop) + const Spacer( + flex: 4, + ), + if (isDesktop) + const SizedBox( + height: 32, + ), + Padding( + padding: isDesktop + ? const EdgeInsets.all(0) + : const EdgeInsets.symmetric( + horizontal: 16, + vertical: 16, + ), + child: Row( + children: [ + Expanded( + child: PrimaryButton( + label: + !widget.isSettings ? "Continue" : "Save changes", + onPressed: () { + ref + .read(prefsChangeNotifierProvider) + .externalCalls = isEasy; - DB.instance - .put<dynamic>( - boxName: DB.boxNamePrefs, - key: "externalCalls", - value: isEasy) - .then((_) { - if (isEasy) { - unawaited( - ExchangeDataLoadingService().loadAll(ref)); - ref - .read(priceAnd24hChangeNotifierProvider) - .start(true); - } - }); - if (!widget.isSettings) { - if (isDesktop) { - Navigator.of(context).pushNamed( - CreatePasswordView.routeName, - ); + DB.instance + .put<dynamic>( + boxName: DB.boxNamePrefs, + key: "externalCalls", + value: isEasy) + .then((_) { + if (isEasy) { + unawaited( + ExchangeDataLoadingService().loadAll(ref)); + ref + .read(priceAnd24hChangeNotifierProvider) + .start(true); + } + }); + if (!widget.isSettings) { + if (isDesktop) { + Navigator.of(context).pushNamed( + CreatePasswordView.routeName, + ); + } else { + Navigator.of(context).pushNamed( + CreatePinView.routeName, + ); + } } else { - Navigator.of(context).pushNamed( - CreatePinView.routeName, - ); + Navigator.pop(context); } - } else { - Navigator.pop(context); - } - }, + }, + ), ), - ), - ], + ], + ), ), - ), - ], + if (isDesktop) + const SizedBox( + height: kDesktopAppBarHeight, + ), + ], + ), ), ), ), @@ -226,8 +268,11 @@ class PrivacyToggle extends StatefulWidget { class _PrivacyToggleState extends State<PrivacyToggle> { late bool externalCallsEnabled; + late final bool isDesktop; + @override void initState() { + isDesktop = Util.isDesktop; // initial toggle state externalCallsEnabled = widget.externalCallsEnabled; super.initState(); @@ -270,24 +315,39 @@ class _PrivacyToggleState extends State<PrivacyToggle> { Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + if (isDesktop) + const SizedBox( + height: 10, + ), SvgPicture.asset( Assets.svg.personaEasy, - width: 140, - height: 140, + width: isDesktop ? 120 : 140, + height: isDesktop ? 120 : 140, ), - Center( - child: Text( - "Easy Crypto", - style: STextStyles.label(context).copyWith( - fontWeight: FontWeight.bold, + if (isDesktop) + const SizedBox( + height: 12, ), - )), + Center( + child: Text( + "Easy Crypto", + style: isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.label700(context), + ), + ), Center( child: Text( "Recommended", - style: STextStyles.label(context), + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.label(context), ), ), + if (isDesktop) + const SizedBox( + height: 12, + ), ], ), if (externalCallsEnabled) @@ -360,25 +420,39 @@ class _PrivacyToggleState extends State<PrivacyToggle> { Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + if (isDesktop) + const SizedBox( + height: 10, + ), SvgPicture.asset( Assets.svg.personaIncognito, - width: 140, - height: 140, + width: isDesktop ? 120 : 140, + height: isDesktop ? 120 : 140, ), + if (isDesktop) + const SizedBox( + height: 12, + ), Center( child: Text( "Incognito", - style: STextStyles.label(context).copyWith( - fontWeight: FontWeight.bold, - ), + style: isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.label700(context), ), ), Center( child: Text( "Privacy conscious", - style: STextStyles.label(context), + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.label(context), ), ), + if (isDesktop) + const SizedBox( + height: 12, + ), ], ), if (!externalCallsEnabled) @@ -419,49 +493,49 @@ class _PrivacyToggleState extends State<PrivacyToggle> { } } -class ContinueButton extends ConsumerWidget { - const ContinueButton({ - Key? key, - required this.isDesktop, - required this.onPressed, - required this.label, - }) : super(key: key); - - final String label; - final bool isDesktop; - final VoidCallback onPressed; - - @override - Widget build(BuildContext context, WidgetRef ref) { - if (isDesktop) { - return SizedBox( - width: 328, - height: 70, - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: onPressed, - child: Text( - label, - style: STextStyles.button(context).copyWith(fontSize: 20), - ), - ), - ); - } else { - return TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: onPressed, - child: Text( - label, - style: STextStyles.button(context), - ), - ); - } - } -} +// class ContinueButton extends ConsumerWidget { +// const ContinueButton({ +// Key? key, +// required this.isDesktop, +// required this.onPressed, +// required this.label, +// }) : super(key: key); +// +// final String label; +// final bool isDesktop; +// final VoidCallback onPressed; +// +// @override +// Widget build(BuildContext context, WidgetRef ref) { +// if (isDesktop) { +// return SizedBox( +// width: 328, +// height: 70, +// child: TextButton( +// style: Theme.of(context) +// .extension<StackColors>()! +// .getPrimaryEnabledButtonColor(context), +// onPressed: onPressed, +// child: Text( +// label, +// style: STextStyles.button(context).copyWith(fontSize: 20), +// ), +// ), +// ); +// } else { +// return TextButton( +// style: Theme.of(context) +// .extension<StackColors>()! +// .getPrimaryEnabledButtonColor(context), +// onPressed: onPressed, +// child: Text( +// label, +// style: STextStyles.button(context), +// ), +// ); +// } +// } +// } // class CustomRadio extends StatefulWidget { // CustomRadio(this.upperCall, {Key? key}) : super(key: key); diff --git a/lib/utilities/text_styles.dart b/lib/utilities/text_styles.dart index b583dba98..299ba5bec 100644 --- a/lib/utilities/text_styles.dart +++ b/lib/utilities/text_styles.dart @@ -755,6 +755,25 @@ class STextStyles { } } + static TextStyle desktopTextExtraExtraSmall600(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 14, + height: 21 / 14, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 14, + height: 21 / 14, + ); + } + } + static TextStyle desktopButtonSmallSecondaryEnabled(BuildContext context) { switch (_theme(context).themeType) { case ThemeType.light: From c0fddcd8226b54d147c48815602ea305ca4ac6a1 Mon Sep 17 00:00:00 2001 From: Marco <marco@cypherstack.com> Date: Fri, 28 Oct 2022 12:03:52 -0600 Subject: [PATCH 059/426] add litecoin support --- assets/images/litecoin.png | Bin 0 -> 329261 bytes assets/svg/coin_icons/Litecoin.svg | 11 ++ lib/main.dart | 103 ++++++------- lib/pages/exchange_view/send_from_view.dart | 2 + .../add_edit_node_view.dart | 2 + .../manage_nodes_views/node_details_view.dart | 2 + lib/services/coins/coin_service.dart | 22 +++ .../coins/litecoin/litecoin_wallet.dart | 138 +++++++++++------- lib/services/price.dart | 2 +- lib/utilities/address_utils.dart | 5 + lib/utilities/assets.dart | 8 + lib/utilities/block_explorers.dart | 4 + lib/utilities/constants.dart | 6 + lib/utilities/default_nodes.dart | 32 ++++ lib/utilities/enums/coin_enum.dart | 35 +++++ lib/utilities/theme/color_theme.dart | 4 + lib/utilities/theme/stack_colors.dart | 3 + pubspec.lock | 43 +++--- pubspec.yaml | 2 + test/price_test.dart | 20 +-- 20 files changed, 305 insertions(+), 139 deletions(-) create mode 100644 assets/images/litecoin.png create mode 100644 assets/svg/coin_icons/Litecoin.svg diff --git a/assets/images/litecoin.png b/assets/images/litecoin.png new file mode 100644 index 0000000000000000000000000000000000000000..17994bd47ed0244f5777f3f026759b1f266a8594 GIT binary patch literal 329261 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 S4$|U=p8v!CZ1owh)c*ndihia5 literal 0 HcmV?d00001 diff --git a/assets/svg/coin_icons/Litecoin.svg b/assets/svg/coin_icons/Litecoin.svg new file mode 100644 index 000000000..2b89ca50b --- /dev/null +++ b/assets/svg/coin_icons/Litecoin.svg @@ -0,0 +1,11 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0_6052_99642)"> +<rect width="24" height="24" rx="12" fill="white"/> +<path d="M11.9976 0C9.62389 0 7.30353 0.703873 5.3299 2.02261C3.35627 3.34135 1.81802 5.21572 0.909655 7.4087C0.00129377 9.60167 -0.236375 12.0148 0.226704 14.3428C0.689782 16.6709 1.83281 18.8093 3.51124 20.4878C5.18968 22.1662 7.32813 23.3092 9.65618 23.7723C11.9842 24.2354 14.3973 23.9977 16.5903 23.0893C18.7833 22.181 20.6577 20.6427 21.9764 18.6691C23.2951 16.6955 23.999 14.3751 23.999 12.0015C23.999 10.4254 23.6886 8.86478 23.0854 7.4087C22.4823 5.95261 21.5983 4.62958 20.4839 3.51514C19.3694 2.40071 18.0464 1.51669 16.5903 0.913556C15.1342 0.310427 13.5736 0 11.9976 0V0ZM12.1921 12.3963L10.9437 16.6087H17.6209C17.674 16.6088 17.7263 16.6213 17.7738 16.6451C17.8212 16.669 17.8625 16.7035 17.8943 16.746C17.9261 16.7885 17.9476 16.8378 17.9571 16.8901C17.9666 16.9423 17.9638 16.9961 17.9489 17.0471L17.3683 19.0473C17.3406 19.1429 17.2826 19.2268 17.203 19.2865C17.1234 19.3462 17.0265 19.3784 16.927 19.3783H6.72841L8.45286 13.5546L6.54551 14.1352L6.96646 12.7737L8.87671 12.1931L11.2979 4.0121C11.3245 3.91623 11.3818 3.8317 11.4609 3.77142C11.5401 3.71114 11.6368 3.67842 11.7363 3.67824H14.32C14.3731 3.67805 14.4254 3.69017 14.4729 3.71364C14.5205 3.73712 14.5619 3.77131 14.594 3.81352C14.6261 3.85573 14.6479 3.90482 14.6578 3.95691C14.6677 4.009 14.6654 4.06267 14.651 4.11371L12.6188 11.0318L14.5262 10.4512L14.1168 11.836L12.1921 12.3963Z" fill="#315D9E"/> +</g> +<defs> +<clipPath id="clip0_6052_99642"> +<rect width="24" height="24" rx="12" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/lib/main.dart b/lib/main.dart index aa2155478..58a287b31 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -68,7 +68,7 @@ final openedFromSWBFileStringStateProvider = void main() async { WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); GoogleFonts.config.allowRuntimeFetching = false; - if(Platform.isIOS){ + if (Platform.isIOS) { Util.libraryPath = await getLibraryDirectory(); } @@ -209,56 +209,59 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme> bool didLoad = false; Future<void> load() async { - if (didLoad) { - return; - } - didLoad = true; - - await DB.instance.init(); - await _prefs.init(); - - _notificationsService = ref.read(notificationsProvider); - _nodeService = ref.read(nodeServiceChangeNotifierProvider); - _tradesService = ref.read(tradesServiceProvider); - - NotificationApi.prefs = _prefs; - NotificationApi.notificationsService = _notificationsService; - - unawaited(ref.read(baseCurrenciesProvider).update()); - - await _nodeService.updateDefaults(); - await _notificationsService.init( - nodeService: _nodeService, - tradesService: _tradesService, - prefs: _prefs, - ); - ref.read(priceAnd24hChangeNotifierProvider).start(true); - await _wallets.load(_prefs); - loadingCompleter.complete(); - // TODO: this should probably run unawaited. Keep commented out for now as proper community nodes ui hasn't been implemented yet - // unawaited(_nodeService.updateCommunityNodes()); - - // run without awaiting - if (Constants.enableExchange && - _prefs.externalCalls && - await _prefs.isExternalCallsSet()) { - unawaited(ExchangeDataLoadingService().loadAll(ref)); - } - - if (_prefs.isAutoBackupEnabled) { - switch (_prefs.backupFrequencyType) { - case BackupFrequencyType.everyTenMinutes: - ref - .read(autoSWBServiceProvider) - .startPeriodicBackupTimer(duration: const Duration(minutes: 10)); - break; - case BackupFrequencyType.everyAppStart: - unawaited(ref.read(autoSWBServiceProvider).doBackup()); - break; - case BackupFrequencyType.afterClosingAWallet: - // ignore this case here - break; + try { + if (didLoad) { + return; } + didLoad = true; + + await DB.instance.init(); + await _prefs.init(); + + _notificationsService = ref.read(notificationsProvider); + _nodeService = ref.read(nodeServiceChangeNotifierProvider); + _tradesService = ref.read(tradesServiceProvider); + + NotificationApi.prefs = _prefs; + NotificationApi.notificationsService = _notificationsService; + + unawaited(ref.read(baseCurrenciesProvider).update()); + + await _nodeService.updateDefaults(); + await _notificationsService.init( + nodeService: _nodeService, + tradesService: _tradesService, + prefs: _prefs, + ); + ref.read(priceAnd24hChangeNotifierProvider).start(true); + await _wallets.load(_prefs); + loadingCompleter.complete(); + // TODO: this should probably run unawaited. Keep commented out for now as proper community nodes ui hasn't been implemented yet + // unawaited(_nodeService.updateCommunityNodes()); + + // run without awaiting + if (Constants.enableExchange && + _prefs.externalCalls && + await _prefs.isExternalCallsSet()) { + unawaited(ExchangeDataLoadingService().loadAll(ref)); + } + + if (_prefs.isAutoBackupEnabled) { + switch (_prefs.backupFrequencyType) { + case BackupFrequencyType.everyTenMinutes: + ref.read(autoSWBServiceProvider).startPeriodicBackupTimer( + duration: const Duration(minutes: 10)); + break; + case BackupFrequencyType.everyAppStart: + unawaited(ref.read(autoSWBServiceProvider).doBackup()); + break; + case BackupFrequencyType.afterClosingAWallet: + // ignore this case here + break; + } + } + } catch (e, s) { + Logger.print("$e $s", normalLength: false); } } diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart index 51c420c82..20fc81903 100644 --- a/lib/pages/exchange_view/send_from_view.dart +++ b/lib/pages/exchange_view/send_from_view.dart @@ -52,11 +52,13 @@ class _SendFromViewState extends ConsumerState<SendFromView> { switch (coin) { case Coin.bitcoin: case Coin.bitcoincash: + case Coin.litecoin: case Coin.dogecoin: case Coin.epicCash: case Coin.firo: case Coin.namecoin: case Coin.bitcoinTestNet: + case Coin.litecoinTestNet: case Coin.bitcoincashTestnet: case Coin.dogecoinTestNet: case Coin.firoTestNet: diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index 100c03e1b..ada3c45f9 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -531,11 +531,13 @@ class _NodeFormState extends ConsumerState<NodeForm> { // TODO: which coin servers can have username and password? switch (coin) { case Coin.bitcoin: + case Coin.litecoin: case Coin.dogecoin: case Coin.firo: case Coin.namecoin: case Coin.bitcoincash: case Coin.bitcoinTestNet: + case Coin.litecoinTestNet: case Coin.bitcoincashTestnet: case Coin.firoTestNet: case Coin.dogecoinTestNet: diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart index 8baddb700..c5f797e37 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart @@ -98,6 +98,7 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> { break; case Coin.bitcoin: + case Coin.litecoin: case Coin.dogecoin: case Coin.firo: case Coin.bitcoinTestNet: @@ -105,6 +106,7 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> { case Coin.dogecoinTestNet: case Coin.bitcoincash: case Coin.namecoin: + case Coin.litecoinTestNet: case Coin.bitcoincashTestnet: final client = ElectrumX( host: node!.host, diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart index 089b20a33..c36fa9eee 100644 --- a/lib/services/coins/coin_service.dart +++ b/lib/services/coins/coin_service.dart @@ -15,6 +15,8 @@ import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/prefs.dart'; +import 'litecoin/litecoin_wallet.dart'; + abstract class CoinServiceAPI { CoinServiceAPI(); @@ -90,6 +92,26 @@ abstract class CoinServiceAPI { tracker: tracker, ); + case Coin.litecoin: + return LitecoinWallet( + walletId: walletId, + walletName: walletName, + coin: coin, + client: client, + cachedClient: cachedClient, + tracker: tracker, + ); + + case Coin.litecoinTestNet: + return LitecoinWallet( + walletId: walletId, + walletName: walletName, + coin: coin, + client: client, + cachedClient: cachedClient, + tracker: tracker, + ); + case Coin.bitcoinTestNet: return BitcoinWallet( walletId: walletId, diff --git a/lib/services/coins/litecoin/litecoin_wallet.dart b/lib/services/coins/litecoin/litecoin_wallet.dart index da3bdfed0..0ab3a92a8 100644 --- a/lib/services/coins/litecoin/litecoin_wallet.dart +++ b/lib/services/coins/litecoin/litecoin_wallet.dart @@ -46,9 +46,9 @@ const int MINIMUM_CONFIRMATIONS = 1; const int DUST_LIMIT = 294; const String GENESIS_HASH_MAINNET = - "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"; + "12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2"; const String GENESIS_HASH_TESTNET = - "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"; + "4966625a4b2851d9fdee139e56211a0d88575f59ed816ff5e6a63deb4e3e29a0"; enum DerivePathType { bip44, bip49, bip84 } @@ -86,14 +86,14 @@ bip32.BIP32 getBip32NodeFromRoot( ) { String coinType; switch (root.network.wif) { - case 0x80: // btc mainnet wif - coinType = "0"; // btc mainnet + case 0xb0: // ltc mainnet wif + coinType = "2"; // ltc mainnet break; - case 0xef: // btc testnet wif - coinType = "1"; // btc testnet + case 0xef: // ltc testnet wif + coinType = "1"; // ltc testnet break; default: - throw Exception("Invalid Bitcoin network type used!"); + throw Exception("Invalid Litecoin network type used!"); } switch (derivePathType) { case DerivePathType.bip44: @@ -138,7 +138,7 @@ bip32.BIP32 getBip32RootWrapper(Tuple2<String, NetworkType> args) { return getBip32Root(args.item1, args.item2); } -class BitcoinWallet extends CoinServiceAPI { +class LitecoinWallet extends CoinServiceAPI { static const integrationTestFlag = bool.fromEnvironment("IS_INTEGRATION_TEST"); @@ -151,10 +151,10 @@ class BitcoinWallet extends CoinServiceAPI { NetworkType get _network { switch (coin) { - case Coin.bitcoin: - return bitcoin; - case Coin.bitcoinTestNet: - return testnet; + case Coin.litecoin: + return litecoin; + case Coin.litecoinTestNet: + return litecointestnet; default: throw Exception("Invalid network type!"); } @@ -314,7 +314,7 @@ class BitcoinWallet extends CoinServiceAPI { throw ArgumentError('Invalid version or Network mismatch'); } else { try { - decodeBech32 = segwit.decode(address); + decodeBech32 = segwit.decode(address, _network.bech32!); } catch (err) { // Bech32 decode fail } @@ -347,19 +347,19 @@ class BitcoinWallet extends CoinServiceAPI { final features = await electrumXClient.getServerFeatures(); Logging.instance.log("features: $features", level: LogLevel.Info); switch (coin) { - case Coin.bitcoin: + case Coin.litecoin: if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { throw Exception("genesis hash does not match main net!"); } break; - case Coin.bitcoinTestNet: + case Coin.litecoinTestNet: if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { throw Exception("genesis hash does not match test net!"); } break; default: throw Exception( - "Attempted to generate a BitcoinWallet using a non bitcoin coin type: ${coin.name}"); + "Attempted to generate a LitecoinWallet using a non litecoin coin type: ${coin.name}"); } // if (_networkType == BasicNetworkType.main) { // if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { @@ -446,7 +446,8 @@ class BitcoinWallet extends CoinServiceAPI { data: PaymentData( redeem: P2WPKH( data: PaymentData(pubkey: node.publicKey), - network: _network) + network: _network, + overridePrefix: _network.bech32!) .data), network: _network) .data @@ -455,7 +456,8 @@ class BitcoinWallet extends CoinServiceAPI { case DerivePathType.bip84: address = P2WPKH( network: _network, - data: PaymentData(pubkey: node.publicKey)) + data: PaymentData(pubkey: node.publicKey), + overridePrefix: _network.bech32!) .data .address!; break; @@ -1284,7 +1286,7 @@ class BitcoinWallet extends CoinServiceAPI { @override bool validateAddress(String address) { - return Address.validateAddress(address, _network); + return Address.validateAddress(address, _network, _network.bech32!); } @override @@ -1311,7 +1313,7 @@ class BitcoinWallet extends CoinServiceAPI { late PriceAPI _priceAPI; - BitcoinWallet({ + LitecoinWallet({ required String walletId, required String walletName, required Coin coin, @@ -1469,19 +1471,20 @@ class BitcoinWallet extends CoinServiceAPI { final features = await electrumXClient.getServerFeatures(); Logging.instance.log("features: $features", level: LogLevel.Info); switch (coin) { - case Coin.bitcoin: + case Coin.litecoin: if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { + print(features['genesis_hash']); throw Exception("genesis hash does not match main net!"); } break; - case Coin.bitcoinTestNet: + case Coin.litecoinTestNet: if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { throw Exception("genesis hash does not match test net!"); } break; default: throw Exception( - "Attempted to generate a BitcoinWallet using a non bitcoin coin type: ${coin.name}"); + "Attempted to generate a LitecoinWallet using a non litecoin coin type: ${coin.name}"); } } @@ -1636,13 +1639,20 @@ class BitcoinWallet extends CoinServiceAPI { case DerivePathType.bip49: address = P2SH( data: PaymentData( - redeem: P2WPKH(data: data, network: _network).data), + redeem: P2WPKH( + data: data, + network: _network, + overridePrefix: _network.bech32!) + .data), network: _network) .data .address!; break; case DerivePathType.bip84: - address = P2WPKH(network: _network, data: data).data.address!; + address = P2WPKH( + network: _network, data: data, overridePrefix: _network.bech32!) + .data + .address!; break; } @@ -2216,10 +2226,11 @@ class BitcoinWallet extends CoinServiceAPI { /// attempts to convert a string to a valid scripthash /// - /// Returns the scripthash or throws an exception on invalid bitcoin address - String _convertToScriptHash(String bitcoinAddress, NetworkType network) { + /// Returns the scripthash or throws an exception on invalid litecoin address + String _convertToScriptHash(String litecoinAddress, NetworkType network) { try { - final output = Address.addressToOutputScript(bitcoinAddress, network); + final output = Address.addressToOutputScript( + litecoinAddress, network, _network.bech32!); final hash = sha256.convert(output.toList(growable: false)).toString(); final chars = hash.split(""); @@ -2442,7 +2453,7 @@ class BitcoinWallet extends CoinServiceAPI { for (final out in tx["vout"] as List) { if (prevOut == out["n"]) { - final address = out["scriptPubKey"]["address"] as String?; + final address = out["scriptPubKey"]["addresses"][0] as String?; if (address != null) { sendersArray.add(address); } @@ -2453,7 +2464,7 @@ class BitcoinWallet extends CoinServiceAPI { Logging.instance.log("sendersArray: $sendersArray", level: LogLevel.Info); for (final output in txObject["vout"] as List) { - final address = output["scriptPubKey"]["address"] as String?; + final address = output["scriptPubKey"]["addresses"][0] as String?; if (address != null) { recipientsArray.add(address); } @@ -2493,7 +2504,8 @@ class BitcoinWallet extends CoinServiceAPI { int totalOutput = 0; for (final output in txObject["vout"] as List) { - final String address = output["scriptPubKey"]!["address"] as String; + final String address = + output["scriptPubKey"]!["addresses"][0] as String; final value = output["value"]!; final _value = (Decimal.parse(value.toString()) * Decimal.fromInt(Constants.satsPerCoin)) @@ -2518,7 +2530,7 @@ class BitcoinWallet extends CoinServiceAPI { // add up received tx value for (final output in txObject["vout"] as List) { - final address = output["scriptPubKey"]["address"]; + final address = output["scriptPubKey"]["addresses"][0]; if (address != null) { final value = (Decimal.parse(output["value"].toString()) * Decimal.fromInt(Constants.satsPerCoin)) @@ -3034,7 +3046,7 @@ class BitcoinWallet extends CoinServiceAPI { for (final output in tx["vout"] as List) { final n = output["n"]; if (n != null && n == utxosToUse[i].vout) { - final address = output["scriptPubKey"]["address"] as String; + final address = output["scriptPubKey"]["addresses"][0] as String; if (!addressTxid.containsKey(address)) { addressTxid[address] = <String>[]; } @@ -3132,7 +3144,8 @@ class BitcoinWallet extends CoinServiceAPI { data: PaymentData( pubkey: Format.stringToUint8List( receiveDerivation["pubKey"] as String)), - network: _network) + network: _network, + overridePrefix: _network.bech32!) .data; final redeemScript = p2wpkh.output; @@ -3159,7 +3172,8 @@ class BitcoinWallet extends CoinServiceAPI { data: PaymentData( pubkey: Format.stringToUint8List( changeDerivation["pubKey"] as String)), - network: _network) + network: _network, + overridePrefix: _network.bech32!) .data; final redeemScript = p2wpkh.output; @@ -3201,11 +3215,12 @@ class BitcoinWallet extends CoinServiceAPI { // if a match exists it will not be null if (receiveDerivation != null) { final data = P2WPKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - receiveDerivation["pubKey"] as String)), - network: _network, - ).data; + data: PaymentData( + pubkey: Format.stringToUint8List( + receiveDerivation["pubKey"] as String)), + network: _network, + overridePrefix: _network.bech32!) + .data; for (String tx in addressTxid[addressesP2WPKH[i]]!) { results[tx] = { @@ -3222,11 +3237,12 @@ class BitcoinWallet extends CoinServiceAPI { // if a match exists it will not be null if (changeDerivation != null) { final data = P2WPKH( - data: PaymentData( - pubkey: Format.stringToUint8List( - changeDerivation["pubKey"] as String)), - network: _network, - ).data; + data: PaymentData( + pubkey: Format.stringToUint8List( + changeDerivation["pubKey"] as String)), + network: _network, + overridePrefix: _network.bech32!) + .data; for (String tx in addressTxid[addressesP2WPKH[i]]!) { results[tx] = { @@ -3267,12 +3283,12 @@ class BitcoinWallet extends CoinServiceAPI { for (var i = 0; i < utxosToUse.length; i++) { final txid = utxosToUse[i].txid; txb.addInput(txid, utxosToUse[i].vout, null, - utxoSigningData[txid]["output"] as Uint8List); + utxoSigningData[txid]["output"] as Uint8List, _network.bech32!); } // Add transaction output for (var i = 0; i < recipients.length; i++) { - txb.addOutput(recipients[i], satoshiAmounts[i]); + txb.addOutput(recipients[i], satoshiAmounts[i], _network.bech32!); } try { @@ -3280,11 +3296,11 @@ class BitcoinWallet extends CoinServiceAPI { for (var i = 0; i < utxosToUse.length; i++) { final txid = utxosToUse[i].txid; txb.sign( - vin: i, - keyPair: utxoSigningData[txid]["keyPair"] as ECPair, - witnessValue: utxosToUse[i].value, - redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?, - ); + vin: i, + keyPair: utxoSigningData[txid]["keyPair"] as ECPair, + witnessValue: utxosToUse[i].value, + redeemScript: utxoSigningData[txid]["redeemScript"] as Uint8List?, + overridePrefix: _network.bech32!); } } catch (e, s) { Logging.instance.log("Caught exception while signing transaction: $e\n$s", @@ -3292,7 +3308,7 @@ class BitcoinWallet extends CoinServiceAPI { rethrow; } - final builtTx = txb.build(); + final builtTx = txb.build(_network.bech32!); final vSize = builtTx.virtualSize(); return {"hex": builtTx.toHex(), "vSize": vSize}; @@ -3794,3 +3810,19 @@ class BitcoinWallet extends CoinServiceAPI { } } } + +final litecoin = NetworkType( + messagePrefix: '\x19Litecoin Signed Message:\n', + bech32: 'ltc', + bip32: Bip32Type(public: 0x0488b21e, private: 0x0488ade4), + pubKeyHash: 0x30, + scriptHash: 0x32, + wif: 0xb0); + +final litecointestnet = NetworkType( + messagePrefix: '\x19Litecoin Signed Message:\n', + bech32: 'tltc', + bip32: Bip32Type(public: 0x043587cf, private: 0x04358394), + pubKeyHash: 0x6f, + scriptHash: 0x3a, + wif: 0xef); diff --git a/lib/services/price.dart b/lib/services/price.dart index b44e055d5..2514cc12a 100644 --- a/lib/services/price.dart +++ b/lib/services/price.dart @@ -87,7 +87,7 @@ class PriceAPI { Map<Coin, Tuple2<Decimal, double>> result = {}; try { final uri = Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=${baseCurrency.toLowerCase()}&ids=monero,bitcoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"); + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=${baseCurrency.toLowerCase()}&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"); // final uri = Uri.parse( // "https://api.coingecko.com/api/v3/coins/markets?vs_currency=${baseCurrency.toLowerCase()}&ids=monero%2Cbitcoin%2Cepic-cash%2Czcoin%2Cdogecoin&order=market_cap_desc&per_page=10&page=1&sparkline=false"); diff --git a/lib/utilities/address_utils.dart b/lib/utilities/address_utils.dart index d73c5d3ce..65303b24d 100644 --- a/lib/utilities/address_utils.dart +++ b/lib/utilities/address_utils.dart @@ -6,6 +6,7 @@ import 'package:flutter_libepiccash/epic_cash.dart'; import 'package:stackwallet/services/coins/bitcoincash/bitcoincash_wallet.dart'; import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; +import 'package:stackwallet/services/coins/litecoin/litecoin_wallet.dart'; import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; @@ -42,6 +43,8 @@ class AddressUtils { switch (coin) { case Coin.bitcoin: return Address.validateAddress(address, bitcoin); + case Coin.litecoin: + return Address.validateAddress(address, litecoin); case Coin.bitcoincash: return Address.validateAddress(address, bitcoincash); case Coin.dogecoin: @@ -60,6 +63,8 @@ class AddressUtils { return Address.validateAddress(address, namecoin, namecoin.bech32!); case Coin.bitcoinTestNet: return Address.validateAddress(address, testnet); + case Coin.litecoinTestNet: + return Address.validateAddress(address, litecointestnet); case Coin.bitcoincashTestnet: return Address.validateAddress(address, bitcoincashtestnet); case Coin.firoTestNet: diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index ebe2d9848..ba93f3a31 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -149,6 +149,7 @@ class _SVG { String get ellipse2 => "assets/svg/Ellipse-42.svg"; String get bitcoin => "assets/svg/coin_icons/Bitcoin.svg"; + String get litecoin => "assets/svg/coin_icons/Litecoin.svg"; String get bitcoincash => "assets/svg/coin_icons/Bitcoincash.svg"; String get dogecoin => "assets/svg/coin_icons/Dogecoin.svg"; String get epicCash => "assets/svg/coin_icons/EpicCash.svg"; @@ -173,6 +174,9 @@ class _SVG { switch (coin) { case Coin.bitcoin: return bitcoin; + case Coin.litecoin: + case Coin.litecoinTestNet: + return litecoin; case Coin.bitcoincash: return bitcoincash; case Coin.dogecoin: @@ -210,6 +214,7 @@ class _PNG { String get firo => "assets/images/firo.png"; String get dogecoin => "assets/images/doge.png"; String get bitcoin => "assets/images/bitcoin.png"; + String get litecoin => "assets/images/litecoin.png"; String get epicCash => "assets/images/epic-cash.png"; String get bitcoincash => "assets/images/bitcoincash.png"; String get namecoin => "assets/images/namecoin.png"; @@ -219,6 +224,9 @@ class _PNG { case Coin.bitcoin: case Coin.bitcoinTestNet: return bitcoin; + case Coin.litecoin: + case Coin.litecoinTestNet: + return litecoin; case Coin.bitcoincash: case Coin.bitcoincashTestnet: return bitcoincash; diff --git a/lib/utilities/block_explorers.dart b/lib/utilities/block_explorers.dart index f89f270be..69afb4a83 100644 --- a/lib/utilities/block_explorers.dart +++ b/lib/utilities/block_explorers.dart @@ -7,6 +7,10 @@ Uri getBlockExplorerTransactionUrlFor({ switch (coin) { case Coin.bitcoin: return Uri.parse("https://chain.so/tx/BTC/$txid"); + case Coin.litecoin: + return Uri.parse("https://chain.so/tx/LTC/$txid"); + case Coin.litecoinTestNet: + return Uri.parse("https://chain.so/tx/LTCTEST/$txid"); case Coin.bitcoinTestNet: return Uri.parse("https://chain.so/tx/BTCTEST/$txid"); case Coin.dogecoin: diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index c4ac4bbe9..4fb3fb54b 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -48,6 +48,8 @@ abstract class Constants { final List<int> values = []; switch (coin) { case Coin.bitcoin: + case Coin.litecoin: + case Coin.litecoinTestNet: case Coin.bitcoincash: case Coin.bitcoincashTestnet: case Coin.dogecoin: @@ -85,6 +87,10 @@ abstract class Constants { case Coin.dogecoinTestNet: return 60; + case Coin.litecoin: + case Coin.litecoinTestNet: + return 150; + case Coin.firo: case Coin.firoTestNet: return 150; diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index 566b829ce..bfe5ada88 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -9,6 +9,7 @@ abstract class DefaultNodes { static List<NodeModel> get all => [ bitcoin, + litecoin, dogecoin, firo, monero, @@ -17,6 +18,7 @@ abstract class DefaultNodes { namecoin, wownero, bitcoinTestnet, + litecoinTestNet, bitcoincashTestnet, dogecoinTestnet, firoTestnet, @@ -34,6 +36,30 @@ abstract class DefaultNodes { isDown: false, ); + static NodeModel get litecoin => NodeModel( + host: "electrum1.cipig.net", + port: 20063, + name: defaultName, + id: _nodeId(Coin.litecoin), + useSSL: true, + enabled: true, + coinName: Coin.litecoin.name, + isFailover: true, + isDown: false, + ); + + static NodeModel get litecoinTestNet => NodeModel( + host: "electrum.ltc.xurious.com", + port: 51002, + name: defaultName, + id: _nodeId(Coin.litecoinTestNet), + useSSL: true, + enabled: true, + coinName: Coin.litecoinTestNet.name, + isFailover: true, + isDown: false, + ); + static NodeModel get bitcoincash => NodeModel( host: "bitcoincash.stackwallet.com", port: 50002, @@ -171,6 +197,9 @@ abstract class DefaultNodes { case Coin.bitcoin: return bitcoin; + case Coin.litecoin: + return litecoin; + case Coin.bitcoincash: return bitcoincash; @@ -195,6 +224,9 @@ abstract class DefaultNodes { case Coin.bitcoinTestNet: return bitcoinTestnet; + case Coin.litecoinTestNet: + return litecoinTestNet; + case Coin.bitcoincashTestnet: return bitcoincashTestnet; diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index c25a5ca4e..86054979a 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -1,4 +1,6 @@ import 'package:stackwallet/services/coins/bitcoin/bitcoin_wallet.dart' as btc; +import 'package:stackwallet/services/coins/litecoin/litecoin_wallet.dart' + as ltc; import 'package:stackwallet/services/coins/bitcoincash/bitcoincash_wallet.dart' as bch; import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart' @@ -22,10 +24,13 @@ enum Coin { namecoin, /// + litecoin, + /// /// bitcoinTestNet, + litecoinTestNet, bitcoincashTestnet, dogecoinTestNet, firoTestNet, @@ -39,6 +44,8 @@ extension CoinExt on Coin { switch (this) { case Coin.bitcoin: return "Bitcoin"; + case Coin.litecoin: + return "Litecoin"; case Coin.bitcoincash: return "Bitcoin Cash"; case Coin.dogecoin: @@ -55,6 +62,8 @@ extension CoinExt on Coin { return "Namecoin"; case Coin.bitcoinTestNet: return "tBitcoin"; + case Coin.litecoinTestNet: + return "tLitecoin"; case Coin.bitcoincashTestnet: return "tBitcoin Cash"; case Coin.firoTestNet: @@ -68,6 +77,8 @@ extension CoinExt on Coin { switch (this) { case Coin.bitcoin: return "BTC"; + case Coin.litecoin: + return "LTC"; case Coin.bitcoincash: return "BCH"; case Coin.dogecoin: @@ -84,6 +95,8 @@ extension CoinExt on Coin { return "NMC"; case Coin.bitcoinTestNet: return "tBTC"; + case Coin.litecoinTestNet: + return "tLTC"; case Coin.bitcoincashTestnet: return "tBCH"; case Coin.firoTestNet: @@ -97,6 +110,8 @@ extension CoinExt on Coin { switch (this) { case Coin.bitcoin: return "bitcoin"; + case Coin.litecoin: + return "litecoin"; case Coin.bitcoincash: return "bitcoincash"; case Coin.dogecoin: @@ -114,6 +129,8 @@ extension CoinExt on Coin { return "namecoin"; case Coin.bitcoinTestNet: return "bitcoin"; + case Coin.litecoinTestNet: + return "litecoin"; case Coin.bitcoincashTestnet: return "bitcoincash"; case Coin.firoTestNet: @@ -126,11 +143,13 @@ extension CoinExt on Coin { bool get isElectrumXCoin { switch (this) { case Coin.bitcoin: + case Coin.litecoin: case Coin.bitcoincash: case Coin.dogecoin: case Coin.firo: case Coin.namecoin: case Coin.bitcoinTestNet: + case Coin.litecoinTestNet: case Coin.bitcoincashTestnet: case Coin.firoTestNet: case Coin.dogecoinTestNet: @@ -149,6 +168,10 @@ extension CoinExt on Coin { case Coin.bitcoinTestNet: return btc.MINIMUM_CONFIRMATIONS; + case Coin.litecoin: + case Coin.litecoinTestNet: + return ltc.MINIMUM_CONFIRMATIONS; + case Coin.bitcoincash: case Coin.bitcoincashTestnet: return bch.MINIMUM_CONFIRMATIONS; @@ -182,6 +205,10 @@ Coin coinFromPrettyName(String name) { case "bitcoin": return Coin.bitcoin; + case "Litecoin": + case "litecoin": + return Coin.litecoin; + case "Bitcoincash": case "bitcoincash": case "Bitcoin Cash": @@ -212,6 +239,12 @@ Coin coinFromPrettyName(String name) { case "bitcoinTestNet": return Coin.bitcoinTestNet; + case "Litecoin Testnet": + case "tlitecoin": + case "litecoinTestNet": + case "tLitecoin": + return Coin.litecoinTestNet; + case "Bitcoincash Testnet": case "tBitcoin Cash": case "Bitcoin Cash Testnet": @@ -243,6 +276,8 @@ Coin coinFromTickerCaseInsensitive(String ticker) { switch (ticker.toLowerCase()) { case "btc": return Coin.bitcoin; + case "ltc": + return Coin.litecoin; case "bch": return Coin.bitcoincash; case "doge": diff --git a/lib/utilities/theme/color_theme.dart b/lib/utilities/theme/color_theme.dart index 361d922dc..49fd41a6e 100644 --- a/lib/utilities/theme/color_theme.dart +++ b/lib/utilities/theme/color_theme.dart @@ -181,6 +181,7 @@ class CoinThemeColor { const CoinThemeColor(); Color get bitcoin => const Color(0xFFFCC17B); + Color get litecoin => const Color(0xFF7FA6E1); Color get bitcoincash => const Color(0xFF7BCFB8); Color get firo => const Color(0xFFFF897A); Color get dogecoin => const Color(0xFFFFE079); @@ -194,6 +195,9 @@ class CoinThemeColor { case Coin.bitcoin: case Coin.bitcoinTestNet: return bitcoin; + case Coin.litecoin: + case Coin.litecoinTestNet: + return litecoin; case Coin.bitcoincash: case Coin.bitcoincashTestnet: return bitcoincash; diff --git a/lib/utilities/theme/stack_colors.dart b/lib/utilities/theme/stack_colors.dart index 67f1601f1..8a241db06 100644 --- a/lib/utilities/theme/stack_colors.dart +++ b/lib/utilities/theme/stack_colors.dart @@ -1406,6 +1406,9 @@ class StackColors extends ThemeExtension<StackColors> { case Coin.bitcoin: case Coin.bitcoinTestNet: return _coin.bitcoin; + case Coin.litecoin: + case Coin.litecoinTestNet: + return _coin.litecoin; case Coin.bitcoincash: case Coin.bitcoincashTestnet: return _coin.bitcoincash; diff --git a/pubspec.lock b/pubspec.lock index 394d1ec8e..6b8a140f6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -42,7 +42,7 @@ packages: name: archive url: "https://pub.dartlang.org" source: hosted - version: "3.1.11" + version: "3.3.0" args: dependency: transitive description: @@ -63,7 +63,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.8.2" + version: "2.9.0" barcode_scan2: dependency: "direct main" description: @@ -190,14 +190,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" + version: "1.2.1" checked_yaml: dependency: transitive description: @@ -211,7 +204,7 @@ packages: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: @@ -281,7 +274,7 @@ packages: name: coverage url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.5.0" cross_file: dependency: transitive description: @@ -435,7 +428,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.3.1" ffi: dependency: "direct main" description: @@ -864,21 +857,21 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.11" + version: "0.12.12" material_color_utilities: dependency: transitive description: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.1.5" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0" mime: dependency: transitive description: @@ -990,7 +983,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" path_drawing: dependency: transitive description: @@ -1366,7 +1359,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.2" + version: "1.9.0" stack_trace: dependency: transitive description: @@ -1424,35 +1417,35 @@ packages: name: sync_http url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.3.1" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" test: dependency: transitive description: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.21.1" + version: "1.21.4" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.9" + version: "0.4.12" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.4.13" + version: "0.4.16" time: dependency: transitive description: @@ -1501,7 +1494,7 @@ packages: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.3.1" universal_io: dependency: transitive description: @@ -1585,7 +1578,7 @@ packages: name: vm_service url: "https://pub.dartlang.org" source: hosted - version: "8.2.2" + version: "9.0.0" wakelock: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 42934fd5c..ca3c253ac 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -196,6 +196,7 @@ flutter: - assets/images/monero.png - assets/images/wownero.png - assets/images/firo.png + - assets/images/litecoin.png - assets/images/doge.png - assets/images/bitcoin.png - assets/images/epic-cash.png @@ -310,6 +311,7 @@ flutter: - assets/svg/exit-desktop.svg # coin icons - assets/svg/coin_icons/Bitcoin.svg + - assets/svg/coin_icons/Litecoin.svg - assets/svg/coin_icons/Bitcoincash.svg - assets/svg/coin_icons/Dogecoin.svg - assets/svg/coin_icons/EpicCash.svg diff --git a/test/price_test.dart b/test/price_test.dart index 6abbb6741..0dbcdd450 100644 --- a/test/price_test.dart +++ b/test/price_test.dart @@ -26,7 +26,7 @@ void main() { when(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), headers: { 'Content-Type': 'application/json' })).thenAnswer((_) async => Response( @@ -39,10 +39,10 @@ void main() { final price = await priceAPI.getPricesAnd24hChange(baseCurrency: "btc"); expect(price.toString(), - '{Coin.bitcoin: [1, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0.00000315, -2.68533], Coin.epicCash: [0.00002803, 7.27524], Coin.firo: [0.0001096, -0.89304], Coin.monero: [0.00717236, -0.77656], Coin.wownero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); + '{Coin.bitcoin: [1, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0.00000315, -2.68533], Coin.epicCash: [0.00002803, 7.27524], Coin.firo: [0.0001096, -0.89304], Coin.monero: [0.00717236, -0.77656], Coin.wownero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.litecoin: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); verify(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), headers: {'Content-Type': 'application/json'})).called(1); verifyNoMoreInteractions(client); @@ -53,7 +53,7 @@ void main() { when(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), headers: { 'Content-Type': 'application/json' })).thenAnswer((_) async => Response( @@ -71,12 +71,12 @@ void main() { await priceAPI.getPricesAnd24hChange(baseCurrency: "btc"); expect(cachedPrice.toString(), - '{Coin.bitcoin: [1, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0.00000315, -2.68533], Coin.epicCash: [0.00002803, 7.27524], Coin.firo: [0.0001096, -0.89304], Coin.monero: [0.00717236, -0.77656], Coin.wownero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); + '{Coin.bitcoin: [1, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0.00000315, -2.68533], Coin.epicCash: [0.00002803, 7.27524], Coin.firo: [0.0001096, -0.89304], Coin.monero: [0.00717236, -0.77656], Coin.wownero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.litecoin: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); // verify only called once during filling of cache verify(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), headers: {'Content-Type': 'application/json'})).called(1); verifyNoMoreInteractions(client); @@ -87,7 +87,7 @@ void main() { when(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), headers: { 'Content-Type': 'application/json' })).thenAnswer((_) async => Response( @@ -100,7 +100,7 @@ void main() { final price = await priceAPI.getPricesAnd24hChange(baseCurrency: "btc"); expect(price.toString(), - '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.firo: [0, 0.0], Coin.monero: [0, 0.0], Coin.wownero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); + '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.firo: [0, 0.0], Coin.monero: [0, 0.0], Coin.wownero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.litecoin: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); }); test("no internet available", () async { @@ -108,7 +108,7 @@ void main() { when(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), headers: { 'Content-Type': 'application/json' })).thenThrow(const SocketException( @@ -120,7 +120,7 @@ void main() { final price = await priceAPI.getPricesAnd24hChange(baseCurrency: "btc"); expect(price.toString(), - '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.firo: [0, 0.0], Coin.monero: [0, 0.0], Coin.wownero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); + '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.firo: [0, 0.0], Coin.monero: [0, 0.0], Coin.wownero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.litecoin: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); }); tearDown(() async { From 86119f7189f469bac6a51bc7f208deceb40bdca6 Mon Sep 17 00:00:00 2001 From: Marco <marco@cypherstack.com> Date: Fri, 28 Oct 2022 13:17:35 -0600 Subject: [PATCH 060/426] change to stack wallet node --- lib/utilities/default_nodes.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index bfe5ada88..abe702b78 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -37,7 +37,7 @@ abstract class DefaultNodes { ); static NodeModel get litecoin => NodeModel( - host: "electrum1.cipig.net", + host: "litecoin.stackwallet.com", port: 20063, name: defaultName, id: _nodeId(Coin.litecoin), @@ -49,7 +49,7 @@ abstract class DefaultNodes { ); static NodeModel get litecoinTestNet => NodeModel( - host: "electrum.ltc.xurious.com", + host: "litecoin.stackwallet.com", port: 51002, name: defaultName, id: _nodeId(Coin.litecoinTestNet), From 3a15538273bebc377b1c2b22fd5c91ee81fb441a Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 28 Oct 2022 13:51:25 -0600 Subject: [PATCH 061/426] WIP: desktop wallet keys popup layouts --- assets/svg/keys.svg | 23 ++ .../sub_widgets/mnemonic_table.dart | 4 + .../sub_widgets/mnemonic_table_item.dart | 3 + .../wallet_view/desktop_wallet_view.dart | 332 +++++++++++++++++- lib/utilities/assets.dart | 1 + lib/widgets/rounded_white_container.dart | 2 +- pubspec.yaml | 1 + 7 files changed, 363 insertions(+), 3 deletions(-) create mode 100644 assets/svg/keys.svg diff --git a/assets/svg/keys.svg b/assets/svg/keys.svg new file mode 100644 index 000000000..cf86daea5 --- /dev/null +++ b/assets/svg/keys.svg @@ -0,0 +1,23 @@ +<svg width="99" height="57" viewBox="0 0 99 57" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0_6055_8386)"> +<path d="M90.1893 8.04883H18.9405C14.0708 8.04883 10.123 11.9965 10.123 16.8663V47.6738C10.123 52.5436 14.0708 56.4913 18.9405 56.4913H90.1893C95.0591 56.4913 99.0068 52.5436 99.0068 47.6738V16.8663C99.0068 11.9965 95.0591 8.04883 90.1893 8.04883Z" fill="#E1E2E3"/> +<path d="M91.9528 5.84445V44.9928C91.9528 47.7275 89.7366 49.9437 87.002 49.9437H5.85138C3.11082 49.9437 0.894531 47.7275 0.894531 44.9928V5.84445C0.894531 3.10984 3.11082 0.893555 5.85138 0.893555L88.2888 1.06037C90.4038 1.63232 91.9528 3.55667 91.9528 5.84445V5.84445Z" stroke="#222222" stroke-width="3" stroke-miterlimit="10"/> +<path d="M15.9121 18.4336V32.4045" stroke="#222222" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round"/> +<path d="M9.86523 21.9248L21.9595 28.9073" stroke="#222222" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round"/> +<path d="M21.9595 21.9248L9.86523 28.9073" stroke="#222222" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round"/> +<path d="M36.252 18.4336V32.4045" stroke="#222222" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round"/> +<path d="M30.2051 21.9248L42.2993 28.9073" stroke="#222222" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round"/> +<path d="M42.2993 21.9248L30.2051 28.9073" stroke="#222222" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round"/> +<path d="M56.5918 18.4336V32.4045" stroke="#222222" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round"/> +<path d="M50.5449 21.9248L62.6392 28.9073" stroke="#222222" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round"/> +<path d="M62.6392 21.9248L50.5449 28.9073" stroke="#222222" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round"/> +<path d="M76.9316 18.4336V32.4045" stroke="#222222" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round"/> +<path d="M70.8848 21.9248L82.979 28.9073" stroke="#222222" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round"/> +<path d="M82.979 21.9248L70.8848 28.9073" stroke="#222222" stroke-width="3" stroke-miterlimit="10" stroke-linecap="round"/> +</g> +<defs> +<clipPath id="clip0_6055_8386"> +<rect width="99" height="56.4914" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart index 946f54d4a..7d21dfec4 100644 --- a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart +++ b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart @@ -6,10 +6,12 @@ class MnemonicTable extends StatelessWidget { Key? key, required this.words, required this.isDesktop, + this.itemBorderColor, }) : super(key: key); final List<String> words; final bool isDesktop; + final Color? itemBorderColor; @override Widget build(BuildContext context) { @@ -40,6 +42,7 @@ class MnemonicTable extends StatelessWidget { number: ++index, word: words[index - 1], isDesktop: isDesktop, + borderColor: itemBorderColor, ), ), ], @@ -61,6 +64,7 @@ class MnemonicTable extends StatelessWidget { number: i + 1, word: words[i], isDesktop: isDesktop, + borderColor: itemBorderColor, ), ), ], diff --git a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table_item.dart b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table_item.dart index 8928ff3a6..ec103dfc6 100644 --- a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table_item.dart +++ b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table_item.dart @@ -9,16 +9,19 @@ class MnemonicTableItem extends StatelessWidget { required this.number, required this.word, required this.isDesktop, + this.borderColor, }) : super(key: key); final int number; final String word; final bool isDesktop; + final Color? borderColor; @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); return RoundedWhiteContainer( + borderColor: borderColor, padding: isDesktop ? const EdgeInsets.symmetric(horizontal: 12, vertical: 9) : const EdgeInsets.all(8), diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index 35b63174c..513be380b 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -4,6 +4,7 @@ import 'package:event_bus/event_bus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart'; import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart'; import 'package:stackwallet/pages/exchange_view/wallet_initiated_exchange_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart'; @@ -30,10 +31,14 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:tuple/tuple.dart'; /// [eventBus] should only be set during testing @@ -246,7 +251,9 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { const SizedBox( width: 32, ), - const WalletKeysButton(), + WalletKeysButton( + walletId: walletId, + ), const SizedBox( width: 32, ), @@ -766,11 +773,24 @@ class _NetworkInfoButtonState extends ConsumerState<NetworkInfoButton> { } class WalletKeysButton extends StatelessWidget { - const WalletKeysButton({Key? key}) : super(key: key); + const WalletKeysButton({ + Key? key, + required this.walletId, + }) : super(key: key); + + final String walletId; @override Widget build(BuildContext context) { return GestureDetector( + onTap: () { + showDialog<void>( + context: context, + builder: (context) => UnlockWalletKeysDesktop( + walletId: walletId, + ), + ); + }, child: Container( color: Colors.transparent, child: Row( @@ -796,3 +816,311 @@ class WalletKeysButton extends StatelessWidget { ); } } + +class UnlockWalletKeysDesktop extends ConsumerStatefulWidget { + const UnlockWalletKeysDesktop({ + Key? key, + required this.walletId, + }) : super(key: key); + + final String walletId; + + @override + ConsumerState<UnlockWalletKeysDesktop> createState() => + _UnlockWalletKeysDesktopState(); +} + +class _UnlockWalletKeysDesktopState + extends ConsumerState<UnlockWalletKeysDesktop> { + late final TextEditingController passwordController; + + late final FocusNode passwordFocusNode; + + bool continueEnabled = false; + bool hidePassword = true; + + @override + void initState() { + passwordController = TextEditingController(); + passwordFocusNode = FocusNode(); + super.initState(); + } + + @override + void dispose() { + passwordController.dispose(); + passwordFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxWidth: 579, + maxHeight: double.infinity, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: const [ + DesktopDialogCloseButton(), + ], + ), + const SizedBox( + height: 12, + ), + SvgPicture.asset( + Assets.svg.keys, + width: 100, + height: 58, + ), + const SizedBox( + height: 55, + ), + Text( + "Wallet keys", + style: STextStyles.desktopH2(context), + ), + const SizedBox( + height: 16, + ), + Text( + "Enter your password", + style: STextStyles.desktopTextMedium(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark3, + ), + ), + const SizedBox( + height: 24, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("enterPasswordUnlockWalletKeysDesktopFieldKey"), + focusNode: passwordFocusNode, + controller: passwordController, + style: STextStyles.desktopTextMedium(context).copyWith( + height: 2, + ), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Enter password", + passwordFocusNode, + context, + ).copyWith( + suffixIcon: UnconstrainedBox( + child: SizedBox( + height: 70, + child: Row( + children: [ + GestureDetector( + key: const Key( + "enterUnlockWalletKeysDesktopFieldShowPasswordButtonKey"), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(1000), + ), + height: 32, + width: 32, + child: Center( + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 24, + height: 19, + ), + ), + ), + ), + const SizedBox( + width: 10, + ), + ], + ), + ), + ), + ), + onChanged: (newValue) { + setState(() { + continueEnabled = newValue.isNotEmpty; + }); + }, + ), + ), + ), + const SizedBox( + height: 55, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + onPressed: Navigator.of(context).pop, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Continue", + enabled: continueEnabled, + onPressed: continueEnabled + ? () async { + // todo: check password + Navigator.of(context).pop(); + final words = await ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .mnemonic; + await showDialog<void>( + context: context, + builder: (context) => WalletKeysDesktopPopup( + words: words, + ), + ); + } + : null, + ), + ), + ], + ), + ), + const SizedBox( + height: 32, + ), + ], + ), + ); + } +} + +class WalletKeysDesktopPopup extends StatelessWidget { + const WalletKeysDesktopPopup({ + Key? key, + required this.words, + }) : super(key: key); + + final List<String> words; + + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxWidth: 614, + maxHeight: double.infinity, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Wallet keys", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + const SizedBox( + height: 28, + ), + Text( + "Recovery phrase", + style: STextStyles.desktopTextMedium(context), + ), + const SizedBox( + height: 8, + ), + Center( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: Text( + "Please write down your recovery phrase in the correct order and save it to keep your funds secure. You will also be asked to verify the words on the next screen.", + style: STextStyles.desktopTextExtraExtraSmall(context), + textAlign: TextAlign.center, + ), + ), + ), + const SizedBox( + height: 24, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: MnemonicTable( + words: words, + isDesktop: true, + itemBorderColor: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + ), + ), + const SizedBox( + height: 24, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Show QR code", + onPressed: () { + // todo show qr code + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Copy", + onPressed: () { + // todo copy to clipboard + }, + ), + ), + ], + ), + ), + const SizedBox( + height: 32, + ), + ], + ), + ); + } +} diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index ebe2d9848..c730b1aa1 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -144,6 +144,7 @@ class _SVG { String get aboutDesktop => "assets/svg/about-desktop.svg"; String get walletDesktop => "assets/svg/wallet-desktop.svg"; String get exitDesktop => "assets/svg/exit-desktop.svg"; + String get keys => "assets/svg/keys.svg"; String get ellipse1 => "assets/svg/Ellipse-43.svg"; String get ellipse2 => "assets/svg/Ellipse-42.svg"; diff --git a/lib/widgets/rounded_white_container.dart b/lib/widgets/rounded_white_container.dart index a574dc1ce..1173e95b1 100644 --- a/lib/widgets/rounded_white_container.dart +++ b/lib/widgets/rounded_white_container.dart @@ -28,8 +28,8 @@ class RoundedWhiteContainer extends StatelessWidget { radiusMultiplier: radiusMultiplier, width: width, height: height, - child: child, borderColor: borderColor, + child: child, ); } } diff --git a/pubspec.yaml b/pubspec.yaml index 42934fd5c..75cdc985a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -308,6 +308,7 @@ flutter: - assets/svg/exchange-desktop.svg - assets/svg/wallet-desktop.svg - assets/svg/exit-desktop.svg + - assets/svg/keys.svg # coin icons - assets/svg/coin_icons/Bitcoin.svg - assets/svg/coin_icons/Bitcoincash.svg From 549087a4f45fb00e7f474eaa37ec67cd1c0640ec Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 28 Oct 2022 14:42:50 -0600 Subject: [PATCH 062/426] WIP: desktop wallet keys popup qr code --- .../wallet_view/desktop_wallet_view.dart | 91 +++++++++++++++++-- lib/route_generator.dart | 28 ++++++ 2 files changed, 113 insertions(+), 6 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index 513be380b..0e5aa925f 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -2,8 +2,11 @@ import 'dart:async'; import 'package:event_bus/event_bus.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart'; import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart'; import 'package:stackwallet/pages/exchange_view/wallet_initiated_exchange_view.dart'; @@ -16,15 +19,19 @@ import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_vie import 'package:stackwallet/providers/global/auto_swb_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/transaction_filter_provider.dart'; +import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart'; import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart'; +import 'package:stackwallet/utilities/address_utils.dart'; import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -786,6 +793,7 @@ class WalletKeysButton extends StatelessWidget { onTap: () { showDialog<void>( context: context, + barrierDismissible: false, builder: (context) => UnlockWalletKeysDesktop( walletId: walletId, ), @@ -998,8 +1006,20 @@ class _UnlockWalletKeysDesktopState .mnemonic; await showDialog<void>( context: context, - builder: (context) => WalletKeysDesktopPopup( - words: words, + barrierDismissible: false, + builder: (context) => Navigator( + initialRoute: WalletKeysDesktopPopup.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + RouteGenerator.generateRoute( + RouteSettings( + name: WalletKeysDesktopPopup.routeName, + arguments: words, + ), + ) + ]; + }, ), ); } @@ -1022,9 +1042,13 @@ class WalletKeysDesktopPopup extends StatelessWidget { const WalletKeysDesktopPopup({ Key? key, required this.words, + this.clipboardInterface = const ClipboardWrapper(), }) : super(key: key); final List<String> words; + final ClipboardInterface clipboardInterface; + + static const String routeName = "walletKeysDesktopPopup"; @override Widget build(BuildContext context) { @@ -1045,7 +1069,11 @@ class WalletKeysDesktopPopup extends StatelessWidget { style: STextStyles.desktopH3(context), ), ), - const DesktopDialogCloseButton(), + DesktopDialogCloseButton( + onPressedOverride: () { + Navigator.of(context, rootNavigator: true).pop(); + }, + ), ], ), const SizedBox( @@ -1098,7 +1126,11 @@ class WalletKeysDesktopPopup extends StatelessWidget { child: SecondaryButton( label: "Show QR code", onPressed: () { - // todo show qr code + final String value = AddressUtils.encodeQRSeedData(words); + Navigator.of(context).pushNamed( + QRCodeDesktopPopupContent.routeName, + arguments: value, + ); }, ), ), @@ -1108,8 +1140,18 @@ class WalletKeysDesktopPopup extends StatelessWidget { Expanded( child: PrimaryButton( label: "Copy", - onPressed: () { - // todo copy to clipboard + onPressed: () async { + await clipboardInterface.setData( + ClipboardData(text: words.join(" ")), + ); + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ), + ); }, ), ), @@ -1124,3 +1166,40 @@ class WalletKeysDesktopPopup extends StatelessWidget { ); } } + +class QRCodeDesktopPopupContent extends StatelessWidget { + const QRCodeDesktopPopupContent({ + Key? key, + required this.value, + }) : super(key: key); + + final String value; + + static const String routeName = "qrCodeDesktopPopupContent"; + + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxWidth: 614, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: const [ + DesktopDialogCloseButton(), + ], + ), + const SizedBox( + height: 14, + ), + QrImage( + data: value, + size: 300, + foregroundColor: + Theme.of(context).extension<StackColors>()!.accentColorDark, + ), + ], + ), + ); + } +} diff --git a/lib/route_generator.dart b/lib/route_generator.dart index f915e7837..c37e0da5c 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -1048,6 +1048,34 @@ class RouteGenerator { builder: (_) => const AdvancedSettings(), settings: RouteSettings(name: settings.name)); + case WalletKeysDesktopPopup.routeName: + if (args is List<String>) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => WalletKeysDesktopPopup( + words: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case QRCodeDesktopPopupContent.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => QRCodeDesktopPopupContent( + value: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + // == End of desktop specific routes ===================================== default: From e64c0672121fd0e0151d3ec3b70125623059a97a Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 28 Oct 2022 15:11:01 -0600 Subject: [PATCH 063/426] desktop all transaction list item layout --- .../all_transactions_view.dart | 268 +++++++++++++++++- 1 file changed, 257 insertions(+), 11 deletions(-) diff --git a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart index 7122f5379..192389720 100644 --- a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart +++ b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart @@ -1,15 +1,23 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/models/contact.dart'; import 'package:stackwallet/models/paymint/transactions_model.dart'; import 'package:stackwallet/models/transaction_filter.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/wallet_view/sub_widgets/tx_icon.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_search_filter_view.dart'; import 'package:stackwallet/providers/global/address_book_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/transaction_filter_provider.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/flush_bar_type.dart'; +import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; @@ -437,21 +445,46 @@ class _TransactionDetailsViewState extends ConsumerState<AllTransactionsView> { const SizedBox( height: 12, ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: Column( - children: [ - ...month.item2.map( - (tx) => TransactionCard( - key: Key( - "transactionCard_key_${tx.txid}"), - transaction: tx, + if (isDesktop) + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: ListView.separated( + shrinkWrap: true, + primary: false, + separatorBuilder: (context, _) => + Container( + height: 1, + color: Theme.of(context) + .extension<StackColors>()! + .background, + ), + itemCount: month.item2.length, + itemBuilder: (context, index) => + Padding( + padding: const EdgeInsets.all(4), + child: DesktopTransactionCardRow( + transaction: month.item2[index], walletId: walletId, ), ), - ], + ), + ), + if (!isDesktop) + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: Column( + children: [ + ...month.item2.map( + (tx) => TransactionCard( + key: Key( + "transactionCard_key_${tx.txid}"), + transaction: tx, + walletId: walletId, + ), + ), + ], + ), ), - ), ], ), ); @@ -472,3 +505,216 @@ class _TransactionDetailsViewState extends ConsumerState<AllTransactionsView> { ); } } + +class DesktopTransactionCardRow extends ConsumerStatefulWidget { + const DesktopTransactionCardRow({ + Key? key, + required this.transaction, + required this.walletId, + }) : super(key: key); + + final Transaction transaction; + final String walletId; + + @override + ConsumerState<DesktopTransactionCardRow> createState() => + _DesktopTransactionCardRowState(); +} + +class _DesktopTransactionCardRowState + extends ConsumerState<DesktopTransactionCardRow> { + late final Transaction _transaction; + late final String walletId; + + String whatIsIt(String type, Coin coin) { + if (coin == Coin.epicCash && _transaction.slateId == null) { + return "Restored Funds"; + } + + if (_transaction.subType == "mint") { + if (_transaction.confirmedStatus) { + return "Anonymized"; + } else { + return "Anonymizing"; + } + } + + if (type == "Received") { + if (_transaction.confirmedStatus) { + return "Received"; + } else { + return "Receiving"; + } + } else if (type == "Sent") { + if (_transaction.confirmedStatus) { + return "Sent"; + } else { + return "Sending"; + } + } else { + return type; + } + } + + @override + void initState() { + walletId = widget.walletId; + _transaction = widget.transaction; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final locale = ref.watch( + localeServiceChangeNotifierProvider.select((value) => value.locale)); + final manager = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletId))); + + final baseCurrency = ref + .watch(prefsChangeNotifierProvider.select((value) => value.currency)); + + final coin = manager.coin; + + final price = ref + .watch(priceAnd24hChangeNotifierProvider + .select((value) => value.getPrice(coin))) + .item1; + + late final String prefix; + if (Util.isDesktop) { + if (_transaction.txType == "Sent") { + prefix = "-"; + } else if (_transaction.txType == "Received") { + prefix = "+"; + } + } else { + prefix = ""; + } + + return Material( + color: Theme.of(context).extension<StackColors>()!.popupBG, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(Constants.size.circularBorderRadius), + ), + child: RawMaterialButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () async { + if (coin == Coin.epicCash && _transaction.slateId == null) { + unawaited( + showFloatingFlushBar( + context: context, + message: + "Restored Epic funds from your Seed have no Data.\nUse Stack Backup to keep your transaction history.", + type: FlushBarType.warning, + duration: const Duration(seconds: 5), + ), + ); + return; + } + unawaited( + Navigator.of(context).pushNamed( + TransactionDetailsView.routeName, + arguments: Tuple3( + _transaction, + coin, + walletId, + ), + ), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 16, + ), + child: Row( + children: [ + TxIcon(transaction: _transaction), + const SizedBox( + width: 12, + ), + Expanded( + flex: 3, + child: Text( + _transaction.isCancelled + ? "Cancelled" + : whatIsIt(_transaction.txType, coin), + style: + STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark, + ), + ), + ), + Expanded( + flex: 4, + child: Text( + Format.extractDateFrom(_transaction.timestamp), + style: STextStyles.label(context), + ), + ), + Expanded( + flex: 6, + child: Builder( + builder: (_) { + final amount = coin == Coin.monero + ? (_transaction.amount ~/ 10000) + : coin == Coin.wownero + ? (_transaction.amount ~/ 1000) + : _transaction.amount; + return Text( + "$prefix${Format.satoshiAmountToPrettyString(amount, locale)} ${coin.ticker}", + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ); + }, + ), + ), + if (ref.watch(prefsChangeNotifierProvider + .select((value) => value.externalCalls))) + Expanded( + flex: 4, + child: Builder( + builder: (_) { + // TODO: modify Format.<functions> to take optional Coin parameter so this type oif check isn't done in ui + int value = _transaction.amount; + if (coin == Coin.monero) { + value = (value ~/ 10000); + } else if (coin == Coin.wownero) { + value = (value ~/ 1000); + } + + return Text( + "$prefix${Format.localizedStringAsFixed( + value: Format.satoshisToAmount(value) * price, + locale: locale, + decimalPlaces: 2, + )} $baseCurrency", + style: STextStyles.desktopTextExtraExtraSmall(context), + ); + }, + ), + ), + SvgPicture.asset( + Assets.svg.circleInfo, + width: 20, + height: 20, + color: + Theme.of(context).extension<StackColors>()!.textSubtitle2, + ), + ], + ), + ), + ), + ); + } +} From 0074fcabd250106d383f1b054296d941c75c94d7 Mon Sep 17 00:00:00 2001 From: Marco <marco@cypherstack.com> Date: Fri, 28 Oct 2022 15:45:03 -0600 Subject: [PATCH 064/426] add new litecoin nodes --- .../manage_nodes_views/add_edit_node_view.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index ada3c45f9..77349f399 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -117,10 +117,12 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> { case Coin.bitcoin: case Coin.bitcoincash: + case Coin.litecoin: case Coin.dogecoin: case Coin.firo: case Coin.namecoin: case Coin.bitcoinTestNet: + case Coin.litecoinTestNet: case Coin.bitcoincashTestnet: case Coin.firoTestNet: case Coin.dogecoinTestNet: From d75f2ed13dc1473e8db8c1b8de7805e1def7f3a5 Mon Sep 17 00:00:00 2001 From: Marco <marco@cypherstack.com> Date: Fri, 28 Oct 2022 15:50:40 -0600 Subject: [PATCH 065/426] another node switching bug --- lib/widgets/node_options_sheet.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/widgets/node_options_sheet.dart b/lib/widgets/node_options_sheet.dart index effe97097..a5345161c 100644 --- a/lib/widgets/node_options_sheet.dart +++ b/lib/widgets/node_options_sheet.dart @@ -102,12 +102,14 @@ class NodeOptionsSheet extends ConsumerWidget { break; case Coin.bitcoin: + case Coin.litecoin: case Coin.dogecoin: case Coin.firo: case Coin.bitcoinTestNet: case Coin.firoTestNet: case Coin.dogecoinTestNet: case Coin.bitcoincash: + case Coin.litecoinTestNet: case Coin.namecoin: case Coin.bitcoincashTestnet: final client = ElectrumX( From 2512d65678abfccb6b2646cf3661202a306ff43e Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Fri, 28 Oct 2022 16:10:26 -0600 Subject: [PATCH 066/426] v1.5.11 build 83 --- ios/Runner.xcodeproj/project.pbxproj | 16 +++++++++------- pubspec.yaml | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 4b7b1058d..2e5d4bac0 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 46; objects = { /* Begin PBXBuildFile section */ @@ -255,6 +255,7 @@ "${BUILT_PRODUCTS_DIR}/cw_monero/cw_monero.framework", "${BUILT_PRODUCTS_DIR}/cw_shared_external/cw_shared_external.framework", "${BUILT_PRODUCTS_DIR}/cw_wownero/cw_wownero.framework", + "${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework", "${BUILT_PRODUCTS_DIR}/devicelocale/devicelocale.framework", "${BUILT_PRODUCTS_DIR}/file_picker/file_picker.framework", "${BUILT_PRODUCTS_DIR}/flutter_libmonero/flutter_libmonero.framework", @@ -288,6 +289,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/cw_monero.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/cw_shared_external.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/cw_wownero.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/devicelocale.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_picker.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_libmonero.framework", @@ -454,7 +456,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 79; + CURRENT_PROJECT_VERSION = 83; DEVELOPMENT_TEAM = 4DQKUWSG6C; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -508,7 +510,7 @@ "$(PROJECT_DIR)/../crypto_plugins/flutter_libmonero/cw_shared_external/ios/External/ios/**", "$(PROJECT_DIR)/../crypto_plugins/flutter_libepiccash/ios/libs", ); - MARKETING_VERSION = 1.5.9; + MARKETING_VERSION = 1.5.11; ONLY_ACTIVE_ARCH = NO; PRODUCT_BUNDLE_IDENTIFIER = com.cypherstack.stackwallet; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -641,7 +643,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 79; + CURRENT_PROJECT_VERSION = 83; DEVELOPMENT_TEAM = 4DQKUWSG6C; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -695,7 +697,7 @@ "$(PROJECT_DIR)/../crypto_plugins/flutter_libmonero/cw_shared_external/ios/External/ios/**", "$(PROJECT_DIR)/../crypto_plugins/flutter_libepiccash/ios/libs", ); - MARKETING_VERSION = 1.5.9; + MARKETING_VERSION = 1.5.11; ONLY_ACTIVE_ARCH = NO; PRODUCT_BUNDLE_IDENTIFIER = com.cypherstack.stackwallet; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -720,7 +722,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 79; + CURRENT_PROJECT_VERSION = 83; DEVELOPMENT_TEAM = 4DQKUWSG6C; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -774,7 +776,7 @@ "$(PROJECT_DIR)/../crypto_plugins/flutter_libmonero/cw_shared_external/ios/External/ios/**", "$(PROJECT_DIR)/../crypto_plugins/flutter_libepiccash/ios/libs", ); - MARKETING_VERSION = 1.5.9; + MARKETING_VERSION = 1.5.11; ONLY_ACTIVE_ARCH = NO; PRODUCT_BUNDLE_IDENTIFIER = com.cypherstack.stackwallet; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/pubspec.yaml b/pubspec.yaml index ca3c253ac..ea2fbaa6d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.5.9+79 +version: 1.5.11+83 environment: sdk: ">=2.17.0 <3.0.0" From ccdfa8db44a199f891dd35ab457759570f15cbcd Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 28 Oct 2022 16:51:50 -0600 Subject: [PATCH 067/426] WIP: transaction filter+search fixes --- lib/models/transaction_filter.dart | 10 +- .../all_transactions_view.dart | 260 +++++++++++++++++- .../transaction_search_filter_view.dart | 38 ++- 3 files changed, 287 insertions(+), 21 deletions(-) diff --git a/lib/models/transaction_filter.dart b/lib/models/transaction_filter.dart index 119ac7015..7ef5f0bff 100644 --- a/lib/models/transaction_filter.dart +++ b/lib/models/transaction_filter.dart @@ -1,14 +1,16 @@ class TransactionFilter { final bool sent; final bool received; - final DateTime from; - final DateTime to; + final bool trade; + final DateTime? from; + final DateTime? to; final int? amount; final String keyword; TransactionFilter({ required this.sent, required this.received, + required this.trade, required this.from, required this.to, required this.amount, @@ -18,6 +20,7 @@ class TransactionFilter { TransactionFilter copyWith({ bool? sent, bool? received, + bool? trade, DateTime? from, DateTime? to, int? amount, @@ -26,6 +29,7 @@ class TransactionFilter { return TransactionFilter( sent: sent ?? this.sent, received: received ?? this.received, + trade: trade ?? this.trade, from: from ?? this.from, to: to ?? this.to, amount: amount ?? this.amount, @@ -35,6 +39,6 @@ class TransactionFilter { @override String toString() { - return "TxFilter { sent: $sent, received: $received, from: $from, to: $to, amount: $amount, keyword: $keyword }"; + return "TxFilter { sent: $sent, received: $received, trade: $trade, from: $from, to: $to, amount: $amount, keyword: $keyword }"; } } diff --git a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart index 192389720..120425c2a 100644 --- a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart +++ b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart @@ -96,8 +96,12 @@ class _TransactionDetailsViewState extends ConsumerState<AllTransactionsView> { } final date = DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000); - if (date.millisecondsSinceEpoch > filter.to.millisecondsSinceEpoch || - date.millisecondsSinceEpoch < filter.from.millisecondsSinceEpoch) { + if ((filter.to != null && + date.millisecondsSinceEpoch > + filter.to!.millisecondsSinceEpoch) || + (filter.from != null && + date.millisecondsSinceEpoch < + filter.from!.millisecondsSinceEpoch)) { return false; } @@ -125,13 +129,25 @@ class _TransactionDetailsViewState extends ConsumerState<AllTransactionsView> { .isNotEmpty; // check if address contains - contains |= tx.address.contains(keyword); + contains |= tx.address.toLowerCase().contains(keyword); // check if note contains - contains |= notes[tx.txid] != null && notes[tx.txid]!.contains(keyword); + contains |= notes[tx.txid] != null && + notes[tx.txid]!.toLowerCase().contains(keyword); // check if txid contains - contains |= tx.txid.contains(keyword); + contains |= tx.txid.toLowerCase().contains(keyword); + + // check if subType contains + contains |= + tx.subType.isNotEmpty && tx.subType.toLowerCase().contains(keyword); + + // check if txType contains + contains |= tx.txType.toLowerCase().contains(keyword); + + // check if date contains + contains |= + Format.extractDateFrom(tx.timestamp).toLowerCase().contains(keyword); return contains; } @@ -311,7 +327,7 @@ class _TransactionDetailsViewState extends ConsumerState<AllTransactionsView> { ) : STextStyles.field(context), decoration: standardInputDecoration( - "Search", + "Search...", searchFieldFocusNode, context, desktopMed: isDesktop, @@ -393,6 +409,22 @@ class _TransactionDetailsViewState extends ConsumerState<AllTransactionsView> { ], ), ), + if (isDesktop) + const SizedBox( + height: 8, + ), + if (isDesktop && + ref.watch(transactionFilterProvider.state).state != null) + Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + ), + child: Row( + children: const [ + TransactionFilterOptionBar(), + ], + ), + ), const SizedBox( height: 8, ), @@ -463,6 +495,8 @@ class _TransactionDetailsViewState extends ConsumerState<AllTransactionsView> { Padding( padding: const EdgeInsets.all(4), child: DesktopTransactionCardRow( + key: Key( + "transactionCard_key_${month.item2[index].txid}"), transaction: month.item2[index], walletId: walletId, ), @@ -506,6 +540,220 @@ class _TransactionDetailsViewState extends ConsumerState<AllTransactionsView> { } } +class TransactionFilterOptionBar extends ConsumerStatefulWidget { + const TransactionFilterOptionBar({Key? key}) : super(key: key); + + @override + ConsumerState<TransactionFilterOptionBar> createState() => + _TransactionFilterOptionBarState(); +} + +class _TransactionFilterOptionBarState + extends ConsumerState<TransactionFilterOptionBar> { + final List<TransactionFilterOptionBarItem> items = []; + TransactionFilter? _filter; + + @override + void initState() { + _filter = ref.read(transactionFilterProvider.state).state; + + if (_filter != null) { + if (_filter!.sent) { + const label = "Sent"; + final item = TransactionFilterOptionBarItem( + label: label, + onPressed: (s) { + items.removeWhere((e) => e.label == label); + if (items.isEmpty) { + ref.read(transactionFilterProvider.state).state = null; + } else { + ref.read(transactionFilterProvider.state).state = + ref.read(transactionFilterProvider.state).state?.copyWith( + sent: false, + ); + setState(() {}); + } + }, + ); + items.add(item); + } + if (_filter!.received) { + const label = ("Received"); + final item = TransactionFilterOptionBarItem( + label: label, + onPressed: (s) { + items.removeWhere((e) => e.label == label); + if (items.isEmpty) { + ref.read(transactionFilterProvider.state).state = null; + } else { + ref.read(transactionFilterProvider.state).state = + ref.read(transactionFilterProvider.state).state?.copyWith( + received: false, + ); + setState(() {}); + } + }, + ); + items.add(item); + } + + if (_filter!.to != null) { + final label = _filter!.from.toString(); + final item = TransactionFilterOptionBarItem( + label: label, + onPressed: (s) { + items.removeWhere((e) => e.label == label); + if (items.isEmpty) { + ref.read(transactionFilterProvider.state).state = null; + } else { + ref.read(transactionFilterProvider.state).state = + ref.read(transactionFilterProvider.state).state?.copyWith( + to: null, + ); + setState(() {}); + } + }, + ); + items.add(item); + } + if (_filter!.from != null) { + final label2 = _filter!.to.toString(); + final item2 = TransactionFilterOptionBarItem( + label: label2, + onPressed: (s) { + items.removeWhere((e) => e.label == label2); + if (items.isEmpty) { + ref.read(transactionFilterProvider.state).state = null; + } else { + ref.read(transactionFilterProvider.state).state = + ref.read(transactionFilterProvider.state).state?.copyWith( + from: null, + ); + setState(() {}); + } + }, + ); + items.add(item2); + } + + if (_filter!.amount != null) { + final label = _filter!.amount!.toString(); + final item = TransactionFilterOptionBarItem( + label: label, + onPressed: (s) { + items.removeWhere((e) => e.label == label); + if (items.isEmpty) { + ref.read(transactionFilterProvider.state).state = null; + } else { + ref.read(transactionFilterProvider.state).state = + ref.read(transactionFilterProvider.state).state?.copyWith( + amount: null, + ); + setState(() {}); + } + }, + ); + items.add(item); + } + if (_filter!.keyword.isNotEmpty) { + final label = _filter!.keyword; + final item = TransactionFilterOptionBarItem( + label: label, + onPressed: (s) { + items.removeWhere((e) => e.label == label); + if (items.isEmpty) { + ref.read(transactionFilterProvider.state).state = null; + } else { + ref.read(transactionFilterProvider.state).state = + ref.read(transactionFilterProvider.state).state?.copyWith( + keyword: "", + ); + setState(() {}); + } + }, + ); + items.add(item); + } + } + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 32, + child: ListView.separated( + primary: false, + scrollDirection: Axis.horizontal, + shrinkWrap: true, + itemCount: items.length, + separatorBuilder: (_, __) => const SizedBox( + width: 16, + ), + itemBuilder: (context, index) => items[index], + ), + ); + } +} + +class TransactionFilterOptionBarItem extends StatelessWidget { + const TransactionFilterOptionBarItem({ + Key? key, + required this.label, + this.onPressed, + }) : super(key: key); + + final String label; + final void Function(String)? onPressed; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => onPressed?.call(label), + child: Container( + height: 32, + decoration: BoxDecoration( + color: + Theme.of(context).extension<StackColors>()!.buttonBackSecondary, + borderRadius: BorderRadius.circular(1000)), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 14, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: Padding( + padding: const EdgeInsets.only(top: 2), + child: Text( + label, + textAlign: TextAlign.center, + style: STextStyles.labelExtraExtraSmall(context).copyWith( + color: + Theme.of(context).extension<StackColors>()!.textDark, + ), + ), + ), + ), + const SizedBox( + width: 10, + ), + XIcon( + width: 16, + height: 16, + color: Theme.of(context).extension<StackColors>()!.textDark, + ), + ], + ), + ), + ), + ); + } +} + class DesktopTransactionCardRow extends ConsumerStatefulWidget { const DesktopTransactionCardRow({ Key? key, diff --git a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart index 7d18d4428..abebb71e4 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart @@ -68,8 +68,11 @@ class _TransactionSearchViewState _selectedFromDate = filterState.from; _keywordTextEditingController.text = filterState.keyword; - _fromDateString = Format.formatDate(_selectedFromDate); - _toDateString = Format.formatDate(_selectedToDate); + _fromDateString = _selectedFromDate == null + ? "" + : Format.formatDate(_selectedFromDate!); + _toDateString = + _selectedToDate == null ? "" : Format.formatDate(_selectedToDate!); // TODO: Fix XMR (modify Format.funcs to take optional Coin parameter) // final amt = Format.satoshisToAmount(widget.coin == Coin.monero ? ) @@ -118,8 +121,8 @@ class _TransactionSearchViewState ); } - var _selectedFromDate = DateTime(2007); - var _selectedToDate = DateTime.now(); + DateTime? _selectedFromDate = DateTime(2007); + DateTime? _selectedToDate = DateTime.now(); MaterialRoundedDatePickerStyle _buildDatePickerStyle() { return MaterialRoundedDatePickerStyle( @@ -226,17 +229,22 @@ class _TransactionSearchViewState _selectedFromDate = date; // flag to adjust date so from date is always before to date - final flag = !_selectedFromDate.isBefore(_selectedToDate); + final flag = _selectedToDate != null && + !_selectedFromDate!.isBefore(_selectedToDate!); if (flag) { _selectedToDate = DateTime.fromMillisecondsSinceEpoch( - _selectedFromDate.millisecondsSinceEpoch); + _selectedFromDate!.millisecondsSinceEpoch); } setState(() { if (flag) { - _toDateString = Format.formatDate(_selectedToDate); + _toDateString = _selectedToDate == null + ? "" + : Format.formatDate(_selectedToDate!); } - _fromDateString = Format.formatDate(_selectedFromDate); + _fromDateString = _selectedFromDate == null + ? "" + : Format.formatDate(_selectedFromDate!); }); } }, @@ -333,17 +341,22 @@ class _TransactionSearchViewState _selectedToDate = date; // flag to adjust date so from date is always before to date - final flag = !_selectedToDate.isAfter(_selectedFromDate); + final flag = _selectedFromDate != null && + !_selectedToDate!.isAfter(_selectedFromDate!); if (flag) { _selectedFromDate = DateTime.fromMillisecondsSinceEpoch( - _selectedToDate.millisecondsSinceEpoch); + _selectedToDate!.millisecondsSinceEpoch); } setState(() { if (flag) { - _fromDateString = Format.formatDate(_selectedFromDate); + _fromDateString = _selectedFromDate == null + ? "" + : Format.formatDate(_selectedFromDate!); } - _toDateString = Format.formatDate(_selectedToDate); + _toDateString = _selectedToDate == null + ? "" + : Format.formatDate(_selectedToDate!); }); } }, @@ -975,6 +988,7 @@ class _TransactionSearchViewState final TransactionFilter filter = TransactionFilter( sent: _isActiveSentCheckbox, received: _isActiveReceivedCheckbox, + trade: _isActiveTradeCheckbox, from: _selectedFromDate, to: _selectedToDate, amount: amount, From 300b1fa0018236a124837281f400d920c64c5424 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Fri, 28 Oct 2022 17:04:48 -0600 Subject: [PATCH 068/426] route update --- .../home/desktop_settings_view.dart | 2 +- .../backup_and_restore_settings.dart | 311 ++++++++++++++++++ .../create_auto_backup.dart | 136 ++++++++ .../enable_backup_dialog.dart | 87 +++++ .../restore_backup_dialog.dart | 93 ++++++ .../home/settings_menu/nodes_settings.dart | 5 +- lib/route_generator.dart | 2 +- 7 files changed, 631 insertions(+), 5 deletions(-) create mode 100644 lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart create mode 100644 lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart create mode 100644 lib/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart create mode 100644 lib/pages_desktop_specific/home/settings_menu/backup_and_restore/restore_backup_dialog.dart diff --git a/lib/pages_desktop_specific/home/desktop_settings_view.dart b/lib/pages_desktop_specific/home/desktop_settings_view.dart index 7aff94b66..719a37a3a 100644 --- a/lib/pages_desktop_specific/home/desktop_settings_view.dart +++ b/lib/pages_desktop_specific/home/desktop_settings_view.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/advanced_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/appearance_settings.dart'; -import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore_settings.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/currency_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/language_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/nodes_settings.dart'; diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart new file mode 100644 index 000000000..6ab82401b --- /dev/null +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart @@ -0,0 +1,311 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/restore_backup_dialog.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/rounded_white_container.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import 'enable_backup_dialog.dart'; + +class BackupRestoreSettings extends ConsumerStatefulWidget { + const BackupRestoreSettings({Key? key}) : super(key: key); + + static const String routeName = "/settingsMenuBackupRestore"; + + @override + ConsumerState<BackupRestoreSettings> createState() => + _BackupRestoreSettings(); +} + +class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + return ListView( + shrinkWrap: true, + scrollDirection: Axis.vertical, + children: [ + Padding( + padding: const EdgeInsets.only( + right: 30, + ), + child: RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset( + Assets.svg.backupAuto, + width: 48, + height: 48, + ), + Center( + child: Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.start, + text: TextSpan( + children: [ + TextSpan( + text: "Auto Backup", + style: STextStyles.desktopTextSmall(context), + ), + TextSpan( + text: + "\n\nAuto backup is a custom Stack Wallet feature that offers a convenient backup of your data." + "To ensure maximum security, we recommend using a unique password that you haven't used anywhere " + "else on the internet before. Your password is not stored.", + style: + STextStyles.desktopTextExtraExtraSmall(context), + ), + TextSpan( + text: + "\n\nFor more information, please see our website ", + style: + STextStyles.desktopTextExtraExtraSmall(context), + ), + TextSpan( + text: "stackwallet.com", + style: STextStyles.richLink(context) + .copyWith(fontSize: 14), + recognizer: TapGestureRecognizer() + ..onTap = () { + launchUrl( + Uri.parse("https://stackwallet.com/"), + mode: LaunchMode.externalApplication, + ); + }, + ), + ], + ), + ), + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Padding( + padding: EdgeInsets.all( + 10, + ), + child: AutoBackupButton(), + ), + ], + ), + ], + ), + ), + ), + const SizedBox( + height: 25, + ), + Padding( + padding: const EdgeInsets.only( + right: 30, + ), + child: RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset( + Assets.svg.backupAdd, + width: 48, + height: 48, + alignment: Alignment.topLeft, + ), + Center( + child: Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.start, + text: TextSpan( + children: [ + TextSpan( + text: "Manual Backup", + style: STextStyles.desktopTextSmall(context), + ), + TextSpan( + text: + "\n\nCreate manual backup to easily transfer your data between devices. " + "You will create a backup file that can be later used in the Restore option. " + "Use a strong password to encrypt your data.", + style: + STextStyles.desktopTextExtraExtraSmall(context), + ), + ], + ), + ), + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Padding( + padding: EdgeInsets.all( + 10, + ), + child: ManualBackupButton(), + ), + ], + ), + ], + ), + ), + ), + const SizedBox( + height: 25, + ), + Padding( + padding: const EdgeInsets.only( + right: 30, + ), + child: RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset( + Assets.svg.backupRestore, + width: 48, + height: 48, + alignment: Alignment.topLeft, + ), + Center( + child: Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.start, + text: TextSpan( + children: [ + TextSpan( + text: "Restore Backup", + style: STextStyles.desktopTextSmall(context), + ), + TextSpan( + text: + "\n\nUse your Stack Wallet backup file to restore your wallets, address book " + "and wallet preferences.", + style: + STextStyles.desktopTextExtraExtraSmall(context), + ), + ], + ), + ), + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Padding( + padding: EdgeInsets.all( + 10, + ), + child: RestoreBackupButton(), + ), + ], + ), + ], + ), + ), + ), + ], + ); + } +} + +class AutoBackupButton extends ConsumerWidget { + const AutoBackupButton({ + Key? key, + }) : super(key: key); + @override + Widget build(BuildContext context, WidgetRef ref) { + Future<void> enableAutoBackup() async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return const EnableBackupDialog(); + }, + ); + } + + return SizedBox( + width: 200, + height: 48, + child: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + onPressed: () { + enableAutoBackup(); + }, + child: Text( + "Enable auto backup", + style: STextStyles.button(context), + ), + ), + ); + } +} + +class ManualBackupButton extends ConsumerWidget { + const ManualBackupButton({ + Key? key, + }) : super(key: key); + @override + Widget build(BuildContext context, WidgetRef ref) { + return SizedBox( + width: 200, + height: 48, + child: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + onPressed: () {}, + child: Text( + "Create manual backup", + style: STextStyles.button(context), + ), + ), + ); + } +} + +class RestoreBackupButton extends ConsumerWidget { + const RestoreBackupButton({ + Key? key, + }) : super(key: key); + @override + Widget build(BuildContext context, WidgetRef ref) { + Future<void> restoreBackup() async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return const RestoreBackupDialog(); + }, + ); + } + + return SizedBox( + width: 200, + height: 48, + child: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + onPressed: () { + restoreBackup(); + }, + child: Text( + "Restore", + style: STextStyles.button(context), + ), + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart new file mode 100644 index 000000000..29be17228 --- /dev/null +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.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 '../../../../utilities/assets.dart'; +import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../../../widgets/stack_text_field.dart'; + +class CreateAutoBackup extends StatelessWidget { + // const CreateAutoBackup({Key? key, required this.chooseFileLocation}) + // : super(key: key); + + late final TextEditingController fileLocationController; + + late final FocusNode chooseFileLocation; + + @override + void initState() { + fileLocationController = TextEditingController(); + // passwordRepeatController = TextEditingController(); + + chooseFileLocation = FocusNode(); + // passwordRepeatFocusNode = FocusNode(); + + super.initState(); + } + + @override + void dispose() { + fileLocationController.dispose(); + // passwordRepeatController.dispose(); + + chooseFileLocation.dispose(); + // passwordRepeatFocusNode.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxHeight: 600, + maxWidth: 600, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(32), + child: Text( + "Create auto backup", + style: STextStyles.desktopH3(context), + textAlign: TextAlign.center, + ), + ), + Padding( + padding: const EdgeInsets.all(20.0), + child: AppBarIconButton( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + size: 40, + icon: SvgPicture.asset( + Assets.svg.x, + color: Theme.of(context).extension<StackColors>()!.textDark, + width: 22, + height: 22, + ), + onPressed: () { + int count = 0; + Navigator.of(context).popUntil((_) => count++ >= 2); + }, + ), + ), + ], + ), + const SizedBox( + height: 30, + ), + Container( + alignment: Alignment.centerLeft, + padding: EdgeInsets.only(left: 32), + child: Text( + "Choose file location", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark3, + ), + textAlign: TextAlign.left, + ), + ), + TextField( + key: const Key("backupChooseFileLocation"), + style: STextStyles.desktopTextMedium(context).copyWith( + height: 2, + ), + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Save to...", chooseFileLocation, context), + ), + const Spacer(), + Padding( + padding: const EdgeInsets.all(32), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + onPressed: () { + int count = 0; + Navigator.of(context).popUntil((_) => count++ >= 2); + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Enable Auto Backup", + onPressed: () {}, + ), + ) + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart new file mode 100644 index 000000000..046d136a8 --- /dev/null +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.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'; + +class EnableBackupDialog extends StatelessWidget { + const EnableBackupDialog({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + Future<void> createAutoBackup() async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return CreateAutoBackup(); + }, + ); + } + + return DesktopDialog( + maxHeight: 300, + maxWidth: 570, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(32), + child: Text( + "Enable Auto Backup", + style: STextStyles.desktopH3(context), + textAlign: TextAlign.center, + ), + ), + const DesktopDialogCloseButton(), + ], + ), + const SizedBox( + height: 30, + ), + Text( + "To enable Auto Backup, you need to create a backup file.", + style: STextStyles.desktopTextSmall(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark3, + ), + textAlign: TextAlign.center, + ), + const Spacer(), + Padding( + padding: const EdgeInsets.all(32), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + onPressed: () { + int count = 0; + Navigator.of(context).popUntil((_) => count++ >= 2); + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Continue", + onPressed: () { + createAutoBackup(); + }, + ), + ) + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/restore_backup_dialog.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/restore_backup_dialog.dart new file mode 100644 index 000000000..07d49274a --- /dev/null +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/restore_backup_dialog.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.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'; + +class RestoreBackupDialog extends StatelessWidget { + const RestoreBackupDialog({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return DesktopDialog( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(32), + child: Text( + "Restoring Stack Wallet", + style: STextStyles.desktopH3(context), + textAlign: TextAlign.center, + ), + ), + const DesktopDialogCloseButton(), + ], + ), + const SizedBox( + height: 30, + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + ), + child: Row( + children: [ + Text( + "Settings", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of(context).extension<StackColors>()!.textDark3, + ), + textAlign: TextAlign.left, + ), + ], + ), + ), + // RoundedWhiteContainer( + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Row(), + // ], + // ), + // ), + const Spacer(), + Padding( + padding: const EdgeInsets.all(32), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Continue", + onPressed: () { + // Navigator.of(context).pop(); + // onConfirm.call(); + }, + ), + ) + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart b/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart index f354927c4..bb60061d8 100644 --- a/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart @@ -2,16 +2,15 @@ import 'package:flutter/material.dart'; import 'package:flutter/src/widgets/framework.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/coin_nodes_view.dart'; import 'package:stackwallet/providers/global/node_service_provider.dart'; import 'package:stackwallet/providers/global/prefs_provider.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/text_styles.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; -import '../../../pages/settings_views/global_settings_view/manage_nodes_views/coin_nodes_view.dart'; -import '../../../utilities/constants.dart'; - class NodesSettings extends ConsumerStatefulWidget { const NodesSettings({Key? key}) : super(key: key); diff --git a/lib/route_generator.dart b/lib/route_generator.dart index c37e0da5c..202edd972 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -89,7 +89,7 @@ import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_v import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/advanced_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/appearance_settings.dart'; -import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore_settings.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/currency_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/language_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/nodes_settings.dart'; From 234794e4ca003e9f93e3fb7139b6e327099cf2be Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Fri, 28 Oct 2022 17:07:13 -0600 Subject: [PATCH 069/426] WIP: security_settings and dialog popups --- .../backup_and_restore_settings.dart | 282 ------------------ .../settings_menu/enable_backup_dialog.dart | 73 ----- .../home/settings_menu/security_settings.dart | 44 +-- 3 files changed, 23 insertions(+), 376 deletions(-) delete mode 100644 lib/pages_desktop_specific/home/settings_menu/backup_and_restore_settings.dart delete mode 100644 lib/pages_desktop_specific/home/settings_menu/enable_backup_dialog.dart diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore_settings.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore_settings.dart deleted file mode 100644 index 04c23ee83..000000000 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore_settings.dart +++ /dev/null @@ -1,282 +0,0 @@ -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/flutter_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/rounded_white_container.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class BackupRestoreSettings extends ConsumerStatefulWidget { - const BackupRestoreSettings({Key? key}) : super(key: key); - - static const String routeName = "/settingsMenuBackupRestore"; - - @override - ConsumerState<BackupRestoreSettings> createState() => - _BackupRestoreSettings(); -} - -class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { - @override - Widget build(BuildContext context) { - debugPrint("BUILD: $runtimeType"); - return ListView( - shrinkWrap: true, - scrollDirection: Axis.vertical, - children: [ - Padding( - padding: const EdgeInsets.only( - right: 30, - ), - child: RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SvgPicture.asset( - Assets.svg.backupAuto, - width: 48, - height: 48, - ), - Center( - child: Padding( - padding: const EdgeInsets.all(10), - child: RichText( - textAlign: TextAlign.start, - text: TextSpan( - children: [ - TextSpan( - text: "Auto Backup", - style: STextStyles.desktopTextSmall(context), - ), - TextSpan( - text: - "\n\nAuto backup is a custom Stack Wallet feature that offers a convenient backup of your data." - "To ensure maximum security, we recommend using a unique password that you haven't used anywhere " - "else on the internet before. Your password is not stored.", - style: - STextStyles.desktopTextExtraExtraSmall(context), - ), - TextSpan( - text: - "\n\nFor more information, please see our website ", - style: - STextStyles.desktopTextExtraExtraSmall(context), - ), - TextSpan( - text: "stackwallet.com", - style: STextStyles.richLink(context) - .copyWith(fontSize: 14), - recognizer: TapGestureRecognizer() - ..onTap = () { - launchUrl( - Uri.parse("https://stackwallet.com/"), - mode: LaunchMode.externalApplication, - ); - }, - ), - ], - ), - ), - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: const [ - Padding( - padding: EdgeInsets.all( - 10, - ), - child: AutoBackupButton(), - ), - ], - ), - ], - ), - ), - ), - const SizedBox( - height: 25, - ), - Padding( - padding: const EdgeInsets.only( - right: 30, - ), - child: RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SvgPicture.asset( - Assets.svg.backupAdd, - width: 48, - height: 48, - alignment: Alignment.topLeft, - ), - Center( - child: Padding( - padding: const EdgeInsets.all(10), - child: RichText( - textAlign: TextAlign.start, - text: TextSpan( - children: [ - TextSpan( - text: "Manual Backup", - style: STextStyles.desktopTextSmall(context), - ), - TextSpan( - text: - "\n\nCreate manual backup to easily transfer your data between devices. " - "You will create a backup file that can be later used in the Restore option. " - "Use a strong password to encrypt your data.", - style: - STextStyles.desktopTextExtraExtraSmall(context), - ), - ], - ), - ), - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: const [ - Padding( - padding: EdgeInsets.all( - 10, - ), - child: ManualBackupButton(), - ), - ], - ), - ], - ), - ), - ), - const SizedBox( - height: 25, - ), - Padding( - padding: const EdgeInsets.only( - right: 30, - ), - child: RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SvgPicture.asset( - Assets.svg.backupRestore, - width: 48, - height: 48, - alignment: Alignment.topLeft, - ), - Center( - child: Padding( - padding: const EdgeInsets.all(10), - child: RichText( - textAlign: TextAlign.start, - text: TextSpan( - children: [ - TextSpan( - text: "Restore Backup", - style: STextStyles.desktopTextSmall(context), - ), - TextSpan( - text: - "\n\nUse your Stack Wallet backup file to restore your wallets, address book " - "and wallet preferences.", - style: - STextStyles.desktopTextExtraExtraSmall(context), - ), - ], - ), - ), - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: const [ - Padding( - padding: EdgeInsets.all( - 10, - ), - child: ManualBackupButton(), - ), - ], - ), - ], - ), - ), - ), - ], - ); - } -} - -class AutoBackupButton extends ConsumerWidget { - const AutoBackupButton({ - Key? key, - }) : super(key: key); - @override - Widget build(BuildContext context, WidgetRef ref) { - return SizedBox( - width: 200, - height: 48, - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () {}, - child: Text( - "Enable auto backup", - style: STextStyles.button(context), - ), - ), - ); - } -} - -class ManualBackupButton extends ConsumerWidget { - const ManualBackupButton({ - Key? key, - }) : super(key: key); - @override - Widget build(BuildContext context, WidgetRef ref) { - return SizedBox( - width: 200, - height: 48, - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () {}, - child: Text( - "Create manual backup", - style: STextStyles.button(context), - ), - ), - ); - } -} - -class RestoreBackupButton extends ConsumerWidget { - const RestoreBackupButton({ - Key? key, - }) : super(key: key); - @override - Widget build(BuildContext context, WidgetRef ref) { - return SizedBox( - width: 200, - height: 48, - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () {}, - child: Text( - "Restore backup", - style: STextStyles.button(context), - ), - ), - ); - } -} diff --git a/lib/pages_desktop_specific/home/settings_menu/enable_backup_dialog.dart b/lib/pages_desktop_specific/home/settings_menu/enable_backup_dialog.dart deleted file mode 100644 index 4925177c3..000000000 --- a/lib/pages_desktop_specific/home/settings_menu/enable_backup_dialog.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/utilities/theme/stack_colors.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'; - -class EnableBackupDialog extends StatelessWidget { - const EnableBackupDialog({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return DesktopDialog( - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.all(32), - child: Text( - "Enable Auto Backup", - style: STextStyles.desktopH3(context), - textAlign: TextAlign.center, - ), - ), - const DesktopDialogCloseButton(), - ], - ), - const SizedBox( - height: 30, - ), - Text( - "To enable Auto Backup, you need to create a backup file.", - style: STextStyles.desktopTextSmall(context).copyWith( - color: Theme.of(context).extension<StackColors>()!.textDark3, - ), - textAlign: TextAlign.center, - ), - const Spacer(), - Padding( - padding: const EdgeInsets.all(32), - child: Row( - children: [ - Expanded( - child: SecondaryButton( - label: "Cancel", - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: PrimaryButton( - label: "Continue", - onPressed: () { - // Navigator.of(context).pop(); - // onConfirm.call(); - }, - ), - ) - ], - ), - ), - ], - ), - ); - } -} diff --git a/lib/pages_desktop_specific/home/settings_menu/security_settings.dart b/lib/pages_desktop_specific/home/settings_menu/security_settings.dart index 7c36be8cd..cdcaed49a 100644 --- a/lib/pages_desktop_specific/home/settings_menu/security_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/security_settings.dart @@ -2,13 +2,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/src/widgets/framework.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.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/rounded_white_container.dart'; -import '../../../utilities/theme/stack_colors.dart'; -import 'enable_backup_dialog.dart'; - class SecuritySettings extends ConsumerStatefulWidget { const SecuritySettings({Key? key}) : super(key: key); @@ -104,23 +103,6 @@ class NewPasswordButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - Future<void> enableAutoBackup() async { - // wait for keyboard to disappear - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration(milliseconds: 100), - ); - - await showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return EnableBackupDialog(); - }, - ); - } - return SizedBox( width: 200, height: 48, @@ -129,7 +111,27 @@ class NewPasswordButton extends ConsumerWidget { .extension<StackColors>()! .getPrimaryEnabledButtonColor(context), onPressed: () { - enableAutoBackup(); + // Expandable( + // header: Row( + // mainAxisAlignment: MainAxisAlignment.start, + // children: [ + // NewPasswordButton(), + // ], + // ), + // body: Column( + // mainAxisAlignment: MainAxisAlignment.start, + // children: [ + // Text( + // "Current Password", + // style: STextStyles.desktopTextExtraSmall(context).copyWith( + // color: + // Theme.of(context).extension<StackColors>()!.textDark3, + // ), + // textAlign: TextAlign.left, + // ), + // ], + // ), + // ); }, child: Text( "Set up new password", From 5b2d9ce141910e0093a3b19c2c0c77a40b173e1b Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Sat, 29 Oct 2022 09:14:59 -0600 Subject: [PATCH 070/426] Coin enum clean up and bug fixes --- .../add_wallet_view/add_wallet_view.dart | 1 - .../sub_widgets/mobile_coin_list.dart | 2 -- .../sub_widgets/searchable_coin_list.dart | 3 ++- lib/utilities/enums/coin_enum.dart | 17 +++++++++++------ 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index df5c44d18..e964610ae 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -206,7 +206,6 @@ class _AddWalletViewState extends State<AddWalletView> { Expanded( child: MobileCoinList( coins: coins, - isDesktop: false, ), ), const SizedBox( diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart index 1f36f3b65..fd950963c 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart @@ -8,11 +8,9 @@ class MobileCoinList extends StatelessWidget { const MobileCoinList({ Key? key, required this.coins, - required this.isDesktop, }) : super(key: key); final List<Coin> coins; - final bool isDesktop; @override Widget build(BuildContext context) { diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart index fb443b915..d89d42bbf 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart @@ -26,7 +26,8 @@ class SearchableCoinList extends ConsumerWidget { e.name.toLowerCase().contains(lowercaseTerm)); } if (!showTestNetCoins) { - _coins.removeWhere((e) => e.name.endsWith("TestNet")); + _coins.removeWhere( + (e) => e.name.endsWith("TestNet") || e == Coin.bitcoincashTestnet); } // remove firo testnet regardless _coins.remove(Coin.firoTestNet); diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index 86054979a..95294c8aa 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -1,6 +1,4 @@ import 'package:stackwallet/services/coins/bitcoin/bitcoin_wallet.dart' as btc; -import 'package:stackwallet/services/coins/litecoin/litecoin_wallet.dart' - as ltc; import 'package:stackwallet/services/coins/bitcoincash/bitcoincash_wallet.dart' as bch; import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart' @@ -8,6 +6,8 @@ import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart' import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart' as epic; import 'package:stackwallet/services/coins/firo/firo_wallet.dart' as firo; +import 'package:stackwallet/services/coins/litecoin/litecoin_wallet.dart' + as ltc; import 'package:stackwallet/services/coins/monero/monero_wallet.dart' as xmr; import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart' as nmc; @@ -19,12 +19,12 @@ enum Coin { dogecoin, epicCash, firo, + litecoin, monero, - wownero, namecoin, + wownero, /// - litecoin, /// /// @@ -37,7 +37,7 @@ enum Coin { } // remove firotestnet for now -const int kTestNetCoinCount = 3; +const int kTestNetCoinCount = 4; extension CoinExt on Coin { String get prettyName { @@ -268,7 +268,10 @@ Coin coinFromPrettyName(String name) { default: throw ArgumentError.value( - name, "name", "No Coin enum value with that prettyName"); + name, + "name", + "No Coin enum value with that prettyName", + ); } } @@ -290,6 +293,8 @@ Coin coinFromTickerCaseInsensitive(String ticker) { return Coin.monero; case "nmc": return Coin.namecoin; + case "tltc": + return Coin.litecoinTestNet; case "tbtc": return Coin.bitcoinTestNet; case "tbch": From f7dd7b01aa43e4900796b295632afe99e6f0fc43 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Sat, 29 Oct 2022 09:14:59 -0600 Subject: [PATCH 071/426] Coin enum clean up and bug fixes --- .../add_wallet_view/add_wallet_view.dart | 1 - .../sub_widgets/mobile_coin_list.dart | 2 -- .../sub_widgets/searchable_coin_list.dart | 3 ++- lib/utilities/enums/coin_enum.dart | 17 +++++++++++------ 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index df5c44d18..e964610ae 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -206,7 +206,6 @@ class _AddWalletViewState extends State<AddWalletView> { Expanded( child: MobileCoinList( coins: coins, - isDesktop: false, ), ), const SizedBox( diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart index 1f36f3b65..fd950963c 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart @@ -8,11 +8,9 @@ class MobileCoinList extends StatelessWidget { const MobileCoinList({ Key? key, required this.coins, - required this.isDesktop, }) : super(key: key); final List<Coin> coins; - final bool isDesktop; @override Widget build(BuildContext context) { diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart index fb443b915..d89d42bbf 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart @@ -26,7 +26,8 @@ class SearchableCoinList extends ConsumerWidget { e.name.toLowerCase().contains(lowercaseTerm)); } if (!showTestNetCoins) { - _coins.removeWhere((e) => e.name.endsWith("TestNet")); + _coins.removeWhere( + (e) => e.name.endsWith("TestNet") || e == Coin.bitcoincashTestnet); } // remove firo testnet regardless _coins.remove(Coin.firoTestNet); diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index 86054979a..95294c8aa 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -1,6 +1,4 @@ import 'package:stackwallet/services/coins/bitcoin/bitcoin_wallet.dart' as btc; -import 'package:stackwallet/services/coins/litecoin/litecoin_wallet.dart' - as ltc; import 'package:stackwallet/services/coins/bitcoincash/bitcoincash_wallet.dart' as bch; import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart' @@ -8,6 +6,8 @@ import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart' import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart' as epic; import 'package:stackwallet/services/coins/firo/firo_wallet.dart' as firo; +import 'package:stackwallet/services/coins/litecoin/litecoin_wallet.dart' + as ltc; import 'package:stackwallet/services/coins/monero/monero_wallet.dart' as xmr; import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart' as nmc; @@ -19,12 +19,12 @@ enum Coin { dogecoin, epicCash, firo, + litecoin, monero, - wownero, namecoin, + wownero, /// - litecoin, /// /// @@ -37,7 +37,7 @@ enum Coin { } // remove firotestnet for now -const int kTestNetCoinCount = 3; +const int kTestNetCoinCount = 4; extension CoinExt on Coin { String get prettyName { @@ -268,7 +268,10 @@ Coin coinFromPrettyName(String name) { default: throw ArgumentError.value( - name, "name", "No Coin enum value with that prettyName"); + name, + "name", + "No Coin enum value with that prettyName", + ); } } @@ -290,6 +293,8 @@ Coin coinFromTickerCaseInsensitive(String ticker) { return Coin.monero; case "nmc": return Coin.namecoin; + case "tltc": + return Coin.litecoinTestNet; case "tbtc": return Coin.bitcoinTestNet; case "tbch": From 1f6338892efe71efb050712889ed0cf6e33d0909 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Sat, 29 Oct 2022 09:30:39 -0600 Subject: [PATCH 072/426] desktop favorite card navigation fix --- .../sub_widgets/favorite_card.dart | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/lib/pages/wallets_view/sub_widgets/favorite_card.dart b/lib/pages/wallets_view/sub_widgets/favorite_card.dart index 924e904f3..8ce8add17 100644 --- a/lib/pages/wallets_view/sub_widgets/favorite_card.dart +++ b/lib/pages/wallets_view/sub_widgets/favorite_card.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -11,6 +12,7 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; import 'package:tuple/tuple.dart'; class FavoriteCard extends ConsumerStatefulWidget { @@ -54,13 +56,20 @@ class _FavoriteCardState extends ConsumerState<FavoriteCard> { return GestureDetector( onTap: () { - Navigator.of(context).pushNamed( - WalletView.routeName, - arguments: Tuple2( - walletId, - managerProvider, - ), - ); + if (Util.isDesktop) { + Navigator.of(context).pushNamed( + DesktopWalletView.routeName, + arguments: walletId, + ); + } else { + Navigator.of(context).pushNamed( + WalletView.routeName, + arguments: Tuple2( + walletId, + managerProvider, + ), + ); + } }, child: SizedBox( width: widget.width, From 5fd47de9a2473b0d682c3bcfd2bf4cc29c63ddd0 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Sat, 29 Oct 2022 13:35:03 -0600 Subject: [PATCH 073/426] desktop tx details + edit note ui --- .../all_transactions_view.dart | 34 +- .../transaction_views/edit_note_view.dart | 272 ++- .../transaction_details_view.dart | 1474 +++++++++++------ lib/widgets/icon_widgets/copy_icon.dart | 27 + lib/widgets/icon_widgets/pencil_icon.dart | 27 + lib/widgets/transaction_card.dart | 34 +- pubspec.lock | 43 +- 7 files changed, 1302 insertions(+), 609 deletions(-) create mode 100644 lib/widgets/icon_widgets/copy_icon.dart create mode 100644 lib/widgets/icon_widgets/pencil_icon.dart diff --git a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart index 120425c2a..8604ae721 100644 --- a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart +++ b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart @@ -23,6 +23,7 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.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_dialog.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; @@ -865,16 +866,31 @@ class _DesktopTransactionCardRowState ); return; } - unawaited( - Navigator.of(context).pushNamed( - TransactionDetailsView.routeName, - arguments: Tuple3( - _transaction, - coin, - walletId, + if (Util.isDesktop) { + await showDialog<void>( + context: context, + builder: (context) => DesktopDialog( + maxHeight: MediaQuery.of(context).size.height - 64, + maxWidth: 580, + child: TransactionDetailsView( + transaction: _transaction, + coin: coin, + walletId: walletId, + ), ), - ), - ); + ); + } else { + unawaited( + Navigator.of(context).pushNamed( + TransactionDetailsView.routeName, + arguments: Tuple3( + _transaction, + coin, + walletId, + ), + ), + ); + } }, child: Padding( padding: const EdgeInsets.symmetric( diff --git a/lib/pages/wallet_view/transaction_views/edit_note_view.dart b/lib/pages/wallet_view/transaction_views/edit_note_view.dart index b811dc62d..4baaaffc9 100644 --- a/lib/pages/wallet_view/transaction_views/edit_note_view.dart +++ b/lib/pages/wallet_view/transaction_views/edit_note_view.dart @@ -4,13 +4,14 @@ import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; -import 'package:stackwallet/utilities/util.dart'; - class EditNoteView extends ConsumerStatefulWidget { const EditNoteView({ Key? key, @@ -33,8 +34,11 @@ class _EditNoteViewState extends ConsumerState<EditNoteView> { late final TextEditingController _noteController; final noteFieldFocusNode = FocusNode(); + late final bool isDesktop; + @override void initState() { + isDesktop = Util.isDesktop; _noteController = TextEditingController(); _noteController.text = widget.note; super.initState(); @@ -50,29 +54,178 @@ class _EditNoteViewState extends ConsumerState<EditNoteView> { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - backgroundColor: - Theme.of(context).extension<StackColors>()!.background, - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "Edit note", - style: STextStyles.navBarTitle(context), - ), + backgroundColor: isDesktop + ? Colors.transparent + : Theme.of(context).extension<StackColors>()!.background, + appBar: isDesktop + ? null + : AppBar( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Edit note", + style: STextStyles.navBarTitle(context), + ), + ), + body: MobileEditNoteScaffold( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (isDesktop) + Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 12, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Edit note", + style: STextStyles.desktopH3(context), + ), + const DesktopDialogCloseButton(), + ], + ), + ), + Padding( + padding: isDesktop + ? const EdgeInsets.symmetric( + horizontal: 32, + ) + : const EdgeInsets.all(0), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _noteController, + style: isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveText, + height: 1.8, + ) + : STextStyles.field(context), + focusNode: noteFieldFocusNode, + decoration: standardInputDecoration( + "Note", + noteFieldFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + contentPadding: isDesktop + ? const EdgeInsets.only( + left: 16, + top: 11, + bottom: 12, + right: 5, + ) + : null, + suffixIcon: _noteController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _noteController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + ), + // if (!isDesktop) + const Spacer(), + if (isDesktop) + Padding( + padding: const EdgeInsets.all(32), + child: PrimaryButton( + label: "Save", + onPressed: () async { + await ref + .read( + notesServiceChangeNotifierProvider(widget.walletId)) + .editOrAddNote( + txid: widget.txid, + note: _noteController.text, + ); + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + ), + if (!isDesktop) + TextButton( + onPressed: () async { + await ref + .read(notesServiceChangeNotifierProvider(widget.walletId)) + .editOrAddNote( + txid: widget.txid, + note: _noteController.text, + ); + if (mounted) { + Navigator.of(context).pop(); + } + }, + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + child: Text( + "Save", + style: STextStyles.button(context), + ), + ) + ], ), - body: Padding( - padding: const EdgeInsets.all(12), - child: LayoutBuilder(builder: (context, constraints) { + ), + ); + } +} + +class MobileEditNoteScaffold extends StatelessWidget { + const MobileEditNoteScaffold({ + Key? key, + required this.child, + }) : super(key: key); + + final Widget child; + + @override + Widget build(BuildContext context) { + if (Util.isDesktop) { + return child; + } else { + return Padding( + padding: const EdgeInsets.all(12), + child: LayoutBuilder( + builder: (context, constraints) { return SingleChildScrollView( child: ConstrainedBox( constraints: BoxConstraints( @@ -81,75 +234,14 @@ class _EditNoteViewState extends ConsumerState<EditNoteView> { child: IntrinsicHeight( child: Padding( padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: _noteController, - style: STextStyles.field(context), - focusNode: noteFieldFocusNode, - decoration: standardInputDecoration( - "Note", - noteFieldFocusNode, - context, - ).copyWith( - suffixIcon: _noteController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _noteController.text = ""; - }); - }, - ), - ], - ), - ), - ) - : null, - ), - ), - ), - const Spacer(), - TextButton( - onPressed: () async { - await ref - .read(notesServiceChangeNotifierProvider( - widget.walletId)) - .editOrAddNote( - txid: widget.txid, - note: _noteController.text, - ); - if (mounted) { - Navigator.of(context).pop(); - } - }, - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - child: Text( - "Save", - style: STextStyles.button(context), - ), - ) - ], - ), + child: child, ), ), ), ); - }), - )); + }, + ), + ); + } } } diff --git a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart index 8d7aac85f..d7f06095d 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/models/models.dart'; @@ -23,8 +24,13 @@ import 'package:stackwallet/utilities/format.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/utilities/util.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/icon_widgets/copy_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/pencil_icon.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:tuple/tuple.dart'; @@ -51,6 +57,7 @@ class TransactionDetailsView extends ConsumerStatefulWidget { class _TransactionDetailsViewState extends ConsumerState<TransactionDetailsView> { + late final bool isDesktop; late Transaction _transaction; late final String walletId; @@ -63,6 +70,7 @@ class _TransactionDetailsViewState @override void initState() { + isDesktop = Util.isDesktop; _transaction = widget.transaction; walletId = widget.walletId; @@ -74,7 +82,7 @@ class _TransactionDetailsViewState _transaction.subType == "mint") { amountPrefix = ""; } else { - amountPrefix = _transaction.txType.toLowerCase() == "sent" ? "- " : "+ "; + amountPrefix = _transaction.txType.toLowerCase() == "sent" ? "-" : "+"; } // if (coin == Coin.firo || coin == Coin.firoTestNet) { @@ -205,267 +213,524 @@ class _TransactionDetailsViewState @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - leading: AppBarBackButton( - onPressed: () async { - // if (FocusScope.of(context).hasFocus) { - // FocusScope.of(context).unfocus(); - // await Future<void>.delayed(Duration(milliseconds: 50)); - // } - Navigator.of(context).pop(); - }, - ), - title: Text( - "Transaction details", - style: STextStyles.navBarTitle(context), - ), - ), + backgroundColor: isDesktop + ? Colors.transparent + : Theme.of(context).extension<StackColors>()!.background, + appBar: isDesktop + ? null + : AppBar( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + leading: AppBarBackButton( + onPressed: () async { + // if (FocusScope.of(context).hasFocus) { + // FocusScope.of(context).unfocus(); + // await Future<void>.delayed(Duration(milliseconds: 50)); + // } + Navigator.of(context).pop(); + }, + ), + title: Text( + "Transaction details", + style: STextStyles.navBarTitle(context), + ), + ), body: Padding( - padding: const EdgeInsets.all(12), - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SelectableText( - "$amountPrefix${Format.localizedStringAsFixed( - value: coin == Coin.monero - ? (amount / 10000.toDecimal()).toDecimal() - : coin == Coin.wownero - ? (amount / 1000.toDecimal()).toDecimal() - : amount, - locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), - decimalPlaces: Constants.decimalPlaces, - )} ${coin.ticker}", - style: STextStyles.titleBold12(context), - ), - const SizedBox( - height: 2, - ), - if (ref.watch(prefsChangeNotifierProvider - .select((value) => value.externalCalls))) - SelectableText( - "${Format.localizedStringAsFixed(value: (coin == Coin.monero ? (amount / 10000.toDecimal()).toDecimal() : coin == Coin.wownero ? (amount / 1000.toDecimal()).toDecimal() : amount) * ref.watch(priceAnd24hChangeNotifierProvider.select((value) => value.getPrice(coin).item1)), locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), decimalPlaces: 2)} ${ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.currency, - ), - )}", - style: STextStyles.itemSubtitle(context), - ), - ], - ), - TxIcon( - transaction: _transaction, - ), - ], + padding: isDesktop + ? const EdgeInsets.only(left: 32) + : const EdgeInsets.all(12), + child: Column( + children: [ + if (isDesktop) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transaction details", + style: STextStyles.desktopH3(context), ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Status", - style: STextStyles.itemSubtitle(context), - ), - // Flexible( - // child: FittedBox( - // fit: BoxFit.scaleDown, - // child: - SelectableText( - _transaction.isCancelled - ? "Cancelled" - : whatIsIt(_transaction.txType), - style: STextStyles.itemSubtitle12(context), - ), - // ), - // ), - ], - ), - ), - if (!((coin == Coin.monero || coin == Coin.wownero) && - _transaction.txType.toLowerCase() == "sent") && - !((coin == Coin.firo || coin == Coin.firoTestNet) && - _transaction.subType == "mint")) - const SizedBox( - height: 12, - ), - if (!((coin == Coin.monero || coin == Coin.wownero) && - _transaction.txType.toLowerCase() == "sent") && - !((coin == Coin.firo || coin == Coin.firoTestNet) && - _transaction.subType == "mint")) - RoundedWhiteContainer( + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: isDesktop + ? const EdgeInsets.only( + right: 32, + bottom: 32, + ) + : const EdgeInsets.all(0), + child: RoundedWhiteContainer( + borderColor: isDesktop + ? Theme.of(context).extension<StackColors>()!.background + : null, + padding: const EdgeInsets.all(0), + child: SingleChildScrollView( + child: Padding( + padding: isDesktop + ? const EdgeInsets.all(0) + : const EdgeInsets.all(4), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text( - _transaction.txType.toLowerCase() == "sent" - ? "Sent to" - : "Received on", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 8, - ), - _transaction.txType.toLowerCase() == "received" - ? FutureBuilder( - future: - fetchContactNameFor(_transaction.address), - builder: (builderContext, - AsyncSnapshot<String> snapshot) { - String addressOrContactName = - _transaction.address; - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - addressOrContactName = snapshot.data!; - } - return SelectableText( - addressOrContactName, - style: STextStyles.itemSubtitle12(context), - ); - }, - ) - : SelectableText( - _transaction.address, - style: STextStyles.itemSubtitle12(context), + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(0) + : const EdgeInsets.all(12), + child: Container( + decoration: isDesktop + ? BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .background, + borderRadius: BorderRadius.vertical( + top: Radius.circular( + Constants.size.circularBorderRadius, + ), + ), + ) + : null, + child: Padding( + padding: isDesktop + ? const EdgeInsets.all(12) + : const EdgeInsets.all(0), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + if (isDesktop) + Row( + children: [ + TxIcon( + transaction: _transaction, + ), + const SizedBox( + width: 16, + ), + SelectableText( + _transaction.isCancelled + ? "Cancelled" + : whatIsIt(_transaction.txType), + style: STextStyles.desktopTextMedium( + context), + ), + ], + ), + Column( + crossAxisAlignment: isDesktop + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + SelectableText( + "$amountPrefix${Format.localizedStringAsFixed( + value: coin == Coin.monero + ? (amount / 10000.toDecimal()) + .toDecimal() + : coin == Coin.wownero + ? (amount / 1000.toDecimal()) + .toDecimal() + : amount, + locale: ref.watch( + localeServiceChangeNotifierProvider + .select( + (value) => value.locale), + ), + decimalPlaces: + Constants.decimalPlaces, + )} ${coin.ticker}", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.titleBold12(context), + ), + const SizedBox( + height: 2, + ), + if (ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.externalCalls))) + SelectableText( + "$amountPrefix${Format.localizedStringAsFixed(value: (coin == Coin.monero ? (amount / 10000.toDecimal()).toDecimal() : coin == Coin.wownero ? (amount / 1000.toDecimal()).toDecimal() : amount) * ref.watch(priceAnd24hChangeNotifierProvider.select((value) => value.getPrice(coin).item1)), locale: ref.watch( + localeServiceChangeNotifierProvider + .select((value) => + value.locale), + ), decimalPlaces: 2)} ${ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.currency, + ), + )}", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle( + context), + ), + ], + ), + if (!isDesktop) + TxIcon( + transaction: _transaction, + ), + ], ), - ], - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Note", - style: STextStyles.itemSubtitle(context), + ), ), - GestureDetector( - onTap: () { - Navigator.of(context).pushNamed( - EditNoteView.routeName, - arguments: Tuple3( - _transaction.txid, - walletId, - _note, + ), + + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Status", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle(context), + ), + // Flexible( + // child: FittedBox( + // fit: BoxFit.scaleDown, + // child: + SelectableText( + _transaction.isCancelled + ? "Cancelled" + : whatIsIt(_transaction.txType), + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: _transaction.txType == "Sent" + ? Theme.of(context) + .extension<StackColors>()! + .accentColorOrange + : Theme.of(context) + .extension<StackColors>()! + .accentColorGreen, + ) + : STextStyles.itemSubtitle12(context), + ), + // ), + // ), + ], + ), + ), + if (!((coin == Coin.monero || coin == Coin.wownero) && + _transaction.txType.toLowerCase() == "sent") && + !((coin == Coin.firo || coin == Coin.firoTestNet) && + _transaction.subType == "mint")) + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, ), - ); - }, + if (!((coin == Coin.monero || coin == Coin.wownero) && + _transaction.txType.toLowerCase() == "sent") && + !((coin == Coin.firo || coin == Coin.firoTestNet) && + _transaction.subType == "mint")) + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - SvgPicture.asset( - Assets.svg.pencil, - width: 10, - height: 10, - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - ), - const SizedBox( - width: 4, - ), - Text( - "Edit", - style: STextStyles.link2(context), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _transaction.txType.toLowerCase() == + "sent" + ? "Sent to" + : "Receiving address", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle(context), + ), + const SizedBox( + height: 8, + ), + _transaction.txType.toLowerCase() == + "received" + ? FutureBuilder( + future: fetchContactNameFor( + _transaction.address), + builder: (builderContext, + AsyncSnapshot<String> + snapshot) { + String addressOrContactName = + _transaction.address; + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + addressOrContactName = + snapshot.data!; + } + return SelectableText( + addressOrContactName, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textDark, + ) + : STextStyles + .itemSubtitle12( + context), + ); + }, + ) + : SelectableText( + _transaction.address, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context), + ), + ], ), + if (isDesktop) + IconCopyButton( + data: _transaction.address, + ), ], ), ), - ], - ), - const SizedBox( - height: 8, - ), - FutureBuilder( - future: ref.watch( - notesServiceChangeNotifierProvider(walletId).select( - (value) => - value.getNoteFor(txid: _transaction.txid))), - builder: - (builderContext, AsyncSnapshot<String> snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - _note = snapshot.data ?? ""; - } - return SelectableText( - _note, - style: STextStyles.itemSubtitle12(context), - ); - }, - ), - ], - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Date", - style: STextStyles.itemSubtitle(context), - ), - // Flexible( - // child: FittedBox( - // fit: BoxFit.scaleDown, - // child: - SelectableText( - Format.extractDateFrom(_transaction.timestamp), - style: STextStyles.itemSubtitle12(context), - ), - // ), - // ), - ], - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Transaction fee", - style: STextStyles.itemSubtitle(context), - ), - // Flexible( - // child: FittedBox( - // fit: BoxFit.scaleDown, - // child: - SelectableText( - showFeePending - ? _transaction.confirmedStatus - ? Format.localizedStringAsFixed( + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Note", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall(context) + : STextStyles.itemSubtitle(context), + ), + isDesktop + ? IconPencilButton( + onPressed: () { + showDialog<void>( + context: context, + builder: (context) { + return DesktopDialog( + maxWidth: 580, + maxHeight: 360, + child: EditNoteView( + txid: _transaction.txid, + walletId: walletId, + note: _note, + ), + ); + }, + ); + }, + ) + : GestureDetector( + onTap: () { + Navigator.of(context).pushNamed( + EditNoteView.routeName, + arguments: Tuple3( + _transaction.txid, + walletId, + _note, + ), + ); + }, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.pencil, + width: 10, + height: 10, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + ), + const SizedBox( + width: 4, + ), + Text( + "Edit", + style: + STextStyles.link2(context), + ), + ], + ), + ), + ], + ), + const SizedBox( + height: 8, + ), + FutureBuilder( + future: ref.watch( + notesServiceChangeNotifierProvider(walletId) + .select((value) => value.getNoteFor( + txid: _transaction.txid))), + builder: (builderContext, + AsyncSnapshot<String> snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + _note = snapshot.data ?? ""; + } + return SelectableText( + _note, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12(context), + ); + }, + ), + ], + ), + ), + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Date", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall(context) + : STextStyles.itemSubtitle(context), + ), + if (isDesktop) + const SizedBox( + height: 2, + ), + if (isDesktop) + SelectableText( + Format.extractDateFrom( + _transaction.timestamp, + ), + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12(context), + ), + ], + ), + if (!isDesktop) + SelectableText( + Format.extractDateFrom( + _transaction.timestamp, + ), + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12(context), + ), + if (isDesktop) + IconCopyButton( + data: Format.extractDateFrom( + _transaction.timestamp, + ), + ), + ], + ), + ), + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Builder(builder: (context) { + final feeString = showFeePending + ? _transaction.confirmedStatus + ? Format.localizedStringAsFixed( + value: coin == Coin.monero + ? (fee / 10000.toDecimal()) + .toDecimal() + : coin == Coin.wownero + ? (fee / 1000.toDecimal()) + .toDecimal() + : fee, + locale: ref.watch( + localeServiceChangeNotifierProvider + .select( + (value) => value.locale)), + decimalPlaces: Constants.decimalPlaces) + : "Pending" + : Format.localizedStringAsFixed( value: coin == Coin.monero ? (fee / 10000.toDecimal()).toDecimal() : coin == Coin.wownero @@ -475,251 +740,403 @@ class _TransactionDetailsViewState locale: ref.watch( localeServiceChangeNotifierProvider .select((value) => value.locale)), - decimalPlaces: Constants.decimalPlaces) - : "Pending" - : Format.localizedStringAsFixed( - value: coin == Coin.monero - ? (fee / 10000.toDecimal()).toDecimal() - : coin == Coin.wownero - ? (fee / 1000.toDecimal()).toDecimal() - : fee, - locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale)), - decimalPlaces: Constants.decimalPlaces), - style: STextStyles.itemSubtitle12(context), - ), - // ), - // ), - ], - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Block height", - style: STextStyles.itemSubtitle(context), - ), - // Flexible( - // child: FittedBox( - // fit: BoxFit.scaleDown, - // child: - SelectableText( - widget.coin != Coin.epicCash && - _transaction.confirmedStatus - ? "${_transaction.height == 0 ? "Unknown" : _transaction.height}" - : _transaction.confirmations > 0 - ? "${_transaction.height}" - : "Pending", - style: STextStyles.itemSubtitle12(context), - ), - // ), - // ), - ], - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Transaction ID", - style: STextStyles.itemSubtitle(context), - ), - ], - ), - const SizedBox( - height: 8, - ), - // Flexible( - // child: FittedBox( - // fit: BoxFit.scaleDown, - // child: - SelectableText( - _transaction.txid, - style: STextStyles.itemSubtitle12(context), - ), - if (coin != Coin.epicCash) - const SizedBox( - height: 8, - ), - if (coin != Coin.epicCash) - BlueTextButton( - text: "Open in block explorer", - onTap: () async { - final uri = getBlockExplorerTransactionUrlFor( - coin: coin, - txid: _transaction.txid, - ); + decimalPlaces: Constants.decimalPlaces); - if (ref - .read(prefsChangeNotifierProvider) - .hideBlockExplorerWarning == - false) { - final shouldContinue = await showExplorerWarning( - "${uri.scheme}://${uri.host}"); - - if (!shouldContinue) { - return; - } - } - - // ref - // .read( - // shouldShowLockscreenOnResumeStateProvider - // .state) - // .state = false; - try { - await launchUrl( - uri, - mode: LaunchMode.externalApplication, - ); - } catch (_) { - unawaited(showDialog<void>( - context: context, - builder: (_) => StackOkDialog( - title: "Could not open in block explorer", - message: - "Failed to open \"${uri.toString()}\"", + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Transaction fee", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle(context), + ), + if (isDesktop) + const SizedBox( + height: 2, + ), + if (isDesktop) + SelectableText( + feeString, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context), + ), + ], ), - )); - } finally { - // Future<void>.delayed( - // const Duration(seconds: 1), - // () => ref - // .read( - // shouldShowLockscreenOnResumeStateProvider - // .state) - // .state = true, - // ); - } - }, + if (!isDesktop) + SelectableText( + feeString, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12(context), + ), + if (isDesktop) IconCopyButton(data: feeString) + ], + ); + }), ), - // ), - // ), - ], - ), - ), - // if ((coin == Coin.firoTestNet || coin == Coin.firo) && - // _transaction.subType == "mint") - // const SizedBox( - // height: 12, - // ), - // if ((coin == Coin.firoTestNet || coin == Coin.firo) && - // _transaction.subType == "mint") - // RoundedWhiteContainer( - // child: Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // Text( - // "Mint Transaction ID", - // style: STextStyles.itemSubtitle(context), - // ), - // ], - // ), - // const SizedBox( - // height: 8, - // ), - // // Flexible( - // // child: FittedBox( - // // fit: BoxFit.scaleDown, - // // child: - // SelectableText( - // _transaction.otherData ?? "Unknown", - // style: STextStyles.itemSubtitle12(context), - // ), - // // ), - // // ), - // const SizedBox( - // height: 8, - // ), - // BlueTextButton( - // text: "Open in block explorer", - // onTap: () async { - // final uri = getBlockExplorerTransactionUrlFor( - // coin: coin, - // txid: _transaction.otherData ?? "Unknown", - // ); - // // ref - // // .read( - // // shouldShowLockscreenOnResumeStateProvider - // // .state) - // // .state = false; - // try { - // await launchUrl( - // uri, - // mode: LaunchMode.externalApplication, - // ); - // } catch (_) { - // unawaited(showDialog<void>( - // context: context, - // builder: (_) => StackOkDialog( - // title: "Could not open in block explorer", - // message: - // "Failed to open \"${uri.toString()}\"", - // ), - // )); - // } finally { - // // Future<void>.delayed( - // // const Duration(seconds: 1), - // // () => ref - // // .read( - // // shouldShowLockscreenOnResumeStateProvider - // // .state) - // // .state = true, - // // ); - // } - // }, - // ), - // ], - // ), - // ), - if (coin == Coin.epicCash) - const SizedBox( - height: 12, - ), - if (coin == Coin.epicCash) - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Slate ID", - style: STextStyles.itemSubtitle(context), + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Builder(builder: (context) { + final height = widget.coin != Coin.epicCash && + _transaction.confirmedStatus + ? "${_transaction.height == 0 ? "Unknown" : _transaction.height}" + : _transaction.confirmations > 0 + ? "${_transaction.height}" + : "Pending"; + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Block height", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle(context), + ), + if (isDesktop) + const SizedBox( + height: 2, + ), + if (isDesktop) + SelectableText( + height, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context), + ), + ], + ), + if (!isDesktop) + SelectableText( + height, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12(context), + ), + if (isDesktop) IconCopyButton(data: height), + ], + ); + }), ), - // Flexible( - // child: FittedBox( - // fit: BoxFit.scaleDown, - // child: - SelectableText( - _transaction.slateId ?? "Unknown", - style: STextStyles.itemSubtitle12(context), + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Transaction ID", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle(context), + ), + const SizedBox( + height: 8, + ), + // Flexible( + // child: FittedBox( + // fit: BoxFit.scaleDown, + // child: + SelectableText( + _transaction.txid, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12(context), + ), + if (coin != Coin.epicCash) + const SizedBox( + height: 8, + ), + if (coin != Coin.epicCash) + BlueTextButton( + text: "Open in block explorer", + onTap: () async { + final uri = + getBlockExplorerTransactionUrlFor( + coin: coin, + txid: _transaction.txid, + ); + + if (ref + .read( + prefsChangeNotifierProvider) + .hideBlockExplorerWarning == + false) { + final shouldContinue = + await showExplorerWarning( + "${uri.scheme}://${uri.host}"); + + if (!shouldContinue) { + return; + } + } + + // ref + // .read( + // shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = false; + try { + await launchUrl( + uri, + mode: LaunchMode + .externalApplication, + ); + } catch (_) { + unawaited(showDialog<void>( + context: context, + builder: (_) => StackOkDialog( + title: + "Could not open in block explorer", + message: + "Failed to open \"${uri.toString()}\"", + ), + )); + } finally { + // Future<void>.delayed( + // const Duration(seconds: 1), + // () => ref + // .read( + // shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = true, + // ); + } + }, + ), + // ), + // ), + ], + ), + ), + if (isDesktop) + const SizedBox( + width: 12, + ), + if (isDesktop) + IconCopyButton( + data: _transaction.txid, + ), + ], + ), ), + // if ((coin == Coin.firoTestNet || coin == Coin.firo) && + // _transaction.subType == "mint") + // const SizedBox( + // height: 12, // ), - // ), + // if ((coin == Coin.firoTestNet || coin == Coin.firo) && + // _transaction.subType == "mint") + // RoundedWhiteContainer( + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // Text( + // "Mint Transaction ID", + // style: STextStyles.itemSubtitle(context), + // ), + // ], + // ), + // const SizedBox( + // height: 8, + // ), + // // Flexible( + // // child: FittedBox( + // // fit: BoxFit.scaleDown, + // // child: + // SelectableText( + // _transaction.otherData ?? "Unknown", + // style: STextStyles.itemSubtitle12(context), + // ), + // // ), + // // ), + // const SizedBox( + // height: 8, + // ), + // BlueTextButton( + // text: "Open in block explorer", + // onTap: () async { + // final uri = getBlockExplorerTransactionUrlFor( + // coin: coin, + // txid: _transaction.otherData ?? "Unknown", + // ); + // // ref + // // .read( + // // shouldShowLockscreenOnResumeStateProvider + // // .state) + // // .state = false; + // try { + // await launchUrl( + // uri, + // mode: LaunchMode.externalApplication, + // ); + // } catch (_) { + // unawaited(showDialog<void>( + // context: context, + // builder: (_) => StackOkDialog( + // title: "Could not open in block explorer", + // message: + // "Failed to open \"${uri.toString()}\"", + // ), + // )); + // } finally { + // // Future<void>.delayed( + // // const Duration(seconds: 1), + // // () => ref + // // .read( + // // shouldShowLockscreenOnResumeStateProvider + // // .state) + // // .state = true, + // // ); + // } + // }, + // ), + // ], + // ), + // ), + if (coin == Coin.epicCash) + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + if (coin == Coin.epicCash) + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Slate ID", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle(context), + ), + // Flexible( + // child: FittedBox( + // fit: BoxFit.scaleDown, + // child: + SelectableText( + _transaction.slateId ?? "Unknown", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12(context), + ), + // ), + // ), + ], + ), + if (isDesktop) + const SizedBox( + width: 12, + ), + if (isDesktop) + IconCopyButton( + data: _transaction.slateId ?? "Unknown", + ), + ], + ), + ), + if (!isDesktop) + const SizedBox( + height: 12, + ), ], ), ), - const SizedBox( - height: 12, ), - ], + ), ), - ), + ], ), ), floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, @@ -805,3 +1222,94 @@ class _TransactionDetailsViewState ); } } + +class _Divider extends StatelessWidget { + const _Divider({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ); + } +} + +class IconCopyButton extends StatelessWidget { + const IconCopyButton({ + Key? key, + required this.data, + }) : super(key: key); + + final String data; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 26, + width: 26, + child: RawMaterialButton( + fillColor: + Theme.of(context).extension<StackColors>()!.buttonBackSecondary, + elevation: 0, + hoverElevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + onPressed: () async { + await Clipboard.setData(ClipboardData(text: data)); + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, + ), + ); + }, + child: Padding( + padding: const EdgeInsets.all(5), + child: CopyIcon( + width: 16, + height: 16, + color: Theme.of(context).extension<StackColors>()!.textDark, + ), + ), + ), + ); + } +} + +class IconPencilButton extends StatelessWidget { + const IconPencilButton({ + Key? key, + this.onPressed, + }) : super(key: key); + + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 26, + width: 26, + child: RawMaterialButton( + fillColor: + Theme.of(context).extension<StackColors>()!.buttonBackSecondary, + elevation: 0, + hoverElevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + onPressed: () => onPressed?.call(), + child: Padding( + padding: const EdgeInsets.all(5), + child: PencilIcon( + width: 16, + height: 16, + color: Theme.of(context).extension<StackColors>()!.textDark, + ), + ), + ), + ); + } +} diff --git a/lib/widgets/icon_widgets/copy_icon.dart b/lib/widgets/icon_widgets/copy_icon.dart new file mode 100644 index 000000000..9f82a8066 --- /dev/null +++ b/lib/widgets/icon_widgets/copy_icon.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; + +class CopyIcon extends StatelessWidget { + const CopyIcon({ + Key? key, + this.width = 18, + this.height = 18, + this.color, + }) : super(key: key); + + final double width; + final double height; + final Color? color; + + @override + Widget build(BuildContext context) { + return SvgPicture.asset( + Assets.svg.copy, + width: width, + height: height, + color: color ?? Theme.of(context).extension<StackColors>()!.textDark3, + ); + } +} diff --git a/lib/widgets/icon_widgets/pencil_icon.dart b/lib/widgets/icon_widgets/pencil_icon.dart new file mode 100644 index 000000000..cb14f1cbf --- /dev/null +++ b/lib/widgets/icon_widgets/pencil_icon.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; + +class PencilIcon extends StatelessWidget { + const PencilIcon({ + Key? key, + this.width = 18, + this.height = 18, + this.color, + }) : super(key: key); + + final double width; + final double height; + final Color? color; + + @override + Widget build(BuildContext context) { + return SvgPicture.asset( + Assets.svg.pencil, + width: width, + height: height, + color: color ?? Theme.of(context).extension<StackColors>()!.textDark3, + ); + } +} diff --git a/lib/widgets/transaction_card.dart b/lib/widgets/transaction_card.dart index de0447684..8867e34f6 100644 --- a/lib/widgets/transaction_card.dart +++ b/lib/widgets/transaction_card.dart @@ -14,6 +14,7 @@ import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:tuple/tuple.dart'; class TransactionCard extends ConsumerStatefulWidget { @@ -138,16 +139,31 @@ class _TransactionCardState extends ConsumerState<TransactionCard> { )); return; } - unawaited( - Navigator.of(context).pushNamed( - TransactionDetailsView.routeName, - arguments: Tuple3( - _transaction, - coin, - walletId, + if (Util.isDesktop) { + await showDialog<void>( + context: context, + builder: (context) => DesktopDialog( + maxHeight: MediaQuery.of(context).size.height - 64, + maxWidth: 580, + child: TransactionDetailsView( + transaction: _transaction, + coin: coin, + walletId: walletId, + ), ), - ), - ); + ); + } else { + unawaited( + Navigator.of(context).pushNamed( + TransactionDetailsView.routeName, + arguments: Tuple3( + _transaction, + coin, + walletId, + ), + ), + ); + } }, child: Padding( padding: const EdgeInsets.all(8), diff --git a/pubspec.lock b/pubspec.lock index 4f886e8bb..20992d827 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -42,7 +42,7 @@ packages: name: archive url: "https://pub.dartlang.org" source: hosted - version: "3.3.0" + version: "3.1.11" args: dependency: transitive description: @@ -63,7 +63,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.9.0" + version: "2.8.2" barcode_scan2: dependency: "direct main" description: @@ -190,7 +190,14 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "1.2.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" checked_yaml: dependency: transitive description: @@ -204,7 +211,7 @@ packages: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" + version: "1.1.0" code_builder: dependency: transitive description: @@ -274,7 +281,7 @@ packages: name: coverage url: "https://pub.dartlang.org" source: hosted - version: "1.5.0" + version: "1.2.0" cross_file: dependency: transitive description: @@ -428,7 +435,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.3.1" + version: "1.3.0" ffi: dependency: "direct main" description: @@ -857,21 +864,21 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.12" + version: "0.12.11" material_color_utilities: dependency: transitive description: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.5" + version: "0.1.4" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.7.0" mime: dependency: transitive description: @@ -983,7 +990,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.2" + version: "1.8.1" path_drawing: dependency: transitive description: @@ -1359,7 +1366,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.9.0" + version: "1.8.2" stack_trace: dependency: transitive description: @@ -1417,35 +1424,35 @@ packages: name: sync_http url: "https://pub.dartlang.org" source: hosted - version: "0.3.1" + version: "0.3.0" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "1.2.0" test: dependency: transitive description: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.21.4" + version: "1.21.1" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.12" + version: "0.4.9" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.4.16" + version: "0.4.13" time: dependency: transitive description: @@ -1494,7 +1501,7 @@ packages: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.3.1" + version: "1.3.0" universal_io: dependency: transitive description: @@ -1578,7 +1585,7 @@ packages: name: vm_service url: "https://pub.dartlang.org" source: hosted - version: "9.0.0" + version: "8.2.2" wakelock: dependency: "direct main" description: From 32eb9bb9200d16a6160700897cfe3464067a1ad5 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Sat, 29 Oct 2022 13:44:37 -0600 Subject: [PATCH 074/426] desktop tx details scroll fix --- .../transaction_details_view.dart | 1659 +++++++++-------- 1 file changed, 845 insertions(+), 814 deletions(-) diff --git a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart index d7f06095d..d1e415b26 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart @@ -252,91 +252,438 @@ class _TransactionDetailsViewState const DesktopDialogCloseButton(), ], ), - Padding( - padding: isDesktop - ? const EdgeInsets.only( - right: 32, - bottom: 32, - ) - : const EdgeInsets.all(0), - child: RoundedWhiteContainer( - borderColor: isDesktop - ? Theme.of(context).extension<StackColors>()!.background - : null, - padding: const EdgeInsets.all(0), - child: SingleChildScrollView( - child: Padding( - padding: isDesktop - ? const EdgeInsets.all(0) - : const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(0) - : const EdgeInsets.all(12), - child: Container( - decoration: isDesktop - ? BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .background, - borderRadius: BorderRadius.vertical( - top: Radius.circular( - Constants.size.circularBorderRadius, + Expanded( + child: Padding( + padding: isDesktop + ? const EdgeInsets.only( + right: 32, + bottom: 32, + ) + : const EdgeInsets.all(0), + child: RoundedWhiteContainer( + borderColor: isDesktop + ? Theme.of(context).extension<StackColors>()!.background + : null, + padding: const EdgeInsets.all(0), + child: SingleChildScrollView( + primary: isDesktop ? false : null, + child: Padding( + padding: isDesktop + ? const EdgeInsets.all(0) + : const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(0) + : const EdgeInsets.all(12), + child: Container( + decoration: isDesktop + ? BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .background, + borderRadius: BorderRadius.vertical( + top: Radius.circular( + Constants.size.circularBorderRadius, + ), ), + ) + : null, + child: Padding( + padding: isDesktop + ? const EdgeInsets.all(12) + : const EdgeInsets.all(0), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + if (isDesktop) + Row( + children: [ + TxIcon( + transaction: _transaction, + ), + const SizedBox( + width: 16, + ), + SelectableText( + _transaction.isCancelled + ? "Cancelled" + : whatIsIt(_transaction.txType), + style: + STextStyles.desktopTextMedium( + context), + ), + ], + ), + Column( + crossAxisAlignment: isDesktop + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + SelectableText( + "$amountPrefix${Format.localizedStringAsFixed( + value: coin == Coin.monero + ? (amount / 10000.toDecimal()) + .toDecimal() + : coin == Coin.wownero + ? (amount / + 1000.toDecimal()) + .toDecimal() + : amount, + locale: ref.watch( + localeServiceChangeNotifierProvider + .select( + (value) => value.locale), + ), + decimalPlaces: + Constants.decimalPlaces, + )} ${coin.ticker}", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.titleBold12( + context), + ), + const SizedBox( + height: 2, + ), + if (ref.watch( + prefsChangeNotifierProvider.select( + (value) => + value.externalCalls))) + SelectableText( + "$amountPrefix${Format.localizedStringAsFixed(value: (coin == Coin.monero ? (amount / 10000.toDecimal()).toDecimal() : coin == Coin.wownero ? (amount / 1000.toDecimal()).toDecimal() : amount) * ref.watch(priceAnd24hChangeNotifierProvider.select((value) => value.getPrice(coin).item1)), locale: ref.watch( + localeServiceChangeNotifierProvider + .select((value) => + value.locale), + ), decimalPlaces: 2)} ${ref.watch( + prefsChangeNotifierProvider + .select( + (value) => value.currency, + ), + )}", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle( + context), + ), + ], ), - ) - : null, - child: Padding( + if (!isDesktop) + TxIcon( + transaction: _transaction, + ), + ], + ), + ), + ), + ), + + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Status", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle(context), + ), + // Flexible( + // child: FittedBox( + // fit: BoxFit.scaleDown, + // child: + SelectableText( + _transaction.isCancelled + ? "Cancelled" + : whatIsIt(_transaction.txType), + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: _transaction.txType == "Sent" + ? Theme.of(context) + .extension<StackColors>()! + .accentColorOrange + : Theme.of(context) + .extension<StackColors>()! + .accentColorGreen, + ) + : STextStyles.itemSubtitle12(context), + ), + // ), + // ), + ], + ), + ), + if (!((coin == Coin.monero || coin == Coin.wownero) && + _transaction.txType.toLowerCase() == + "sent") && + !((coin == Coin.firo || + coin == Coin.firoTestNet) && + _transaction.subType == "mint")) + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + if (!((coin == Coin.monero || coin == Coin.wownero) && + _transaction.txType.toLowerCase() == + "sent") && + !((coin == Coin.firo || + coin == Coin.firoTestNet) && + _transaction.subType == "mint")) + RoundedWhiteContainer( padding: isDesktop - ? const EdgeInsets.all(12) - : const EdgeInsets.all(0), + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (isDesktop) - Row( - children: [ - TxIcon( - transaction: _transaction, - ), - const SizedBox( - width: 16, - ), - SelectableText( - _transaction.isCancelled - ? "Cancelled" - : whatIsIt(_transaction.txType), - style: STextStyles.desktopTextMedium( - context), - ), - ], - ), Column( - crossAxisAlignment: isDesktop - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, children: [ - SelectableText( - "$amountPrefix${Format.localizedStringAsFixed( - value: coin == Coin.monero - ? (amount / 10000.toDecimal()) - .toDecimal() - : coin == Coin.wownero - ? (amount / 1000.toDecimal()) - .toDecimal() - : amount, - locale: ref.watch( - localeServiceChangeNotifierProvider - .select( - (value) => value.locale), + Text( + _transaction.txType.toLowerCase() == + "sent" + ? "Sent to" + : "Receiving address", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle(context), + ), + const SizedBox( + height: 8, + ), + _transaction.txType.toLowerCase() == + "received" + ? FutureBuilder( + future: fetchContactNameFor( + _transaction.address), + builder: (builderContext, + AsyncSnapshot<String> + snapshot) { + String addressOrContactName = + _transaction.address; + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + addressOrContactName = + snapshot.data!; + } + return SelectableText( + addressOrContactName, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textDark, + ) + : STextStyles + .itemSubtitle12( + context), + ); + }, + ) + : SelectableText( + _transaction.address, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context), + ), + ], + ), + if (isDesktop) + IconCopyButton( + data: _transaction.address, + ), + ], + ), + ), + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Note", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle(context), + ), + isDesktop + ? IconPencilButton( + onPressed: () { + showDialog<void>( + context: context, + builder: (context) { + return DesktopDialog( + maxWidth: 580, + maxHeight: 360, + child: EditNoteView( + txid: _transaction.txid, + walletId: walletId, + note: _note, + ), + ); + }, + ); + }, + ) + : GestureDetector( + onTap: () { + Navigator.of(context).pushNamed( + EditNoteView.routeName, + arguments: Tuple3( + _transaction.txid, + walletId, + _note, + ), + ); + }, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.pencil, + width: 10, + height: 10, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + ), + const SizedBox( + width: 4, + ), + Text( + "Edit", + style: STextStyles.link2( + context), + ), + ], + ), ), - decimalPlaces: - Constants.decimalPlaces, - )} ${coin.ticker}", + ], + ), + const SizedBox( + height: 8, + ), + FutureBuilder( + future: ref.watch( + notesServiceChangeNotifierProvider( + walletId) + .select((value) => value.getNoteFor( + txid: _transaction.txid))), + builder: (builderContext, + AsyncSnapshot<String> snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + _note = snapshot.data ?? ""; + } + return SelectableText( + _note, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12(context), + ); + }, + ), + ], + ), + ), + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Date", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle(context), + ), + if (isDesktop) + const SizedBox( + height: 2, + ), + if (isDesktop) + SelectableText( + Format.extractDateFrom( + _transaction.timestamp, + ), style: isDesktop ? STextStyles .desktopTextExtraExtraSmall( @@ -346,774 +693,319 @@ class _TransactionDetailsViewState .extension<StackColors>()! .textDark, ) - : STextStyles.titleBold12(context), + : STextStyles.itemSubtitle12( + context), ), - const SizedBox( - height: 2, + ], + ), + if (!isDesktop) + SelectableText( + Format.extractDateFrom( + _transaction.timestamp, + ), + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12(context), + ), + if (isDesktop) + IconCopyButton( + data: Format.extractDateFrom( + _transaction.timestamp, + ), + ), + ], + ), + ), + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Builder(builder: (context) { + final feeString = showFeePending + ? _transaction.confirmedStatus + ? Format.localizedStringAsFixed( + value: coin == Coin.monero + ? (fee / 10000.toDecimal()) + .toDecimal() + : coin == Coin.wownero + ? (fee / 1000.toDecimal()) + .toDecimal() + : fee, + locale: ref.watch( + localeServiceChangeNotifierProvider + .select( + (value) => value.locale)), + decimalPlaces: + Constants.decimalPlaces) + : "Pending" + : Format.localizedStringAsFixed( + value: coin == Coin.monero + ? (fee / 10000.toDecimal()) + .toDecimal() + : coin == Coin.wownero + ? (fee / 1000.toDecimal()) + .toDecimal() + : fee, + locale: ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale)), + decimalPlaces: Constants.decimalPlaces); + + return Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Transaction fee", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle(context), ), - if (ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.externalCalls))) + if (isDesktop) + const SizedBox( + height: 2, + ), + if (isDesktop) SelectableText( - "$amountPrefix${Format.localizedStringAsFixed(value: (coin == Coin.monero ? (amount / 10000.toDecimal()).toDecimal() : coin == Coin.wownero ? (amount / 1000.toDecimal()).toDecimal() : amount) * ref.watch(priceAnd24hChangeNotifierProvider.select((value) => value.getPrice(coin).item1)), locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => - value.locale), - ), decimalPlaces: 2)} ${ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.currency, - ), - )}", + feeString, style: isDesktop ? STextStyles - .desktopTextExtraExtraSmall( - context) - : STextStyles.itemSubtitle( + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12( context), ), ], ), if (!isDesktop) - TxIcon( - transaction: _transaction, + SelectableText( + feeString, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12(context), ), + if (isDesktop) IconCopyButton(data: feeString) ], - ), - ), + ); + }), + ), + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Builder(builder: (context) { + final height = widget.coin != Coin.epicCash && + _transaction.confirmedStatus + ? "${_transaction.height == 0 ? "Unknown" : _transaction.height}" + : _transaction.confirmations > 0 + ? "${_transaction.height}" + : "Pending"; + + return Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Block height", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle(context), + ), + if (isDesktop) + const SizedBox( + height: 2, + ), + if (isDesktop) + SelectableText( + height, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context), + ), + ], + ), + if (!isDesktop) + SelectableText( + height, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12(context), + ), + if (isDesktop) IconCopyButton(data: height), + ], + ); + }), ), - ), - - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Status", - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall( - context) - : STextStyles.itemSubtitle(context), - ), - // Flexible( - // child: FittedBox( - // fit: BoxFit.scaleDown, - // child: - SelectableText( - _transaction.isCancelled - ? "Cancelled" - : whatIsIt(_transaction.txType), - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall( - context) - .copyWith( - color: _transaction.txType == "Sent" - ? Theme.of(context) - .extension<StackColors>()! - .accentColorOrange - : Theme.of(context) - .extension<StackColors>()! - .accentColorGreen, - ) - : STextStyles.itemSubtitle12(context), - ), - // ), - // ), - ], - ), - ), - if (!((coin == Coin.monero || coin == Coin.wownero) && - _transaction.txType.toLowerCase() == "sent") && - !((coin == Coin.firo || coin == Coin.firoTestNet) && - _transaction.subType == "mint")) isDesktop ? const _Divider() : const SizedBox( height: 12, ), - if (!((coin == Coin.monero || coin == Coin.wownero) && - _transaction.txType.toLowerCase() == "sent") && - !((coin == Coin.firo || coin == Coin.firoTestNet) && - _transaction.subType == "mint")) RoundedWhiteContainer( padding: isDesktop ? const EdgeInsets.all(16) : const EdgeInsets.all(12), child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - _transaction.txType.toLowerCase() == - "sent" - ? "Sent to" - : "Receiving address", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - : STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 8, - ), - _transaction.txType.toLowerCase() == - "received" - ? FutureBuilder( - future: fetchContactNameFor( - _transaction.address), - builder: (builderContext, - AsyncSnapshot<String> - snapshot) { - String addressOrContactName = - _transaction.address; - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - addressOrContactName = - snapshot.data!; - } - return SelectableText( - addressOrContactName, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles - .itemSubtitle12( - context), - ); - }, - ) - : SelectableText( - _transaction.address, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles.itemSubtitle12( - context), - ), - ], - ), - if (isDesktop) - IconCopyButton( - data: _transaction.address, - ), - ], - ), - ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Note", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall(context) - : STextStyles.itemSubtitle(context), - ), - isDesktop - ? IconPencilButton( - onPressed: () { - showDialog<void>( - context: context, - builder: (context) { - return DesktopDialog( - maxWidth: 580, - maxHeight: 360, - child: EditNoteView( - txid: _transaction.txid, - walletId: walletId, - note: _note, - ), - ); - }, - ); - }, - ) - : GestureDetector( - onTap: () { - Navigator.of(context).pushNamed( - EditNoteView.routeName, - arguments: Tuple3( - _transaction.txid, - walletId, - _note, - ), - ); - }, - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.pencil, - width: 10, - height: 10, - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - ), - const SizedBox( - width: 4, - ), - Text( - "Edit", - style: - STextStyles.link2(context), - ), - ], - ), - ), - ], - ), - const SizedBox( - height: 8, - ), - FutureBuilder( - future: ref.watch( - notesServiceChangeNotifierProvider(walletId) - .select((value) => value.getNoteFor( - txid: _transaction.txid))), - builder: (builderContext, - AsyncSnapshot<String> snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - _note = snapshot.data ?? ""; - } - return SelectableText( - _note, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ) - : STextStyles.itemSubtitle12(context), - ); - }, - ), - ], - ), - ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Date", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall(context) - : STextStyles.itemSubtitle(context), - ), - if (isDesktop) - const SizedBox( - height: 2, - ), - if (isDesktop) - SelectableText( - Format.extractDateFrom( - _transaction.timestamp, - ), - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ) - : STextStyles.itemSubtitle12(context), - ), - ], - ), - if (!isDesktop) - SelectableText( - Format.extractDateFrom( - _transaction.timestamp, - ), - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ) - : STextStyles.itemSubtitle12(context), - ), - if (isDesktop) - IconCopyButton( - data: Format.extractDateFrom( - _transaction.timestamp, - ), - ), - ], - ), - ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Builder(builder: (context) { - final feeString = showFeePending - ? _transaction.confirmedStatus - ? Format.localizedStringAsFixed( - value: coin == Coin.monero - ? (fee / 10000.toDecimal()) - .toDecimal() - : coin == Coin.wownero - ? (fee / 1000.toDecimal()) - .toDecimal() - : fee, - locale: ref.watch( - localeServiceChangeNotifierProvider - .select( - (value) => value.locale)), - decimalPlaces: Constants.decimalPlaces) - : "Pending" - : Format.localizedStringAsFixed( - value: coin == Coin.monero - ? (fee / 10000.toDecimal()).toDecimal() - : coin == Coin.wownero - ? (fee / 1000.toDecimal()) - .toDecimal() - : fee, - locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale)), - decimalPlaces: Constants.decimalPlaces); - - return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Transaction fee", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - : STextStyles.itemSubtitle(context), - ), - if (isDesktop) - const SizedBox( - height: 2, - ), - if (isDesktop) - SelectableText( - feeString, + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Transaction ID", style: isDesktop ? STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ) - : STextStyles.itemSubtitle12( - context), - ), - ], - ), - if (!isDesktop) - SelectableText( - feeString, - style: isDesktop - ? STextStyles .desktopTextExtraExtraSmall( context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ) - : STextStyles.itemSubtitle12(context), - ), - if (isDesktop) IconCopyButton(data: feeString) - ], - ); - }), - ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Builder(builder: (context) { - final height = widget.coin != Coin.epicCash && - _transaction.confirmedStatus - ? "${_transaction.height == 0 ? "Unknown" : _transaction.height}" - : _transaction.confirmations > 0 - ? "${_transaction.height}" - : "Pending"; - - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Block height", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - : STextStyles.itemSubtitle(context), - ), - if (isDesktop) - const SizedBox( - height: 2, + : STextStyles.itemSubtitle(context), ), - if (isDesktop) - SelectableText( - height, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ) - : STextStyles.itemSubtitle12( - context), - ), - ], - ), - if (!isDesktop) - SelectableText( - height, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ) - : STextStyles.itemSubtitle12(context), - ), - if (isDesktop) IconCopyButton(data: height), - ], - ); - }), - ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Transaction ID", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - : STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 8, - ), - // Flexible( - // child: FittedBox( - // fit: BoxFit.scaleDown, - // child: - SelectableText( - _transaction.txid, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ) - : STextStyles.itemSubtitle12(context), - ), - if (coin != Coin.epicCash) const SizedBox( height: 8, ), - if (coin != Coin.epicCash) - BlueTextButton( - text: "Open in block explorer", - onTap: () async { - final uri = - getBlockExplorerTransactionUrlFor( - coin: coin, - txid: _transaction.txid, - ); - - if (ref - .read( - prefsChangeNotifierProvider) - .hideBlockExplorerWarning == - false) { - final shouldContinue = - await showExplorerWarning( - "${uri.scheme}://${uri.host}"); - - if (!shouldContinue) { - return; - } - } - - // ref - // .read( - // shouldShowLockscreenOnResumeStateProvider - // .state) - // .state = false; - try { - await launchUrl( - uri, - mode: LaunchMode - .externalApplication, - ); - } catch (_) { - unawaited(showDialog<void>( - context: context, - builder: (_) => StackOkDialog( - title: - "Could not open in block explorer", - message: - "Failed to open \"${uri.toString()}\"", - ), - )); - } finally { - // Future<void>.delayed( - // const Duration(seconds: 1), - // () => ref - // .read( - // shouldShowLockscreenOnResumeStateProvider - // .state) - // .state = true, - // ); - } - }, + // Flexible( + // child: FittedBox( + // fit: BoxFit.scaleDown, + // child: + SelectableText( + _transaction.txid, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context), ), - // ), - // ), - ], - ), - ), - if (isDesktop) - const SizedBox( - width: 12, - ), - if (isDesktop) - IconCopyButton( - data: _transaction.txid, - ), - ], - ), - ), - // if ((coin == Coin.firoTestNet || coin == Coin.firo) && - // _transaction.subType == "mint") - // const SizedBox( - // height: 12, - // ), - // if ((coin == Coin.firoTestNet || coin == Coin.firo) && - // _transaction.subType == "mint") - // RoundedWhiteContainer( - // child: Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // Text( - // "Mint Transaction ID", - // style: STextStyles.itemSubtitle(context), - // ), - // ], - // ), - // const SizedBox( - // height: 8, - // ), - // // Flexible( - // // child: FittedBox( - // // fit: BoxFit.scaleDown, - // // child: - // SelectableText( - // _transaction.otherData ?? "Unknown", - // style: STextStyles.itemSubtitle12(context), - // ), - // // ), - // // ), - // const SizedBox( - // height: 8, - // ), - // BlueTextButton( - // text: "Open in block explorer", - // onTap: () async { - // final uri = getBlockExplorerTransactionUrlFor( - // coin: coin, - // txid: _transaction.otherData ?? "Unknown", - // ); - // // ref - // // .read( - // // shouldShowLockscreenOnResumeStateProvider - // // .state) - // // .state = false; - // try { - // await launchUrl( - // uri, - // mode: LaunchMode.externalApplication, - // ); - // } catch (_) { - // unawaited(showDialog<void>( - // context: context, - // builder: (_) => StackOkDialog( - // title: "Could not open in block explorer", - // message: - // "Failed to open \"${uri.toString()}\"", - // ), - // )); - // } finally { - // // Future<void>.delayed( - // // const Duration(seconds: 1), - // // () => ref - // // .read( - // // shouldShowLockscreenOnResumeStateProvider - // // .state) - // // .state = true, - // // ); - // } - // }, - // ), - // ], - // ), - // ), - if (coin == Coin.epicCash) - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - if (coin == Coin.epicCash) - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Slate ID", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - : STextStyles.itemSubtitle(context), - ), - // Flexible( - // child: FittedBox( - // fit: BoxFit.scaleDown, - // child: - SelectableText( - _transaction.slateId ?? "Unknown", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ) - : STextStyles.itemSubtitle12(context), - ), - // ), - // ), - ], + if (coin != Coin.epicCash) + const SizedBox( + height: 8, + ), + if (coin != Coin.epicCash) + BlueTextButton( + text: "Open in block explorer", + onTap: () async { + final uri = + getBlockExplorerTransactionUrlFor( + coin: coin, + txid: _transaction.txid, + ); + + if (ref + .read( + prefsChangeNotifierProvider) + .hideBlockExplorerWarning == + false) { + final shouldContinue = + await showExplorerWarning( + "${uri.scheme}://${uri.host}"); + + if (!shouldContinue) { + return; + } + } + + // ref + // .read( + // shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = false; + try { + await launchUrl( + uri, + mode: LaunchMode + .externalApplication, + ); + } catch (_) { + unawaited(showDialog<void>( + context: context, + builder: (_) => StackOkDialog( + title: + "Could not open in block explorer", + message: + "Failed to open \"${uri.toString()}\"", + ), + )); + } finally { + // Future<void>.delayed( + // const Duration(seconds: 1), + // () => ref + // .read( + // shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = true, + // ); + } + }, + ), + // ), + // ), + ], + ), ), if (isDesktop) const SizedBox( @@ -1121,16 +1013,155 @@ class _TransactionDetailsViewState ), if (isDesktop) IconCopyButton( - data: _transaction.slateId ?? "Unknown", + data: _transaction.txid, ), ], ), ), - if (!isDesktop) - const SizedBox( - height: 12, - ), - ], + // if ((coin == Coin.firoTestNet || coin == Coin.firo) && + // _transaction.subType == "mint") + // const SizedBox( + // height: 12, + // ), + // if ((coin == Coin.firoTestNet || coin == Coin.firo) && + // _transaction.subType == "mint") + // RoundedWhiteContainer( + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // Text( + // "Mint Transaction ID", + // style: STextStyles.itemSubtitle(context), + // ), + // ], + // ), + // const SizedBox( + // height: 8, + // ), + // // Flexible( + // // child: FittedBox( + // // fit: BoxFit.scaleDown, + // // child: + // SelectableText( + // _transaction.otherData ?? "Unknown", + // style: STextStyles.itemSubtitle12(context), + // ), + // // ), + // // ), + // const SizedBox( + // height: 8, + // ), + // BlueTextButton( + // text: "Open in block explorer", + // onTap: () async { + // final uri = getBlockExplorerTransactionUrlFor( + // coin: coin, + // txid: _transaction.otherData ?? "Unknown", + // ); + // // ref + // // .read( + // // shouldShowLockscreenOnResumeStateProvider + // // .state) + // // .state = false; + // try { + // await launchUrl( + // uri, + // mode: LaunchMode.externalApplication, + // ); + // } catch (_) { + // unawaited(showDialog<void>( + // context: context, + // builder: (_) => StackOkDialog( + // title: "Could not open in block explorer", + // message: + // "Failed to open \"${uri.toString()}\"", + // ), + // )); + // } finally { + // // Future<void>.delayed( + // // const Duration(seconds: 1), + // // () => ref + // // .read( + // // shouldShowLockscreenOnResumeStateProvider + // // .state) + // // .state = true, + // // ); + // } + // }, + // ), + // ], + // ), + // ), + if (coin == Coin.epicCash) + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + if (coin == Coin.epicCash) + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Slate ID", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle(context), + ), + // Flexible( + // child: FittedBox( + // fit: BoxFit.scaleDown, + // child: + SelectableText( + _transaction.slateId ?? "Unknown", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context), + ), + // ), + // ), + ], + ), + if (isDesktop) + const SizedBox( + width: 12, + ), + if (isDesktop) + IconCopyButton( + data: _transaction.slateId ?? "Unknown", + ), + ], + ), + ), + if (!isDesktop) + const SizedBox( + height: 12, + ), + ], + ), ), ), ), From c9220c5c11518da877b5736f79dceb98f9744658 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Sun, 30 Oct 2022 11:13:32 -0600 Subject: [PATCH 075/426] desktop wallet renaming --- .../wallet_view/desktop_wallet_view.dart | 156 ++++++++++++------ lib/widgets/desktop/desktop_app_bar.dart | 10 +- lib/widgets/hover_text_field.dart | 123 ++++++++++++++ 3 files changed, 236 insertions(+), 53 deletions(-) create mode 100644 lib/widgets/hover_text_field.dart diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index 0e5aa925f..5dadd511c 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -43,6 +43,7 @@ import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/hover_text_field.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; @@ -66,6 +67,7 @@ class DesktopWalletView extends ConsumerStatefulWidget { } class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { + late final TextEditingController controller; late final String walletId; late final EventBus eventBus; @@ -179,10 +181,13 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { @override void initState() { + controller = TextEditingController(); walletId = widget.walletId; final managerProvider = ref.read(walletsChangeNotifierProvider).getManagerProvider(walletId); + controller.text = ref.read(managerProvider).walletName; + eventBus = widget.eventBus != null ? widget.eventBus! : GlobalEventBus.instance; @@ -211,61 +216,110 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { return DesktopScaffold( appBar: DesktopAppBar( background: Theme.of(context).extension<StackColors>()!.popupBG, - leading: Row( - children: [ - const SizedBox( - width: 32, - ), - AppBarIconButton( - size: 32, - color: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - shadows: const [], - icon: SvgPicture.asset( - Assets.svg.arrowLeft, - width: 18, - height: 18, + leading: Expanded( + child: Row( + children: [ + const SizedBox( + width: 32, + ), + AppBarIconButton( + size: 32, color: Theme.of(context) .extension<StackColors>()! - .topNavIconPrimary, + .textFieldDefaultBG, + shadows: const [], + icon: SvgPicture.asset( + Assets.svg.arrowLeft, + width: 18, + height: 18, + color: Theme.of(context) + .extension<StackColors>()! + .topNavIconPrimary, + ), + onPressed: onBackPressed, ), - onPressed: onBackPressed, - ), - const SizedBox( - width: 15, - ), - SvgPicture.asset( - Assets.svg.iconFor(coin: coin), - width: 32, - height: 32, - ), - const SizedBox( - width: 12, - ), - Text( - manager.walletName, - style: STextStyles.desktopH3(context), - ), - ], - ), - trailing: Row( - children: [ - NetworkInfoButton( - walletId: walletId, - eventBus: eventBus, - ), - const SizedBox( - width: 32, - ), - WalletKeysButton( - walletId: walletId, - ), - const SizedBox( - width: 32, - ), - ], + const SizedBox( + width: 15, + ), + SvgPicture.asset( + Assets.svg.iconFor(coin: coin), + width: 32, + height: 32, + ), + const SizedBox( + width: 12, + ), + ConstrainedBox( + constraints: const BoxConstraints( + minWidth: 48, + ), + child: IntrinsicWidth( + child: HoverTextField( + controller: controller, + style: STextStyles.desktopH3(context), + readOnly: true, + onDone: () async { + final currentWalletName = + ref.read(managerProvider).walletName; + final newName = controller.text; + if (newName != currentWalletName) { + final success = await ref + .read(walletsServiceChangeNotifierProvider) + .renameWallet( + from: currentWalletName, + to: newName, + shouldNotifyListeners: true, + ); + if (success) { + ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .walletName = newName; + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "Wallet renamed", + context: context, + ), + ); + } else { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: + "Wallet named \"$newName\" already exists", + context: context, + ), + ); + controller.text = currentWalletName; + } + } + }, + ), + ), + ), + const Spacer(), + Row( + children: [ + NetworkInfoButton( + walletId: walletId, + eventBus: eventBus, + ), + const SizedBox( + width: 32, + ), + WalletKeysButton( + walletId: walletId, + ), + const SizedBox( + width: 32, + ), + ], + ), + ], + ), ), + useSpacers: false, isCompactHeight: true, ), body: Padding( diff --git a/lib/widgets/desktop/desktop_app_bar.dart b/lib/widgets/desktop/desktop_app_bar.dart index 1c825382c..bbad0385a 100644 --- a/lib/widgets/desktop/desktop_app_bar.dart +++ b/lib/widgets/desktop/desktop_app_bar.dart @@ -11,6 +11,7 @@ class DesktopAppBar extends StatefulWidget { this.trailing, this.background = Colors.transparent, required this.isCompactHeight, + this.useSpacers = true, }) : super(key: key); final Widget? leading; @@ -18,6 +19,7 @@ class DesktopAppBar extends StatefulWidget { final Widget? trailing; final Color background; final bool isCompactHeight; + final bool useSpacers; @override State<DesktopAppBar> createState() => _DesktopAppBarState(); @@ -33,11 +35,15 @@ class _DesktopAppBarState extends State<DesktopAppBar> { items.add(widget.leading!); } - items.add(const Spacer()); + if (widget.useSpacers) { + items.add(const Spacer()); + } if (widget.center != null) { items.add(widget.center!); - items.add(const Spacer()); + if (widget.useSpacers) { + items.add(const Spacer()); + } } if (widget.trailing != null) { diff --git a/lib/widgets/hover_text_field.dart b/lib/widgets/hover_text_field.dart new file mode 100644 index 000000000..475d6c2ec --- /dev/null +++ b/lib/widgets/hover_text_field.dart @@ -0,0 +1,123 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; + +class HoverTextField extends StatefulWidget { + const HoverTextField({ + Key? key, + this.controller, + this.focusNode, + this.readOnly = false, + this.enabled, + this.onTap, + this.onChanged, + this.onEditingComplete, + this.style, + this.onDone, + }) : super(key: key); + + final TextEditingController? controller; + final FocusNode? focusNode; + final bool readOnly; + final bool? enabled; + final GestureTapCallback? onTap; + final ValueChanged<String>? onChanged; + final VoidCallback? onEditingComplete; + final TextStyle? style; + final VoidCallback? onDone; + + @override + State<HoverTextField> createState() => _HoverTextFieldState(); +} + +class _HoverTextFieldState extends State<HoverTextField> { + late final TextEditingController? controller; + late final FocusNode? focusNode; + late bool readOnly; + late bool? enabled; + late final GestureTapCallback? onTap; + late final ValueChanged<String>? onChanged; + late final VoidCallback? onEditingComplete; + late final TextStyle? style; + late final VoidCallback? onDone; + + final InputBorder inputBorder = OutlineInputBorder( + borderSide: const BorderSide( + width: 0, + color: Colors.transparent, + ), + borderRadius: BorderRadius.circular(Constants.size.circularBorderRadius), + ); + + @override + void initState() { + controller = widget.controller; + focusNode = widget.focusNode ?? FocusNode(); + readOnly = widget.readOnly; + enabled = widget.enabled; + onChanged = widget.onChanged; + style = widget.style; + onTap = widget.onTap; + onEditingComplete = widget.onEditingComplete; + onDone = widget.onDone; + + focusNode!.addListener(() { + if (!focusNode!.hasPrimaryFocus && !readOnly) { + setState(() { + readOnly = true; + }); + onDone?.call(); + } + }); + super.initState(); + } + + @override + void dispose() { + controller?.dispose(); + focusNode?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return TextField( + autocorrect: !Util.isDesktop, + enableSuggestions: !Util.isDesktop, + controller: controller, + focusNode: focusNode, + readOnly: readOnly, + enabled: enabled, + onTap: () { + setState(() { + readOnly = false; + }); + onTap?.call(); + }, + onChanged: onChanged, + onEditingComplete: () { + setState(() { + readOnly = true; + }); + onEditingComplete?.call(); + onDone?.call(); + }, + style: style, + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric( + vertical: 4, + horizontal: 12, + ), + border: inputBorder, + focusedBorder: inputBorder, + disabledBorder: inputBorder, + enabledBorder: inputBorder, + errorBorder: inputBorder, + fillColor: readOnly + ? Colors.transparent + : Theme.of(context).extension<StackColors>()!.textFieldDefaultBG, + ), + ); + } +} From a1f4ec87de975904c8c8f17c7876ff23e209ba4b Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Sun, 30 Oct 2022 11:26:58 -0600 Subject: [PATCH 076/426] desktop wallet view refactor --- .../wallet_view/desktop_wallet_view.dart | 873 +----------------- .../desktop_receive.dart | 0 .../{send => sub_widgets}/desktop_send.dart | 0 .../desktop_wallet_summary.dart | 0 .../wallet_view/sub_widgets/my_wallet.dart | 89 ++ .../sub_widgets/network_info_button.dart | 176 ++++ .../qr_code_desktop_popup_content.dart | 42 + .../recent_desktop_transactions.dart | 64 ++ .../sub_widgets/send_receive_tab_menu.dart | 128 +++ .../unlock_wallet_keys_desktop.dart | 228 +++++ .../sub_widgets/wallet_keys_button.dart | 52 ++ .../wallet_keys_desktop_popup.dart | 146 +++ lib/route_generator.dart | 2 + 13 files changed, 932 insertions(+), 868 deletions(-) rename lib/pages_desktop_specific/home/my_stack_view/wallet_view/{receive => sub_widgets}/desktop_receive.dart (100%) rename lib/pages_desktop_specific/home/my_stack_view/wallet_view/{send => sub_widgets}/desktop_send.dart (100%) rename lib/pages_desktop_specific/home/my_stack_view/wallet_view/{ => sub_widgets}/desktop_wallet_summary.dart (100%) create mode 100644 lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/my_wallet.dart create mode 100644 lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart create mode 100644 lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/qr_code_desktop_popup_content.dart create mode 100644 lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/recent_desktop_transactions.dart create mode 100644 lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/send_receive_tab_menu.dart create mode 100644 lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart create mode 100644 lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_button.dart create mode 100644 lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index 5dadd511c..97efb2f68 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -2,33 +2,24 @@ import 'dart:async'; import 'package:event_bus/event_bus.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:qr_flutter/qr_flutter.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; -import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart'; import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart'; import 'package:stackwallet/pages/exchange_view/wallet_initiated_exchange_view.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart'; -import 'package:stackwallet/pages/wallet_view/sub_widgets/transactions_list.dart'; -import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_view.dart'; -import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_summary.dart'; -import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/receive/desktop_receive.dart'; -import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/my_wallet.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/recent_desktop_transactions.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_button.dart'; import 'package:stackwallet/providers/global/auto_swb_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/transaction_filter_provider.dart'; -import 'package:stackwallet/route_generator.dart'; -import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart'; import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart'; -import 'package:stackwallet/utilities/address_utils.dart'; import 'package:stackwallet/utilities/assets.dart'; -import 'package:stackwallet/utilities/clipboard_interface.dart'; -import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; @@ -36,17 +27,12 @@ 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_buttons/app_bar_icon_button.dart'; -import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; -import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; -import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; -import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/hover_text_field.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; -import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:tuple/tuple.dart'; /// [eventBus] should only be set during testing @@ -408,852 +394,3 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { ); } } - -class MyWallet extends StatefulWidget { - const MyWallet({ - Key? key, - required this.walletId, - }) : super(key: key); - - final String walletId; - - @override - State<MyWallet> createState() => _MyWalletState(); -} - -class _MyWalletState extends State<MyWallet> { - int _selectedIndex = 0; - - @override - Widget build(BuildContext context) { - return ListView( - primary: false, - children: [ - Text( - "My wallet", - style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textFieldActiveSearchIconLeft, - ), - ), - const SizedBox( - height: 16, - ), - Container( - decoration: BoxDecoration( - color: Theme.of(context).extension<StackColors>()!.popupBG, - borderRadius: BorderRadius.vertical( - top: Radius.circular( - Constants.size.circularBorderRadius, - ), - ), - ), - child: SendReceiveTabMenu( - onChanged: (index) { - setState(() { - _selectedIndex = index; - }); - }, - ), - ), - Container( - decoration: BoxDecoration( - color: Theme.of(context).extension<StackColors>()!.popupBG, - borderRadius: BorderRadius.vertical( - bottom: Radius.circular( - Constants.size.circularBorderRadius, - ), - ), - ), - child: IndexedStack( - index: _selectedIndex, - children: [ - Padding( - key: const Key("desktopSendViewPortKey"), - padding: const EdgeInsets.all(20), - child: DesktopSend( - walletId: widget.walletId, - ), - ), - Padding( - key: const Key("desktopReceiveViewPortKey"), - padding: const EdgeInsets.all(20), - child: DesktopReceive( - walletId: widget.walletId, - ), - ), - ], - ), - ), - ], - ); - } -} - -class SendReceiveTabMenu extends StatefulWidget { - const SendReceiveTabMenu({ - Key? key, - this.initialIndex = 0, - this.onChanged, - }) : super(key: key); - - final int initialIndex; - final void Function(int)? onChanged; - - @override - State<SendReceiveTabMenu> createState() => _SendReceiveTabMenuState(); -} - -class _SendReceiveTabMenuState extends State<SendReceiveTabMenu> { - late int _selectedIndex; - - void _onChanged(int newIndex) { - if (_selectedIndex != newIndex) { - setState(() { - _selectedIndex = newIndex; - }); - widget.onChanged?.call(_selectedIndex); - } - } - - @override - void initState() { - _selectedIndex = widget.initialIndex; - super.initState(); - } - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Expanded( - child: GestureDetector( - onTap: () => _onChanged(0), - child: Container( - color: Colors.transparent, - child: Column( - children: [ - const SizedBox( - height: 16, - ), - Text( - "Send", - style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: _selectedIndex == 0 - ? Theme.of(context) - .extension<StackColors>()! - .accentColorBlue - : Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ), - const SizedBox( - height: 19, - ), - Container( - height: 2, - decoration: BoxDecoration( - color: _selectedIndex == 0 - ? Theme.of(context) - .extension<StackColors>()! - .accentColorBlue - : Theme.of(context) - .extension<StackColors>()! - .background, - ), - ), - ], - ), - ), - ), - ), - Expanded( - child: GestureDetector( - onTap: () => _onChanged(1), - child: Container( - color: Colors.transparent, - child: Column( - children: [ - const SizedBox( - height: 16, - ), - Text( - "Receive", - style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: _selectedIndex == 1 - ? Theme.of(context) - .extension<StackColors>()! - .accentColorBlue - : Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ), - const SizedBox( - height: 19, - ), - Container( - height: 2, - decoration: BoxDecoration( - color: _selectedIndex == 1 - ? Theme.of(context) - .extension<StackColors>()! - .accentColorBlue - : Theme.of(context) - .extension<StackColors>()! - .background, - ), - ), - ], - ), - ), - ), - ), - ], - ); - } -} - -class RecentDesktopTransactions extends ConsumerStatefulWidget { - const RecentDesktopTransactions({ - Key? key, - required this.walletId, - }) : super(key: key); - - final String walletId; - - @override - ConsumerState<RecentDesktopTransactions> createState() => - _RecentDesktopTransactionsState(); -} - -class _RecentDesktopTransactionsState - extends ConsumerState<RecentDesktopTransactions> { - @override - Widget build(BuildContext context) { - return Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Recent transactions", - style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textFieldActiveSearchIconLeft, - ), - ), - BlueTextButton( - text: "See all", - onTap: () { - Navigator.of(context).pushNamed( - AllTransactionsView.routeName, - arguments: widget.walletId, - ); - }, - ), - ], - ), - const SizedBox( - height: 16, - ), - Expanded( - child: TransactionsList( - managerProvider: ref.watch(walletsChangeNotifierProvider - .select((value) => value.getManagerProvider(widget.walletId))), - walletId: widget.walletId, - ), - ), - ], - ); - } -} - -class NetworkInfoButton extends ConsumerStatefulWidget { - const NetworkInfoButton({ - Key? key, - required this.walletId, - this.eventBus, - }) : super(key: key); - - final String walletId; - final EventBus? eventBus; - - @override - ConsumerState<NetworkInfoButton> createState() => _NetworkInfoButtonState(); -} - -class _NetworkInfoButtonState extends ConsumerState<NetworkInfoButton> { - late final String walletId; - late final EventBus eventBus; - - late WalletSyncStatus _currentSyncStatus; - late NodeConnectionStatus _currentNodeStatus; - - late StreamSubscription<dynamic> _syncStatusSubscription; - late StreamSubscription<dynamic> _nodeStatusSubscription; - - @override - void initState() { - walletId = widget.walletId; - final managerProvider = - ref.read(walletsChangeNotifierProvider).getManagerProvider(walletId); - - eventBus = - widget.eventBus != null ? widget.eventBus! : GlobalEventBus.instance; - - if (ref.read(managerProvider).isRefreshing) { - _currentSyncStatus = WalletSyncStatus.syncing; - _currentNodeStatus = NodeConnectionStatus.connected; - } else { - _currentSyncStatus = WalletSyncStatus.synced; - if (ref.read(managerProvider).isConnected) { - _currentNodeStatus = NodeConnectionStatus.connected; - } else { - _currentNodeStatus = NodeConnectionStatus.disconnected; - _currentSyncStatus = WalletSyncStatus.unableToSync; - } - } - - _syncStatusSubscription = - eventBus.on<WalletSyncStatusChangedEvent>().listen( - (event) async { - if (event.walletId == widget.walletId) { - setState(() { - _currentSyncStatus = event.newStatus; - }); - } - }, - ); - - _nodeStatusSubscription = - eventBus.on<NodeConnectionStatusChangedEvent>().listen( - (event) async { - if (event.walletId == widget.walletId) { - setState(() { - _currentNodeStatus = event.newStatus; - }); - } - }, - ); - - super.initState(); - } - - @override - void dispose() { - _nodeStatusSubscription.cancel(); - _syncStatusSubscription.cancel(); - super.dispose(); - } - - Widget _buildNetworkIcon(WalletSyncStatus status, BuildContext context) { - const size = 24.0; - switch (status) { - case WalletSyncStatus.unableToSync: - return SvgPicture.asset( - Assets.svg.radioProblem, - color: Theme.of(context).extension<StackColors>()!.accentColorRed, - width: size, - height: size, - ); - case WalletSyncStatus.synced: - return SvgPicture.asset( - Assets.svg.radio, - color: Theme.of(context).extension<StackColors>()!.accentColorGreen, - width: size, - height: size, - ); - case WalletSyncStatus.syncing: - return SvgPicture.asset( - Assets.svg.radioSyncing, - color: Theme.of(context).extension<StackColors>()!.accentColorYellow, - width: size, - height: size, - ); - } - } - - Widget _buildText(WalletSyncStatus status, BuildContext context) { - String label; - Color color; - - switch (status) { - case WalletSyncStatus.unableToSync: - label = "Unable to sync"; - color = Theme.of(context).extension<StackColors>()!.accentColorRed; - break; - case WalletSyncStatus.synced: - label = "Synchronised"; - color = Theme.of(context).extension<StackColors>()!.accentColorGreen; - break; - case WalletSyncStatus.syncing: - label = "Synchronising"; - color = Theme.of(context).extension<StackColors>()!.accentColorYellow; - break; - } - - return Text( - label, - style: STextStyles.desktopMenuItemSelected(context).copyWith( - color: color, - ), - ); - } - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () { - Navigator.of(context).pushNamed( - WalletNetworkSettingsView.routeName, - arguments: Tuple3( - walletId, - _currentSyncStatus, - _currentNodeStatus, - ), - ); - }, - child: Container( - color: Colors.transparent, - child: Row( - children: [ - _buildNetworkIcon(_currentSyncStatus, context), - const SizedBox( - width: 6, - ), - _buildText(_currentSyncStatus, context), - ], - ), - ), - ); - } -} - -class WalletKeysButton extends StatelessWidget { - const WalletKeysButton({ - Key? key, - required this.walletId, - }) : super(key: key); - - final String walletId; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () { - showDialog<void>( - context: context, - barrierDismissible: false, - builder: (context) => UnlockWalletKeysDesktop( - walletId: walletId, - ), - ); - }, - child: Container( - color: Colors.transparent, - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.key, - width: 20, - height: 20, - color: Theme.of(context) - .extension<StackColors>()! - .buttonTextSecondary, - ), - const SizedBox( - width: 6, - ), - Text( - "Wallet keys", - style: STextStyles.desktopMenuItemSelected(context), - ) - ], - ), - ), - ); - } -} - -class UnlockWalletKeysDesktop extends ConsumerStatefulWidget { - const UnlockWalletKeysDesktop({ - Key? key, - required this.walletId, - }) : super(key: key); - - final String walletId; - - @override - ConsumerState<UnlockWalletKeysDesktop> createState() => - _UnlockWalletKeysDesktopState(); -} - -class _UnlockWalletKeysDesktopState - extends ConsumerState<UnlockWalletKeysDesktop> { - late final TextEditingController passwordController; - - late final FocusNode passwordFocusNode; - - bool continueEnabled = false; - bool hidePassword = true; - - @override - void initState() { - passwordController = TextEditingController(); - passwordFocusNode = FocusNode(); - super.initState(); - } - - @override - void dispose() { - passwordController.dispose(); - passwordFocusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return DesktopDialog( - maxWidth: 579, - maxHeight: double.infinity, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: const [ - DesktopDialogCloseButton(), - ], - ), - const SizedBox( - height: 12, - ), - SvgPicture.asset( - Assets.svg.keys, - width: 100, - height: 58, - ), - const SizedBox( - height: 55, - ), - Text( - "Wallet keys", - style: STextStyles.desktopH2(context), - ), - const SizedBox( - height: 16, - ), - Text( - "Enter your password", - style: STextStyles.desktopTextMedium(context).copyWith( - color: Theme.of(context).extension<StackColors>()!.textDark3, - ), - ), - const SizedBox( - height: 24, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - ), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("enterPasswordUnlockWalletKeysDesktopFieldKey"), - focusNode: passwordFocusNode, - controller: passwordController, - style: STextStyles.desktopTextMedium(context).copyWith( - height: 2, - ), - obscureText: hidePassword, - enableSuggestions: false, - autocorrect: false, - decoration: standardInputDecoration( - "Enter password", - passwordFocusNode, - context, - ).copyWith( - suffixIcon: UnconstrainedBox( - child: SizedBox( - height: 70, - child: Row( - children: [ - GestureDetector( - key: const Key( - "enterUnlockWalletKeysDesktopFieldShowPasswordButtonKey"), - onTap: () async { - setState(() { - hidePassword = !hidePassword; - }); - }, - child: Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(1000), - ), - height: 32, - width: 32, - child: Center( - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, - width: 24, - height: 19, - ), - ), - ), - ), - const SizedBox( - width: 10, - ), - ], - ), - ), - ), - ), - onChanged: (newValue) { - setState(() { - continueEnabled = newValue.isNotEmpty; - }); - }, - ), - ), - ), - const SizedBox( - height: 55, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - ), - child: Row( - children: [ - Expanded( - child: SecondaryButton( - label: "Cancel", - onPressed: Navigator.of(context).pop, - ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: PrimaryButton( - label: "Continue", - enabled: continueEnabled, - onPressed: continueEnabled - ? () async { - // todo: check password - Navigator.of(context).pop(); - final words = await ref - .read(walletsChangeNotifierProvider) - .getManager(widget.walletId) - .mnemonic; - await showDialog<void>( - context: context, - barrierDismissible: false, - builder: (context) => Navigator( - initialRoute: WalletKeysDesktopPopup.routeName, - onGenerateRoute: RouteGenerator.generateRoute, - onGenerateInitialRoutes: (_, __) { - return [ - RouteGenerator.generateRoute( - RouteSettings( - name: WalletKeysDesktopPopup.routeName, - arguments: words, - ), - ) - ]; - }, - ), - ); - } - : null, - ), - ), - ], - ), - ), - const SizedBox( - height: 32, - ), - ], - ), - ); - } -} - -class WalletKeysDesktopPopup extends StatelessWidget { - const WalletKeysDesktopPopup({ - Key? key, - required this.words, - this.clipboardInterface = const ClipboardWrapper(), - }) : super(key: key); - - final List<String> words; - final ClipboardInterface clipboardInterface; - - static const String routeName = "walletKeysDesktopPopup"; - - @override - Widget build(BuildContext context) { - return DesktopDialog( - maxWidth: 614, - maxHeight: double.infinity, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, - ), - child: Text( - "Wallet keys", - style: STextStyles.desktopH3(context), - ), - ), - DesktopDialogCloseButton( - onPressedOverride: () { - Navigator.of(context, rootNavigator: true).pop(); - }, - ), - ], - ), - const SizedBox( - height: 28, - ), - Text( - "Recovery phrase", - style: STextStyles.desktopTextMedium(context), - ), - const SizedBox( - height: 8, - ), - Center( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - ), - child: Text( - "Please write down your recovery phrase in the correct order and save it to keep your funds secure. You will also be asked to verify the words on the next screen.", - style: STextStyles.desktopTextExtraExtraSmall(context), - textAlign: TextAlign.center, - ), - ), - ), - const SizedBox( - height: 24, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - ), - child: MnemonicTable( - words: words, - isDesktop: true, - itemBorderColor: Theme.of(context) - .extension<StackColors>()! - .buttonBackSecondary, - ), - ), - const SizedBox( - height: 24, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - ), - child: Row( - children: [ - Expanded( - child: SecondaryButton( - label: "Show QR code", - onPressed: () { - final String value = AddressUtils.encodeQRSeedData(words); - Navigator.of(context).pushNamed( - QRCodeDesktopPopupContent.routeName, - arguments: value, - ); - }, - ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: PrimaryButton( - label: "Copy", - onPressed: () async { - await clipboardInterface.setData( - ClipboardData(text: words.join(" ")), - ); - unawaited( - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - iconAsset: Assets.svg.copy, - context: context, - ), - ); - }, - ), - ), - ], - ), - ), - const SizedBox( - height: 32, - ), - ], - ), - ); - } -} - -class QRCodeDesktopPopupContent extends StatelessWidget { - const QRCodeDesktopPopupContent({ - Key? key, - required this.value, - }) : super(key: key); - - final String value; - - static const String routeName = "qrCodeDesktopPopupContent"; - - @override - Widget build(BuildContext context) { - return DesktopDialog( - maxWidth: 614, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: const [ - DesktopDialogCloseButton(), - ], - ), - const SizedBox( - height: 14, - ), - QrImage( - data: value, - size: 300, - foregroundColor: - Theme.of(context).extension<StackColors>()!.accentColorDark, - ), - ], - ), - ); - } -} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/receive/desktop_receive.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart similarity index 100% rename from lib/pages_desktop_specific/home/my_stack_view/wallet_view/receive/desktop_receive.dart rename to lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart similarity index 100% rename from lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart rename to lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_summary.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart similarity index 100% rename from lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_summary.dart rename to lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/my_wallet.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/my_wallet.dart new file mode 100644 index 000000000..82b1f6ee7 --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/my_wallet.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/send_receive_tab_menu.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; + +class MyWallet extends StatefulWidget { + const MyWallet({ + Key? key, + required this.walletId, + }) : super(key: key); + + final String walletId; + + @override + State<MyWallet> createState() => _MyWalletState(); +} + +class _MyWalletState extends State<MyWallet> { + int _selectedIndex = 0; + + @override + Widget build(BuildContext context) { + return ListView( + primary: false, + children: [ + Text( + "My wallet", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveSearchIconLeft, + ), + ), + const SizedBox( + height: 16, + ), + Container( + decoration: BoxDecoration( + color: Theme.of(context).extension<StackColors>()!.popupBG, + borderRadius: BorderRadius.vertical( + top: Radius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + child: SendReceiveTabMenu( + onChanged: (index) { + setState(() { + _selectedIndex = index; + }); + }, + ), + ), + Container( + decoration: BoxDecoration( + color: Theme.of(context).extension<StackColors>()!.popupBG, + borderRadius: BorderRadius.vertical( + bottom: Radius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + child: IndexedStack( + index: _selectedIndex, + children: [ + Padding( + key: const Key("desktopSendViewPortKey"), + padding: const EdgeInsets.all(20), + child: DesktopSend( + walletId: widget.walletId, + ), + ), + Padding( + key: const Key("desktopReceiveViewPortKey"), + padding: const EdgeInsets.all(20), + child: DesktopReceive( + walletId: widget.walletId, + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart new file mode 100644 index 000000000..830b5e667 --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart @@ -0,0 +1,176 @@ +import 'dart:async'; + +import 'package:event_bus/event_bus.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; +import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; +import 'package:stackwallet/services/event_bus/global_event_bus.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:tuple/tuple.dart'; + +class NetworkInfoButton extends ConsumerStatefulWidget { + const NetworkInfoButton({ + Key? key, + required this.walletId, + this.eventBus, + }) : super(key: key); + + final String walletId; + final EventBus? eventBus; + + @override + ConsumerState<NetworkInfoButton> createState() => _NetworkInfoButtonState(); +} + +class _NetworkInfoButtonState extends ConsumerState<NetworkInfoButton> { + late final String walletId; + late final EventBus eventBus; + + late WalletSyncStatus _currentSyncStatus; + late NodeConnectionStatus _currentNodeStatus; + + late StreamSubscription<dynamic> _syncStatusSubscription; + late StreamSubscription<dynamic> _nodeStatusSubscription; + + @override + void initState() { + walletId = widget.walletId; + final managerProvider = + ref.read(walletsChangeNotifierProvider).getManagerProvider(walletId); + + eventBus = + widget.eventBus != null ? widget.eventBus! : GlobalEventBus.instance; + + if (ref.read(managerProvider).isRefreshing) { + _currentSyncStatus = WalletSyncStatus.syncing; + _currentNodeStatus = NodeConnectionStatus.connected; + } else { + _currentSyncStatus = WalletSyncStatus.synced; + if (ref.read(managerProvider).isConnected) { + _currentNodeStatus = NodeConnectionStatus.connected; + } else { + _currentNodeStatus = NodeConnectionStatus.disconnected; + _currentSyncStatus = WalletSyncStatus.unableToSync; + } + } + + _syncStatusSubscription = + eventBus.on<WalletSyncStatusChangedEvent>().listen( + (event) async { + if (event.walletId == widget.walletId) { + setState(() { + _currentSyncStatus = event.newStatus; + }); + } + }, + ); + + _nodeStatusSubscription = + eventBus.on<NodeConnectionStatusChangedEvent>().listen( + (event) async { + if (event.walletId == widget.walletId) { + setState(() { + _currentNodeStatus = event.newStatus; + }); + } + }, + ); + + super.initState(); + } + + @override + void dispose() { + _nodeStatusSubscription.cancel(); + _syncStatusSubscription.cancel(); + super.dispose(); + } + + Widget _buildNetworkIcon(WalletSyncStatus status, BuildContext context) { + const size = 24.0; + switch (status) { + case WalletSyncStatus.unableToSync: + return SvgPicture.asset( + Assets.svg.radioProblem, + color: Theme.of(context).extension<StackColors>()!.accentColorRed, + width: size, + height: size, + ); + case WalletSyncStatus.synced: + return SvgPicture.asset( + Assets.svg.radio, + color: Theme.of(context).extension<StackColors>()!.accentColorGreen, + width: size, + height: size, + ); + case WalletSyncStatus.syncing: + return SvgPicture.asset( + Assets.svg.radioSyncing, + color: Theme.of(context).extension<StackColors>()!.accentColorYellow, + width: size, + height: size, + ); + } + } + + Widget _buildText(WalletSyncStatus status, BuildContext context) { + String label; + Color color; + + switch (status) { + case WalletSyncStatus.unableToSync: + label = "Unable to sync"; + color = Theme.of(context).extension<StackColors>()!.accentColorRed; + break; + case WalletSyncStatus.synced: + label = "Synchronised"; + color = Theme.of(context).extension<StackColors>()!.accentColorGreen; + break; + case WalletSyncStatus.syncing: + label = "Synchronising"; + color = Theme.of(context).extension<StackColors>()!.accentColorYellow; + break; + } + + return Text( + label, + style: STextStyles.desktopMenuItemSelected(context).copyWith( + color: color, + ), + ); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + Navigator.of(context).pushNamed( + WalletNetworkSettingsView.routeName, + arguments: Tuple3( + walletId, + _currentSyncStatus, + _currentNodeStatus, + ), + ); + }, + child: Container( + color: Colors.transparent, + child: Row( + children: [ + _buildNetworkIcon(_currentSyncStatus, context), + const SizedBox( + width: 6, + ), + _buildText(_currentSyncStatus, context), + ], + ), + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/qr_code_desktop_popup_content.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/qr_code_desktop_popup_content.dart new file mode 100644 index 000000000..3a7a94885 --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/qr_code_desktop_popup_content.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; + +class QRCodeDesktopPopupContent extends StatelessWidget { + const QRCodeDesktopPopupContent({ + Key? key, + required this.value, + }) : super(key: key); + + final String value; + + static const String routeName = "qrCodeDesktopPopupContent"; + + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxWidth: 614, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: const [ + DesktopDialogCloseButton(), + ], + ), + const SizedBox( + height: 14, + ), + QrImage( + data: value, + size: 300, + foregroundColor: + Theme.of(context).extension<StackColors>()!.accentColorDark, + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/recent_desktop_transactions.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/recent_desktop_transactions.dart new file mode 100644 index 000000000..59e01f0b7 --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/recent_desktop_transactions.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages/wallet_view/sub_widgets/transactions_list.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_view.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; + +class RecentDesktopTransactions extends ConsumerStatefulWidget { + const RecentDesktopTransactions({ + Key? key, + required this.walletId, + }) : super(key: key); + + final String walletId; + + @override + ConsumerState<RecentDesktopTransactions> createState() => + _RecentDesktopTransactionsState(); +} + +class _RecentDesktopTransactionsState + extends ConsumerState<RecentDesktopTransactions> { + @override + Widget build(BuildContext context) { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Recent transactions", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveSearchIconLeft, + ), + ), + BlueTextButton( + text: "See all", + onTap: () { + Navigator.of(context).pushNamed( + AllTransactionsView.routeName, + arguments: widget.walletId, + ); + }, + ), + ], + ), + const SizedBox( + height: 16, + ), + Expanded( + child: TransactionsList( + managerProvider: ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManagerProvider(widget.walletId))), + walletId: widget.walletId, + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/send_receive_tab_menu.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/send_receive_tab_menu.dart new file mode 100644 index 000000000..54dca9a4c --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/send_receive_tab_menu.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; + +class SendReceiveTabMenu extends StatefulWidget { + const SendReceiveTabMenu({ + Key? key, + this.initialIndex = 0, + this.onChanged, + }) : super(key: key); + + final int initialIndex; + final void Function(int)? onChanged; + + @override + State<SendReceiveTabMenu> createState() => _SendReceiveTabMenuState(); +} + +class _SendReceiveTabMenuState extends State<SendReceiveTabMenu> { + late int _selectedIndex; + + void _onChanged(int newIndex) { + if (_selectedIndex != newIndex) { + setState(() { + _selectedIndex = newIndex; + }); + widget.onChanged?.call(_selectedIndex); + } + } + + @override + void initState() { + _selectedIndex = widget.initialIndex; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () => _onChanged(0), + child: Container( + color: Colors.transparent, + child: Column( + children: [ + const SizedBox( + height: 16, + ), + Text( + "Send", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: _selectedIndex == 0 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ), + const SizedBox( + height: 19, + ), + Container( + height: 2, + decoration: BoxDecoration( + color: _selectedIndex == 0 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Theme.of(context) + .extension<StackColors>()! + .background, + ), + ), + ], + ), + ), + ), + ), + Expanded( + child: GestureDetector( + onTap: () => _onChanged(1), + child: Container( + color: Colors.transparent, + child: Column( + children: [ + const SizedBox( + height: 16, + ), + Text( + "Receive", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: _selectedIndex == 1 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ), + const SizedBox( + height: 19, + ), + Container( + height: 2, + decoration: BoxDecoration( + color: _selectedIndex == 1 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Theme.of(context) + .extension<StackColors>()! + .background, + ), + ), + ], + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart new file mode 100644 index 000000000..37f047eed --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart @@ -0,0 +1,228 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/route_generator.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.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/stack_text_field.dart'; + +class UnlockWalletKeysDesktop extends ConsumerStatefulWidget { + const UnlockWalletKeysDesktop({ + Key? key, + required this.walletId, + }) : super(key: key); + + final String walletId; + + @override + ConsumerState<UnlockWalletKeysDesktop> createState() => + _UnlockWalletKeysDesktopState(); +} + +class _UnlockWalletKeysDesktopState + extends ConsumerState<UnlockWalletKeysDesktop> { + late final TextEditingController passwordController; + + late final FocusNode passwordFocusNode; + + bool continueEnabled = false; + bool hidePassword = true; + + @override + void initState() { + passwordController = TextEditingController(); + passwordFocusNode = FocusNode(); + super.initState(); + } + + @override + void dispose() { + passwordController.dispose(); + passwordFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxWidth: 579, + maxHeight: double.infinity, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: const [ + DesktopDialogCloseButton(), + ], + ), + const SizedBox( + height: 12, + ), + SvgPicture.asset( + Assets.svg.keys, + width: 100, + height: 58, + ), + const SizedBox( + height: 55, + ), + Text( + "Wallet keys", + style: STextStyles.desktopH2(context), + ), + const SizedBox( + height: 16, + ), + Text( + "Enter your password", + style: STextStyles.desktopTextMedium(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark3, + ), + ), + const SizedBox( + height: 24, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("enterPasswordUnlockWalletKeysDesktopFieldKey"), + focusNode: passwordFocusNode, + controller: passwordController, + style: STextStyles.desktopTextMedium(context).copyWith( + height: 2, + ), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Enter password", + passwordFocusNode, + context, + ).copyWith( + suffixIcon: UnconstrainedBox( + child: SizedBox( + height: 70, + child: Row( + children: [ + GestureDetector( + key: const Key( + "enterUnlockWalletKeysDesktopFieldShowPasswordButtonKey"), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(1000), + ), + height: 32, + width: 32, + child: Center( + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 24, + height: 19, + ), + ), + ), + ), + const SizedBox( + width: 10, + ), + ], + ), + ), + ), + ), + onChanged: (newValue) { + setState(() { + continueEnabled = newValue.isNotEmpty; + }); + }, + ), + ), + ), + const SizedBox( + height: 55, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + onPressed: Navigator.of(context).pop, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Continue", + enabled: continueEnabled, + onPressed: continueEnabled + ? () async { + // todo: check password + Navigator.of(context).pop(); + final words = await ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .mnemonic; + await showDialog<void>( + context: context, + barrierDismissible: false, + builder: (context) => Navigator( + initialRoute: WalletKeysDesktopPopup.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + RouteGenerator.generateRoute( + RouteSettings( + name: WalletKeysDesktopPopup.routeName, + arguments: words, + ), + ) + ]; + }, + ), + ); + } + : null, + ), + ), + ], + ), + ), + const SizedBox( + height: 32, + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_button.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_button.dart new file mode 100644 index 000000000..d4921276d --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_button.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; + +class WalletKeysButton extends StatelessWidget { + const WalletKeysButton({ + Key? key, + required this.walletId, + }) : super(key: key); + + final String walletId; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + showDialog<void>( + context: context, + barrierDismissible: false, + builder: (context) => UnlockWalletKeysDesktop( + walletId: walletId, + ), + ); + }, + child: Container( + color: Colors.transparent, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.key, + width: 20, + height: 20, + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, + ), + const SizedBox( + width: 6, + ), + Text( + "Wallet keys", + style: STextStyles.desktopMenuItemSelected(context), + ) + ], + ), + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart new file mode 100644 index 000000000..1ed646b30 --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart @@ -0,0 +1,146 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/qr_code_desktop_popup_content.dart'; +import 'package:stackwallet/utilities/address_utils.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/clipboard_interface.dart'; +import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.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'; + +class WalletKeysDesktopPopup extends StatelessWidget { + const WalletKeysDesktopPopup({ + Key? key, + required this.words, + this.clipboardInterface = const ClipboardWrapper(), + }) : super(key: key); + + final List<String> words; + final ClipboardInterface clipboardInterface; + + static const String routeName = "walletKeysDesktopPopup"; + + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxWidth: 614, + maxHeight: double.infinity, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Wallet keys", + style: STextStyles.desktopH3(context), + ), + ), + DesktopDialogCloseButton( + onPressedOverride: () { + Navigator.of(context, rootNavigator: true).pop(); + }, + ), + ], + ), + const SizedBox( + height: 28, + ), + Text( + "Recovery phrase", + style: STextStyles.desktopTextMedium(context), + ), + const SizedBox( + height: 8, + ), + Center( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: Text( + "Please write down your recovery phrase in the correct order and save it to keep your funds secure. You will also be asked to verify the words on the next screen.", + style: STextStyles.desktopTextExtraExtraSmall(context), + textAlign: TextAlign.center, + ), + ), + ), + const SizedBox( + height: 24, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: MnemonicTable( + words: words, + isDesktop: true, + itemBorderColor: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + ), + ), + const SizedBox( + height: 24, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Show QR code", + onPressed: () { + final String value = AddressUtils.encodeQRSeedData(words); + Navigator.of(context).pushNamed( + QRCodeDesktopPopupContent.routeName, + arguments: value, + ); + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Copy", + onPressed: () async { + await clipboardInterface.setData( + ClipboardData(text: words.join(" ")), + ); + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ), + ); + }, + ), + ), + ], + ), + ), + const SizedBox( + height: 32, + ), + ], + ), + ); + } +} diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 202edd972..5d0f81e20 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -87,6 +87,8 @@ import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_settings_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/qr_code_desktop_popup_content.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/advanced_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/appearance_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart'; From bf9eb66cb453314d97bda48bc6302494099d26e8 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 31 Oct 2022 08:28:19 -0600 Subject: [PATCH 077/426] test fixes --- test/price_test.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/price_test.dart b/test/price_test.dart index 0dbcdd450..89300122e 100644 --- a/test/price_test.dart +++ b/test/price_test.dart @@ -39,7 +39,7 @@ void main() { final price = await priceAPI.getPricesAnd24hChange(baseCurrency: "btc"); expect(price.toString(), - '{Coin.bitcoin: [1, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0.00000315, -2.68533], Coin.epicCash: [0.00002803, 7.27524], Coin.firo: [0.0001096, -0.89304], Coin.monero: [0.00717236, -0.77656], Coin.wownero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.litecoin: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); + '{Coin.bitcoin: [1, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0.00000315, -2.68533], Coin.epicCash: [0.00002803, 7.27524], Coin.firo: [0.0001096, -0.89304], Coin.litecoin: [0, 0.0], Coin.monero: [0.00717236, -0.77656], Coin.namecoin: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); verify(client.get( Uri.parse( "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"), @@ -71,7 +71,7 @@ void main() { await priceAPI.getPricesAnd24hChange(baseCurrency: "btc"); expect(cachedPrice.toString(), - '{Coin.bitcoin: [1, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0.00000315, -2.68533], Coin.epicCash: [0.00002803, 7.27524], Coin.firo: [0.0001096, -0.89304], Coin.monero: [0.00717236, -0.77656], Coin.wownero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.litecoin: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); + '{Coin.bitcoin: [1, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0.00000315, -2.68533], Coin.epicCash: [0.00002803, 7.27524], Coin.firo: [0.0001096, -0.89304], Coin.litecoin: [0, 0.0], Coin.monero: [0.00717236, -0.77656], Coin.namecoin: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); // verify only called once during filling of cache verify(client.get( @@ -100,7 +100,7 @@ void main() { final price = await priceAPI.getPricesAnd24hChange(baseCurrency: "btc"); expect(price.toString(), - '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.firo: [0, 0.0], Coin.monero: [0, 0.0], Coin.wownero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.litecoin: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); + '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.firo: [0, 0.0], Coin.litecoin: [0, 0.0], Coin.monero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); }); test("no internet available", () async { @@ -120,7 +120,7 @@ void main() { final price = await priceAPI.getPricesAnd24hChange(baseCurrency: "btc"); expect(price.toString(), - '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.firo: [0, 0.0], Coin.monero: [0, 0.0], Coin.wownero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.litecoin: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); + '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.firo: [0, 0.0], Coin.litecoin: [0, 0.0], Coin.monero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); }); tearDown(() async { From b3b8d0b057e97b14fef382bc75c9da0ab91f23e5 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 31 Oct 2022 09:45:42 -0600 Subject: [PATCH 078/426] added more customization options to AppBarBackButton --- .../custom_buttons/app_bar_icon_button.dart | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/widgets/custom_buttons/app_bar_icon_button.dart b/lib/widgets/custom_buttons/app_bar_icon_button.dart index eb926112a..9edc1ca5f 100644 --- a/lib/widgets/custom_buttons/app_bar_icon_button.dart +++ b/lib/widgets/custom_buttons/app_bar_icon_button.dart @@ -51,10 +51,14 @@ class AppBarBackButton extends StatelessWidget { Key? key, this.onPressed, this.isCompact = false, + this.size, + this.iconSize, }) : super(key: key); final VoidCallback? onPressed; final bool isCompact; + final double? size; + final double? iconSize; @override Widget build(BuildContext context) { @@ -67,19 +71,20 @@ class AppBarBackButton extends StatelessWidget { ) : const EdgeInsets.all(10), child: AppBarIconButton( - size: isDesktop - ? isCompact - ? 42 - : 56 - : 32, + size: size ?? + (isDesktop + ? isCompact + ? 42 + : 56 + : 32), color: isDesktop ? Theme.of(context).extension<StackColors>()!.textFieldDefaultBG : Theme.of(context).extension<StackColors>()!.background, shadows: const [], icon: SvgPicture.asset( Assets.svg.arrowLeft, - width: isCompact ? 18 : 24, - height: isCompact ? 18 : 24, + width: iconSize ?? (isCompact ? 18 : 24), + height: iconSize ?? (isCompact ? 18 : 24), color: Theme.of(context).extension<StackColors>()!.topNavIconPrimary, ), onPressed: onPressed ?? Navigator.of(context).pop, From 817460b5e1289d5335d28dc09b39a82a3fdafcde Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 31 Oct 2022 11:32:22 -0600 Subject: [PATCH 079/426] proper conditional parent widget class to handle some desktop/mobile differences in a cleaner manner --- lib/widgets/conditional_parent.dart | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 lib/widgets/conditional_parent.dart diff --git a/lib/widgets/conditional_parent.dart b/lib/widgets/conditional_parent.dart new file mode 100644 index 000000000..6db50c6e8 --- /dev/null +++ b/lib/widgets/conditional_parent.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +class ConditionalParent extends StatelessWidget { + const ConditionalParent({ + Key? key, + required this.condition, + required this.child, + required this.builder, + }) : super(key: key); + + final bool condition; + final Widget child; + final Widget Function(Widget) builder; + + @override + Widget build(BuildContext context) { + if (condition) { + return builder(child); + } else { + return child; + } + } +} From 3421602ba2c26c67655d1b20ac37b9a4df8b774e Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 31 Oct 2022 11:51:16 -0600 Subject: [PATCH 080/426] manually toggleable expandable mod --- lib/widgets/expandable.dart | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/widgets/expandable.dart b/lib/widgets/expandable.dart index ddae2201d..a0c2be5a4 100644 --- a/lib/widgets/expandable.dart +++ b/lib/widgets/expandable.dart @@ -5,6 +5,11 @@ enum ExpandableState { collapsed, } +class ExpandableController { + VoidCallback? toggle; + ExpandableState state = ExpandableState.collapsed; +} + class Expandable extends StatefulWidget { const Expandable({ Key? key, @@ -14,6 +19,7 @@ class Expandable extends StatefulWidget { this.animation, this.animationDurationMultiplier = 1.0, this.onExpandChanged, + this.controller, }) : super(key: key); final Widget header; @@ -22,6 +28,7 @@ class Expandable extends StatefulWidget { final Animation<double>? animation; final double animationDurationMultiplier; final void Function(ExpandableState)? onExpandChanged; + final ExpandableController? controller; @override State<Expandable> createState() => _ExpandableState(); @@ -31,19 +38,28 @@ class _ExpandableState extends State<Expandable> with TickerProviderStateMixin { late final AnimationController animationController; late final Animation<double> animation; late final Duration duration; + late final ExpandableController? controller; + + ExpandableState _toggleState = ExpandableState.collapsed; Future<void> toggle() async { if (animation.isDismissed) { await animationController.forward(); - widget.onExpandChanged?.call(ExpandableState.collapsed); + _toggleState = ExpandableState.collapsed; + widget.onExpandChanged?.call(_toggleState); } else if (animation.isCompleted) { await animationController.reverse(); - widget.onExpandChanged?.call(ExpandableState.expanded); + _toggleState = ExpandableState.expanded; + widget.onExpandChanged?.call(_toggleState); } + controller?.state = _toggleState; } @override void initState() { + controller = widget.controller; + controller?.toggle = toggle; + duration = Duration( milliseconds: (500 * widget.animationDurationMultiplier).toInt(), ); From 7540e593a3f9066cab2ab8e0f197c8eccda07d5c Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 31 Oct 2022 12:03:21 -0600 Subject: [PATCH 081/426] desktop receiving popups --- assets/svg/arrow-down.svg | 4 + .../generate_receiving_uri_qr_code_view.dart | 771 +++++++++++------- .../sub_widgets/desktop_receive.dart | 67 +- lib/route_generator.dart | 16 + lib/utilities/assets.dart | 1 + pubspec.yaml | 1 + 6 files changed, 545 insertions(+), 315 deletions(-) create mode 100644 assets/svg/arrow-down.svg diff --git a/assets/svg/arrow-down.svg b/assets/svg/arrow-down.svg new file mode 100644 index 000000000..c96e43ce3 --- /dev/null +++ b/assets/svg/arrow-down.svg @@ -0,0 +1,4 @@ +<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M10.5 4.16602V15.8327" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M16.3327 10L10.4993 15.8333L4.66602 10" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart b/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart index 3e29612d1..4c3c4c968 100644 --- a/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart +++ b/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart @@ -20,7 +20,10 @@ import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; @@ -51,6 +54,10 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> { late TextEditingController amountController; late TextEditingController noteController; + late final bool isDesktop; + late String _uriString; + bool didGenerate = false; + final _amountFocusNode = FocusNode(); final _noteFocusNode = FocusNode(); @@ -81,8 +88,151 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> { } } + String? _generateURI() { + final amountString = amountController.text; + final noteString = noteController.text; + + if (amountString.isNotEmpty && Decimal.tryParse(amountString) == null) { + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Invalid amount", + context: context, + ); + return null; + } + + String query = ""; + + if (amountString.isNotEmpty) { + query += "amount=$amountString"; + } + if (noteString.isNotEmpty) { + if (query.isNotEmpty) { + query += "&"; + } + query += "message=$noteString"; + } + + final uri = Uri( + scheme: widget.coin.uriScheme, + host: widget.receivingAddress, + query: query.isNotEmpty ? query : null, + ); + + final uriString = uri.toString().replaceFirst("://", ":"); + + Logging.instance.log("Generated receiving QR code for: $uriString", + level: LogLevel.Info); + + return uriString; + } + + void onGeneratePressed() { + final uriString = _generateURI(); + + if (uriString == null) { + return; + } + + showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (_) { + final width = MediaQuery.of(context).size.width / 2; + return StackDialogBase( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: Text( + "New QR code", + style: STextStyles.pageTitleH2(context), + ), + ), + const SizedBox( + height: 12, + ), + Center( + child: RepaintBoundary( + key: _qrKey, + child: SizedBox( + width: width + 20, + height: width + 20, + child: QrImage( + data: uriString, + size: width, + backgroundColor: + Theme.of(context).extension<StackColors>()!.popupBG, + foregroundColor: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + ), + const SizedBox( + height: 12, + ), + Center( + child: SizedBox( + width: width, + child: TextButton( + onPressed: () async { + // TODO: add save button as well + await _capturePng(true); + }, + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Center( + child: SvgPicture.asset( + Assets.svg.share, + width: 14, + height: 14, + ), + ), + const SizedBox( + width: 4, + ), + Column( + children: [ + Text( + "Share", + textAlign: TextAlign.center, + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, + ), + ), + const SizedBox( + height: 2, + ), + ], + ), + ], + ), + ), + ), + ), + ], + ), + ); + }, + ); + } + @override void initState() { + isDesktop = Util.isDesktop; + _uriString = Uri( + scheme: widget.coin.uriScheme, + host: widget.receivingAddress, + ).toString().replaceFirst("://", ":"); amountController = TextEditingController(); noteController = TextEditingController(); super.initState(); @@ -101,315 +251,330 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 70)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, + + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 70)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Generate QR code", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Generate QR code", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (buildContext, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - child: Text( - "The new QR code with your address, amount and note will appear in the pop up window.", - style: STextStyles.itemSubtitle(context), - ), - ), - const SizedBox( - height: 12, - ), - Text( - "Amount (Optional)", - style: STextStyles.smallMed12(context), - textAlign: TextAlign.left, - ), - const SizedBox( - height: 8, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: amountController, - focusNode: _amountFocusNode, - style: STextStyles.field(context), - keyboardType: const TextInputType.numberWithOptions( - decimal: true), - onChanged: (_) => setState(() {}), - decoration: standardInputDecoration( - "Amount", - _amountFocusNode, - context, - ).copyWith( - suffixIcon: amountController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - amountController.text = ""; - }); - }, - ), - ], - ), - ), - ) - : null, - ), - ), - ), - const SizedBox( - height: 12, - ), - Text( - "Note (Optional)", - style: STextStyles.smallMed12(context), - textAlign: TextAlign.left, - ), - const SizedBox( - height: 8, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: noteController, - focusNode: _noteFocusNode, - style: STextStyles.field(context), - onChanged: (_) => setState(() {}), - decoration: standardInputDecoration( - "Note", - _noteFocusNode, - context, - ).copyWith( - suffixIcon: noteController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - noteController.text = ""; - }); - }, - ), - ], - ), - ), - ) - : null, - ), - ), - ), - // SizedBox() - // Spacer(), - const SizedBox( - height: 8, - ), - TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () { - final amountString = amountController.text; - final noteString = noteController.text; - - if (amountString.isNotEmpty && - Decimal.tryParse(amountString) == null) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Invalid amount", - context: context, - ); - return; - } - - String query = ""; - - if (amountString.isNotEmpty) { - query += "amount=$amountString"; - } - if (noteString.isNotEmpty) { - if (query.isNotEmpty) { - query += "&"; - } - query += "message=$noteString"; - } - - final uri = Uri( - scheme: widget.coin.uriScheme, - host: widget.receivingAddress, - query: query.isNotEmpty ? query : null, - ); - - final uriString = - uri.toString().replaceFirst("://", ":"); - - Logging.instance.log( - "Generated receiving QR code for: $uriString", - level: LogLevel.Info); - - showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (_) { - final width = - MediaQuery.of(context).size.width / 2; - return StackDialogBase( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: [ - Center( - child: Text( - "New QR code", - style: - STextStyles.pageTitleH2(context), - ), - ), - const SizedBox( - height: 12, - ), - Center( - child: RepaintBoundary( - key: _qrKey, - child: SizedBox( - width: width + 20, - height: width + 20, - child: QrImage( - data: uriString, - size: width, - backgroundColor: Theme.of( - context) - .extension<StackColors>()! - .popupBG, - foregroundColor: Theme.of( - context) - .extension<StackColors>()! - .accentColorDark), - ), - ), - ), - const SizedBox( - height: 12, - ), - Center( - child: SizedBox( - width: width, - child: TextButton( - onPressed: () async { - // TODO: add save button as well - await _capturePng(true); - }, - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor( - context), - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - crossAxisAlignment: - CrossAxisAlignment.center, - children: [ - Center( - child: SvgPicture.asset( - Assets.svg.share, - width: 14, - height: 14, - ), - ), - const SizedBox( - width: 4, - ), - Column( - children: [ - Text( - "Share", - textAlign: - TextAlign.center, - style: STextStyles.button( - context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .buttonTextSecondary, - ), - ), - const SizedBox( - height: 2, - ), - ], - ), - ], - ), - ), - ), - ), - ], - ), - ); - }, - ); - }, - child: Text( - "Generate QR Code", - style: STextStyles.button(context), - ), - ), - ], + body: LayoutBuilder( + builder: (buildContext, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, ), ), ), ), + ); + }, + ), + ), + child: Padding( + padding: isDesktop + ? const EdgeInsets.only( + top: 12, + left: 32, + right: 32, + bottom: 32, + ) + : const EdgeInsets.all(0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max, + children: [ + if (!isDesktop) + RoundedWhiteContainer( + child: Text( + "The new QR code with your address, amount and note will appear in the pop up window.", + style: STextStyles.itemSubtitle(context), + ), + ), + if (!isDesktop) + const SizedBox( + height: 12, + ), + Text( + "Amount (Optional)", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveSearchIconRight, + ) + : STextStyles.smallMed12(context), + textAlign: TextAlign.left, ), - ); - }, + SizedBox( + height: isDesktop ? 10 : 8, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: amountController, + focusNode: _amountFocusNode, + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultText, + height: 1.8, + ) + : STextStyles.field(context), + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Amount", + _amountFocusNode, + context, + ).copyWith( + contentPadding: isDesktop + ? const EdgeInsets.only( + left: 16, + top: 11, + bottom: 12, + right: 5, + ) + : null, + suffixIcon: amountController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + amountController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + SizedBox( + height: isDesktop ? 20 : 12, + ), + Text( + "Note (Optional)", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveSearchIconRight, + ) + : STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + SizedBox( + height: isDesktop ? 10 : 8, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: noteController, + focusNode: _noteFocusNode, + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultText, + height: 1.8, + ) + : STextStyles.field(context), + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Note", + _noteFocusNode, + context, + ).copyWith( + contentPadding: isDesktop + ? const EdgeInsets.only( + left: 16, + top: 11, + bottom: 12, + right: 5, + ) + : null, + suffixIcon: noteController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + noteController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + SizedBox( + height: isDesktop ? 20 : 8, + ), + PrimaryButton( + label: "Generate QR code", + onPressed: isDesktop + ? () { + final uriString = _generateURI(); + if (uriString == null) { + return; + } + + setState(() { + didGenerate = true; + _uriString = uriString; + }); + } + : onGeneratePressed, + desktopMed: true, + ), + if (isDesktop && didGenerate) + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + height: 20, + ), + RoundedWhiteContainer( + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: Column( + children: [ + Text( + "New QR Code", + style: STextStyles.desktopTextMedium(context), + ), + const SizedBox( + height: 16, + ), + Center( + child: RepaintBoundary( + key: _qrKey, + child: SizedBox( + width: 234, + height: 234, + child: QrImage( + data: _uriString, + size: 220, + backgroundColor: Theme.of(context) + .extension<StackColors>()! + .popupBG, + foregroundColor: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + ), + const SizedBox( + height: 12, + ), + Row( + children: [ + SecondaryButton( + width: 170, + desktopMed: true, + onPressed: () async { + await _capturePng(false); + }, + label: "Share", + icon: SvgPicture.asset( + Assets.svg.share, + width: 20, + height: 20, + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, + ), + ), + const SizedBox( + width: 16, + ), + PrimaryButton( + width: 170, + desktopMed: true, + onPressed: () async { + // TODO: add save functionality instead of share + await _capturePng(true); + }, + label: "Save", + icon: SvgPicture.asset( + Assets.svg.arrowDown, + width: 20, + height: 20, + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextPrimary, + ), + ), + ], + ) + ], + ), + ), + ], + ), + ], + ), + ], + ), ), ); } diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart index 46b4cfcfc..9a59c3ec1 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart @@ -16,9 +16,13 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_loading_overlay.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:tuple/tuple.dart'; class DesktopReceive extends ConsumerStatefulWidget { const DesktopReceive({ @@ -216,20 +220,59 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> { // TODO: create transparent button class to account for hover GestureDetector( onTap: () async { - unawaited( - Navigator.of(context).push( - RouteGenerator.getRoute( - shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: (_) => GenerateUriQrCodeView( - coin: coin, - receivingAddress: receivingAddress, - ), - settings: const RouteSettings( - name: GenerateUriQrCodeView.routeName, + if (Util.isDesktop) { + await showDialog<void>( + context: context, + builder: (context) => DesktopDialog( + maxHeight: double.infinity, + maxWidth: 580, + child: Column( + children: [ + Row( + children: [ + const AppBarBackButton( + size: 40, + iconSize: 24, + ), + Text( + "Generate QR code", + style: STextStyles.desktopH3(context), + ), + ], + ), + IntrinsicHeight( + child: Navigator( + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) => [ + RouteGenerator.generateRoute( + RouteSettings( + name: GenerateUriQrCodeView.routeName, + arguments: Tuple2(coin, receivingAddress), + ), + ), + ], + ), + ), + ], ), ), - ), - ); + ); + } else { + unawaited( + Navigator.of(context).push( + RouteGenerator.getRoute( + shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, + builder: (_) => GenerateUriQrCodeView( + coin: coin, + receivingAddress: receivingAddress, + ), + settings: const RouteSettings( + name: GenerateUriQrCodeView.routeName, + ), + ), + ), + ); + } }, child: Container( color: Colors.transparent, diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 5d0f81e20..91d1a3768 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -37,6 +37,7 @@ import 'package:stackwallet/pages/intro_view.dart'; import 'package:stackwallet/pages/manage_favorites_view/manage_favorites_view.dart'; import 'package:stackwallet/pages/notification_views/notifications_view.dart'; import 'package:stackwallet/pages/pinpad_views/create_pin_view.dart'; +import 'package:stackwallet/pages/receive_view/generate_receiving_uri_qr_code_view.dart'; import 'package:stackwallet/pages/receive_view/receive_view.dart'; import 'package:stackwallet/pages/send_view/send_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/about_view.dart'; @@ -955,6 +956,21 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case GenerateUriQrCodeView.routeName: + if (args is Tuple2<Coin, String>) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => GenerateUriQrCodeView( + coin: args.item1, + receivingAddress: args.item2, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + // == Desktop specific routes ============================================ case CreatePasswordView.routeName: return getRoute( diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index c2d488305..432ebbec9 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -145,6 +145,7 @@ class _SVG { String get walletDesktop => "assets/svg/wallet-desktop.svg"; String get exitDesktop => "assets/svg/exit-desktop.svg"; String get keys => "assets/svg/keys.svg"; + String get arrowDown => "assets/svg/arrow-down.svg"; String get ellipse1 => "assets/svg/Ellipse-43.svg"; String get ellipse2 => "assets/svg/Ellipse-42.svg"; diff --git a/pubspec.yaml b/pubspec.yaml index 88951b29b..cc0138b75 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -310,6 +310,7 @@ flutter: - assets/svg/wallet-desktop.svg - assets/svg/exit-desktop.svg - assets/svg/keys.svg + - assets/svg/arrow-down.svg # coin icons - assets/svg/coin_icons/Bitcoin.svg - assets/svg/coin_icons/Litecoin.svg From 5f28a8cb36dd4cf18e3b10f824b6850cddb72a5b Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 31 Oct 2022 12:21:58 -0600 Subject: [PATCH 082/426] desktop send info message popups --- .../wallet_view/sub_widgets/desktop_send.dart | 165 +++++++++++++----- 1 file changed, 121 insertions(+), 44 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index 116262d23..d482f7ab8 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -32,7 +32,10 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/animated_text.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.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/icon_widgets/addressbook_icon.dart'; import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart'; import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart'; @@ -107,23 +110,55 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { useSafeArea: false, barrierDismissible: true, builder: (context) { - return StackDialog( - title: "Transaction failed", - message: "Sending to self is currently disabled", - rightButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Ok", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), + return DesktopDialog( + maxWidth: 400, + maxHeight: double.infinity, + child: Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 32, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transaction failed", + style: STextStyles.desktopH3(context), + ), + const DesktopDialogCloseButton(), + ], + ), + const SizedBox( + height: 12, + ), + Text( + "Sending to self is currently disabled", + textAlign: TextAlign.left, + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + fontSize: 18, + ), + ), + const SizedBox( + height: 40, + ), + Padding( + padding: const EdgeInsets.only( + right: 32, + ), + child: SecondaryButton( + desktopMed: true, + label: "Ok", + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + ], ), - onPressed: () { - Navigator.of(context).pop(); - }, ), ); }, @@ -154,36 +189,78 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { useSafeArea: false, barrierDismissible: true, builder: (context) { - return StackDialog( - title: "Confirm send all", - message: - "You are about to send your entire balance. Would you like to continue?", - leftButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), + return DesktopDialog( + maxWidth: 450, + maxHeight: double.infinity, + child: Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 32, ), - onPressed: () { - Navigator.of(context).pop(false); - }, - ), - rightButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - child: Text( - "Yes", - style: STextStyles.button(context), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Confirm send all", + style: STextStyles.desktopH3(context), + ), + const DesktopDialogCloseButton(), + ], + ), + const SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.only( + right: 32, + ), + child: Text( + "You are about to send your entire balance. Would you like to continue?", + textAlign: TextAlign.left, + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + fontSize: 18, + ), + ), + ), + const SizedBox( + height: 40, + ), + Padding( + padding: const EdgeInsets.only( + right: 32, + ), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + desktopMed: true, + label: "Cancel", + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + desktopMed: true, + label: "Yes", + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ), + ], + ), + ), + ], ), - onPressed: () { - Navigator.of(context).pop(true); - }, ), ); }, From 03f4030d26a17c6d545b41826137f474dbd04164 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 1 Nov 2022 08:37:27 -0600 Subject: [PATCH 083/426] temp disabled erroring calls --- .../settings_menu/backup_and_restore/create_auto_backup.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart index 29be17228..bd7eb9247 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart @@ -26,7 +26,7 @@ class CreateAutoBackup extends StatelessWidget { chooseFileLocation = FocusNode(); // passwordRepeatFocusNode = FocusNode(); - super.initState(); + // super.initState(); } @override @@ -37,7 +37,7 @@ class CreateAutoBackup extends StatelessWidget { chooseFileLocation.dispose(); // passwordRepeatFocusNode.dispose(); - super.dispose(); + // super.dispose(); } @override From 40390140837287f6033bce6c2c05ca7bd3723bf4 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Tue, 1 Nov 2022 08:42:09 -0600 Subject: [PATCH 084/426] WIP: needs drop down menu --- .../create_auto_backup.dart | 281 ++++++++++++++++-- 1 file changed, 261 insertions(+), 20 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart index 29be17228..fb22c8e92 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart @@ -1,30 +1,54 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.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_dialog.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; -import '../../../../utilities/assets.dart'; -import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; -import '../../../../widgets/stack_text_field.dart'; +class CreateAutoBackup extends StatefulWidget { + const CreateAutoBackup({Key? key}) : super(key: key); -class CreateAutoBackup extends StatelessWidget { - // const CreateAutoBackup({Key? key, required this.chooseFileLocation}) - // : super(key: key); + @override + State<StatefulWidget> createState() => _CreateAutoBackup(); +} +class _CreateAutoBackup extends State<CreateAutoBackup> { late final TextEditingController fileLocationController; + late final TextEditingController passphraseController; + late final TextEditingController passphraseRepeatController; late final FocusNode chooseFileLocation; + late final FocusNode passphraseFocusNode; + late final FocusNode passphraseRepeatFocusNode; + + bool shouldShowPasswordHint = true; + bool hidePassword = true; + + bool get fieldsMatch => + passphraseController.text == passphraseRepeatController.text; + + List<DropdownMenuItem<String>> get dropdownItems { + List<DropdownMenuItem<String>> menuItems = [ + DropdownMenuItem( + child: Text("Every 10 minutes"), value: "Every 10 minutes"), + ]; + return menuItems; + } @override void initState() { fileLocationController = TextEditingController(); - // passwordRepeatController = TextEditingController(); + passphraseController = TextEditingController(); + passphraseRepeatController = TextEditingController(); chooseFileLocation = FocusNode(); - // passwordRepeatFocusNode = FocusNode(); + passphraseFocusNode = FocusNode(); + passphraseRepeatFocusNode = FocusNode(); super.initState(); } @@ -32,18 +56,24 @@ class CreateAutoBackup extends StatelessWidget { @override void dispose() { fileLocationController.dispose(); - // passwordRepeatController.dispose(); + passphraseController.dispose(); + passphraseRepeatController.dispose(); chooseFileLocation.dispose(); - // passwordRepeatFocusNode.dispose(); + passphraseFocusNode.dispose(); + passphraseRepeatFocusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType "); + + String? selectedItem = "Every 10 minutes"; + return DesktopDialog( - maxHeight: 600, + maxHeight: 650, maxWidth: 600, child: Column( children: [ @@ -93,16 +123,226 @@ class CreateAutoBackup extends StatelessWidget { textAlign: TextAlign.left, ), ), - TextField( - key: const Key("backupChooseFileLocation"), - style: STextStyles.desktopTextMedium(context).copyWith( - height: 2, - ), - enableSuggestions: false, - autocorrect: false, - decoration: standardInputDecoration( - "Save to...", chooseFileLocation, context), + SizedBox( + height: 10, ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("backupChooseFileLocation"), + focusNode: chooseFileLocation, + controller: fileLocationController, + style: STextStyles.desktopTextMedium(context).copyWith( + height: 2, + ), + textAlign: TextAlign.left, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Save to...", + chooseFileLocation, + context, + ).copyWith( + labelStyle: + STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: + Theme.of(context).extension<StackColors>()!.textDark3, + ), + suffixIcon: Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(1000), + ), + height: 32, + width: 32, + child: Center( + child: SvgPicture.asset( + Assets.svg.folder, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 20, + height: 17.5, + ), + ), + ), + ), + ), + ), + ), + SizedBox( + height: 24, + ), + Container( + alignment: Alignment.centerLeft, + padding: EdgeInsets.only(left: 32), + child: Text( + "Create a passphrase", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark3, + ), + textAlign: TextAlign.left, + ), + ), + SizedBox( + height: 10, + ), + Padding( + padding: EdgeInsets.only( + left: 32, + right: 32, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("createBackupPassphrase"), + focusNode: passphraseFocusNode, + controller: passphraseController, + style: STextStyles.desktopTextMedium(context).copyWith( + height: 2, + ), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Create passphrase", + passphraseFocusNode, + context, + ).copyWith( + labelStyle: + STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: + Theme.of(context).extension<StackColors>()!.textDark3, + ), + suffixIcon: UnconstrainedBox( + child: GestureDetector( + key: const Key( + "createDesktopAutoBackupShowPassphraseButton1"), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(1000), + ), + height: 32, + width: 32, + child: Center( + child: SvgPicture.asset( + hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 20, + height: 17.5, + ), + ), + ), + ), + ), + ), + ), + ), + ), + const SizedBox( + height: 16, + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("createBackupPassphrase"), + focusNode: passphraseRepeatFocusNode, + controller: passphraseRepeatController, + style: STextStyles.desktopTextMedium(context).copyWith( + height: 2, + ), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Confirm passphrase", + passphraseRepeatFocusNode, + context, + ).copyWith( + labelStyle: + STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: + Theme.of(context).extension<StackColors>()!.textDark3, + ), + suffixIcon: UnconstrainedBox( + child: GestureDetector( + key: const Key( + "createDesktopAutoBackupShowPassphraseButton2"), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(1000), + ), + height: 32, + width: 32, + child: Center( + child: SvgPicture.asset( + hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 20, + height: 17.5, + ), + ), + ), + ), + ), + ), + ), + ), + ), + const SizedBox( + height: 24, + ), + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.only(left: 32), + child: Text( + "Auto Backup frequency", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark3, + ), + textAlign: TextAlign.left, + ), + ), + const SizedBox( + height: 10, + ), + // DropdownButton( + // value: dropdownItems, + // items: dropdownItems, + // onChanged: null, + // ), const Spacer(), Padding( padding: const EdgeInsets.all(32), @@ -123,6 +363,7 @@ class CreateAutoBackup extends StatelessWidget { Expanded( child: PrimaryButton( label: "Enable Auto Backup", + enabled: false, onPressed: () {}, ), ) From d9b825b001f6a4e75bb604dd87085c865d8850d8 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 1 Nov 2022 09:36:37 -0600 Subject: [PATCH 085/426] dropdown fix --- .../backup_and_restore_settings.dart | 3 +- .../create_auto_backup.dart | 67 +++++++++++++------ 2 files changed, 48 insertions(+), 22 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart index 6ab82401b..8b74f8d40 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart @@ -2,6 +2,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/restore_backup_dialog.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -9,8 +10,6 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'enable_backup_dialog.dart'; - class BackupRestoreSettings extends ConsumerStatefulWidget { const BackupRestoreSettings({Key? key}) : super(key: key); diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart index 39b77cfea..0c8c47c9e 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart @@ -32,13 +32,27 @@ class _CreateAutoBackup extends State<CreateAutoBackup> { bool get fieldsMatch => passphraseController.text == passphraseRepeatController.text; - List<DropdownMenuItem<String>> get dropdownItems { - List<DropdownMenuItem<String>> menuItems = [ - DropdownMenuItem( - child: Text("Every 10 minutes"), value: "Every 10 minutes"), - ]; - return menuItems; - } + String _currentDropDownValue = "Every 10 minutes"; + + final List<String> _dropDOwnItems = [ + "Every 10 minutes", + "Every 20 minutes", + "Every 30 minutes", + ]; + + // List<DropdownMenuItem<String>> get dropdownItems { + // List<DropdownMenuItem<String>> menuItems = [ + // const DropdownMenuItem( + // value: "Every 10 minutes", + // child: Text("Every 10 minutes"), + // ), + // const DropdownMenuItem( + // value: "Every 20 minutes", + // child: Text("Every 20 minutes"), + // ), + // ]; + // return menuItems; + // } @override void initState() { @@ -50,7 +64,7 @@ class _CreateAutoBackup extends State<CreateAutoBackup> { passphraseFocusNode = FocusNode(); passphraseRepeatFocusNode = FocusNode(); - // super.initState(); + super.initState(); } @override @@ -63,7 +77,7 @@ class _CreateAutoBackup extends State<CreateAutoBackup> { passphraseFocusNode.dispose(); passphraseRepeatFocusNode.dispose(); - // super.dispose(); + super.dispose(); } @override @@ -114,7 +128,7 @@ class _CreateAutoBackup extends State<CreateAutoBackup> { ), Container( alignment: Alignment.centerLeft, - padding: EdgeInsets.only(left: 32), + padding: const EdgeInsets.only(left: 32), child: Text( "Choose file location", style: STextStyles.desktopTextExtraSmall(context).copyWith( @@ -123,7 +137,7 @@ class _CreateAutoBackup extends State<CreateAutoBackup> { textAlign: TextAlign.left, ), ), - SizedBox( + const SizedBox( height: 10, ), Padding( @@ -177,12 +191,12 @@ class _CreateAutoBackup extends State<CreateAutoBackup> { ), ), ), - SizedBox( + const SizedBox( height: 24, ), Container( alignment: Alignment.centerLeft, - padding: EdgeInsets.only(left: 32), + padding: const EdgeInsets.only(left: 32), child: Text( "Create a passphrase", style: STextStyles.desktopTextExtraSmall(context).copyWith( @@ -191,11 +205,11 @@ class _CreateAutoBackup extends State<CreateAutoBackup> { textAlign: TextAlign.left, ), ), - SizedBox( + const SizedBox( height: 10, ), Padding( - padding: EdgeInsets.only( + padding: const EdgeInsets.only( left: 32, right: 32, ), @@ -338,11 +352,24 @@ class _CreateAutoBackup extends State<CreateAutoBackup> { const SizedBox( height: 10, ), - // DropdownButton( - // value: dropdownItems, - // items: dropdownItems, - // onChanged: null, - // ), + DropdownButton( + value: _currentDropDownValue, + items: _dropDOwnItems + .map( + (e) => DropdownMenuItem( + value: e, + child: Text(e), + ), + ) + .toList(), + onChanged: (value) { + if (value is String) { + setState(() { + _currentDropDownValue = value; + }); + } + }, + ), const Spacer(), Padding( padding: const EdgeInsets.all(32), From 99883d30ac2838c2867e7026a79ab73647fae0ff Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 1 Nov 2022 11:42:13 -0600 Subject: [PATCH 086/426] satoshi to decimal amount format function modified to take optional Coin parameter --- lib/utilities/format.dart | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/lib/utilities/format.dart b/lib/utilities/format.dart index f0eafdf94..775780833 100644 --- a/lib/utilities/format.dart +++ b/lib/utilities/format.dart @@ -4,13 +4,39 @@ import 'package:decimal/decimal.dart'; import 'package:intl/number_symbols.dart'; import 'package:intl/number_symbols_data.dart' show numberFormatSymbols; import 'package:stackwallet/utilities/constants.dart'; - -import 'enums/backup_frequency_type.dart'; +import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; abstract class Format { - static Decimal satoshisToAmount(int sats) => - (Decimal.fromInt(sats) / Decimal.fromInt(Constants.satsPerCoin)) - .toDecimal(scaleOnInfinitePrecision: Constants.decimalPlaces); + static Decimal satoshisToAmount(int sats, {Coin? coin}) { + late final int satsPerCoin; + + switch (coin) { + case Coin.wownero: + satsPerCoin = Constants.satsPerCoinWownero; + break; + case Coin.monero: + satsPerCoin = Constants.satsPerCoinMonero; + break; + case Coin.bitcoin: + case Coin.bitcoincash: + case Coin.dogecoin: + case Coin.epicCash: + case Coin.firo: + case Coin.litecoin: + case Coin.namecoin: + case Coin.bitcoinTestNet: + case Coin.litecoinTestNet: + case Coin.bitcoincashTestnet: + case Coin.dogecoinTestNet: + case Coin.firoTestNet: + default: + satsPerCoin = Constants.satsPerCoin; + } + + return (Decimal.fromInt(sats) / Decimal.fromInt(satsPerCoin)) + .toDecimal(scaleOnInfinitePrecision: Constants.decimalPlaces); + } /// static String satoshiAmountToPrettyString(int sats, String locale) { From ec3378fec2d0dfee5c2015c36231e0e52ddcf2d8 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 1 Nov 2022 11:42:33 -0600 Subject: [PATCH 087/426] WIP desktop send flow --- .../send_view/confirm_transaction_view.dart | 779 +++++++++++++----- .../building_transaction_dialog.dart | 98 ++- .../wallet_view/sub_widgets/desktop_send.dart | 142 +++- lib/route_generator.dart | 16 + 4 files changed, 740 insertions(+), 295 deletions(-) diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index 65537f20e..81d5a3da2 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -1,7 +1,9 @@ import 'dart:async'; +import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart'; import 'package:stackwallet/pages/send_view/sub_widgets/sending_transaction_dialog.dart'; @@ -11,12 +13,17 @@ import 'package:stackwallet/providers/wallet/public_private_balance_state_provid import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.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/flush_bar_type.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; @@ -45,6 +52,14 @@ class _ConfirmTransactionViewState late final Map<String, dynamic> transactionInfo; late final String walletId; late final String routeOnSuccessName; + late final bool isDesktop; + + int _fee = 12; + final List<int> _dropDownItems = [ + 12, + 22, + 234, + ]; Future<void> _attemptSend(BuildContext context) async { unawaited(showDialog<dynamic>( @@ -133,6 +148,7 @@ class _ConfirmTransactionViewState @override void initState() { + isDesktop = Util.isDesktop; transactionInfo = widget.transactionInfo; walletId = widget.walletId; routeOnSuccessName = widget.routeOnSuccessName; @@ -143,234 +159,553 @@ class _ConfirmTransactionViewState Widget build(BuildContext context) { final managerProvider = ref.watch(walletsChangeNotifierProvider .select((value) => value.getManagerProvider(walletId))); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - leading: AppBarBackButton( - onPressed: () async { - // if (FocusScope.of(context).hasFocus) { - // FocusScope.of(context).unfocus(); - // await Future<void>.delayed(Duration(milliseconds: 50)); - // } - Navigator.of(context).pop(); - }, - ), - title: Text( - "Confirm transaction", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (builderContext, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - "Send ${ref.watch(managerProvider.select((value) => value.coin)).ticker}", - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - "Recipient", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 4, - ), - Text( - "${transactionInfo["address"] ?? "ERROR"}", - style: STextStyles.itemSubtitle12(context), - ), - ], - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Amount", - style: STextStyles.smallMed12(context), - ), - Text( - "${Format.satoshiAmountToPrettyString( - transactionInfo["recipientAmt"] as int, - ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), - )} ${ref.watch( - managerProvider - .select((value) => value.coin), - ).ticker}", - style: STextStyles.itemSubtitle12(context), - textAlign: TextAlign.right, - ), - ], - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Transaction fee", - style: STextStyles.smallMed12(context), - ), - Text( - "${Format.satoshiAmountToPrettyString( - transactionInfo["fee"] as int, - ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), - )} ${ref.watch( - managerProvider - .select((value) => value.coin), - ).ticker}", - style: STextStyles.itemSubtitle12(context), - textAlign: TextAlign.right, - ), - ], - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - "Note", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 4, - ), - Text( - transactionInfo["note"] as String, - style: STextStyles.itemSubtitle12(context), - ), - ], - ), - ), - const Spacer(), - const SizedBox( - height: 12, - ), - RoundedContainer( - color: Theme.of(context) - .extension<StackColors>()! - .snackBarBackSuccess, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Total amount", - style: - STextStyles.titleBold12(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textConfirmTotalAmount, - ), - ), - Text( - "${Format.satoshiAmountToPrettyString( - (transactionInfo["fee"] as int) + - (transactionInfo["recipientAmt"] as int), - ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), - )} ${ref.watch( - managerProvider - .select((value) => value.coin), - ).ticker}", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textConfirmTotalAmount, - ), - textAlign: TextAlign.right, - ), - ], - ), - ), - const SizedBox( - height: 16, - ), - TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () async { - final unlocked = await Navigator.push( - context, - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator.useMaterialPageRoute, - builder: (_) => const LockscreenView( - showBackButton: true, - popOnSuccess: true, - routeOnSuccessArguments: true, - routeOnSuccess: "", - biometricsCancelButtonString: "CANCEL", - biometricsLocalizedReason: - "Authenticate to send transaction", - biometricsAuthenticationTitle: - "Confirm Transaction", - ), - settings: const RouteSettings( - name: "/confirmsendlockscreen"), - ), - ); - if (unlocked is bool && unlocked && mounted) { - unawaited(_attemptSend(context)); - } - }, - child: Text( - "Send", - style: STextStyles.button(context), - ), - ), - ], + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + leading: AppBarBackButton( + onPressed: () async { + // if (FocusScope.of(context).hasFocus) { + // FocusScope.of(context).unfocus(); + // await Future<void>.delayed(Duration(milliseconds: 50)); + // } + Navigator.of(context).pop(); + }, + ), + title: Text( + "Confirm transaction", + style: STextStyles.navBarTitle(context), + ), + ), + body: LayoutBuilder( + builder: (builderContext, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, ), ), ), ), + ); + }, + ), + ), + child: ConditionalParent( + condition: isDesktop, + builder: (child) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + AppBarBackButton( + size: 40, + iconSize: 24, + onPressed: () => Navigator.of( + context, + rootNavigator: true, + ).pop(), + ), + Text( + "Confirm ${ref.watch(managerProvider.select((value) => value.coin.ticker.toUpperCase()))} transaction", + style: STextStyles.desktopH3(context), + ), + ], ), - ); - }, + child, + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max, + children: [ + if (!isDesktop) + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Send ${ref.watch(managerProvider.select((value) => value.coin)).ticker}", + style: STextStyles.pageTitleH1(context), + ), + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Recipient", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 4, + ), + Text( + "${transactionInfo["address"] ?? "ERROR"}", + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Amount", + style: STextStyles.smallMed12(context), + ), + Text( + "${Format.satoshiAmountToPrettyString( + transactionInfo["recipientAmt"] as int, + ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ), + )} ${ref.watch( + managerProvider.select((value) => value.coin), + ).ticker}", + style: STextStyles.itemSubtitle12(context), + textAlign: TextAlign.right, + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transaction fee", + style: STextStyles.smallMed12(context), + ), + Text( + "${Format.satoshiAmountToPrettyString( + transactionInfo["fee"] as int, + ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ), + )} ${ref.watch( + managerProvider.select((value) => value.coin), + ).ticker}", + style: STextStyles.itemSubtitle12(context), + textAlign: TextAlign.right, + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Note", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 4, + ), + Text( + transactionInfo["note"] as String, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + ], + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only( + top: 16, + left: 32, + right: 32, + bottom: 50, + ), + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + borderColor: + Theme.of(context).extension<StackColors>()!.background, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .background, + borderRadius: BorderRadius.only( + topLeft: Radius.circular( + Constants.size.circularBorderRadius, + ), + topRight: Radius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 22, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.send(context), + width: 32, + height: 32, + ), + const SizedBox( + width: 16, + ), + Text( + "Send ${ref.watch( + managerProvider + .select((value) => value.coin), + ).ticker}", + style: STextStyles.desktopTextMedium(context), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(12), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Amount", + style: STextStyles.desktopTextExtraExtraSmall( + context), + ), + const SizedBox( + height: 2, + ), + Builder( + builder: (context) { + final amount = + transactionInfo["recipientAmt"] as int; + final coin = ref.watch( + managerProvider.select( + (value) => value.coin, + ), + ); + final externalCalls = ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.externalCalls)); + String fiatAmount = "N/A"; + + if (externalCalls) { + final price = ref + .read(priceAnd24hChangeNotifierProvider) + .getPrice(coin) + .item1; + if (price > Decimal.zero) { + fiatAmount = Format.localizedStringAsFixed( + value: Format.satoshisToAmount(amount, + coin: coin) * + price, + locale: ref + .read( + localeServiceChangeNotifierProvider) + .locale, + decimalPlaces: 2, + ); + } + } + + return Row( + children: [ + Text( + "${Format.satoshiAmountToPrettyString( + amount, + ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ), + )} ${coin.ticker}", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + if (externalCalls) + Text( + " | ", + style: STextStyles + .desktopTextExtraExtraSmall( + context), + ), + if (externalCalls) + Text( + "~$fiatAmount ${ref.watch(prefsChangeNotifierProvider.select( + (value) => value.currency, + ))}", + style: STextStyles + .desktopTextExtraExtraSmall( + context), + ), + ], + ); + }, + ), + ], + ), + ), + Container( + height: 1, + color: Theme.of(context) + .extension<StackColors>()! + .background, + ), + Padding( + padding: const EdgeInsets.all(12), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Send to", + style: STextStyles.desktopTextExtraExtraSmall( + context), + ), + const SizedBox( + height: 2, + ), + Text( + "${transactionInfo["address"] ?? "ERROR"}", + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ) + ], + ), + ), + Container( + height: 1, + color: Theme.of(context) + .extension<StackColors>()! + .background, + ), + Padding( + padding: const EdgeInsets.all(12), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Note", + style: STextStyles.desktopTextExtraExtraSmall( + context), + ), + const SizedBox( + height: 2, + ), + Text( + transactionInfo["note"] as String, + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ) + ], + ), + ), + ], + ), + ), + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Transaction fee (estimated)", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only( + top: 10, + left: 32, + right: 32, + ), + child: DropdownButtonFormField( + value: _fee, + items: _dropDownItems + .map( + (e) => DropdownMenuItem( + value: e, + child: Text( + e.toString(), + ), + ), + ) + .toList(), + onChanged: (value) { + if (value is int) { + setState(() { + _fee = value; + }); + } + }, + ), + ), + if (!isDesktop) const Spacer(), + SizedBox( + height: isDesktop ? 23 : 12, + ), + Padding( + padding: isDesktop + ? const EdgeInsets.symmetric( + horizontal: 32, + ) + : const EdgeInsets.all(0), + child: RoundedContainer( + padding: isDesktop + ? const EdgeInsets.symmetric( + horizontal: 16, + vertical: 18, + ) + : const EdgeInsets.all(12), + color: Theme.of(context) + .extension<StackColors>()! + .snackBarBackSuccess, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + isDesktop ? "Total amount to send" : "Total amount", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textConfirmTotalAmount, + ) + : STextStyles.titleBold12(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textConfirmTotalAmount, + ), + ), + Text( + "${Format.satoshiAmountToPrettyString( + (transactionInfo["fee"] as int) + + (transactionInfo["recipientAmt"] as int), + ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ), + )} ${ref.watch( + managerProvider.select((value) => value.coin), + ).ticker}", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textConfirmTotalAmount, + ) + : STextStyles.itemSubtitle12(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textConfirmTotalAmount, + ), + textAlign: TextAlign.right, + ), + ], + ), + ), + ), + SizedBox( + height: isDesktop ? 28 : 16, + ), + Padding( + padding: isDesktop + ? const EdgeInsets.symmetric( + horizontal: 32, + ) + : const EdgeInsets.all(0), + child: PrimaryButton( + label: "Send", + desktopMed: true, + onPressed: () async { + final unlocked = await Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator.useMaterialPageRoute, + builder: (_) => const LockscreenView( + showBackButton: true, + popOnSuccess: true, + routeOnSuccessArguments: true, + routeOnSuccess: "", + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: + "Authenticate to send transaction", + biometricsAuthenticationTitle: "Confirm Transaction", + ), + settings: + const RouteSettings(name: "/confirmsendlockscreen"), + ), + ); + + if (unlocked is bool && unlocked && mounted) { + unawaited(_attemptSend(context)); + } + }, + ), + ), + if (isDesktop) + const SizedBox( + height: 32, + ), + ], + ), ), ); } 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 0b6786915..045218e54 100644 --- a/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart +++ b/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart @@ -3,6 +3,8 @@ 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/utilities/util.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; class BuildingTransactionDialog extends StatefulWidget { @@ -50,37 +52,73 @@ class _RestoringDialogState extends State<BuildingTransactionDialog> @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { - return false; - }, - child: StackDialog( - title: "Generating transaction", - // // TODO get message from design team - // message: "<PLACEHOLDER>", - icon: RotationTransition( - turns: _spinAnimation, - child: SvgPicture.asset( - Assets.svg.arrowRotate, - color: Theme.of(context).extension<StackColors>()!.accentColorDark, - width: 24, - height: 24, + if (Util.isDesktop) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Generating transaction", + style: STextStyles.desktopH3(context), + ), + const SizedBox( + height: 40, + ), + RotationTransition( + turns: _spinAnimation, + child: SvgPicture.asset( + Assets.svg.arrowRotate, + color: + Theme.of(context).extension<StackColors>()!.accentColorDark, + width: 24, + height: 24, + ), + ), + const SizedBox( + height: 40, + ), + SecondaryButton( + desktopMed: true, + label: "Cancel", + onPressed: () { + onCancel.call(); + }, + ) + ], + ); + } else { + return WillPopScope( + onWillPop: () async { + return false; + }, + child: StackDialog( + title: "Generating transaction", + // // TODO get message from design team + // message: "<PLACEHOLDER>", + icon: RotationTransition( + turns: _spinAnimation, + child: SvgPicture.asset( + Assets.svg.arrowRotate, + color: + Theme.of(context).extension<StackColors>()!.accentColorDark, + width: 24, + height: 24, + ), + ), + rightButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Cancel", + style: STextStyles.itemSubtitle12(context), + ), + onPressed: () { + Navigator.of(context).pop(); + onCancel.call(); + }, ), ), - rightButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Cancel", - style: STextStyles.itemSubtitle12(context), - ), - onPressed: () { - Navigator.of(context).pop(); - onCancel.call(); - }, - ), - ), - ); + ); + } } } diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index d482f7ab8..a47b2b9dd 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -40,9 +40,9 @@ import 'package:stackwallet/widgets/icon_widgets/addressbook_icon.dart'; import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart'; import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; -import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; +import 'package:tuple/tuple.dart'; class DesktopSend extends ConsumerStatefulWidget { const DesktopSend({ @@ -280,12 +280,19 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { useSafeArea: false, barrierDismissible: false, builder: (context) { - return BuildingTransactionDialog( - onCancel: () { - wasCancelled = true; + return DesktopDialog( + maxWidth: 400, + maxHeight: double.infinity, + child: Padding( + padding: const EdgeInsets.all(32), + child: BuildingTransactionDialog( + onCancel: () { + wasCancelled = true; - Navigator.of(context).pop(); - }, + Navigator.of(context).pop(); + }, + ), + ), ); }, )); @@ -310,54 +317,103 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { if (!wasCancelled && mounted) { // pop building dialog - Navigator.of(context).pop(); + Navigator.of( + context, + rootNavigator: true, + ).pop(); txData["note"] = noteController.text; txData["address"] = _address; - unawaited(Navigator.of(context).push( - RouteGenerator.getRoute( - shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: (_) => ConfirmTransactionView( - transactionInfo: txData, - walletId: walletId, - ), - settings: const RouteSettings( - name: ConfirmTransactionView.routeName, + unawaited( + showDialog( + context: context, + builder: (context) => DesktopDialog( + maxHeight: double.infinity, + maxWidth: 580, + child: ConfirmTransactionView( + transactionInfo: txData, + walletId: walletId, + ), ), ), - )); + ); } } catch (e) { if (mounted) { // pop building dialog - Navigator.of(context).pop(); + Navigator.of( + context, + rootNavigator: true, + ).pop(); - unawaited(showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return StackDialog( - title: "Transaction failed", - message: e.toString(), - rightButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Ok", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), + unawaited( + showDialog<void>( + context: context, + builder: (context) { + return DesktopDialog( + maxWidth: 450, + maxHeight: double.infinity, + child: Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 32, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transaction failed", + style: STextStyles.desktopH3(context), + ), + const DesktopDialogCloseButton(), + ], + ), + const SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.only( + right: 32, + ), + child: Text( + e.toString(), + textAlign: TextAlign.left, + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + fontSize: 18, + ), + ), + ), + const SizedBox( + height: 40, + ), + Padding( + padding: const EdgeInsets.only( + right: 32, + ), + child: Expanded( + child: SecondaryButton( + desktopMed: true, + label: "Yes", + onPressed: () { + Navigator.of( + context, + rootNavigator: true, + ).pop(); + }, + ), + ), + ), + ], + ), ), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ); - }, - )); + ); + }, + ), + ); } } } diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 91d1a3768..136dc7a34 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -39,6 +39,7 @@ import 'package:stackwallet/pages/notification_views/notifications_view.dart'; import 'package:stackwallet/pages/pinpad_views/create_pin_view.dart'; import 'package:stackwallet/pages/receive_view/generate_receiving_uri_qr_code_view.dart'; import 'package:stackwallet/pages/receive_view/receive_view.dart'; +import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart'; import 'package:stackwallet/pages/send_view/send_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/about_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/advanced_views/advanced_settings_view.dart'; @@ -781,6 +782,21 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case ConfirmTransactionView.routeName: + if (args is Tuple2<Map<String, dynamic>, String>) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => ConfirmTransactionView( + transactionInfo: args.item1, + walletId: args.item2, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case WalletInitiatedExchangeView.routeName: if (args is Tuple3<String, Coin, VoidCallback>) { return getRoute( From 38c6af5caa3111ac0c7e732a473371159f75fdfe Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Tue, 1 Nov 2022 13:56:18 -0600 Subject: [PATCH 088/426] 'can change later in settings' subtitle removed when accessed from settings --- lib/pages/stack_privacy_calls.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/pages/stack_privacy_calls.dart b/lib/pages/stack_privacy_calls.dart index 7ca21c494..d271b239f 100644 --- a/lib/pages/stack_privacy_calls.dart +++ b/lib/pages/stack_privacy_calls.dart @@ -8,7 +8,6 @@ import 'package:stackwallet/pages_desktop_specific/create_password/create_passwo import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; @@ -76,7 +75,7 @@ class _StackPrivacyCalls extends ConsumerState<StackPrivacyCalls> { height: 8, ), Text( - "You can change it later in Settings", + !widget.isSettings ? "You can change it later in Settings" : "", style: STextStyles.subtitle(context), ), const SizedBox( From 60b332ad8ae135efb36c9a391f6b447d0112ace0 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Tue, 1 Nov 2022 14:16:31 -0600 Subject: [PATCH 089/426] auto backup drop down menu --- .../create_auto_backup.dart | 57 ++++++++++++------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart index 0c8c47c9e..16bda2ff7 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart @@ -34,7 +34,7 @@ class _CreateAutoBackup extends State<CreateAutoBackup> { String _currentDropDownValue = "Every 10 minutes"; - final List<String> _dropDOwnItems = [ + final List<String> _dropDownItems = [ "Every 10 minutes", "Every 20 minutes", "Every 30 minutes", @@ -352,25 +352,44 @@ class _CreateAutoBackup extends State<CreateAutoBackup> { const SizedBox( height: 10, ), - DropdownButton( - value: _currentDropDownValue, - items: _dropDOwnItems - .map( - (e) => DropdownMenuItem( - value: e, - child: Text(e), - ), - ) - .toList(), - onChanged: (value) { - if (value is String) { - setState(() { - _currentDropDownValue = value; - }); - } - }, + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + ), + child: DropdownButtonFormField( + isExpanded: true, + elevation: 0, + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark, + ), + icon: SvgPicture.asset( + Assets.svg.chevronDown, + width: 10, + height: 5, + color: Theme.of(context).extension<StackColors>()!.textDark3, + ), + dropdownColor: + Theme.of(context).extension<StackColors>()!.textFieldActiveBG, + // focusColor: , + value: _currentDropDownValue, + items: _dropDownItems + .map( + (e) => DropdownMenuItem( + value: e, + child: Text(e), + ), + ) + .toList(), + onChanged: (value) { + if (value is String) { + setState(() { + _currentDropDownValue = value; + }); + } + }, + ), ), - const Spacer(), Padding( padding: const EdgeInsets.all(32), child: Row( From 4d8804f78b6d5ad40cd38e677063beb886680b73 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 1 Nov 2022 15:58:41 -0600 Subject: [PATCH 090/426] WIP desktop send flow address book address chooser --- .../address_book_address_chooser.dart | 143 +++++++++++++++ .../sub_widgets/contact_list_item.dart | 114 ++++++++++++ .../wallet_view/sub_widgets/desktop_send.dart | 44 +++-- lib/widgets/address_book_card.dart | 168 ++++++++++-------- 4 files changed, 385 insertions(+), 84 deletions(-) create mode 100644 lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart create mode 100644 lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/sub_widgets/contact_list_item.dart diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart new file mode 100644 index 000000000..372c86e2f --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart @@ -0,0 +1,143 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/contact.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/sub_widgets/contact_list_item.dart'; +import 'package:stackwallet/providers/global/address_book_service_provider.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; + +class AddressBookAddressChooser extends StatefulWidget { + const AddressBookAddressChooser({ + Key? key, + this.coin, + }) : super(key: key); + + final Coin? coin; + + @override + State<AddressBookAddressChooser> createState() => + _AddressBookAddressChooserState(); +} + +class _AddressBookAddressChooserState extends State<AddressBookAddressChooser> { + int _compareContactFavorite(Contact a, Contact b) { + if (a.isFavorite && b.isFavorite) { + return 0; + } else if (a.isFavorite) { + return 1; + } else { + return -1; + } + } + + List<Contact> pullOutFavorites(List<Contact> contacts) { + final List<Contact> favorites = []; + contacts.removeWhere((contact) { + if (contact.isFavorite) { + favorites.add(contact); + return true; + } + return false; + }); + + return favorites; + } + + List<Contact> filter(List<Contact> contacts) { + if (widget.coin != null) { + contacts.removeWhere( + (e) => e.addresses.where((a) => a.coin == widget.coin!).isEmpty); + } + + if (contacts.length < 2) { + return contacts; + } + + contacts.sort(_compareContactFavorite); + + // TODO: other filtering? + + return contacts; + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + // search field + const TextField(), + const SizedBox( + height: 16, + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: Consumer( + builder: (context, ref, _) { + List<Contact> contacts = ref + .watch(addressBookServiceProvider + .select((value) => value.contacts)) + .toList(); + + contacts = filter(contacts); + + final favorites = pullOutFavorites(contacts); + + return ListView.builder( + primary: false, + shrinkWrap: true, + itemCount: favorites.length + + contacts.length + + 2, // +2 for "fav" and "all" headers + itemBuilder: (context, index) { + if (index == 0) { + return Padding( + padding: const EdgeInsets.only( + bottom: 10, + ), + child: Text( + "Favorites", + style: + STextStyles.desktopTextExtraExtraSmall(context), + ), + ); + } else if (index <= favorites.length) { + final id = favorites[index - 1].id; + return ContactListItem( + key: Key("contactCard_${id}_key"), + contactId: id, + filterByCoin: widget.coin, + ); + } else if (index == favorites.length + 1) { + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 10, + ), + child: Text( + "All contacts", + style: + STextStyles.desktopTextExtraExtraSmall(context), + ), + ); + } else { + final id = contacts[index - favorites.length - 1].id; + return ContactListItem( + key: Key("contactCard_${id}_key"), + contactId: id, + filterByCoin: widget.coin, + ); + } + }, + ); + }, + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/sub_widgets/contact_list_item.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/sub_widgets/contact_list_item.dart new file mode 100644 index 000000000..593ae2bc4 --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/sub_widgets/contact_list_item.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/providers/global/address_book_service_provider.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/address_book_card.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/expandable.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart'; + +class ContactListItem extends ConsumerWidget { + const ContactListItem({ + Key? key, + required this.contactId, + this.filterByCoin, + }) : super(key: key); + + final String contactId; + final Coin? filterByCoin; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final contact = ref.watch(addressBookServiceProvider + .select((value) => value.getContactById(contactId))); + + return RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + borderColor: Theme.of(context).extension<StackColors>()!.background, + child: Expandable( + header: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 14, + ), + child: AddressBookCard( + contactId: contactId, + ), + ), + body: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // filter addresses by coin is provided before building address list + ...contact.addresses + .where((e) => + filterByCoin != null ? e.coin == filterByCoin! : true) + .map( + (e) => Column( + key: Key("contactAddress_${e.address}_${e.label}_key"), + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: 1, + color: Theme.of(context) + .extension<StackColors>()! + .background, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 14, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + WalletInfoCoinIcon(coin: e.coin), + const SizedBox( + width: 12, + ), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${contactId == "default" ? e.other! : e.label} (${e.coin.ticker})", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + Text( + e.address, + style: STextStyles + .desktopTextExtraExtraSmall(context), + ), + ], + ), + ], + ), + BlueTextButton( + text: "Select wallet", + onTap: () { + Navigator.of(context).pop(e); + }, + ), + ], + ), + ) + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index a47b2b9dd..26f2f806c 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -5,17 +5,17 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/models/contact_address_entry.dart'; import 'package:stackwallet/models/send_view_auto_fill_data.dart'; -import 'package:stackwallet/pages/address_book_views/address_book_view.dart'; import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart'; import 'package:stackwallet/pages/send_view/sub_widgets/building_transaction_dialog.dart'; import 'package:stackwallet/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart'; import 'package:stackwallet/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/fee_rate_type_state_provider.dart'; import 'package:stackwallet/providers/ui/preview_tx_button_state_provider.dart'; import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart'; -import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/utilities/address_utils.dart'; @@ -42,7 +42,6 @@ import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; -import 'package:tuple/tuple.dart'; class DesktopSend extends ConsumerStatefulWidget { const DesktopSend({ @@ -330,10 +329,10 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { builder: (context) => DesktopDialog( maxHeight: double.infinity, maxWidth: 580, - child: ConfirmTransactionView( - transactionInfo: txData, - walletId: walletId, - ), + child: ConfirmTransactionView( + transactionInfo: txData, + walletId: walletId, + ), ), ), ); @@ -1184,11 +1183,34 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { if (sendToController.text.isEmpty) TextFieldIconButton( key: const Key("sendViewAddressBookButtonKey"), - onTap: () { - Navigator.of(context).pushNamed( - AddressBookView.routeName, - arguments: coin, + onTap: () async { + final entry = + await showDialog<ContactAddressEntry?>( + context: context, + builder: (context) => DesktopDialog( + maxWidth: 696, + maxHeight: 600, + child: AddressBookAddressChooser( + coin: coin, + ), + ), ); + + if (entry != null) { + sendToController.text = + entry.other ?? entry.label; + + _address = entry.address; + + _updatePreviewButtonState( + _address, + _amountToSend, + ); + + setState(() { + _addressToggleFlag = true; + }); + } }, child: const AddressBookIcon(), ), diff --git a/lib/widgets/address_book_card.dart b/lib/widgets/address_book_card.dart index 7a2fca19f..6a51edbad 100644 --- a/lib/widgets/address_book_card.dart +++ b/lib/widgets/address_book_card.dart @@ -8,6 +8,8 @@ import 'package:stackwallet/utilities/constants.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/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class AddressBookCard extends ConsumerStatefulWidget { @@ -21,11 +23,12 @@ class AddressBookCard extends ConsumerStatefulWidget { class _AddressBookCardState extends ConsumerState<AddressBookCard> { late final String contactId; + late final bool isDesktop; @override void initState() { contactId = widget.contactId; - + isDesktop = Util.isDesktop; super.initState(); } @@ -51,82 +54,101 @@ class _AddressBookCardState extends ConsumerState<AddressBookCard> { } } - return RoundedWhiteContainer( - padding: const EdgeInsets.all(4), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - padding: const EdgeInsets.all(0), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - showDialog<void>( - context: context, - useSafeArea: true, - barrierDismissible: true, - builder: (_) => ContactPopUp( - contactId: contact.id, + return ConditionalParent( + condition: !isDesktop, + child: Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: contact.id == "default" + ? Theme.of(context) + .extension<StackColors>()! + .myStackContactIconBG + : Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular(32), ), - ); - }, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - Container( - width: 32, - height: 32, - decoration: BoxDecoration( - color: contact.id == "default" - ? Theme.of(context) - .extension<StackColors>()! - .myStackContactIconBG - : Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - borderRadius: BorderRadius.circular(32), - ), - child: contact.id == "default" + child: contact.id == "default" + ? Center( + child: SvgPicture.asset( + Assets.svg.stackIcon(context), + width: 20, + ), + ) + : contact.emojiChar != null ? Center( - child: SvgPicture.asset( - Assets.svg.stackIcon(context), - width: 20, - ), + child: Text(contact.emojiChar!), ) - : contact.emojiChar != null - ? Center( - child: Text(contact.emojiChar!), - ) - : Center( - child: SvgPicture.asset( - Assets.svg.user, - width: 18, - ), - ), + : Center( + child: SvgPicture.asset( + Assets.svg.user, + width: 18, + ), + ), + ), + const SizedBox( + width: 12, + ), + if (isDesktop) + Text( + contact.name, + style: STextStyles.itemSubtitle12(context), + ), + if (isDesktop) + const SizedBox( + width: 16, + ), + if (isDesktop) + Text( + coinsString, + style: STextStyles.label(context), + ), + if (!isDesktop) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + contact.name, + style: STextStyles.itemSubtitle12(context), + ), + const SizedBox( + height: 4, + ), + Text( + coinsString, + style: STextStyles.label(context), + ), + ], + ) + ], + ), + builder: (child) => RoundedWhiteContainer( + padding: const EdgeInsets.all(4), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + showDialog<void>( + context: context, + useSafeArea: true, + barrierDismissible: true, + builder: (_) => ContactPopUp( + contactId: contact.id, ), - const SizedBox( - width: 12, - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - contact.name, - style: STextStyles.itemSubtitle12(context), - ), - const SizedBox( - height: 4, - ), - Text( - coinsString, - style: STextStyles.label(context), - ), - ], - ) - ], + ); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: child, ), ), ), From 6d8142d66a71e167431ca09ee6ffd47fcd7ff395 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 1 Nov 2022 16:04:38 -0600 Subject: [PATCH 091/426] code formatting --- lib/pages/stack_privacy_calls.dart | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/pages/stack_privacy_calls.dart b/lib/pages/stack_privacy_calls.dart index 3d1a071a0..7e981b91a 100644 --- a/lib/pages/stack_privacy_calls.dart +++ b/lib/pages/stack_privacy_calls.dart @@ -3,9 +3,12 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/pages/pinpad_views/create_pin_view.dart'; import 'package:stackwallet/pages_desktop_specific/create_password/create_password_view.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; +import 'package:stackwallet/providers/global/price_provider.dart'; +import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -14,13 +17,9 @@ import 'package:stackwallet/utilities/util.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/primary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; -import '../hive/db.dart'; -import '../providers/global/price_provider.dart'; -import '../services/exchange/exchange_data_loading_service.dart'; -import '../widgets/desktop/primary_button.dart'; - class StackPrivacyCalls extends ConsumerStatefulWidget { const StackPrivacyCalls({ Key? key, @@ -90,7 +89,9 @@ class _StackPrivacyCalls extends ConsumerState<StackPrivacyCalls> { height: isDesktop ? 16 : 8, ), Text( - !widget.isSettings ? "You can change it later in Settings" : "", + !widget.isSettings + ? "You can change it later in Settings" + : "", style: isDesktop ? STextStyles.desktopSubtitleH2(context) : STextStyles.subtitle(context), From 6ebe33c3129bd4599ee35477b9bcb9d345e2e06c Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 1 Nov 2022 16:13:07 -0600 Subject: [PATCH 092/426] temporarily commented out failing widget tests due to platform differences with desktop modifications --- test/widget_tests/address_book_card_test.dart | 97 +-- test/widget_tests/transaction_card_test.dart | 734 +++++++++--------- 2 files changed, 400 insertions(+), 431 deletions(-) diff --git a/test/widget_tests/address_book_card_test.dart b/test/widget_tests/address_book_card_test.dart index ef031eb4e..8c8c44abd 100644 --- a/test/widget_tests/address_book_card_test.dart +++ b/test/widget_tests/address_book_card_test.dart @@ -1,19 +1,6 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; -import 'package:stackwallet/models/contact.dart'; -import 'package:stackwallet/models/contact_address_entry.dart'; -import 'package:stackwallet/pages/address_book_views/subviews/contact_popup.dart'; -import 'package:stackwallet/providers/global/address_book_service_provider.dart'; import 'package:stackwallet/services/address_book_service.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/theme/light_colors.dart'; -import 'package:stackwallet/utilities/theme/stack_colors.dart'; -import 'package:stackwallet/widgets/address_book_card.dart'; - -import 'address_book_card_test.mocks.dart'; class MockedFunctions extends Mock { void showDialog(); @@ -21,46 +8,46 @@ class MockedFunctions extends Mock { @GenerateMocks([AddressBookService]) void main() { - testWidgets('test returns Contact Address Entry', (widgetTester) async { - final service = MockAddressBookService(); - - when(service.getContactById("default")) - .thenAnswer((realInvocation) => Contact( - name: "John Doe", - addresses: [ - const ContactAddressEntry( - coin: Coin.bitcoincash, - address: "some bch address", - label: "Bills") - ], - isFavorite: true)); - - await widgetTester.pumpWidget( - ProviderScope( - overrides: [ - addressBookServiceProvider.overrideWithValue( - service, - ), - ], - child: MaterialApp( - theme: ThemeData( - extensions: [ - StackColors.fromStackColorTheme( - LightColors(), - ), - ], - ), - home: const AddressBookCard( - contactId: "default", - ), - ), - ), - ); - - expect(find.text("John Doe"), findsOneWidget); - expect(find.text("BCH"), findsOneWidget); - expect(find.text(Coin.bitcoincash.ticker), findsOneWidget); - - await widgetTester.tap(find.byType(RawMaterialButton)); - }); + // testWidgets('test returns Contact Address Entry', (widgetTester) async { + // final service = MockAddressBookService(); + // + // when(service.getContactById("default")) + // .thenAnswer((realInvocation) => Contact( + // name: "John Doe", + // addresses: [ + // const ContactAddressEntry( + // coin: Coin.bitcoincash, + // address: "some bch address", + // label: "Bills") + // ], + // isFavorite: true)); + // + // await widgetTester.pumpWidget( + // ProviderScope( + // overrides: [ + // addressBookServiceProvider.overrideWithValue( + // service, + // ), + // ], + // child: MaterialApp( + // theme: ThemeData( + // extensions: [ + // StackColors.fromStackColorTheme( + // LightColors(), + // ), + // ], + // ), + // home: const AddressBookCard( + // contactId: "default", + // ), + // ), + // ), + // ); + // + // expect(find.text("John Doe"), findsOneWidget); + // expect(find.text("BCH"), findsOneWidget); + // expect(find.text(Coin.bitcoincash.ticker), findsOneWidget); + // + // await widgetTester.tap(find.byType(RawMaterialButton)); + // }); } diff --git a/test/widget_tests/transaction_card_test.dart b/test/widget_tests/transaction_card_test.dart index 3f46794bd..6c36fce8d 100644 --- a/test/widget_tests/transaction_card_test.dart +++ b/test/widget_tests/transaction_card_test.dart @@ -1,13 +1,4 @@ -import 'package:decimal/decimal.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_feather_icons/flutter_feather_icons.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockingjay/mockingjay.dart' as mockingjay; import 'package:mockito/annotations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:mockito/mockito.dart'; -import 'package:stackwallet/models/models.dart'; -import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/coins/coin_service.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/manager.dart'; @@ -15,16 +6,7 @@ import 'package:stackwallet/services/locale_service.dart'; import 'package:stackwallet/services/notes_service.dart'; import 'package:stackwallet/services/price_service.dart'; import 'package:stackwallet/services/wallets.dart'; -import 'package:stackwallet/utilities/default_nodes.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/listenable_map.dart'; import 'package:stackwallet/utilities/prefs.dart'; -import 'package:stackwallet/widgets/transaction_card.dart'; -import 'package:stackwallet/utilities/theme/light_colors.dart'; -import 'package:stackwallet/utilities/theme/stack_colors.dart'; -import 'package:tuple/tuple.dart'; - -import 'transaction_card_test.mocks.dart'; @GenerateMocks([ Wallets, @@ -37,362 +19,362 @@ import 'transaction_card_test.mocks.dart'; NotesService ], customMocks: []) void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - testWidgets("Sent confirmed tx displays correctly", (tester) async { - final mockManager = MockManager(); - final mockLocaleService = MockLocaleService(); - final wallets = MockWallets(); - final mockPrefs = MockPrefs(); - final mockPriceService = MockPriceService(); - - final tx = Transaction( - txid: "some txid", - confirmedStatus: true, - timestamp: 1648595998, - txType: "Sent", - amount: 100000000, - aliens: [], - worthNow: "0.01", - worthAtBlockTimestamp: "0.01", - fees: 3794, - inputSize: 1, - outputSize: 1, - inputs: [], - outputs: [], - address: "", - height: 450123, - subType: "", - confirmations: 10, - isCancelled: false); - - final CoinServiceAPI wallet = MockFiroWallet(); - - when(wallet.coin.ticker).thenAnswer((_) => "FIRO"); - when(mockLocaleService.locale).thenAnswer((_) => "en_US"); - when(mockPrefs.currency).thenAnswer((_) => "USD"); - when(mockPrefs.externalCalls).thenAnswer((_) => true); - when(mockPriceService.getPrice(Coin.firo)) - .thenAnswer((realInvocation) => Tuple2(Decimal.ten, 0.00)); - - when(wallet.coin).thenAnswer((_) => Coin.firo); - - when(wallets.getManager("wallet-id")) - .thenAnswer((realInvocation) => Manager(wallet)); - // - await tester.pumpWidget( - ProviderScope( - overrides: [ - walletsChangeNotifierProvider.overrideWithValue(wallets), - localeServiceChangeNotifierProvider - .overrideWithValue(mockLocaleService), - prefsChangeNotifierProvider.overrideWithValue(mockPrefs), - priceAnd24hChangeNotifierProvider.overrideWithValue(mockPriceService) - ], - child: MaterialApp( - theme: ThemeData( - extensions: [ - StackColors.fromStackColorTheme( - LightColors(), - ), - ], - ), - home: TransactionCard(transaction: tx, walletId: "wallet-id"), - ), - ), - ); - - // - final title = find.text("Sent"); - // final price1 = find.text("0.00 USD"); - final amount = find.text("1.00000000 FIRO"); - - final icon = find.byIcon(FeatherIcons.arrowUp); - - expect(title, findsOneWidget); - // expect(price1, findsOneWidget); - expect(amount, findsOneWidget); - // expect(icon, findsOneWidget); - // - await tester.pumpAndSettle(Duration(seconds: 2)); - // - // final price2 = find.text("\$10.00"); - // expect(price2, findsOneWidget); - // - // verify(mockManager.addListener(any)).called(1); - verify(mockLocaleService.addListener(any)).called(1); - - verify(mockPrefs.currency).called(1); - verify(mockPriceService.getPrice(Coin.firo)).called(1); - verify(wallet.coin.ticker).called(1); - - verify(mockLocaleService.locale).called(1); - - verifyNoMoreInteractions(mockManager); - verifyNoMoreInteractions(mockLocaleService); - }); - - testWidgets("Anonymized confirmed tx displays correctly", (tester) async { - final mockManager = MockManager(); - final mockLocaleService = MockLocaleService(); - final wallets = MockWallets(); - final mockPrefs = MockPrefs(); - final mockPriceService = MockPriceService(); - - final tx = Transaction( - txid: "some txid", - confirmedStatus: true, - timestamp: 1648595998, - txType: "Anonymized", - amount: 100000000, - aliens: [], - worthNow: "0.01", - worthAtBlockTimestamp: "0.01", - fees: 3794, - inputSize: 1, - outputSize: 1, - inputs: [], - outputs: [], - address: "", - height: 450123, - subType: "mint", - confirmations: 10, - isCancelled: false); - - final CoinServiceAPI wallet = MockFiroWallet(); - - when(wallet.coin.ticker).thenAnswer((_) => "FIRO"); - when(mockLocaleService.locale).thenAnswer((_) => "en_US"); - when(mockPrefs.currency).thenAnswer((_) => "USD"); - when(mockPrefs.externalCalls).thenAnswer((_) => true); - when(mockPriceService.getPrice(Coin.firo)) - .thenAnswer((realInvocation) => Tuple2(Decimal.ten, 0.00)); - - when(wallet.coin).thenAnswer((_) => Coin.firo); - - when(wallets.getManager("wallet-id")) - .thenAnswer((realInvocation) => Manager(wallet)); - // - await tester.pumpWidget( - ProviderScope( - overrides: [ - walletsChangeNotifierProvider.overrideWithValue(wallets), - localeServiceChangeNotifierProvider - .overrideWithValue(mockLocaleService), - prefsChangeNotifierProvider.overrideWithValue(mockPrefs), - priceAnd24hChangeNotifierProvider.overrideWithValue(mockPriceService) - ], - child: MaterialApp( - theme: ThemeData( - extensions: [ - StackColors.fromStackColorTheme( - LightColors(), - ), - ], - ), - home: TransactionCard(transaction: tx, walletId: "wallet-id"), - ), - ), - ); - - // - final title = find.text("Anonymized"); - // final price1 = find.text("0.00 USD"); - final amount = find.text("1.00000000 FIRO"); - - final icon = find.byIcon(FeatherIcons.arrowUp); - - expect(title, findsOneWidget); - // expect(price1, findsOneWidget); - expect(amount, findsOneWidget); - // expect(icon, findsOneWidget); - // - await tester.pumpAndSettle(Duration(seconds: 2)); - // - // final price2 = find.text("\$10.00"); - // expect(price2, findsOneWidget); - // - // verify(mockManager.addListener(any)).called(1); - verify(mockLocaleService.addListener(any)).called(1); - - verify(mockPrefs.currency).called(1); - verify(mockPriceService.getPrice(Coin.firo)).called(1); - verify(wallet.coin.ticker).called(1); - - verify(mockLocaleService.locale).called(1); - - verifyNoMoreInteractions(mockManager); - verifyNoMoreInteractions(mockLocaleService); - }); - - testWidgets("Received unconfirmed tx displays correctly", (tester) async { - final mockManager = MockManager(); - final mockLocaleService = MockLocaleService(); - final wallets = MockWallets(); - final mockPrefs = MockPrefs(); - final mockPriceService = MockPriceService(); - - final tx = Transaction( - txid: "some txid", - confirmedStatus: false, - timestamp: 1648595998, - txType: "Received", - amount: 100000000, - aliens: [], - worthNow: "0.01", - worthAtBlockTimestamp: "0.01", - fees: 3794, - inputSize: 1, - outputSize: 1, - inputs: [], - outputs: [], - address: "", - height: 0, - subType: "", - confirmations: 0, - ); - - final CoinServiceAPI wallet = MockFiroWallet(); - - when(wallet.coin.ticker).thenAnswer((_) => "FIRO"); - when(mockLocaleService.locale).thenAnswer((_) => "en_US"); - when(mockPrefs.currency).thenAnswer((_) => "USD"); - when(mockPrefs.externalCalls).thenAnswer((_) => true); - when(mockPriceService.getPrice(Coin.firo)) - .thenAnswer((realInvocation) => Tuple2(Decimal.ten, 0.00)); - - when(wallet.coin).thenAnswer((_) => Coin.firo); - - when(wallets.getManager("wallet-id")) - .thenAnswer((realInvocation) => Manager(wallet)); - - await tester.pumpWidget( - ProviderScope( - overrides: [ - walletsChangeNotifierProvider.overrideWithValue(wallets), - localeServiceChangeNotifierProvider - .overrideWithValue(mockLocaleService), - prefsChangeNotifierProvider.overrideWithValue(mockPrefs), - priceAnd24hChangeNotifierProvider.overrideWithValue(mockPriceService) - ], - child: MaterialApp( - theme: ThemeData( - extensions: [ - StackColors.fromStackColorTheme( - LightColors(), - ), - ], - ), - home: TransactionCard(transaction: tx, walletId: "wallet-id"), - ), - ), - ); - - final title = find.text("Receiving"); - final amount = find.text("1.00000000 FIRO"); - - expect(title, findsOneWidget); - expect(amount, findsOneWidget); - - await tester.pumpAndSettle(Duration(seconds: 2)); - - verify(mockLocaleService.addListener(any)).called(1); - - verify(mockPrefs.currency).called(1); - verify(mockPriceService.getPrice(Coin.firo)).called(1); - verify(wallet.coin.ticker).called(1); - - verify(mockLocaleService.locale).called(1); - - verifyNoMoreInteractions(mockManager); - verifyNoMoreInteractions(mockLocaleService); - }); - - testWidgets("Tap gesture", (tester) async { - final mockManager = MockManager(); - final mockLocaleService = MockLocaleService(); - final wallets = MockWallets(); - final mockPrefs = MockPrefs(); - final mockPriceService = MockPriceService(); - final navigator = mockingjay.MockNavigator(); - - final tx = Transaction( - txid: "some txid", - confirmedStatus: false, - timestamp: 1648595998, - txType: "Received", - amount: 100000000, - aliens: [], - worthNow: "0.01", - worthAtBlockTimestamp: "0.01", - fees: 3794, - inputSize: 1, - outputSize: 1, - inputs: [], - outputs: [], - address: "", - height: 250, - subType: "", - confirmations: 10, - ); - - final CoinServiceAPI wallet = MockFiroWallet(); - - when(wallet.coin.ticker).thenAnswer((_) => "FIRO"); - when(mockLocaleService.locale).thenAnswer((_) => "en_US"); - when(mockPrefs.currency).thenAnswer((_) => "USD"); - when(mockPrefs.externalCalls).thenAnswer((_) => true); - when(mockPriceService.getPrice(Coin.firo)) - .thenAnswer((realInvocation) => Tuple2(Decimal.ten, 0.00)); - - when(wallet.coin).thenAnswer((_) => Coin.firo); - - when(wallets.getManager("wallet id")) - .thenAnswer((realInvocation) => Manager(wallet)); - - mockingjay - .when(() => navigator.pushNamed("/transactionDetails", - arguments: Tuple3(tx, Coin.firo, "wallet id"))) - .thenAnswer((_) async => {}); - - await tester.pumpWidget( - ProviderScope( - overrides: [ - walletsChangeNotifierProvider.overrideWithValue(wallets), - localeServiceChangeNotifierProvider - .overrideWithValue(mockLocaleService), - prefsChangeNotifierProvider.overrideWithValue(mockPrefs), - priceAnd24hChangeNotifierProvider.overrideWithValue(mockPriceService) - ], - child: MaterialApp( - theme: ThemeData( - extensions: [ - StackColors.fromStackColorTheme(LightColors()), - ], - ), - home: mockingjay.MockNavigatorProvider( - navigator: navigator, - child: TransactionCard(transaction: tx, walletId: "wallet id")), - ), - ), - ); - - expect(find.byType(GestureDetector), findsOneWidget); - - await tester.tap(find.byType(GestureDetector)); - await tester.pump(); - - verify(mockLocaleService.addListener(any)).called(1); - - verify(mockPrefs.currency).called(1); - verify(mockLocaleService.locale).called(1); - verify(wallet.coin.ticker).called(1); - - verifyNoMoreInteractions(wallet); - verifyNoMoreInteractions(mockLocaleService); - - mockingjay - .verify(() => navigator.pushNamed("/transactionDetails", - arguments: Tuple3(tx, Coin.firo, "wallet id"))) - .called(1); - }); + // TestWidgetsFlutterBinding.ensureInitialized(); + // testWidgets("Sent confirmed tx displays correctly", (tester) async { + // final mockManager = MockManager(); + // final mockLocaleService = MockLocaleService(); + // final wallets = MockWallets(); + // final mockPrefs = MockPrefs(); + // final mockPriceService = MockPriceService(); + // + // final tx = Transaction( + // txid: "some txid", + // confirmedStatus: true, + // timestamp: 1648595998, + // txType: "Sent", + // amount: 100000000, + // aliens: [], + // worthNow: "0.01", + // worthAtBlockTimestamp: "0.01", + // fees: 3794, + // inputSize: 1, + // outputSize: 1, + // inputs: [], + // outputs: [], + // address: "", + // height: 450123, + // subType: "", + // confirmations: 10, + // isCancelled: false); + // + // final CoinServiceAPI wallet = MockFiroWallet(); + // + // when(wallet.coin.ticker).thenAnswer((_) => "FIRO"); + // when(mockLocaleService.locale).thenAnswer((_) => "en_US"); + // when(mockPrefs.currency).thenAnswer((_) => "USD"); + // when(mockPrefs.externalCalls).thenAnswer((_) => true); + // when(mockPriceService.getPrice(Coin.firo)) + // .thenAnswer((realInvocation) => Tuple2(Decimal.ten, 0.00)); + // + // when(wallet.coin).thenAnswer((_) => Coin.firo); + // + // when(wallets.getManager("wallet-id")) + // .thenAnswer((realInvocation) => Manager(wallet)); + // // + // await tester.pumpWidget( + // ProviderScope( + // overrides: [ + // walletsChangeNotifierProvider.overrideWithValue(wallets), + // localeServiceChangeNotifierProvider + // .overrideWithValue(mockLocaleService), + // prefsChangeNotifierProvider.overrideWithValue(mockPrefs), + // priceAnd24hChangeNotifierProvider.overrideWithValue(mockPriceService) + // ], + // child: MaterialApp( + // theme: ThemeData( + // extensions: [ + // StackColors.fromStackColorTheme( + // LightColors(), + // ), + // ], + // ), + // home: TransactionCard(transaction: tx, walletId: "wallet-id"), + // ), + // ), + // ); + // + // // + // final title = find.text("Sent"); + // // final price1 = find.text("0.00 USD"); + // final amount = find.text("1.00000000 FIRO"); + // + // final icon = find.byIcon(FeatherIcons.arrowUp); + // + // expect(title, findsOneWidget); + // // expect(price1, findsOneWidget); + // expect(amount, findsOneWidget); + // // expect(icon, findsOneWidget); + // // + // await tester.pumpAndSettle(Duration(seconds: 2)); + // // + // // final price2 = find.text("\$10.00"); + // // expect(price2, findsOneWidget); + // // + // // verify(mockManager.addListener(any)).called(1); + // verify(mockLocaleService.addListener(any)).called(1); + // + // verify(mockPrefs.currency).called(1); + // verify(mockPriceService.getPrice(Coin.firo)).called(1); + // verify(wallet.coin.ticker).called(1); + // + // verify(mockLocaleService.locale).called(1); + // + // verifyNoMoreInteractions(mockManager); + // verifyNoMoreInteractions(mockLocaleService); + // }); + // + // testWidgets("Anonymized confirmed tx displays correctly", (tester) async { + // final mockManager = MockManager(); + // final mockLocaleService = MockLocaleService(); + // final wallets = MockWallets(); + // final mockPrefs = MockPrefs(); + // final mockPriceService = MockPriceService(); + // + // final tx = Transaction( + // txid: "some txid", + // confirmedStatus: true, + // timestamp: 1648595998, + // txType: "Anonymized", + // amount: 100000000, + // aliens: [], + // worthNow: "0.01", + // worthAtBlockTimestamp: "0.01", + // fees: 3794, + // inputSize: 1, + // outputSize: 1, + // inputs: [], + // outputs: [], + // address: "", + // height: 450123, + // subType: "mint", + // confirmations: 10, + // isCancelled: false); + // + // final CoinServiceAPI wallet = MockFiroWallet(); + // + // when(wallet.coin.ticker).thenAnswer((_) => "FIRO"); + // when(mockLocaleService.locale).thenAnswer((_) => "en_US"); + // when(mockPrefs.currency).thenAnswer((_) => "USD"); + // when(mockPrefs.externalCalls).thenAnswer((_) => true); + // when(mockPriceService.getPrice(Coin.firo)) + // .thenAnswer((realInvocation) => Tuple2(Decimal.ten, 0.00)); + // + // when(wallet.coin).thenAnswer((_) => Coin.firo); + // + // when(wallets.getManager("wallet-id")) + // .thenAnswer((realInvocation) => Manager(wallet)); + // // + // await tester.pumpWidget( + // ProviderScope( + // overrides: [ + // walletsChangeNotifierProvider.overrideWithValue(wallets), + // localeServiceChangeNotifierProvider + // .overrideWithValue(mockLocaleService), + // prefsChangeNotifierProvider.overrideWithValue(mockPrefs), + // priceAnd24hChangeNotifierProvider.overrideWithValue(mockPriceService) + // ], + // child: MaterialApp( + // theme: ThemeData( + // extensions: [ + // StackColors.fromStackColorTheme( + // LightColors(), + // ), + // ], + // ), + // home: TransactionCard(transaction: tx, walletId: "wallet-id"), + // ), + // ), + // ); + // + // // + // final title = find.text("Anonymized"); + // // final price1 = find.text("0.00 USD"); + // final amount = find.text("1.00000000 FIRO"); + // + // final icon = find.byIcon(FeatherIcons.arrowUp); + // + // expect(title, findsOneWidget); + // // expect(price1, findsOneWidget); + // expect(amount, findsOneWidget); + // // expect(icon, findsOneWidget); + // // + // await tester.pumpAndSettle(Duration(seconds: 2)); + // // + // // final price2 = find.text("\$10.00"); + // // expect(price2, findsOneWidget); + // // + // // verify(mockManager.addListener(any)).called(1); + // verify(mockLocaleService.addListener(any)).called(1); + // + // verify(mockPrefs.currency).called(1); + // verify(mockPriceService.getPrice(Coin.firo)).called(1); + // verify(wallet.coin.ticker).called(1); + // + // verify(mockLocaleService.locale).called(1); + // + // verifyNoMoreInteractions(mockManager); + // verifyNoMoreInteractions(mockLocaleService); + // }); + // + // testWidgets("Received unconfirmed tx displays correctly", (tester) async { + // final mockManager = MockManager(); + // final mockLocaleService = MockLocaleService(); + // final wallets = MockWallets(); + // final mockPrefs = MockPrefs(); + // final mockPriceService = MockPriceService(); + // + // final tx = Transaction( + // txid: "some txid", + // confirmedStatus: false, + // timestamp: 1648595998, + // txType: "Received", + // amount: 100000000, + // aliens: [], + // worthNow: "0.01", + // worthAtBlockTimestamp: "0.01", + // fees: 3794, + // inputSize: 1, + // outputSize: 1, + // inputs: [], + // outputs: [], + // address: "", + // height: 0, + // subType: "", + // confirmations: 0, + // ); + // + // final CoinServiceAPI wallet = MockFiroWallet(); + // + // when(wallet.coin.ticker).thenAnswer((_) => "FIRO"); + // when(mockLocaleService.locale).thenAnswer((_) => "en_US"); + // when(mockPrefs.currency).thenAnswer((_) => "USD"); + // when(mockPrefs.externalCalls).thenAnswer((_) => true); + // when(mockPriceService.getPrice(Coin.firo)) + // .thenAnswer((realInvocation) => Tuple2(Decimal.ten, 0.00)); + // + // when(wallet.coin).thenAnswer((_) => Coin.firo); + // + // when(wallets.getManager("wallet-id")) + // .thenAnswer((realInvocation) => Manager(wallet)); + // + // await tester.pumpWidget( + // ProviderScope( + // overrides: [ + // walletsChangeNotifierProvider.overrideWithValue(wallets), + // localeServiceChangeNotifierProvider + // .overrideWithValue(mockLocaleService), + // prefsChangeNotifierProvider.overrideWithValue(mockPrefs), + // priceAnd24hChangeNotifierProvider.overrideWithValue(mockPriceService) + // ], + // child: MaterialApp( + // theme: ThemeData( + // extensions: [ + // StackColors.fromStackColorTheme( + // LightColors(), + // ), + // ], + // ), + // home: TransactionCard(transaction: tx, walletId: "wallet-id"), + // ), + // ), + // ); + // + // final title = find.text("Receiving"); + // final amount = find.text("1.00000000 FIRO"); + // + // expect(title, findsOneWidget); + // expect(amount, findsOneWidget); + // + // await tester.pumpAndSettle(Duration(seconds: 2)); + // + // verify(mockLocaleService.addListener(any)).called(1); + // + // verify(mockPrefs.currency).called(1); + // verify(mockPriceService.getPrice(Coin.firo)).called(1); + // verify(wallet.coin.ticker).called(1); + // + // verify(mockLocaleService.locale).called(1); + // + // verifyNoMoreInteractions(mockManager); + // verifyNoMoreInteractions(mockLocaleService); + // }); + // + // testWidgets("Tap gesture", (tester) async { + // final mockManager = MockManager(); + // final mockLocaleService = MockLocaleService(); + // final wallets = MockWallets(); + // final mockPrefs = MockPrefs(); + // final mockPriceService = MockPriceService(); + // final navigator = mockingjay.MockNavigator(); + // + // final tx = Transaction( + // txid: "some txid", + // confirmedStatus: false, + // timestamp: 1648595998, + // txType: "Received", + // amount: 100000000, + // aliens: [], + // worthNow: "0.01", + // worthAtBlockTimestamp: "0.01", + // fees: 3794, + // inputSize: 1, + // outputSize: 1, + // inputs: [], + // outputs: [], + // address: "", + // height: 250, + // subType: "", + // confirmations: 10, + // ); + // + // final CoinServiceAPI wallet = MockFiroWallet(); + // + // when(wallet.coin.ticker).thenAnswer((_) => "FIRO"); + // when(mockLocaleService.locale).thenAnswer((_) => "en_US"); + // when(mockPrefs.currency).thenAnswer((_) => "USD"); + // when(mockPrefs.externalCalls).thenAnswer((_) => true); + // when(mockPriceService.getPrice(Coin.firo)) + // .thenAnswer((realInvocation) => Tuple2(Decimal.ten, 0.00)); + // + // when(wallet.coin).thenAnswer((_) => Coin.firo); + // + // when(wallets.getManager("wallet id")) + // .thenAnswer((realInvocation) => Manager(wallet)); + // + // mockingjay + // .when(() => navigator.pushNamed("/transactionDetails", + // arguments: Tuple3(tx, Coin.firo, "wallet id"))) + // .thenAnswer((_) async => {}); + // + // await tester.pumpWidget( + // ProviderScope( + // overrides: [ + // walletsChangeNotifierProvider.overrideWithValue(wallets), + // localeServiceChangeNotifierProvider + // .overrideWithValue(mockLocaleService), + // prefsChangeNotifierProvider.overrideWithValue(mockPrefs), + // priceAnd24hChangeNotifierProvider.overrideWithValue(mockPriceService) + // ], + // child: MaterialApp( + // theme: ThemeData( + // extensions: [ + // StackColors.fromStackColorTheme(LightColors()), + // ], + // ), + // home: mockingjay.MockNavigatorProvider( + // navigator: navigator, + // child: TransactionCard(transaction: tx, walletId: "wallet id")), + // ), + // ), + // ); + // + // expect(find.byType(GestureDetector), findsOneWidget); + // + // await tester.tap(find.byType(GestureDetector)); + // await tester.pump(); + // + // verify(mockLocaleService.addListener(any)).called(1); + // + // verify(mockPrefs.currency).called(1); + // verify(mockLocaleService.locale).called(1); + // verify(wallet.coin.ticker).called(1); + // + // verifyNoMoreInteractions(wallet); + // verifyNoMoreInteractions(mockLocaleService); + // + // mockingjay + // .verify(() => navigator.pushNamed("/transactionDetails", + // arguments: Tuple3(tx, Coin.firo, "wallet id"))) + // .called(1); + // }); } From 1d47832718addb4621cbabeb3fe53a70a26348c6 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Tue, 1 Nov 2022 16:39:47 -0600 Subject: [PATCH 093/426] WIP: needs popup for stack experience and debug info --- .../home/settings_menu/advanced_settings.dart | 153 ++++++++++++++++-- 1 file changed, 140 insertions(+), 13 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/advanced_settings.dart b/lib/pages_desktop_specific/home/settings_menu/advanced_settings.dart index e470a73aa..e55326527 100644 --- a/lib/pages_desktop_specific/home/settings_menu/advanced_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/advanced_settings.dart @@ -2,11 +2,16 @@ import 'package:flutter/material.dart'; import 'package:flutter/src/widgets/framework.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages/stack_privacy_calls.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/rounded_white_container.dart'; +import '../../../pages/settings_views/global_settings_view/advanced_views/debug_view.dart'; +import '../../../providers/global/prefs_provider.dart'; +import '../../../widgets/custom_buttons/draggable_switch_button.dart'; + class AdvancedSettings extends ConsumerStatefulWidget { const AdvancedSettings({Key? key}) : super(key: key); @@ -58,16 +63,106 @@ class _AdvancedSettings extends ConsumerState<AdvancedSettings> { ), ), ), - ], - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: const [ - Padding( - padding: EdgeInsets.all( - 10, + const Padding( + padding: EdgeInsets.all(10.0), + child: Divider( + thickness: 0.5, + ), + ), + Padding( + padding: const EdgeInsets.all(10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Toggle testnet coins", + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: ref.watch( + prefsChangeNotifierProvider + .select((value) => value.showTestNetCoins), + ), + onValueChanged: (newValue) { + ref + .read(prefsChangeNotifierProvider) + .showTestNetCoins = newValue; + }, + ), + ), + ], + ), + ), + const Padding( + padding: EdgeInsets.all(10.0), + child: Divider( + thickness: 0.5, + ), + ), + + /// TODO: Make a dialog popup + Padding( + padding: const EdgeInsets.all(10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Stack Experience", + style: + STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark), + textAlign: TextAlign.left, + ), + Text( + "Easy Crypto", + style: STextStyles.desktopTextExtraExtraSmall( + context), + ), + ], + ), + const StackPrivacyButton(), + ], + ), + ), + const Padding( + padding: EdgeInsets.all(10.0), + child: Divider( + thickness: 0.5, + ), + ), + + /// TODO: Make a dialog popup + Padding( + padding: const EdgeInsets.all(10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Debug info", + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark), + textAlign: TextAlign.left, + ), + ShowLogsButton(), + ], ), - child: ShowLogsButton(), ), ], ), @@ -80,6 +175,35 @@ class _AdvancedSettings extends ConsumerState<AdvancedSettings> { } } +class StackPrivacyButton extends ConsumerWidget { + const StackPrivacyButton({ + Key? key, + }) : super(key: key); + @override + Widget build(BuildContext context, WidgetRef ref) { + return SizedBox( + width: 84, + height: 37, + child: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + onPressed: () { + Navigator.of(context).pushNamed( + StackPrivacyCalls.routeName, + arguments: false, + ); + }, + child: Text( + "Change", + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith(color: Colors.white), + ), + ), + ); + } +} + class ShowLogsButton extends ConsumerWidget { const ShowLogsButton({ Key? key, @@ -87,16 +211,19 @@ class ShowLogsButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return SizedBox( - width: 200, - height: 48, + width: 101, + height: 37, child: TextButton( style: Theme.of(context) .extension<StackColors>()! .getPrimaryEnabledButtonColor(context), - onPressed: () {}, + onPressed: () { + Navigator.of(context).pushNamed(DebugView.routeName); + }, child: Text( "Show logs", - style: STextStyles.button(context), + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith(color: Colors.white), ), ), ); From f9eda14752affdc2856a57f0bff139ce922e46be Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Tue, 1 Nov 2022 16:59:37 -0600 Subject: [PATCH 094/426] v1.5.12 build 84 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index ea2fbaa6d..c0aec3eaf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.5.11+83 +version: 1.5.12+84 environment: sdk: ">=2.17.0 <3.0.0" From fc25ea2a244cd633f3ad31e96f24e3631cf452c7 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 2 Nov 2022 08:49:19 -0600 Subject: [PATCH 095/426] fix previous failing widget tests --- lib/widgets/transaction_card.dart | 4 +- test/widget_tests/address_book_card_test.dart | 107 ++- test/widget_tests/transaction_card_test.dart | 742 +++++++++--------- 3 files changed, 450 insertions(+), 403 deletions(-) diff --git a/lib/widgets/transaction_card.dart b/lib/widgets/transaction_card.dart index 8867e34f6..15dcf2b4d 100644 --- a/lib/widgets/transaction_card.dart +++ b/lib/widgets/transaction_card.dart @@ -102,15 +102,13 @@ class _TransactionCardState extends ConsumerState<TransactionCard> { .select((value) => value.getPrice(coin))) .item1; - late final String prefix; + String prefix = ""; if (Util.isDesktop) { if (_transaction.txType == "Sent") { prefix = "-"; } else if (_transaction.txType == "Received") { prefix = "+"; } - } else { - prefix = ""; } return Material( diff --git a/test/widget_tests/address_book_card_test.dart b/test/widget_tests/address_book_card_test.dart index 8c8c44abd..07b1387df 100644 --- a/test/widget_tests/address_book_card_test.dart +++ b/test/widget_tests/address_book_card_test.dart @@ -1,6 +1,22 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; +import 'package:stackwallet/models/contact.dart'; +import 'package:stackwallet/models/contact_address_entry.dart'; +import 'package:stackwallet/pages/address_book_views/subviews/contact_popup.dart'; +import 'package:stackwallet/providers/global/address_book_service_provider.dart'; import 'package:stackwallet/services/address_book_service.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/theme/light_colors.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/address_book_card.dart'; + +import 'address_book_card_test.mocks.dart'; class MockedFunctions extends Mock { void showDialog(); @@ -8,46 +24,53 @@ class MockedFunctions extends Mock { @GenerateMocks([AddressBookService]) void main() { - // testWidgets('test returns Contact Address Entry', (widgetTester) async { - // final service = MockAddressBookService(); - // - // when(service.getContactById("default")) - // .thenAnswer((realInvocation) => Contact( - // name: "John Doe", - // addresses: [ - // const ContactAddressEntry( - // coin: Coin.bitcoincash, - // address: "some bch address", - // label: "Bills") - // ], - // isFavorite: true)); - // - // await widgetTester.pumpWidget( - // ProviderScope( - // overrides: [ - // addressBookServiceProvider.overrideWithValue( - // service, - // ), - // ], - // child: MaterialApp( - // theme: ThemeData( - // extensions: [ - // StackColors.fromStackColorTheme( - // LightColors(), - // ), - // ], - // ), - // home: const AddressBookCard( - // contactId: "default", - // ), - // ), - // ), - // ); - // - // expect(find.text("John Doe"), findsOneWidget); - // expect(find.text("BCH"), findsOneWidget); - // expect(find.text(Coin.bitcoincash.ticker), findsOneWidget); - // - // await widgetTester.tap(find.byType(RawMaterialButton)); - // }); + testWidgets('test returns Contact Address Entry', (widgetTester) async { + final service = MockAddressBookService(); + + when(service.getContactById("default")).thenAnswer( + (realInvocation) => Contact( + name: "John Doe", + addresses: [ + const ContactAddressEntry( + coin: Coin.bitcoincash, + address: "some bch address", + label: "Bills") + ], + isFavorite: true, + ), + ); + + await widgetTester.pumpWidget( + ProviderScope( + overrides: [ + addressBookServiceProvider.overrideWithValue( + service, + ), + ], + child: MaterialApp( + theme: ThemeData( + extensions: [ + StackColors.fromStackColorTheme( + LightColors(), + ), + ], + ), + home: const AddressBookCard( + contactId: "default", + ), + ), + ), + ); + + expect(find.text("John Doe"), findsOneWidget); + expect(find.text("BCH"), findsOneWidget); + expect(find.text(Coin.bitcoincash.ticker), findsOneWidget); + + if (Platform.isIOS || Platform.isAndroid) { + await widgetTester.tap(find.byType(RawMaterialButton)); + expect(find.byType(ContactPopUp), findsOneWidget); + } else if (Util.isDesktop) { + expect(find.byType(RawMaterialButton), findsNothing); + } + }); } diff --git a/test/widget_tests/transaction_card_test.dart b/test/widget_tests/transaction_card_test.dart index 6c36fce8d..f28c5f81d 100644 --- a/test/widget_tests/transaction_card_test.dart +++ b/test/widget_tests/transaction_card_test.dart @@ -1,4 +1,14 @@ +import 'package:decimal/decimal.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_feather_icons/flutter_feather_icons.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockingjay/mockingjay.dart' as mockingjay; import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:stackwallet/models/models.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; +import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/coins/coin_service.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/manager.dart'; @@ -6,7 +16,15 @@ import 'package:stackwallet/services/locale_service.dart'; import 'package:stackwallet/services/notes_service.dart'; import 'package:stackwallet/services/price_service.dart'; import 'package:stackwallet/services/wallets.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/prefs.dart'; +import 'package:stackwallet/utilities/theme/light_colors.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/transaction_card.dart'; +import 'package:tuple/tuple.dart'; + +import 'transaction_card_test.mocks.dart'; @GenerateMocks([ Wallets, @@ -19,362 +37,370 @@ import 'package:stackwallet/utilities/prefs.dart'; NotesService ], customMocks: []) void main() { - // TestWidgetsFlutterBinding.ensureInitialized(); - // testWidgets("Sent confirmed tx displays correctly", (tester) async { - // final mockManager = MockManager(); - // final mockLocaleService = MockLocaleService(); - // final wallets = MockWallets(); - // final mockPrefs = MockPrefs(); - // final mockPriceService = MockPriceService(); - // - // final tx = Transaction( - // txid: "some txid", - // confirmedStatus: true, - // timestamp: 1648595998, - // txType: "Sent", - // amount: 100000000, - // aliens: [], - // worthNow: "0.01", - // worthAtBlockTimestamp: "0.01", - // fees: 3794, - // inputSize: 1, - // outputSize: 1, - // inputs: [], - // outputs: [], - // address: "", - // height: 450123, - // subType: "", - // confirmations: 10, - // isCancelled: false); - // - // final CoinServiceAPI wallet = MockFiroWallet(); - // - // when(wallet.coin.ticker).thenAnswer((_) => "FIRO"); - // when(mockLocaleService.locale).thenAnswer((_) => "en_US"); - // when(mockPrefs.currency).thenAnswer((_) => "USD"); - // when(mockPrefs.externalCalls).thenAnswer((_) => true); - // when(mockPriceService.getPrice(Coin.firo)) - // .thenAnswer((realInvocation) => Tuple2(Decimal.ten, 0.00)); - // - // when(wallet.coin).thenAnswer((_) => Coin.firo); - // - // when(wallets.getManager("wallet-id")) - // .thenAnswer((realInvocation) => Manager(wallet)); - // // - // await tester.pumpWidget( - // ProviderScope( - // overrides: [ - // walletsChangeNotifierProvider.overrideWithValue(wallets), - // localeServiceChangeNotifierProvider - // .overrideWithValue(mockLocaleService), - // prefsChangeNotifierProvider.overrideWithValue(mockPrefs), - // priceAnd24hChangeNotifierProvider.overrideWithValue(mockPriceService) - // ], - // child: MaterialApp( - // theme: ThemeData( - // extensions: [ - // StackColors.fromStackColorTheme( - // LightColors(), - // ), - // ], - // ), - // home: TransactionCard(transaction: tx, walletId: "wallet-id"), - // ), - // ), - // ); - // - // // - // final title = find.text("Sent"); - // // final price1 = find.text("0.00 USD"); - // final amount = find.text("1.00000000 FIRO"); - // - // final icon = find.byIcon(FeatherIcons.arrowUp); - // - // expect(title, findsOneWidget); - // // expect(price1, findsOneWidget); - // expect(amount, findsOneWidget); - // // expect(icon, findsOneWidget); - // // - // await tester.pumpAndSettle(Duration(seconds: 2)); - // // - // // final price2 = find.text("\$10.00"); - // // expect(price2, findsOneWidget); - // // - // // verify(mockManager.addListener(any)).called(1); - // verify(mockLocaleService.addListener(any)).called(1); - // - // verify(mockPrefs.currency).called(1); - // verify(mockPriceService.getPrice(Coin.firo)).called(1); - // verify(wallet.coin.ticker).called(1); - // - // verify(mockLocaleService.locale).called(1); - // - // verifyNoMoreInteractions(mockManager); - // verifyNoMoreInteractions(mockLocaleService); - // }); - // - // testWidgets("Anonymized confirmed tx displays correctly", (tester) async { - // final mockManager = MockManager(); - // final mockLocaleService = MockLocaleService(); - // final wallets = MockWallets(); - // final mockPrefs = MockPrefs(); - // final mockPriceService = MockPriceService(); - // - // final tx = Transaction( - // txid: "some txid", - // confirmedStatus: true, - // timestamp: 1648595998, - // txType: "Anonymized", - // amount: 100000000, - // aliens: [], - // worthNow: "0.01", - // worthAtBlockTimestamp: "0.01", - // fees: 3794, - // inputSize: 1, - // outputSize: 1, - // inputs: [], - // outputs: [], - // address: "", - // height: 450123, - // subType: "mint", - // confirmations: 10, - // isCancelled: false); - // - // final CoinServiceAPI wallet = MockFiroWallet(); - // - // when(wallet.coin.ticker).thenAnswer((_) => "FIRO"); - // when(mockLocaleService.locale).thenAnswer((_) => "en_US"); - // when(mockPrefs.currency).thenAnswer((_) => "USD"); - // when(mockPrefs.externalCalls).thenAnswer((_) => true); - // when(mockPriceService.getPrice(Coin.firo)) - // .thenAnswer((realInvocation) => Tuple2(Decimal.ten, 0.00)); - // - // when(wallet.coin).thenAnswer((_) => Coin.firo); - // - // when(wallets.getManager("wallet-id")) - // .thenAnswer((realInvocation) => Manager(wallet)); - // // - // await tester.pumpWidget( - // ProviderScope( - // overrides: [ - // walletsChangeNotifierProvider.overrideWithValue(wallets), - // localeServiceChangeNotifierProvider - // .overrideWithValue(mockLocaleService), - // prefsChangeNotifierProvider.overrideWithValue(mockPrefs), - // priceAnd24hChangeNotifierProvider.overrideWithValue(mockPriceService) - // ], - // child: MaterialApp( - // theme: ThemeData( - // extensions: [ - // StackColors.fromStackColorTheme( - // LightColors(), - // ), - // ], - // ), - // home: TransactionCard(transaction: tx, walletId: "wallet-id"), - // ), - // ), - // ); - // - // // - // final title = find.text("Anonymized"); - // // final price1 = find.text("0.00 USD"); - // final amount = find.text("1.00000000 FIRO"); - // - // final icon = find.byIcon(FeatherIcons.arrowUp); - // - // expect(title, findsOneWidget); - // // expect(price1, findsOneWidget); - // expect(amount, findsOneWidget); - // // expect(icon, findsOneWidget); - // // - // await tester.pumpAndSettle(Duration(seconds: 2)); - // // - // // final price2 = find.text("\$10.00"); - // // expect(price2, findsOneWidget); - // // - // // verify(mockManager.addListener(any)).called(1); - // verify(mockLocaleService.addListener(any)).called(1); - // - // verify(mockPrefs.currency).called(1); - // verify(mockPriceService.getPrice(Coin.firo)).called(1); - // verify(wallet.coin.ticker).called(1); - // - // verify(mockLocaleService.locale).called(1); - // - // verifyNoMoreInteractions(mockManager); - // verifyNoMoreInteractions(mockLocaleService); - // }); - // - // testWidgets("Received unconfirmed tx displays correctly", (tester) async { - // final mockManager = MockManager(); - // final mockLocaleService = MockLocaleService(); - // final wallets = MockWallets(); - // final mockPrefs = MockPrefs(); - // final mockPriceService = MockPriceService(); - // - // final tx = Transaction( - // txid: "some txid", - // confirmedStatus: false, - // timestamp: 1648595998, - // txType: "Received", - // amount: 100000000, - // aliens: [], - // worthNow: "0.01", - // worthAtBlockTimestamp: "0.01", - // fees: 3794, - // inputSize: 1, - // outputSize: 1, - // inputs: [], - // outputs: [], - // address: "", - // height: 0, - // subType: "", - // confirmations: 0, - // ); - // - // final CoinServiceAPI wallet = MockFiroWallet(); - // - // when(wallet.coin.ticker).thenAnswer((_) => "FIRO"); - // when(mockLocaleService.locale).thenAnswer((_) => "en_US"); - // when(mockPrefs.currency).thenAnswer((_) => "USD"); - // when(mockPrefs.externalCalls).thenAnswer((_) => true); - // when(mockPriceService.getPrice(Coin.firo)) - // .thenAnswer((realInvocation) => Tuple2(Decimal.ten, 0.00)); - // - // when(wallet.coin).thenAnswer((_) => Coin.firo); - // - // when(wallets.getManager("wallet-id")) - // .thenAnswer((realInvocation) => Manager(wallet)); - // - // await tester.pumpWidget( - // ProviderScope( - // overrides: [ - // walletsChangeNotifierProvider.overrideWithValue(wallets), - // localeServiceChangeNotifierProvider - // .overrideWithValue(mockLocaleService), - // prefsChangeNotifierProvider.overrideWithValue(mockPrefs), - // priceAnd24hChangeNotifierProvider.overrideWithValue(mockPriceService) - // ], - // child: MaterialApp( - // theme: ThemeData( - // extensions: [ - // StackColors.fromStackColorTheme( - // LightColors(), - // ), - // ], - // ), - // home: TransactionCard(transaction: tx, walletId: "wallet-id"), - // ), - // ), - // ); - // - // final title = find.text("Receiving"); - // final amount = find.text("1.00000000 FIRO"); - // - // expect(title, findsOneWidget); - // expect(amount, findsOneWidget); - // - // await tester.pumpAndSettle(Duration(seconds: 2)); - // - // verify(mockLocaleService.addListener(any)).called(1); - // - // verify(mockPrefs.currency).called(1); - // verify(mockPriceService.getPrice(Coin.firo)).called(1); - // verify(wallet.coin.ticker).called(1); - // - // verify(mockLocaleService.locale).called(1); - // - // verifyNoMoreInteractions(mockManager); - // verifyNoMoreInteractions(mockLocaleService); - // }); - // - // testWidgets("Tap gesture", (tester) async { - // final mockManager = MockManager(); - // final mockLocaleService = MockLocaleService(); - // final wallets = MockWallets(); - // final mockPrefs = MockPrefs(); - // final mockPriceService = MockPriceService(); - // final navigator = mockingjay.MockNavigator(); - // - // final tx = Transaction( - // txid: "some txid", - // confirmedStatus: false, - // timestamp: 1648595998, - // txType: "Received", - // amount: 100000000, - // aliens: [], - // worthNow: "0.01", - // worthAtBlockTimestamp: "0.01", - // fees: 3794, - // inputSize: 1, - // outputSize: 1, - // inputs: [], - // outputs: [], - // address: "", - // height: 250, - // subType: "", - // confirmations: 10, - // ); - // - // final CoinServiceAPI wallet = MockFiroWallet(); - // - // when(wallet.coin.ticker).thenAnswer((_) => "FIRO"); - // when(mockLocaleService.locale).thenAnswer((_) => "en_US"); - // when(mockPrefs.currency).thenAnswer((_) => "USD"); - // when(mockPrefs.externalCalls).thenAnswer((_) => true); - // when(mockPriceService.getPrice(Coin.firo)) - // .thenAnswer((realInvocation) => Tuple2(Decimal.ten, 0.00)); - // - // when(wallet.coin).thenAnswer((_) => Coin.firo); - // - // when(wallets.getManager("wallet id")) - // .thenAnswer((realInvocation) => Manager(wallet)); - // - // mockingjay - // .when(() => navigator.pushNamed("/transactionDetails", - // arguments: Tuple3(tx, Coin.firo, "wallet id"))) - // .thenAnswer((_) async => {}); - // - // await tester.pumpWidget( - // ProviderScope( - // overrides: [ - // walletsChangeNotifierProvider.overrideWithValue(wallets), - // localeServiceChangeNotifierProvider - // .overrideWithValue(mockLocaleService), - // prefsChangeNotifierProvider.overrideWithValue(mockPrefs), - // priceAnd24hChangeNotifierProvider.overrideWithValue(mockPriceService) - // ], - // child: MaterialApp( - // theme: ThemeData( - // extensions: [ - // StackColors.fromStackColorTheme(LightColors()), - // ], - // ), - // home: mockingjay.MockNavigatorProvider( - // navigator: navigator, - // child: TransactionCard(transaction: tx, walletId: "wallet id")), - // ), - // ), - // ); - // - // expect(find.byType(GestureDetector), findsOneWidget); - // - // await tester.tap(find.byType(GestureDetector)); - // await tester.pump(); - // - // verify(mockLocaleService.addListener(any)).called(1); - // - // verify(mockPrefs.currency).called(1); - // verify(mockLocaleService.locale).called(1); - // verify(wallet.coin.ticker).called(1); - // - // verifyNoMoreInteractions(wallet); - // verifyNoMoreInteractions(mockLocaleService); - // - // mockingjay - // .verify(() => navigator.pushNamed("/transactionDetails", - // arguments: Tuple3(tx, Coin.firo, "wallet id"))) - // .called(1); - // }); + TestWidgetsFlutterBinding.ensureInitialized(); + testWidgets("Sent confirmed tx displays correctly", (tester) async { + final mockManager = MockManager(); + final mockLocaleService = MockLocaleService(); + final wallets = MockWallets(); + final mockPrefs = MockPrefs(); + final mockPriceService = MockPriceService(); + + final tx = Transaction( + txid: "some txid", + confirmedStatus: true, + timestamp: 1648595998, + txType: "Sent", + amount: 100000000, + aliens: [], + worthNow: "0.01", + worthAtBlockTimestamp: "0.01", + fees: 3794, + inputSize: 1, + outputSize: 1, + inputs: [], + outputs: [], + address: "", + height: 450123, + subType: "", + confirmations: 10, + isCancelled: false); + + final CoinServiceAPI wallet = MockFiroWallet(); + + when(wallet.coin.ticker).thenAnswer((_) => "FIRO"); + when(mockLocaleService.locale).thenAnswer((_) => "en_US"); + when(mockPrefs.currency).thenAnswer((_) => "USD"); + when(mockPrefs.externalCalls).thenAnswer((_) => true); + when(mockPriceService.getPrice(Coin.firo)) + .thenAnswer((realInvocation) => Tuple2(Decimal.ten, 0.00)); + + when(wallet.coin).thenAnswer((_) => Coin.firo); + + when(wallets.getManager("wallet-id")) + .thenAnswer((realInvocation) => Manager(wallet)); + // + await tester.pumpWidget( + ProviderScope( + overrides: [ + walletsChangeNotifierProvider.overrideWithValue(wallets), + localeServiceChangeNotifierProvider + .overrideWithValue(mockLocaleService), + prefsChangeNotifierProvider.overrideWithValue(mockPrefs), + priceAnd24hChangeNotifierProvider.overrideWithValue(mockPriceService) + ], + child: MaterialApp( + theme: ThemeData( + extensions: [ + StackColors.fromStackColorTheme( + LightColors(), + ), + ], + ), + home: TransactionCard(transaction: tx, walletId: "wallet-id"), + ), + ), + ); + + // + final title = find.text("Sent"); + // final price1 = find.text("0.00 USD"); + final amount = Util.isDesktop + ? find.text("-1.00000000 FIRO") + : find.text("1.00000000 FIRO"); + + final icon = find.byIcon(FeatherIcons.arrowUp); + + expect(title, findsOneWidget); + // expect(price1, findsOneWidget); + expect(amount, findsOneWidget); + // expect(icon, findsOneWidget); + // + await tester.pumpAndSettle(const Duration(seconds: 2)); + // + // final price2 = find.text("\$10.00"); + // expect(price2, findsOneWidget); + // + // verify(mockManager.addListener(any)).called(1); + verify(mockLocaleService.addListener(any)).called(1); + + verify(mockPrefs.currency).called(1); + verify(mockPriceService.getPrice(Coin.firo)).called(1); + verify(wallet.coin.ticker).called(1); + + verify(mockLocaleService.locale).called(1); + + verifyNoMoreInteractions(mockManager); + verifyNoMoreInteractions(mockLocaleService); + }); + + testWidgets("Anonymized confirmed tx displays correctly", (tester) async { + final mockManager = MockManager(); + final mockLocaleService = MockLocaleService(); + final wallets = MockWallets(); + final mockPrefs = MockPrefs(); + final mockPriceService = MockPriceService(); + + final tx = Transaction( + txid: "some txid", + confirmedStatus: true, + timestamp: 1648595998, + txType: "Anonymized", + amount: 100000000, + aliens: [], + worthNow: "0.01", + worthAtBlockTimestamp: "0.01", + fees: 3794, + inputSize: 1, + outputSize: 1, + inputs: [], + outputs: [], + address: "", + height: 450123, + subType: "mint", + confirmations: 10, + isCancelled: false); + + final CoinServiceAPI wallet = MockFiroWallet(); + + when(wallet.coin.ticker).thenAnswer((_) => "FIRO"); + when(mockLocaleService.locale).thenAnswer((_) => "en_US"); + when(mockPrefs.currency).thenAnswer((_) => "USD"); + when(mockPrefs.externalCalls).thenAnswer((_) => true); + when(mockPriceService.getPrice(Coin.firo)) + .thenAnswer((realInvocation) => Tuple2(Decimal.ten, 0.00)); + + when(wallet.coin).thenAnswer((_) => Coin.firo); + + when(wallets.getManager("wallet-id")) + .thenAnswer((realInvocation) => Manager(wallet)); + // + await tester.pumpWidget( + ProviderScope( + overrides: [ + walletsChangeNotifierProvider.overrideWithValue(wallets), + localeServiceChangeNotifierProvider + .overrideWithValue(mockLocaleService), + prefsChangeNotifierProvider.overrideWithValue(mockPrefs), + priceAnd24hChangeNotifierProvider.overrideWithValue(mockPriceService) + ], + child: MaterialApp( + theme: ThemeData( + extensions: [ + StackColors.fromStackColorTheme( + LightColors(), + ), + ], + ), + home: TransactionCard(transaction: tx, walletId: "wallet-id"), + ), + ), + ); + + // + final title = find.text("Anonymized"); + // final price1 = find.text("0.00 USD"); + final amount = find.text("1.00000000 FIRO"); + + final icon = find.byIcon(FeatherIcons.arrowUp); + + expect(title, findsOneWidget); + // expect(price1, findsOneWidget); + expect(amount, findsOneWidget); + // expect(icon, findsOneWidget); + // + await tester.pumpAndSettle(const Duration(seconds: 2)); + // + // final price2 = find.text("\$10.00"); + // expect(price2, findsOneWidget); + // + // verify(mockManager.addListener(any)).called(1); + verify(mockLocaleService.addListener(any)).called(1); + + verify(mockPrefs.currency).called(1); + verify(mockPriceService.getPrice(Coin.firo)).called(1); + verify(wallet.coin.ticker).called(1); + + verify(mockLocaleService.locale).called(1); + + verifyNoMoreInteractions(mockManager); + verifyNoMoreInteractions(mockLocaleService); + }); + + testWidgets("Received unconfirmed tx displays correctly", (tester) async { + final mockManager = MockManager(); + final mockLocaleService = MockLocaleService(); + final wallets = MockWallets(); + final mockPrefs = MockPrefs(); + final mockPriceService = MockPriceService(); + + final tx = Transaction( + txid: "some txid", + confirmedStatus: false, + timestamp: 1648595998, + txType: "Received", + amount: 100000000, + aliens: [], + worthNow: "0.01", + worthAtBlockTimestamp: "0.01", + fees: 3794, + inputSize: 1, + outputSize: 1, + inputs: [], + outputs: [], + address: "", + height: 0, + subType: "", + confirmations: 0, + ); + + final CoinServiceAPI wallet = MockFiroWallet(); + + when(wallet.coin.ticker).thenAnswer((_) => "FIRO"); + when(mockLocaleService.locale).thenAnswer((_) => "en_US"); + when(mockPrefs.currency).thenAnswer((_) => "USD"); + when(mockPrefs.externalCalls).thenAnswer((_) => true); + when(mockPriceService.getPrice(Coin.firo)) + .thenAnswer((realInvocation) => Tuple2(Decimal.ten, 0.00)); + + when(wallet.coin).thenAnswer((_) => Coin.firo); + + when(wallets.getManager("wallet-id")) + .thenAnswer((realInvocation) => Manager(wallet)); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + walletsChangeNotifierProvider.overrideWithValue(wallets), + localeServiceChangeNotifierProvider + .overrideWithValue(mockLocaleService), + prefsChangeNotifierProvider.overrideWithValue(mockPrefs), + priceAnd24hChangeNotifierProvider.overrideWithValue(mockPriceService) + ], + child: MaterialApp( + theme: ThemeData( + extensions: [ + StackColors.fromStackColorTheme( + LightColors(), + ), + ], + ), + home: TransactionCard(transaction: tx, walletId: "wallet-id"), + ), + ), + ); + + final title = find.text("Receiving"); + final amount = Util.isDesktop + ? find.text("+1.00000000 FIRO") + : find.text("1.00000000 FIRO"); + + expect(title, findsOneWidget); + expect(amount, findsOneWidget); + + await tester.pumpAndSettle(const Duration(seconds: 2)); + + verify(mockLocaleService.addListener(any)).called(1); + + verify(mockPrefs.currency).called(1); + verify(mockPriceService.getPrice(Coin.firo)).called(1); + verify(wallet.coin.ticker).called(1); + + verify(mockLocaleService.locale).called(1); + + verifyNoMoreInteractions(mockManager); + verifyNoMoreInteractions(mockLocaleService); + }); + + testWidgets("Tap gesture", (tester) async { + final mockManager = MockManager(); + final mockLocaleService = MockLocaleService(); + final wallets = MockWallets(); + final mockPrefs = MockPrefs(); + final mockPriceService = MockPriceService(); + final navigator = mockingjay.MockNavigator(); + + final tx = Transaction( + txid: "some txid", + confirmedStatus: false, + timestamp: 1648595998, + txType: "Received", + amount: 100000000, + aliens: [], + worthNow: "0.01", + worthAtBlockTimestamp: "0.01", + fees: 3794, + inputSize: 1, + outputSize: 1, + inputs: [], + outputs: [], + address: "", + height: 250, + subType: "", + confirmations: 10, + ); + + final CoinServiceAPI wallet = MockFiroWallet(); + + when(wallet.coin.ticker).thenAnswer((_) => "FIRO"); + when(mockLocaleService.locale).thenAnswer((_) => "en_US"); + when(mockPrefs.currency).thenAnswer((_) => "USD"); + when(mockPrefs.externalCalls).thenAnswer((_) => true); + when(mockPriceService.getPrice(Coin.firo)) + .thenAnswer((realInvocation) => Tuple2(Decimal.ten, 0.00)); + + when(wallet.coin).thenAnswer((_) => Coin.firo); + + when(wallets.getManager("wallet id")) + .thenAnswer((realInvocation) => Manager(wallet)); + + mockingjay + .when(() => navigator.pushNamed("/transactionDetails", + arguments: Tuple3(tx, Coin.firo, "wallet id"))) + .thenAnswer((_) async => {}); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + walletsChangeNotifierProvider.overrideWithValue(wallets), + localeServiceChangeNotifierProvider + .overrideWithValue(mockLocaleService), + prefsChangeNotifierProvider.overrideWithValue(mockPrefs), + priceAnd24hChangeNotifierProvider.overrideWithValue(mockPriceService) + ], + child: MaterialApp( + theme: ThemeData( + extensions: [ + StackColors.fromStackColorTheme(LightColors()), + ], + ), + home: mockingjay.MockNavigatorProvider( + navigator: navigator, + child: TransactionCard(transaction: tx, walletId: "wallet id")), + ), + ), + ); + + expect(find.byType(GestureDetector), findsOneWidget); + + await tester.tap(find.byType(GestureDetector)); + await tester.pump(); + + verify(mockLocaleService.addListener(any)).called(1); + + verify(mockPrefs.currency).called(2); + verify(mockLocaleService.locale).called(4); + verify(wallet.coin.ticker).called(1); + + verifyNoMoreInteractions(wallet); + verifyNoMoreInteractions(mockLocaleService); + + if (Util.isDesktop) { + expect(find.byType(TransactionDetailsView), findsOneWidget); + } else { + mockingjay + .verify(() => navigator.pushNamed("/transactionDetails", + arguments: Tuple3(tx, Coin.firo, "wallet id"))) + .called(1); + } + }); } From 712b33833b39fe603bd11b9f333549ca2f9283a2 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Wed, 2 Nov 2022 09:02:53 -0600 Subject: [PATCH 096/426] stack experience dialog for desktop advanced settings --- .../advanced_settings.dart | 159 ++++--- .../advanced_settings/debug_info_dialog.dart | 0 .../stack_privacy_dialog.dart | 437 ++++++++++++++++++ 3 files changed, 531 insertions(+), 65 deletions(-) rename lib/pages_desktop_specific/home/{settings_menu => advanced_settings}/advanced_settings.dart (59%) create mode 100644 lib/pages_desktop_specific/home/advanced_settings/debug_info_dialog.dart create mode 100644 lib/pages_desktop_specific/home/advanced_settings/stack_privacy_dialog.dart diff --git a/lib/pages_desktop_specific/home/settings_menu/advanced_settings.dart b/lib/pages_desktop_specific/home/advanced_settings/advanced_settings.dart similarity index 59% rename from lib/pages_desktop_specific/home/settings_menu/advanced_settings.dart rename to lib/pages_desktop_specific/home/advanced_settings/advanced_settings.dart index e55326527..85eca20c0 100644 --- a/lib/pages_desktop_specific/home/settings_menu/advanced_settings.dart +++ b/lib/pages_desktop_specific/home/advanced_settings/advanced_settings.dart @@ -2,16 +2,15 @@ import 'package:flutter/material.dart'; import 'package:flutter/src/widgets/framework.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/pages/stack_privacy_calls.dart'; +import 'package:stackwallet/pages_desktop_specific/home/advanced_settings/debug_info_dialog.dart'; +import 'package:stackwallet/pages_desktop_specific/home/advanced_settings/stack_privacy_dialog.dart'; +import 'package:stackwallet/providers/global/prefs_provider.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/draggable_switch_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; -import '../../../pages/settings_views/global_settings_view/advanced_views/debug_view.dart'; -import '../../../providers/global/prefs_provider.dart'; -import '../../../widgets/custom_buttons/draggable_switch_button.dart'; - class AdvancedSettings extends ConsumerStatefulWidget { const AdvancedSettings({Key? key}) : super(key: key); @@ -109,63 +108,69 @@ class _AdvancedSettings extends ConsumerState<AdvancedSettings> { ), /// TODO: Make a dialog popup - Padding( - padding: const EdgeInsets.all(10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Stack Experience", - style: - STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark), - textAlign: TextAlign.left, - ), - Text( - "Easy Crypto", - style: STextStyles.desktopTextExtraExtraSmall( - context), - ), - ], - ), - const StackPrivacyButton(), - ], - ), - ), - const Padding( - padding: EdgeInsets.all(10.0), - child: Divider( - thickness: 0.5, - ), - ), - - /// TODO: Make a dialog popup - Padding( - padding: const EdgeInsets.all(10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Debug info", - style: STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark), - textAlign: TextAlign.left, - ), - ShowLogsButton(), - ], - ), - ), + Consumer(builder: (_, ref, __) { + final externalCalls = ref.watch( + prefsChangeNotifierProvider + .select((value) => value.externalCalls), + ); + return Padding( + padding: const EdgeInsets.all(10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Stack Experience", + style: + STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark), + textAlign: TextAlign.left, + ), + Text( + externalCalls ? "Easy crypto" : "Incognito", + style: STextStyles.desktopTextExtraExtraSmall( + context), + ), + ], + ), + const StackPrivacyButton(), + ], + ), + ); + }), ], ), + const Padding( + padding: EdgeInsets.all(10.0), + child: Divider( + thickness: 0.5, + ), + ), + + /// TODO: Make a dialog popup + Padding( + padding: const EdgeInsets.all(10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Debug info", + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark), + textAlign: TextAlign.left, + ), + ShowLogsButton(), + ], + ), + ), ], ), ), @@ -181,6 +186,17 @@ class StackPrivacyButton extends ConsumerWidget { }) : super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { + Future<void> changePrivacySettings() async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return StackPrivacyDialog(); + }, + ); + } + return SizedBox( width: 84, height: 37, @@ -189,10 +205,11 @@ class StackPrivacyButton extends ConsumerWidget { .extension<StackColors>()! .getPrimaryEnabledButtonColor(context), onPressed: () { - Navigator.of(context).pushNamed( - StackPrivacyCalls.routeName, - arguments: false, - ); + // Navigator.of(context).pushNamed( + // StackPrivacyCalls.routeName, + // arguments: false, + // ); + changePrivacySettings(); }, child: Text( "Change", @@ -210,6 +227,17 @@ class ShowLogsButton extends ConsumerWidget { }) : super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { + Future<void> viewDebugLogs() async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return const DebugInfoDialog(); + }, + ); + } + return SizedBox( width: 101, height: 37, @@ -218,7 +246,8 @@ class ShowLogsButton extends ConsumerWidget { .extension<StackColors>()! .getPrimaryEnabledButtonColor(context), onPressed: () { - Navigator.of(context).pushNamed(DebugView.routeName); + // + viewDebugLogs(); }, child: Text( "Show logs", diff --git a/lib/pages_desktop_specific/home/advanced_settings/debug_info_dialog.dart b/lib/pages_desktop_specific/home/advanced_settings/debug_info_dialog.dart new file mode 100644 index 000000000..e69de29bb diff --git a/lib/pages_desktop_specific/home/advanced_settings/stack_privacy_dialog.dart b/lib/pages_desktop_specific/home/advanced_settings/stack_privacy_dialog.dart new file mode 100644 index 000000000..8e385fd37 --- /dev/null +++ b/lib/pages_desktop_specific/home/advanced_settings/stack_privacy_dialog.dart @@ -0,0 +1,437 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/utilities/text_styles.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 '../../../hive/db.dart'; +import '../../../providers/global/prefs_provider.dart'; +import '../../../providers/global/price_provider.dart'; +import '../../../services/exchange/exchange_data_loading_service.dart'; +import '../../../utilities/assets.dart'; +import '../../../utilities/constants.dart'; +import '../../../utilities/theme/stack_colors.dart'; +import '../../../utilities/util.dart'; +import '../../../widgets/rounded_white_container.dart'; + +class StackPrivacyDialog extends ConsumerStatefulWidget { + const StackPrivacyDialog({Key? key}) : super(key: key); + + @override + ConsumerState<StackPrivacyDialog> createState() => _StackPrivacyDialog(); +} + +class _StackPrivacyDialog extends ConsumerState<StackPrivacyDialog> { + late final bool isDesktop; + late bool isEasy; + late bool infoToggle; + + @override + void initState() { + isDesktop = Util.isDesktop; + isEasy = ref.read(prefsChangeNotifierProvider).externalCalls; + infoToggle = isEasy; + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxHeight: 650, + maxWidth: 600, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(32), + child: Text( + "Choose Your Stack Experience", + style: STextStyles.desktopH3(context), + textAlign: TextAlign.center, + ), + ), + const DesktopDialogCloseButton(), + ], + ), + const SizedBox( + height: 35, + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 32), + child: PrivacyToggle( + externalCallsEnabled: isEasy, + onChanged: (externalCalls) { + isEasy = externalCalls; + setState(() { + infoToggle = isEasy; + }); + }, + ), + ), + Padding( + padding: const EdgeInsets.all(32.0), + child: RoundedWhiteContainer( + borderColor: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + child: Center( + child: RichText( + textAlign: TextAlign.left, + text: TextSpan( + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.label(context).copyWith( + fontSize: 12.0, + ), + children: infoToggle + ? [ + const TextSpan( + text: + "Exchange data preloaded for a seamless experience."), + const TextSpan( + text: + "\n\nCoinGecko enabled: (24 hour price change shown in-app, total wallet value shown in USD or other currency)."), + TextSpan( + text: "\n\nRecommended for most crypto users.", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall600( + context) + : TextStyle( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + fontWeight: FontWeight.w600, + ), + ), + ] + : [ + const TextSpan( + text: + "Exchange data not preloaded (slower experience)."), + const TextSpan( + text: + "\n\nCoinGecko disabled (price changes not shown, no wallet value shown in other currencies)."), + TextSpan( + text: + "\n\nRecommended for the privacy conscious.", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall600( + context) + : TextStyle( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ), + ), + // const Spacer(), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + onPressed: () {}, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Save", + onPressed: () { + ref.read(prefsChangeNotifierProvider).externalCalls = + isEasy; + + DB.instance + .put<dynamic>( + boxName: DB.boxNamePrefs, + key: "externalCalls", + value: isEasy) + .then((_) { + if (isEasy) { + unawaited(ExchangeDataLoadingService().loadAll(ref)); + ref + .read(priceAnd24hChangeNotifierProvider) + .start(true); + } + }); + if (isDesktop) { + Navigator.pop(context); + } + }, + ), + ) + ], + ), + ), + ], + ), + ); + } +} + +class PrivacyToggle extends StatefulWidget { + const PrivacyToggle({ + Key? key, + required this.externalCallsEnabled, + this.onChanged, + }) : super(key: key); + + final bool externalCallsEnabled; + final void Function(bool)? onChanged; + + @override + State<PrivacyToggle> createState() => _PrivacyToggleState(); +} + +class _PrivacyToggleState extends State<PrivacyToggle> { + late bool externalCallsEnabled; + + late final bool isDesktop; + + @override + void initState() { + isDesktop = Util.isDesktop; + // initial toggle state + externalCallsEnabled = widget.externalCallsEnabled; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: RawMaterialButton( + elevation: 0, + hoverElevation: 0, + fillColor: Theme.of(context).extension<StackColors>()!.popupBG, + shape: RoundedRectangleBorder( + side: !externalCallsEnabled + ? BorderSide.none + : BorderSide( + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + width: 2, + ), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius * 2, + ), + ), + onPressed: () { + setState(() { + // update toggle state + externalCallsEnabled = true; + }); + // call callback with newly set value + widget.onChanged?.call(externalCallsEnabled); + }, + child: Padding( + padding: const EdgeInsets.all( + 12, + ), + child: Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (isDesktop) + const SizedBox( + height: 10, + ), + SvgPicture.asset( + Assets.svg.personaEasy, + width: 120, + height: 120, + ), + if (isDesktop) + const SizedBox( + height: 12, + ), + Center( + child: Text( + "Easy Crypto", + style: isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.label700(context), + ), + ), + Center( + child: Text( + "Recommended", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.label(context), + ), + ), + if (isDesktop) + const SizedBox( + height: 12, + ), + ], + ), + if (externalCallsEnabled) + Positioned( + top: 4, + right: 4, + child: SvgPicture.asset( + Assets.svg.checkCircle, + width: 20, + height: 20, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + ), + ), + if (!externalCallsEnabled) + Positioned( + top: 4, + right: 4, + child: Container( + width: 20, + height: 20, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(1000), + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + ), + ), + ), + ], + ), + ), + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: RawMaterialButton( + elevation: 0, + hoverElevation: 0, + fillColor: Theme.of(context).extension<StackColors>()!.popupBG, + shape: RoundedRectangleBorder( + side: externalCallsEnabled + ? BorderSide.none + : BorderSide( + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + width: 2, + ), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius * 2, + ), + ), + onPressed: () { + setState(() { + // update toggle state + externalCallsEnabled = false; + }); + // call callback with newly set value + widget.onChanged?.call(externalCallsEnabled); + }, + child: Padding( + padding: const EdgeInsets.all( + 12, + ), + child: Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (isDesktop) + const SizedBox( + height: 10, + ), + SvgPicture.asset( + Assets.svg.personaIncognito, + width: 120, + height: 120, + ), + if (isDesktop) + const SizedBox( + height: 12, + ), + Center( + child: Text( + "Incognito", + style: isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.label700(context), + ), + ), + Center( + child: Text( + "Privacy conscious", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.label(context), + ), + ), + if (isDesktop) + const SizedBox( + height: 12, + ), + ], + ), + if (!externalCallsEnabled) + Positioned( + top: 4, + right: 4, + child: SvgPicture.asset( + Assets.svg.checkCircle, + width: 20, + height: 20, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + ), + ), + if (externalCallsEnabled) + Positioned( + top: 4, + right: 4, + child: Container( + width: 20, + height: 20, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(1000), + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + ), + ), + ), + ], + ), + ), + ), + ), + ], + ); + } +} From fc981ef6e01a1db71823818070207dbecdfc9e23 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 2 Nov 2022 10:01:03 -0600 Subject: [PATCH 097/426] AddressBookAddressChooser search and ui tweaks --- assets/svg/chevron-up.svg | 3 + .../address_book_address_chooser.dart | 131 +++++++++++++++++- .../sub_widgets/contact_list_item.dart | 27 +++- .../wallet_view/sub_widgets/desktop_send.dart | 28 +++- lib/utilities/assets.dart | 1 + lib/widgets/address_book_card.dart | 19 ++- pubspec.yaml | 1 + 7 files changed, 197 insertions(+), 13 deletions(-) create mode 100644 assets/svg/chevron-up.svg diff --git a/assets/svg/chevron-up.svg b/assets/svg/chevron-up.svg new file mode 100644 index 000000000..630f8df69 --- /dev/null +++ b/assets/svg/chevron-up.svg @@ -0,0 +1,3 @@ +<svg width="12" height="7" viewBox="0 0 12 7" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M11 6L6 1L1 6" stroke="#8E9192" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart index 372c86e2f..9f309a08e 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart @@ -1,10 +1,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/models/contact.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/sub_widgets/contact_list_item.dart'; import 'package:stackwallet/providers/global/address_book_service_provider.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/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; class AddressBookAddressChooser extends StatefulWidget { const AddressBookAddressChooser({ @@ -20,6 +28,12 @@ class AddressBookAddressChooser extends StatefulWidget { } class _AddressBookAddressChooserState extends State<AddressBookAddressChooser> { + late final bool isDesktop; + late final TextEditingController _searchController; + late final FocusNode searchFieldFocusNode; + + String _searchTerm = ""; + int _compareContactFavorite(Contact a, Contact b) { if (a.isFavorite && b.isFavorite) { return 0; @@ -43,29 +57,128 @@ class _AddressBookAddressChooserState extends State<AddressBookAddressChooser> { return favorites; } - List<Contact> filter(List<Contact> contacts) { + List<Contact> filter(List<Contact> contacts, String searchTerm) { if (widget.coin != null) { contacts.removeWhere( (e) => e.addresses.where((a) => a.coin == widget.coin!).isEmpty); } + contacts.retainWhere((e) => _matches(searchTerm, e)); + if (contacts.length < 2) { return contacts; } + // redundant due to pullOutFavorites? contacts.sort(_compareContactFavorite); - // TODO: other filtering? - return contacts; } + bool _matches(String term, Contact contact) { + final text = term.toLowerCase(); + if (contact.name.toLowerCase().contains(text)) { + return true; + } + for (int i = 0; i < contact.addresses.length; i++) { + if (contact.addresses[i].label.toLowerCase().contains(text) || + contact.addresses[i].coin.name.toLowerCase().contains(text) || + contact.addresses[i].coin.prettyName.toLowerCase().contains(text) || + contact.addresses[i].coin.ticker.toLowerCase().contains(text) || + contact.addresses[i].address.toLowerCase().contains(text)) { + return true; + } + } + return false; + } + + @override + void initState() { + isDesktop = Util.isDesktop; + searchFieldFocusNode = FocusNode(); + _searchController = TextEditingController(); + super.initState(); + } + + @override + void dispose() { + _searchController.dispose(); + searchFieldFocusNode.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Column( children: [ // search field - const TextField(), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: _searchController, + focusNode: searchFieldFocusNode, + onChanged: (value) { + setState(() { + _searchTerm = value; + }); + }, + style: isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveText, + height: 1.8, + ) + : STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + searchFieldFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + prefixIcon: Padding( + padding: EdgeInsets.symmetric( + horizontal: isDesktop ? 12 : 10, + vertical: isDesktop ? 18 : 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: isDesktop ? 20 : 16, + height: isDesktop ? 20 : 16, + ), + ), + suffixIcon: _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + _searchTerm = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + ), const SizedBox( height: 16, ), @@ -83,7 +196,7 @@ class _AddressBookAddressChooserState extends State<AddressBookAddressChooser> { .select((value) => value.contacts)) .toList(); - contacts = filter(contacts); + contacts = filter(contacts, _searchTerm); final favorites = pullOutFavorites(contacts); @@ -96,6 +209,8 @@ class _AddressBookAddressChooserState extends State<AddressBookAddressChooser> { itemBuilder: (context, index) { if (index == 0) { return Padding( + key: const Key( + "addressBookCAddressChooserFavoritesHeaderItemKey"), padding: const EdgeInsets.only( bottom: 10, ), @@ -108,12 +223,14 @@ class _AddressBookAddressChooserState extends State<AddressBookAddressChooser> { } else if (index <= favorites.length) { final id = favorites[index - 1].id; return ContactListItem( - key: Key("contactCard_${id}_key"), + key: Key("contactContactListItem_${id}_key"), contactId: id, filterByCoin: widget.coin, ); } else if (index == favorites.length + 1) { return Padding( + key: const Key( + "addressBookCAddressChooserAllContactsHeaderItemKey"), padding: const EdgeInsets.symmetric( vertical: 10, ), @@ -126,7 +243,7 @@ class _AddressBookAddressChooserState extends State<AddressBookAddressChooser> { } else { final id = contacts[index - favorites.length - 1].id; return ContactListItem( - key: Key("contactCard_${id}_key"), + key: Key("contactContactListItem_${id}_key"), contactId: id, filterByCoin: widget.coin, ); diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/sub_widgets/contact_list_item.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/sub_widgets/contact_list_item.dart index 593ae2bc4..e030f9882 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/sub_widgets/contact_list_item.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/sub_widgets/contact_list_item.dart @@ -10,7 +10,7 @@ import 'package:stackwallet/widgets/expandable.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart'; -class ContactListItem extends ConsumerWidget { +class ContactListItem extends ConsumerStatefulWidget { const ContactListItem({ Key? key, required this.contactId, @@ -21,7 +21,24 @@ class ContactListItem extends ConsumerWidget { final Coin? filterByCoin; @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState<ContactListItem> createState() => _ContactListItemState(); +} + +class _ContactListItemState extends ConsumerState<ContactListItem> { + late final String contactId; + late final Coin? filterByCoin; + + ExpandableState _state = ExpandableState.collapsed; + + @override + void initState() { + contactId = widget.contactId; + filterByCoin = widget.filterByCoin; + super.initState(); + } + + @override + Widget build(BuildContext context) { final contact = ref.watch(addressBookServiceProvider .select((value) => value.getContactById(contactId))); @@ -29,6 +46,11 @@ class ContactListItem extends ConsumerWidget { padding: const EdgeInsets.all(0), borderColor: Theme.of(context).extension<StackColors>()!.background, child: Expandable( + onExpandChanged: (state) { + setState(() { + _state = state; + }); + }, header: Padding( padding: const EdgeInsets.symmetric( horizontal: 20, @@ -36,6 +58,7 @@ class ContactListItem extends ConsumerWidget { ), child: AddressBookCard( contactId: contactId, + indicatorDown: _state == ExpandableState.expanded, ), ), body: Column( diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index 26f2f806c..710bc8685 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -1190,8 +1190,32 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { builder: (context) => DesktopDialog( maxWidth: 696, maxHeight: 600, - child: AddressBookAddressChooser( - coin: coin, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Address book", + style: + STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: AddressBookAddressChooser( + coin: coin, + ), + ), + ], ), ), ); diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 432ebbec9..78535c19b 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -96,6 +96,7 @@ class _SVG { String get qrcode => "assets/svg/qrcode1.svg"; String get ellipsis => "assets/svg/gear-3.svg"; String get chevronDown => "assets/svg/chevron-down.svg"; + String get chevronUp => "assets/svg/chevron-up.svg"; String get swap => "assets/svg/swap.svg"; String get downloadFolder => "assets/svg/folder-down.svg"; String get lock => "assets/svg/lock-keyhole.svg"; diff --git a/lib/widgets/address_book_card.dart b/lib/widgets/address_book_card.dart index 6a51edbad..cebcf166f 100644 --- a/lib/widgets/address_book_card.dart +++ b/lib/widgets/address_book_card.dart @@ -13,9 +13,14 @@ import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class AddressBookCard extends ConsumerStatefulWidget { - const AddressBookCard({Key? key, required this.contactId}) : super(key: key); + const AddressBookCard({ + Key? key, + required this.contactId, + this.indicatorDown, + }) : super(key: key); final String contactId; + final bool? indicatorDown; @override ConsumerState<AddressBookCard> createState() => _AddressBookCardState(); @@ -122,7 +127,17 @@ class _AddressBookCardState extends ConsumerState<AddressBookCard> { style: STextStyles.label(context), ), ], - ) + ), + if (isDesktop) const Spacer(), + if (isDesktop) + SvgPicture.asset( + widget.indicatorDown == true + ? Assets.svg.chevronDown + : Assets.svg.chevronUp, + width: 10, + height: 5, + color: Theme.of(context).extension<StackColors>()!.textSubtitle2, + ), ], ), builder: (child) => RoundedWhiteContainer( diff --git a/pubspec.yaml b/pubspec.yaml index 7bd53de06..29d25cb84 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -232,6 +232,7 @@ flutter: - assets/svg/gear-3.svg - assets/svg/swap.svg - assets/svg/chevron-down.svg + - assets/svg/chevron-up.svg - assets/svg/lock-keyhole.svg - assets/svg/rotate-exclamation.svg - assets/svg/folder-down.svg From b915b266ae515bcdbc587c2cb337d594c032b0a5 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 2 Nov 2022 10:09:42 -0600 Subject: [PATCH 098/426] imports fix --- .../advanced_settings/advanced_settings.dart | 29 +++++++++---------- .../home/desktop_settings_view.dart | 2 +- lib/route_generator.dart | 2 +- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/lib/pages_desktop_specific/home/advanced_settings/advanced_settings.dart b/lib/pages_desktop_specific/home/advanced_settings/advanced_settings.dart index 85eca20c0..7361d9773 100644 --- a/lib/pages_desktop_specific/home/advanced_settings/advanced_settings.dart +++ b/lib/pages_desktop_specific/home/advanced_settings/advanced_settings.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter/src/widgets/framework.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/pages_desktop_specific/home/advanced_settings/debug_info_dialog.dart'; import 'package:stackwallet/pages_desktop_specific/home/advanced_settings/stack_privacy_dialog.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -167,7 +165,7 @@ class _AdvancedSettings extends ConsumerState<AdvancedSettings> { .textDark), textAlign: TextAlign.left, ), - ShowLogsButton(), + const ShowLogsButton(), ], ), ), @@ -192,7 +190,7 @@ class StackPrivacyButton extends ConsumerWidget { useSafeArea: false, barrierDismissible: true, builder: (context) { - return StackPrivacyDialog(); + return const StackPrivacyDialog(); }, ); } @@ -225,19 +223,20 @@ class ShowLogsButton extends ConsumerWidget { const ShowLogsButton({ Key? key, }) : super(key: key); + + Future<void> viewDebugLogs() async { + // await showDialog<dynamic>( + // context: context, + // useSafeArea: false, + // barrierDismissible: true, + // builder: (context) { + // return const DebugInfoDialog(); + // }, + // ); + } + @override Widget build(BuildContext context, WidgetRef ref) { - Future<void> viewDebugLogs() async { - await showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return const DebugInfoDialog(); - }, - ); - } - return SizedBox( width: 101, height: 37, diff --git a/lib/pages_desktop_specific/home/desktop_settings_view.dart b/lib/pages_desktop_specific/home/desktop_settings_view.dart index 719a37a3a..34cb07f27 100644 --- a/lib/pages_desktop_specific/home/desktop_settings_view.dart +++ b/lib/pages_desktop_specific/home/desktop_settings_view.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/pages_desktop_specific/home/settings_menu/advanced_settings.dart'; +import 'package:stackwallet/pages_desktop_specific/home/advanced_settings/advanced_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/appearance_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/currency_settings.dart'; diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 136dc7a34..613e0d509 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -85,13 +85,13 @@ 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/create_password/create_password_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/advanced_settings/advanced_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_settings_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/qr_code_desktop_popup_content.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart'; -import 'package:stackwallet/pages_desktop_specific/home/settings_menu/advanced_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/appearance_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/currency_settings.dart'; From 2e5a5e35a0c57455831225a1c2bd186a46f66535 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Wed, 2 Nov 2022 10:27:07 -0600 Subject: [PATCH 099/426] import changes --- .../advanced_settings/debug_info_dialog.dart | 0 .../advanced_settings/advanced_settings.dart | 0 .../advanced_settings/debug_info_dialog.dart | 188 ++++++++++++++++++ .../stack_privacy_dialog.dart | 19 +- 4 files changed, 197 insertions(+), 10 deletions(-) delete mode 100644 lib/pages_desktop_specific/home/advanced_settings/debug_info_dialog.dart rename lib/pages_desktop_specific/home/{ => settings_menu}/advanced_settings/advanced_settings.dart (100%) create mode 100644 lib/pages_desktop_specific/home/settings_menu/advanced_settings/debug_info_dialog.dart rename lib/pages_desktop_specific/home/{ => settings_menu}/advanced_settings/stack_privacy_dialog.dart (96%) diff --git a/lib/pages_desktop_specific/home/advanced_settings/debug_info_dialog.dart b/lib/pages_desktop_specific/home/advanced_settings/debug_info_dialog.dart deleted file mode 100644 index e69de29bb..000000000 diff --git a/lib/pages_desktop_specific/home/advanced_settings/advanced_settings.dart b/lib/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart similarity index 100% rename from lib/pages_desktop_specific/home/advanced_settings/advanced_settings.dart rename to lib/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart diff --git a/lib/pages_desktop_specific/home/settings_menu/advanced_settings/debug_info_dialog.dart b/lib/pages_desktop_specific/home/settings_menu/advanced_settings/debug_info_dialog.dart new file mode 100644 index 000000000..342d9180a --- /dev/null +++ b/lib/pages_desktop_specific/home/settings_menu/advanced_settings/debug_info_dialog.dart @@ -0,0 +1,188 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/models/isar/models/log.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/text_styles.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 '../../../utilities/assets.dart'; +// import '../../../utilities/util.dart'; +// import '../../../widgets/icon_widgets/x_icon.dart'; +// import '../../../widgets/stack_text_field.dart'; +// import '../../../widgets/textfield_icon_button.dart'; + +class DebugInfoDialog extends StatefulWidget { + const DebugInfoDialog({Key? key}) : super(key: key); + + @override + State<StatefulWidget> createState() => _DebugInfoDialog(); +} + +class _DebugInfoDialog extends State<DebugInfoDialog> { + final _searchController = TextEditingController(); + final _searchFocusNode = FocusNode(); + + final scrollController = ScrollController(); + + String _searchTerm = ""; + + List<Log> filtered(List<Log> unfiltered, String filter) { + if (filter == "") { + return unfiltered; + } + return unfiltered + .where( + (e) => (e.toString().toLowerCase().contains(filter.toLowerCase()))) + .toList(); + } + + BorderRadius? _borderRadius(int index, int listLength) { + if (index == 0 && listLength == 1) { + return BorderRadius.circular( + Constants.size.circularBorderRadius, + ); + } else if (index == 0) { + return BorderRadius.vertical( + bottom: Radius.circular( + Constants.size.circularBorderRadius, + ), + ); + } else if (index == listLength - 1) { + return BorderRadius.vertical( + top: Radius.circular( + Constants.size.circularBorderRadius, + ), + ); + } + return null; + } + + @override + void initState() { + // ref.read(debugServiceProvider).updateRecentLogs(); + super.initState(); + } + + @override + void dispose() { + _searchController.dispose(); + scrollController.dispose(); + _searchFocusNode.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxHeight: 800, + maxWidth: 600, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(32), + child: Text( + "Debug info", + style: STextStyles.desktopH3(context), + textAlign: TextAlign.center, + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Row( + children: [ + // ClipRRect( + // borderRadius: BorderRadius.circular( + // Constants.size.circularBorderRadius, + // ), + // child: TextField( + // key: const Key("desktopSettingDebugInfo"), + // autocorrect: Util.isDesktop ? false : true, + // enableSuggestions: Util.isDesktop ? false : true, + // controller: _searchController, + // focusNode: _searchFocusNode, + // // onChanged: (newString) { + // // setState(() => _searchTerm = newString); + // // }, + // style: STextStyles.field(context), + // decoration: standardInputDecoration( + // "Search", + // _searchFocusNode, + // context, + // ).copyWith( + // prefixIcon: Padding( + // padding: const EdgeInsets.symmetric( + // horizontal: 10, + // vertical: 16, + // ), + // child: SvgPicture.asset( + // Assets.svg.search, + // width: 16, + // height: 16, + // ), + // ), + // suffixIcon: _searchController.text.isNotEmpty + // ? Padding( + // padding: const EdgeInsets.only(right: 0), + // child: UnconstrainedBox( + // child: Row( + // children: [ + // TextFieldIconButton( + // child: const XIcon(), + // onTap: () async { + // setState(() { + // _searchController.text = ""; + // _searchTerm = ""; + // }); + // }, + // ), + // ], + // ), + // ), + // ) + // : null, + // ), + // ), + // ), + ], + ), + // Column( + // children: [ + // + // ], + // ), + const Spacer(), + Padding( + padding: const EdgeInsets.all(32), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Clear logs", + onPressed: () {}, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Save logs to file", + onPressed: () {}, + ), + ) + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/advanced_settings/stack_privacy_dialog.dart b/lib/pages_desktop_specific/home/settings_menu/advanced_settings/stack_privacy_dialog.dart similarity index 96% rename from lib/pages_desktop_specific/home/advanced_settings/stack_privacy_dialog.dart rename to lib/pages_desktop_specific/home/settings_menu/advanced_settings/stack_privacy_dialog.dart index 8e385fd37..32c20c010 100644 --- a/lib/pages_desktop_specific/home/advanced_settings/stack_privacy_dialog.dart +++ b/lib/pages_desktop_specific/home/settings_menu/advanced_settings/stack_privacy_dialog.dart @@ -3,21 +3,20 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/providers/global/prefs_provider.dart'; +import 'package:stackwallet/providers/global/price_provider.dart'; +import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.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 '../../../hive/db.dart'; -import '../../../providers/global/prefs_provider.dart'; -import '../../../providers/global/price_provider.dart'; -import '../../../services/exchange/exchange_data_loading_service.dart'; -import '../../../utilities/assets.dart'; -import '../../../utilities/constants.dart'; -import '../../../utilities/theme/stack_colors.dart'; -import '../../../utilities/util.dart'; -import '../../../widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; class StackPrivacyDialog extends ConsumerStatefulWidget { const StackPrivacyDialog({Key? key}) : super(key: key); From 8fff27523ab5e4105dfac12d6f18d99e0fa6db61 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Wed, 2 Nov 2022 10:27:50 -0600 Subject: [PATCH 100/426] import change --- lib/route_generator.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 613e0d509..edc677946 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -85,13 +85,13 @@ 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/create_password/create_password_view.dart'; -import 'package:stackwallet/pages_desktop_specific/home/advanced_settings/advanced_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_settings_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/qr_code_desktop_popup_content.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart'; +import 'package:stackwallet/pages_desktop_specific/home/advanced_settings/advanced_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/appearance_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/currency_settings.dart'; From cb39ff62b813251cec130601cde7d684ba7dbae5 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Wed, 2 Nov 2022 10:28:34 -0600 Subject: [PATCH 101/426] easy crypto elevation fix --- lib/pages/stack_privacy_calls.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pages/stack_privacy_calls.dart b/lib/pages/stack_privacy_calls.dart index 7e981b91a..fd6f60def 100644 --- a/lib/pages/stack_privacy_calls.dart +++ b/lib/pages/stack_privacy_calls.dart @@ -285,6 +285,7 @@ class _PrivacyToggleState extends State<PrivacyToggle> { children: [ Expanded( child: RawMaterialButton( + elevation: 0, fillColor: Theme.of(context).extension<StackColors>()!.popupBG, shape: RoundedRectangleBorder( side: !externalCallsEnabled From bf0f0bc48cfbf78f34c97f9a5f59a12a62c0d80e Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Wed, 2 Nov 2022 14:14:18 -0600 Subject: [PATCH 102/426] language dialog ui --- .../language_settings/language_dialog.dart | 362 ++++++++++++++++++ .../language_settings.dart | 16 +- 2 files changed, 377 insertions(+), 1 deletion(-) create mode 100644 lib/pages_desktop_specific/home/settings_menu/language_settings/language_dialog.dart rename lib/pages_desktop_specific/home/settings_menu/{ => language_settings}/language_settings.dart (88%) diff --git a/lib/pages_desktop_specific/home/settings_menu/language_settings/language_dialog.dart b/lib/pages_desktop_specific/home/settings_menu/language_settings/language_dialog.dart new file mode 100644 index 000000000..0bf5d0fcd --- /dev/null +++ b/lib/pages_desktop_specific/home/settings_menu/language_settings/language_dialog.dart @@ -0,0 +1,362 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/providers/global/prefs_provider.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/languages_enum.dart'; +import 'package:stackwallet/utilities/text_styles.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/stack_text_field.dart'; + +import '../../../../utilities/assets.dart'; + +class LanguageDialog extends ConsumerStatefulWidget { + const LanguageDialog({Key? key}) : super(key: key); + + @override + ConsumerState<LanguageDialog> createState() => _LanguageDialog(); +} + +class _LanguageDialog extends ConsumerState<LanguageDialog> { + late final TextEditingController searchLanguageController; + + late final FocusNode searchLanguageFocusNode; + + final languages = Language.values.map((e) => e.description).toList(); + + late String current; + late List<String> listWithoutSelected; + + void onTap(int index) { + if (index == 0 || current.isEmpty) { + // ignore if already selected language + return; + } + current = listWithoutSelected[index]; + listWithoutSelected.remove(current); + listWithoutSelected.insert(0, current); + ref.read(prefsChangeNotifierProvider).language = current; + } + + BorderRadius? _borderRadius(int index) { + if (index == 0 && listWithoutSelected.length == 1) { + return BorderRadius.circular( + Constants.size.circularBorderRadius, + ); + } else if (index == 0) { + return BorderRadius.vertical( + top: Radius.circular( + Constants.size.circularBorderRadius, + ), + ); + } else if (index == listWithoutSelected.length - 1) { + return BorderRadius.vertical( + bottom: Radius.circular( + Constants.size.circularBorderRadius, + ), + ); + } + return null; + } + + String filter = ""; + + List<String> _filtered() { + return listWithoutSelected + .where( + (element) => element.toLowerCase().contains(filter.toLowerCase())) + .toList(); + } + + @override + void initState() { + searchLanguageController = TextEditingController(); + + searchLanguageFocusNode = FocusNode(); + + super.initState(); + } + + @override + void dispose() { + searchLanguageController.dispose(); + + searchLanguageFocusNode.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + current = ref + .watch(prefsChangeNotifierProvider.select((value) => value.language)); + + return DesktopDialog( + maxHeight: 700, + maxWidth: 600, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(32), + child: Text( + "Select language", + style: STextStyles.desktopH3(context), + textAlign: TextAlign.center, + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 16, horizontal: 32), + child: Column( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: false, + enableSuggestions: false, + controller: searchLanguageController, + focusNode: searchLanguageFocusNode, + style: STextStyles.desktopTextMedium(context).copyWith( + height: 2, + ), + textAlign: TextAlign.left, + decoration: standardInputDecoration( + "Search", searchLanguageFocusNode, context) + .copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), + ), + ), + ), + ), + ], + ), + ), + const Spacer(), + Padding( + padding: const EdgeInsets.all(32), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Save Changes", + onPressed: () {}, + ), + ) + ], + ), + ), + ], + ), + ); + } +} + +// NestedScrollView( +// floatHeaderSlivers: true, +// headerSliverBuilder: (context, innerBoxIsScrolled) { +// return [ +// SliverOverlapAbsorber( +// handle: NestedScrollView.sliverOverlapAbsorberHandleFor( +// context), +// sliver: SliverToBoxAdapter( +// child: Padding( +// padding: const EdgeInsets.only(bottom: 16), +// child: ClipRRect( +// borderRadius: BorderRadius.circular( +// Constants.size.circularBorderRadius, +// ), +// child: TextField( +// autocorrect: Util.isDesktop ? false : true, +// enableSuggestions: +// Util.isDesktop ? false : true, +// controller: searchLanguageController, +// focusNode: searchLanguageFocusNode, +// onChanged: (newString) { +// setState(() => filter = newString); +// }, +// style: STextStyles.field(context), +// decoration: standardInputDecoration( +// "Search", +// searchLanguageFocusNode, +// context, +// ).copyWith( +// prefixIcon: Padding( +// padding: const EdgeInsets.symmetric( +// horizontal: 10, +// vertical: 16, +// ), +// child: SvgPicture.asset( +// Assets.svg.search, +// width: 16, +// height: 16, +// ), +// ), +// suffixIcon: searchLanguageController +// .text.isNotEmpty +// ? Padding( +// padding: +// const EdgeInsets.only(right: 0), +// child: UnconstrainedBox( +// child: Row( +// children: [ +// TextFieldIconButton( +// child: const XIcon(), +// onTap: () async { +// setState(() { +// searchLanguageController +// .text = ""; +// filter = ""; +// }); +// }, +// ), +// ], +// ), +// ), +// ) +// : null, +// ), +// ), +// ), +// ), +// ), +// ), +// ]; +// }, +// body: Builder( +// builder: (context) { +// return CustomScrollView( +// slivers: [ +// SliverOverlapInjector( +// handle: +// NestedScrollView.sliverOverlapAbsorberHandleFor( +// context, +// ), +// ), +// SliverList( +// delegate: SliverChildBuilderDelegate( +// (context, index) { +// return Container( +// decoration: BoxDecoration( +// color: Theme.of(context) +// .extension<StackColors>()! +// .popupBG, +// borderRadius: _borderRadius(index), +// ), +// child: Padding( +// padding: const EdgeInsets.all(4), +// key: Key( +// "languageSelect_${listWithoutSelected[index]}"), +// child: RoundedContainer( +// padding: const EdgeInsets.all(0), +// color: index == 0 +// ? Theme.of(context) +// .extension<StackColors>()! +// .currencyListItemBG +// : Theme.of(context) +// .extension<StackColors>()! +// .popupBG, +// child: RawMaterialButton( +// onPressed: () async { +// onTap(index); +// }, +// shape: RoundedRectangleBorder( +// borderRadius: BorderRadius.circular( +// Constants.size.circularBorderRadius, +// ), +// ), +// child: Padding( +// padding: const EdgeInsets.all(12.0), +// child: Row( +// crossAxisAlignment: +// CrossAxisAlignment.start, +// children: [ +// SizedBox( +// width: 20, +// height: 20, +// child: Radio( +// activeColor: Theme.of(context) +// .extension<StackColors>()! +// .radioButtonIconEnabled, +// value: true, +// groupValue: index == 0, +// onChanged: (_) { +// onTap(index); +// }, +// ), +// ), +// const SizedBox( +// width: 12, +// ), +// Column( +// crossAxisAlignment: +// CrossAxisAlignment.start, +// children: [ +// Text( +// listWithoutSelected[index], +// key: (index == 0) +// ? const Key( +// "selectedLanguageSettingsLanguageText") +// : null, +// style: STextStyles +// .largeMedium14(context), +// ), +// const SizedBox( +// height: 2, +// ), +// Text( +// listWithoutSelected[index], +// key: (index == 0) +// ? const Key( +// "selectedLanguageSettingsLanguageTextDescription") +// : null, +// style: STextStyles +// .itemSubtitle(context), +// ), +// ], +// ), +// ], +// ), +// ), +// ), +// ), +// ), +// ); +// }, +// childCount: listWithoutSelected.length, +// ), +// ), +// ], +// ); +// }, +// ), +// ), diff --git a/lib/pages_desktop_specific/home/settings_menu/language_settings.dart b/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart similarity index 88% rename from lib/pages_desktop_specific/home/settings_menu/language_settings.dart rename to lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart index 7655188e1..97b807b3b 100644 --- a/lib/pages_desktop_specific/home/settings_menu/language_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/src/widgets/framework.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/language_settings/language_dialog.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -87,6 +88,17 @@ class ChangeLanguageButton extends ConsumerWidget { }) : super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { + Future<void> chooseLanguage() async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return LanguageDialog(); + }, + ); + } + return SizedBox( width: 200, height: 48, @@ -94,7 +106,9 @@ class ChangeLanguageButton extends ConsumerWidget { style: Theme.of(context) .extension<StackColors>()! .getPrimaryEnabledButtonColor(context), - onPressed: () {}, + onPressed: () { + chooseLanguage(); + }, child: Text( "Change language", style: STextStyles.button(context), From 95a35a5dd0a960b6ca1a03b4f075d53040ad8de4 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Wed, 2 Nov 2022 14:16:16 -0600 Subject: [PATCH 103/426] more import fixes --- .../home/desktop_settings_view.dart | 4 +-- .../advanced_settings/advanced_settings.dart | 32 +++++++++---------- lib/route_generator.dart | 5 +-- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/lib/pages_desktop_specific/home/desktop_settings_view.dart b/lib/pages_desktop_specific/home/desktop_settings_view.dart index 34cb07f27..9b3f897a9 100644 --- a/lib/pages_desktop_specific/home/desktop_settings_view.dart +++ b/lib/pages_desktop_specific/home/desktop_settings_view.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/pages_desktop_specific/home/advanced_settings/advanced_settings.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/appearance_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/currency_settings.dart'; -import 'package:stackwallet/pages_desktop_specific/home/settings_menu/language_settings.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/nodes_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/security_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/settings_menu.dart'; diff --git a/lib/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart b/lib/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart index 7361d9773..1632576fd 100644 --- a/lib/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/framework.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/pages_desktop_specific/home/advanced_settings/stack_privacy_dialog.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/advanced_settings/stack_privacy_dialog.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -165,7 +166,7 @@ class _AdvancedSettings extends ConsumerState<AdvancedSettings> { .textDark), textAlign: TextAlign.left, ), - const ShowLogsButton(), + ShowLogsButton(), ], ), ), @@ -190,7 +191,7 @@ class StackPrivacyButton extends ConsumerWidget { useSafeArea: false, barrierDismissible: true, builder: (context) { - return const StackPrivacyDialog(); + return StackPrivacyDialog(); }, ); } @@ -223,20 +224,19 @@ class ShowLogsButton extends ConsumerWidget { const ShowLogsButton({ Key? key, }) : super(key: key); - - Future<void> viewDebugLogs() async { - // await showDialog<dynamic>( - // context: context, - // useSafeArea: false, - // barrierDismissible: true, - // builder: (context) { - // return const DebugInfoDialog(); - // }, - // ); - } - @override Widget build(BuildContext context, WidgetRef ref) { + // Future<void> viewDebugLogs() async { + // await showDialog<dynamic>( + // context: context, + // useSafeArea: false, + // barrierDismissible: true, + // builder: (context) { + // return const DebugInfoDialog(); + // }, + // ); + // } + return SizedBox( width: 101, height: 37, @@ -246,7 +246,7 @@ class ShowLogsButton extends ConsumerWidget { .getPrimaryEnabledButtonColor(context), onPressed: () { // - viewDebugLogs(); + // viewDebugLogs(); }, child: Text( "Show logs", diff --git a/lib/route_generator.dart b/lib/route_generator.dart index edc677946..c5b764995 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -91,11 +91,10 @@ import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_v import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/qr_code_desktop_popup_content.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart'; -import 'package:stackwallet/pages_desktop_specific/home/advanced_settings/advanced_settings.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/appearance_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/currency_settings.dart'; -import 'package:stackwallet/pages_desktop_specific/home/settings_menu/language_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/nodes_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/security_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/settings_menu.dart'; @@ -107,6 +106,8 @@ import 'package:stackwallet/utilities/enums/add_wallet_type_enum.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:tuple/tuple.dart'; +import 'pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart'; + class RouteGenerator { static const bool useMaterialPageRoute = true; From 4aec412ce70ba38a149bc3c5e6d7742cce488edb Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Wed, 2 Nov 2022 14:16:38 -0600 Subject: [PATCH 104/426] working language dialog --- .../language_settings/language_dialog.dart | 408 +++++++++--------- 1 file changed, 197 insertions(+), 211 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/language_settings/language_dialog.dart b/lib/pages_desktop_specific/home/settings_menu/language_settings/language_dialog.dart index 0bf5d0fcd..f68196710 100644 --- a/lib/pages_desktop_specific/home/settings_menu/language_settings/language_dialog.dart +++ b/lib/pages_desktop_specific/home/settings_menu/language_settings/language_dialog.dart @@ -12,6 +12,10 @@ import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import '../../../../utilities/assets.dart'; +import '../../../../utilities/theme/stack_colors.dart'; +import '../../../../widgets/icon_widgets/x_icon.dart'; +import '../../../../widgets/rounded_container.dart'; +import '../../../../widgets/textfield_icon_button.dart'; class LanguageDialog extends ConsumerStatefulWidget { const LanguageDialog({Key? key}) : super(key: key); @@ -94,6 +98,13 @@ class _LanguageDialog extends ConsumerState<LanguageDialog> { current = ref .watch(prefsChangeNotifierProvider.select((value) => value.language)); + listWithoutSelected = languages; + if (current.isNotEmpty) { + listWithoutSelected.remove(current); + listWithoutSelected.insert(0, current); + } + listWithoutSelected = _filtered(); + return DesktopDialog( maxHeight: 700, maxWidth: 600, @@ -113,41 +124,196 @@ class _LanguageDialog extends ConsumerState<LanguageDialog> { const DesktopDialogCloseButton(), ], ), - Padding( - padding: EdgeInsets.symmetric(vertical: 16, horizontal: 32), - child: Column( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: false, - enableSuggestions: false, - controller: searchLanguageController, - focusNode: searchLanguageFocusNode, - style: STextStyles.desktopTextMedium(context).copyWith( - height: 2, - ), - textAlign: TextAlign.left, - decoration: standardInputDecoration( - "Search", searchLanguageFocusNode, context) - .copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, + Expanded( + child: NestedScrollView( + floatHeaderSlivers: true, + headerSliverBuilder: (context, innerBoxIsScrolled) { + return [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor( + context), + sliver: SliverToBoxAdapter( + child: Padding( + padding: + EdgeInsets.symmetric(vertical: 16, horizontal: 32), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: false, + enableSuggestions: false, + controller: searchLanguageController, + focusNode: searchLanguageFocusNode, + style: STextStyles.desktopTextMedium(context) + .copyWith( + height: 2, + ), + textAlign: TextAlign.left, + decoration: standardInputDecoration("Search", + searchLanguageFocusNode, context) + .copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), + ), + suffixIcon: searchLanguageController + .text.isNotEmpty + ? Padding( + padding: + const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + searchLanguageController + .text = ""; + filter = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + ), + ], ), ), ), ), - ), - ], + ]; + }, + body: Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor( + context, + ), + ), + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .popupBG, + borderRadius: _borderRadius(index), + ), + child: Padding( + padding: const EdgeInsets.all(4), + key: Key( + "desktopSelectLanguage_${listWithoutSelected[index]}"), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32), + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: index == 0 + ? Theme.of(context) + .extension<StackColors>()! + .currencyListItemBG + : Theme.of(context) + .extension<StackColors>()! + .popupBG, + child: RawMaterialButton( + onPressed: () async { + onTap(index); + }, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: true, + groupValue: index == 0, + onChanged: (_) { + onTap(index); + }, + ), + ), + const SizedBox( + width: 12, + ), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + listWithoutSelected[index], + key: (index == 0) + ? const Key( + "desktopSettingsSelectedLanguageText") + : null, + style: + STextStyles.largeMedium14( + context), + ), + const SizedBox( + height: 2, + ), + Text( + listWithoutSelected[index], + key: (index == 0) + ? const Key( + "desktopSettingsSelectedLanguageTextDescription") + : null, + style: + STextStyles.itemSubtitle( + context), + ), + ], + ), + ], + ), + ), + ), + ), + ), + ), + ); + }, + childCount: listWithoutSelected.length, + ), + ), + ], + ); + }, + ), ), ), const Spacer(), @@ -180,183 +346,3 @@ class _LanguageDialog extends ConsumerState<LanguageDialog> { ); } } - -// NestedScrollView( -// floatHeaderSlivers: true, -// headerSliverBuilder: (context, innerBoxIsScrolled) { -// return [ -// SliverOverlapAbsorber( -// handle: NestedScrollView.sliverOverlapAbsorberHandleFor( -// context), -// sliver: SliverToBoxAdapter( -// child: Padding( -// padding: const EdgeInsets.only(bottom: 16), -// child: ClipRRect( -// borderRadius: BorderRadius.circular( -// Constants.size.circularBorderRadius, -// ), -// child: TextField( -// autocorrect: Util.isDesktop ? false : true, -// enableSuggestions: -// Util.isDesktop ? false : true, -// controller: searchLanguageController, -// focusNode: searchLanguageFocusNode, -// onChanged: (newString) { -// setState(() => filter = newString); -// }, -// style: STextStyles.field(context), -// decoration: standardInputDecoration( -// "Search", -// searchLanguageFocusNode, -// context, -// ).copyWith( -// prefixIcon: Padding( -// padding: const EdgeInsets.symmetric( -// horizontal: 10, -// vertical: 16, -// ), -// child: SvgPicture.asset( -// Assets.svg.search, -// width: 16, -// height: 16, -// ), -// ), -// suffixIcon: searchLanguageController -// .text.isNotEmpty -// ? Padding( -// padding: -// const EdgeInsets.only(right: 0), -// child: UnconstrainedBox( -// child: Row( -// children: [ -// TextFieldIconButton( -// child: const XIcon(), -// onTap: () async { -// setState(() { -// searchLanguageController -// .text = ""; -// filter = ""; -// }); -// }, -// ), -// ], -// ), -// ), -// ) -// : null, -// ), -// ), -// ), -// ), -// ), -// ), -// ]; -// }, -// body: Builder( -// builder: (context) { -// return CustomScrollView( -// slivers: [ -// SliverOverlapInjector( -// handle: -// NestedScrollView.sliverOverlapAbsorberHandleFor( -// context, -// ), -// ), -// SliverList( -// delegate: SliverChildBuilderDelegate( -// (context, index) { -// return Container( -// decoration: BoxDecoration( -// color: Theme.of(context) -// .extension<StackColors>()! -// .popupBG, -// borderRadius: _borderRadius(index), -// ), -// child: Padding( -// padding: const EdgeInsets.all(4), -// key: Key( -// "languageSelect_${listWithoutSelected[index]}"), -// child: RoundedContainer( -// padding: const EdgeInsets.all(0), -// color: index == 0 -// ? Theme.of(context) -// .extension<StackColors>()! -// .currencyListItemBG -// : Theme.of(context) -// .extension<StackColors>()! -// .popupBG, -// child: RawMaterialButton( -// onPressed: () async { -// onTap(index); -// }, -// shape: RoundedRectangleBorder( -// borderRadius: BorderRadius.circular( -// Constants.size.circularBorderRadius, -// ), -// ), -// child: Padding( -// padding: const EdgeInsets.all(12.0), -// child: Row( -// crossAxisAlignment: -// CrossAxisAlignment.start, -// children: [ -// SizedBox( -// width: 20, -// height: 20, -// child: Radio( -// activeColor: Theme.of(context) -// .extension<StackColors>()! -// .radioButtonIconEnabled, -// value: true, -// groupValue: index == 0, -// onChanged: (_) { -// onTap(index); -// }, -// ), -// ), -// const SizedBox( -// width: 12, -// ), -// Column( -// crossAxisAlignment: -// CrossAxisAlignment.start, -// children: [ -// Text( -// listWithoutSelected[index], -// key: (index == 0) -// ? const Key( -// "selectedLanguageSettingsLanguageText") -// : null, -// style: STextStyles -// .largeMedium14(context), -// ), -// const SizedBox( -// height: 2, -// ), -// Text( -// listWithoutSelected[index], -// key: (index == 0) -// ? const Key( -// "selectedLanguageSettingsLanguageTextDescription") -// : null, -// style: STextStyles -// .itemSubtitle(context), -// ), -// ], -// ), -// ], -// ), -// ), -// ), -// ), -// ), -// ); -// }, -// childCount: listWithoutSelected.length, -// ), -// ), -// ], -// ); -// }, -// ), -// ), From 1f1253e070d72829a6867510a6ceb55bfef9c2be Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 2 Nov 2022 13:43:23 -0600 Subject: [PATCH 105/426] WIP: desktop wallet network settings popups --- .../wallet_network_settings_view.dart | 891 +++++++++--------- .../sub_widgets/network_info_button.dart | 62 +- 2 files changed, 513 insertions(+), 440 deletions(-) diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart index 5b1c57214..8195491d8 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart @@ -23,7 +23,9 @@ import 'package:stackwallet/utilities/constants.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/utilities/util.dart'; import 'package:stackwallet/widgets/animated_text.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/progress_bar.dart'; @@ -75,17 +77,19 @@ class _WalletNetworkSettingsViewState late int _blocksRemaining; Future<void> _attemptRescan() async { - if (!Platform.isLinux) Wakelock.enable(); + if (!Platform.isLinux) await Wakelock.enable(); int maxUnusedAddressGap = 20; const int maxNumberOfIndexesToCheck = 1000; - showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: false, - builder: (context) => const RescanningDialog(), + unawaited( + showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) => const RescanningDialog(), + ), ); try { @@ -131,7 +135,7 @@ class _WalletNetworkSettingsViewState ); } } catch (e) { - if (!Platform.isLinux) Wakelock.disable(); + if (!Platform.isLinux) await Wakelock.disable(); if (mounted) { // pop rescanning dialog @@ -162,7 +166,7 @@ class _WalletNetworkSettingsViewState } } - if (!Platform.isLinux) Wakelock.disable(); + if (!Platform.isLinux) await Wakelock.disable(); } String _percentString(double value) { @@ -262,9 +266,11 @@ class _WalletNetworkSettingsViewState @override Widget build(BuildContext context) { final screenWidth = MediaQuery.of(context).size.width; + final bool isDesktop = Util.isDesktop; - final progressLength = - screenWidth - (_padding * 2) - (_boxPadding * 3) - _iconSize; + final progressLength = isDesktop + ? 450.0 + : screenWidth - (_padding * 2) - (_boxPadding * 3) - _iconSize; final coin = ref .read(walletsChangeNotifierProvider) @@ -300,443 +306,464 @@ class _WalletNetworkSettingsViewState } } - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Network", - style: STextStyles.navBarTitle(context), - ), - actions: [ - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("walletNetworkSettingsAddNewNodeViewButton"), - size: 36, - shadows: const [], - color: Theme.of(context).extension<StackColors>()!.background, - icon: SvgPicture.asset( - Assets.svg.verticalEllipsis, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - width: 20, - height: 20, + title: Text( + "Network", + style: STextStyles.navBarTitle(context), + ), + actions: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, ), - onPressed: () { - showDialog<dynamic>( - barrierColor: Colors.transparent, - barrierDismissible: true, - context: context, - builder: (_) { - return Stack( - children: [ - Positioned( - top: 9, - right: 10, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .popupBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius), - // boxShadow: [CFColors.standardBoxShadow], - boxShadow: const [], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - onTap: () { - Navigator.of(context).pop(); - showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return ConfirmFullRescanDialog( - onConfirm: _attemptRescan, + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("walletNetworkSettingsAddNewNodeViewButton"), + size: 36, + shadows: const [], + color: + Theme.of(context).extension<StackColors>()!.background, + icon: SvgPicture.asset( + Assets.svg.verticalEllipsis, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + width: 20, + height: 20, + ), + onPressed: () { + showDialog<dynamic>( + barrierColor: Colors.transparent, + barrierDismissible: true, + context: context, + builder: (_) { + return Stack( + children: [ + Positioned( + top: 9, + right: 10, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .popupBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius), + // boxShadow: [CFColors.standardBoxShadow], + boxShadow: const [], + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: () { + Navigator.of(context).pop(); + showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return ConfirmFullRescanDialog( + onConfirm: _attemptRescan, + ); + }, ); }, - ); - }, - child: RoundedWhiteContainer( - child: Material( - color: Colors.transparent, - child: Text( - "Rescan blockchain", - style: STextStyles.baseXS(context), - ), - ), - ), - ), - ], - ), - ), - ), - ], - ); - }, - ); - }, - ), - ), - ), - ], - ), - body: Padding( - padding: EdgeInsets.only( - top: 12, - left: _padding, - right: _padding, - ), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Blockchain status", - textAlign: TextAlign.left, - style: STextStyles.smallMed12(context), - ), - GestureDetector( - onTap: () { - ref - .read(walletsChangeNotifierProvider) - .getManager(widget.walletId) - .refresh(); - }, - child: Text( - "Resync", - style: STextStyles.link2(context), - ), - ), - ], - ), - const SizedBox( - height: 9, - ), - if (_currentSyncStatus == WalletSyncStatus.synced) - RoundedWhiteContainer( - child: Row( - children: [ - Container( - width: _iconSize, - height: _iconSize, - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorGreen - .withOpacity(0.2), - borderRadius: BorderRadius.circular(_iconSize), - ), - child: Center( - child: SvgPicture.asset( - Assets.svg.radio, - height: 14, - width: 14, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorGreen, - ), - ), - ), - SizedBox( - width: _boxPadding, - ), - Column( - children: [ - SizedBox( - width: progressLength, - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Synchronized", - style: STextStyles.w600_10(context), - ), - Text( - "100%", - style: STextStyles.syncPercent(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorGreen, - ), - ), - ], - ), - ), - const SizedBox( - height: 4, - ), - ProgressBar( - width: progressLength, - height: 5, - fillColor: Theme.of(context) - .extension<StackColors>()! - .accentColorGreen, - backgroundColor: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - percent: 1, - ), - ], - ), - ], - ), - ), - if (_currentSyncStatus == WalletSyncStatus.syncing) - RoundedWhiteContainer( - child: Row( - children: [ - Container( - width: _iconSize, - height: _iconSize, - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorYellow - .withOpacity(0.2), - borderRadius: BorderRadius.circular(_iconSize), - ), - child: Center( - child: SvgPicture.asset( - Assets.svg.radioSyncing, - height: 14, - width: 14, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorYellow, - ), - ), - ), - SizedBox( - width: _boxPadding, - ), - Column( - children: [ - SizedBox( - width: progressLength, - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - AnimatedText( - style: STextStyles.w600_10(context), - stringsToLoopThrough: const [ - "Synchronizing", - "Synchronizing.", - "Synchronizing..", - "Synchronizing...", - ], - ), - Row( - children: [ - Text( - _percentString(_percent), - style: - STextStyles.syncPercent(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorYellow, - ), - ), - if (coin == Coin.monero || - coin == Coin.wownero || - coin == Coin.epicCash) - Text( - " (Blocks to go: ${_blocksRemaining == -1 ? "?" : _blocksRemaining})", - style: - STextStyles.syncPercent(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorYellow, + child: RoundedWhiteContainer( + child: Material( + color: Colors.transparent, + child: Text( + "Rescan blockchain", + style: + STextStyles.baseXS(context), ), ), - ], - ) - ], + ), + ), + ], + ), ), ), - const SizedBox( - height: 4, - ), - ProgressBar( - width: progressLength, - height: 5, - fillColor: Theme.of(context) - .extension<StackColors>()! - .accentColorYellow, - backgroundColor: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - percent: _percent, - ), ], - ), - ], - ), - ), - if (_currentSyncStatus == WalletSyncStatus.unableToSync) - RoundedWhiteContainer( - child: Row( - children: [ - Container( - width: _iconSize, - height: _iconSize, - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorRed - .withOpacity(0.2), - borderRadius: BorderRadius.circular(_iconSize), - ), - child: Center( - child: SvgPicture.asset( - Assets.svg.radioProblem, - height: 14, - width: 14, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorRed, - ), - ), - ), - SizedBox( - width: _boxPadding, - ), - Column( - children: [ - SizedBox( - width: progressLength, - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Unable to synchronize", - style: - STextStyles.w600_10(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorRed, - ), - ), - Text( - "0%", - style: STextStyles.syncPercent(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorRed, - ), - ), - ], - ), - ), - const SizedBox( - height: 4, - ), - ProgressBar( - width: progressLength, - height: 5, - fillColor: Theme.of(context) - .extension<StackColors>()! - .accentColorRed, - backgroundColor: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - percent: 0, - ), - ], - ), - ], - ), - ), - if (_currentSyncStatus == WalletSyncStatus.unableToSync) - Padding( - padding: const EdgeInsets.only( - top: 12, - ), - child: RoundedContainer( - color: Theme.of(context) - .extension<StackColors>()! - .warningBackground, - child: Text( - "Please check your internet connection and make sure your current node is not having issues.", - style: STextStyles.baseXS(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .warningForeground, - ), - ), - ), - ), - const SizedBox( - height: 20, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "${ref.watch(walletsChangeNotifierProvider.select((value) => value.getManager(widget.walletId).coin)).prettyName} nodes", - textAlign: TextAlign.left, - style: STextStyles.smallMed12(context), - ), - BlueTextButton( - text: "Add new node", - onTap: () { - Navigator.of(context).pushNamed( - AddEditNodeView.routeName, - arguments: Tuple4( - AddEditNodeViewType.add, - ref - .read(walletsChangeNotifierProvider) - .getManager(widget.walletId) - .coin, - null, - WalletNetworkSettingsView.routeName, - ), ); }, - ), - ], + ); + }, ), - const SizedBox( - height: 8, - ), - NodesList( - coin: ref.watch(walletsChangeNotifierProvider.select( - (value) => value.getManager(widget.walletId).coin)), - popBackToRoute: WalletNetworkSettingsView.routeName, - ), - ], + ), ), ], ), - ), + body: Padding( + padding: EdgeInsets.only( + top: 12, + left: _padding, + right: _padding, + ), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + child, + ], + ), + ), + ), + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Blockchain status", + textAlign: TextAlign.left, + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.smallMed12(context), + ), + GestureDetector( + onTap: () { + ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .refresh(); + }, + child: Text( + "Resync", + style: STextStyles.link2(context), + ), + ), + ], + ), + const SizedBox( + height: 9, + ), + if (_currentSyncStatus == WalletSyncStatus.synced) + RoundedWhiteContainer( + child: Row( + children: [ + Container( + width: _iconSize, + height: _iconSize, + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorGreen + .withOpacity(0.2), + borderRadius: BorderRadius.circular(_iconSize), + ), + child: Center( + child: SvgPicture.asset( + Assets.svg.radio, + height: 14, + width: 14, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorGreen, + ), + ), + ), + SizedBox( + width: _boxPadding, + ), + Column( + children: [ + SizedBox( + width: progressLength, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Synchronized", + style: STextStyles.w600_10(context), + ), + Text( + "100%", + style: STextStyles.syncPercent(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorGreen, + ), + ), + ], + ), + ), + const SizedBox( + height: 4, + ), + ProgressBar( + width: progressLength, + height: 5, + fillColor: Theme.of(context) + .extension<StackColors>()! + .accentColorGreen, + backgroundColor: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + percent: 1, + ), + ], + ), + ], + ), + ), + if (_currentSyncStatus == WalletSyncStatus.syncing) + RoundedWhiteContainer( + child: Row( + children: [ + Container( + width: _iconSize, + height: _iconSize, + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorYellow + .withOpacity(0.2), + borderRadius: BorderRadius.circular(_iconSize), + ), + child: Center( + child: SvgPicture.asset( + Assets.svg.radioSyncing, + height: 14, + width: 14, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorYellow, + ), + ), + ), + SizedBox( + width: _boxPadding, + ), + Column( + children: [ + SizedBox( + width: progressLength, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + AnimatedText( + style: STextStyles.w600_10(context), + stringsToLoopThrough: const [ + "Synchronizing", + "Synchronizing.", + "Synchronizing..", + "Synchronizing...", + ], + ), + Row( + children: [ + Text( + _percentString(_percent), + style: + STextStyles.syncPercent(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorYellow, + ), + ), + if (coin == Coin.monero || + coin == Coin.wownero || + coin == Coin.epicCash) + Text( + " (Blocks to go: ${_blocksRemaining == -1 ? "?" : _blocksRemaining})", + style: STextStyles.syncPercent(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorYellow, + ), + ), + ], + ) + ], + ), + ), + const SizedBox( + height: 4, + ), + ProgressBar( + width: progressLength, + height: 5, + fillColor: Theme.of(context) + .extension<StackColors>()! + .accentColorYellow, + backgroundColor: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + percent: _percent, + ), + ], + ), + ], + ), + ), + if (_currentSyncStatus == WalletSyncStatus.unableToSync) + RoundedWhiteContainer( + child: Row( + children: [ + Container( + width: _iconSize, + height: _iconSize, + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorRed + .withOpacity(0.2), + borderRadius: BorderRadius.circular(_iconSize), + ), + child: Center( + child: SvgPicture.asset( + Assets.svg.radioProblem, + height: 14, + width: 14, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorRed, + ), + ), + ), + SizedBox( + width: _boxPadding, + ), + Column( + children: [ + SizedBox( + width: progressLength, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Unable to synchronize", + style: STextStyles.w600_10(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorRed, + ), + ), + Text( + "0%", + style: STextStyles.syncPercent(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorRed, + ), + ), + ], + ), + ), + const SizedBox( + height: 4, + ), + ProgressBar( + width: progressLength, + height: 5, + fillColor: Theme.of(context) + .extension<StackColors>()! + .accentColorRed, + backgroundColor: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + percent: 0, + ), + ], + ), + ], + ), + ), + if (_currentSyncStatus == WalletSyncStatus.unableToSync) + Padding( + padding: const EdgeInsets.only( + top: 12, + ), + child: RoundedContainer( + color: Theme.of(context) + .extension<StackColors>()! + .warningBackground, + child: Text( + "Please check your internet connection and make sure your current node is not having issues.", + style: STextStyles.baseXS(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .warningForeground, + ), + ), + ), + ), + const SizedBox( + height: 20, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "${ref.watch(walletsChangeNotifierProvider.select((value) => value.getManager(widget.walletId).coin)).prettyName} nodes", + textAlign: TextAlign.left, + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.smallMed12(context), + ), + BlueTextButton( + text: "Add new node", + onTap: () { + Navigator.of(context).pushNamed( + AddEditNodeView.routeName, + arguments: Tuple4( + AddEditNodeViewType.add, + ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .coin, + null, + WalletNetworkSettingsView.routeName, + ), + ); + }, + ), + ], + ), + const SizedBox( + height: 8, + ), + NodesList( + coin: ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(widget.walletId).coin)), + popBackToRoute: WalletNetworkSettingsView.routeName, + ), + if (isDesktop) + const SizedBox( + height: 20, + ), + if (isDesktop) + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + "Advanced", + textAlign: TextAlign.left, + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ], + ), + ], ), ); } diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart index 830b5e667..37cd414c7 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart @@ -12,6 +12,9 @@ import 'package:stackwallet/services/event_bus/global_event_bus.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/utilities/util.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:tuple/tuple.dart'; class NetworkInfoButton extends ConsumerStatefulWidget { @@ -150,14 +153,57 @@ class _NetworkInfoButtonState extends ConsumerState<NetworkInfoButton> { Widget build(BuildContext context) { return GestureDetector( onTap: () { - Navigator.of(context).pushNamed( - WalletNetworkSettingsView.routeName, - arguments: Tuple3( - walletId, - _currentSyncStatus, - _currentNodeStatus, - ), - ); + if (Util.isDesktop) { + showDialog<void>( + context: context, + builder: (context) => DesktopDialog( + maxHeight: 600, + maxWidth: 580, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Network", + style: STextStyles.desktopH3(context), + ), + const DesktopDialogCloseButton(), + ], + ), + ), + Padding( + padding: const EdgeInsets.only( + top: 16, + left: 32, + right: 32, + bottom: 32, + ), + child: WalletNetworkSettingsView( + walletId: walletId, + initialSyncStatus: _currentSyncStatus, + initialNodeStatus: _currentNodeStatus, + ), + ), + ], + ), + ), + ); + } else { + Navigator.of(context).pushNamed( + WalletNetworkSettingsView.routeName, + arguments: Tuple3( + walletId, + _currentSyncStatus, + _currentNodeStatus, + ), + ); + } }, child: Container( color: Colors.transparent, From 2afec92279c8faab60ba1baec3eb3bd29caeaff6 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 2 Nov 2022 14:24:41 -0600 Subject: [PATCH 106/426] desktop wallet network settings expanding advanced rescan --- assets/svg/network-wired-2.svg | 10 ++ .../sub_widgets/confirm_full_rescan.dart | 139 +++++++++++---- .../wallet_network_settings_view.dart | 163 ++++++++++++++++-- lib/utilities/assets.dart | 1 + lib/widgets/conditional_parent.dart | 4 +- pubspec.yaml | 1 + 6 files changed, 266 insertions(+), 52 deletions(-) create mode 100644 assets/svg/network-wired-2.svg diff --git a/assets/svg/network-wired-2.svg b/assets/svg/network-wired-2.svg new file mode 100644 index 000000000..bbbfa056f --- /dev/null +++ b/assets/svg/network-wired-2.svg @@ -0,0 +1,10 @@ +<svg width="24" height="19" viewBox="0 0 24 19" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0_6129_18406)"> +<path d="M14.9173 0C15.8835 0 16.6673 0.769499 16.6673 1.71875V5.15625C16.6673 6.10514 15.8835 6.875 14.9173 6.875H13.1673V8.02083H22.5007C23.146 8.02083 23.6673 8.53288 23.6673 9.16667C23.6673 9.80046 23.146 10.3125 22.5007 10.3125H19.0007V11.4583H20.7507C21.7168 11.4583 22.5007 12.2282 22.5007 13.1771V16.6146C22.5007 17.5635 21.7168 18.3333 20.7507 18.3333H14.9173C13.9512 18.3333 13.1673 17.5635 13.1673 16.6146V13.1771C13.1673 12.2282 13.9512 11.4583 14.9173 11.4583H16.6673V10.3125H7.33398V11.4583H9.08398C10.0501 11.4583 10.834 12.2282 10.834 13.1771V16.6146C10.834 17.5635 10.0501 18.3333 9.08398 18.3333H3.25065C2.28414 18.3333 1.50065 17.5635 1.50065 16.6146V13.1771C1.50065 12.2282 2.28414 11.4583 3.25065 11.4583H5.00065V10.3125H1.50065C0.856432 10.3125 0.333984 9.80046 0.333984 9.16667C0.333984 8.53288 0.856432 8.02083 1.50065 8.02083H10.834V6.875H9.08398C8.11784 6.875 7.33398 6.10514 7.33398 5.15625V1.71875C7.33398 0.769499 8.11784 0 9.08398 0H14.9173ZM9.66732 2.29167V4.58333H14.334V2.29167H9.66732ZM8.50065 16.0417V13.75H3.83398V16.0417H8.50065ZM15.5007 13.75V16.0417H20.1673V13.75H15.5007Z" fill="#232323"/> +</g> +<defs> +<clipPath id="clip0_6129_18406"> +<rect width="23.3333" height="18.3333" fill="white" transform="translate(0.333984)"/> +</clipPath> +</defs> +</svg> 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 141eb4c99..950d8d79e 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 @@ -1,6 +1,11 @@ import 'package:flutter/material.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.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/stack_dialog.dart'; class ConfirmFullRescanDialog extends StatelessWidget { @@ -11,40 +16,110 @@ class ConfirmFullRescanDialog extends StatelessWidget { @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { - return true; - }, - child: StackDialog( - title: "Rescan blockchain", - message: - "Warning! It may take a while. If you exit before completion, you will have to redo the process.", - leftButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Cancel", - style: STextStyles.itemSubtitle12(context), - ), - onPressed: () { - Navigator.of(context).pop(); - }, + if (Util.isDesktop) { + return DesktopDialog( + maxWidth: 576, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Rescan blockchain", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: const EdgeInsets.only( + top: 8, + left: 32, + right: 32, + bottom: 32, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "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, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + desktopMed: true, + onPressed: Navigator.of(context).pop, + label: "Cancel", + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + desktopMed: true, + onPressed: () { + Navigator.of(context).pop(); + onConfirm.call(); + }, + label: "Rescan", + ), + ), + ], + ) + ], + ), + ) + ], ), - rightButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - child: Text( - "Rescan", - style: STextStyles.button(context), + ); + } else { + return WillPopScope( + onWillPop: () async { + return true; + }, + child: StackDialog( + title: "Rescan blockchain", + message: + "Warning! It may take a while. If you exit before completion, you will have to redo the process.", + leftButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Cancel", + style: STextStyles.itemSubtitle12(context), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + rightButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + child: Text( + "Rescan", + style: STextStyles.button(context), + ), + onPressed: () { + Navigator.of(context).pop(); + onConfirm.call(); + }, ), - onPressed: () { - Navigator.of(context).pop(); - onConfirm.call(); - }, ), - ), - ); + ); + } } } diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart index 8195491d8..fdb73fc88 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart @@ -28,6 +28,7 @@ import 'package:stackwallet/widgets/animated_text.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/expandable.dart'; import 'package:stackwallet/widgets/progress_bar.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -61,7 +62,7 @@ class _WalletNetworkSettingsViewState extends ConsumerState<WalletNetworkSettingsView> { final double _padding = 16; final double _boxPadding = 12; - final double _iconSize = 28; + final double _iconSize = Util.isDesktop ? 40 : 28; late final EventBus eventBus; @@ -75,6 +76,7 @@ class _WalletNetworkSettingsViewState late double _percent; late int _blocksRemaining; + bool _advancedIsExpanded = true; Future<void> _attemptRescan() async { if (!Platform.isLinux) await Wakelock.enable(); @@ -269,7 +271,7 @@ class _WalletNetworkSettingsViewState final bool isDesktop = Util.isDesktop; final progressLength = isDesktop - ? 450.0 + ? 430.0 : screenWidth - (_padding * 2) - (_boxPadding * 3) - _iconSize; final coin = ref @@ -373,7 +375,7 @@ class _WalletNetworkSettingsViewState GestureDetector( onTap: () { Navigator.of(context).pop(); - showDialog<dynamic>( + showDialog<void>( context: context, useSafeArea: false, barrierDismissible: true, @@ -453,11 +455,17 @@ class _WalletNetworkSettingsViewState ), ], ), - const SizedBox( - height: 9, + SizedBox( + height: isDesktop ? 12 : 9, ), if (_currentSyncStatus == WalletSyncStatus.synced) RoundedWhiteContainer( + borderColor: isDesktop + ? Theme.of(context).extension<StackColors>()!.background + : null, + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( children: [ Container( @@ -527,6 +535,12 @@ class _WalletNetworkSettingsViewState ), if (_currentSyncStatus == WalletSyncStatus.syncing) RoundedWhiteContainer( + borderColor: isDesktop + ? Theme.of(context).extension<StackColors>()!.background + : null, + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( children: [ Container( @@ -618,6 +632,12 @@ class _WalletNetworkSettingsViewState ), if (_currentSyncStatus == WalletSyncStatus.unableToSync) RoundedWhiteContainer( + borderColor: isDesktop + ? Theme.of(context).extension<StackColors>()!.background + : null, + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( children: [ Container( @@ -708,8 +728,8 @@ class _WalletNetworkSettingsViewState ), ), ), - const SizedBox( - height: 20, + SizedBox( + height: isDesktop ? 32 : 20, ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -740,8 +760,8 @@ class _WalletNetworkSettingsViewState ), ], ), - const SizedBox( - height: 8, + SizedBox( + height: isDesktop ? 12 : 8, ), NodesList( coin: ref.watch(walletsChangeNotifierProvider @@ -750,18 +770,125 @@ class _WalletNetworkSettingsViewState ), if (isDesktop) const SizedBox( - height: 20, + height: 32, ), if (isDesktop) - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text( - "Advanced", - textAlign: TextAlign.left, - style: STextStyles.desktopTextExtraExtraSmall(context), + Padding( + padding: const EdgeInsets.only( + bottom: 12, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + "Advanced", + textAlign: TextAlign.left, + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ], + ), + ), + if (isDesktop) + RoundedWhiteContainer( + borderColor: isDesktop + ? Theme.of(context).extension<StackColors>()!.background + : null, + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Expandable( + onExpandChanged: (state) { + setState(() { + _advancedIsExpanded = state == ExpandableState.expanded; + }); + }, + header: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + width: _iconSize, + height: _iconSize, + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular(_iconSize), + ), + child: Center( + child: SvgPicture.asset( + Assets.svg.networkWired, + width: 24, + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + ), + const SizedBox( + width: 10, + ), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Advanced", + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + Text( + "Rescan blockchain", + style: STextStyles.desktopTextExtraExtraSmall( + context), + ), + ], + ) + ], + ), + SvgPicture.asset( + _advancedIsExpanded + ? Assets.svg.chevronDown + : Assets.svg.chevronUp, + width: 12, + height: 6, + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ], ), - ], + body: Row( + children: [ + Padding( + padding: const EdgeInsets.only( + left: 50, + top: 16, + bottom: 6, + ), + child: BlueTextButton( + text: "Rescan", + onTap: () async { + await showDialog<dynamic>( + context: context, + builder: (context) { + return ConfirmFullRescanDialog( + onConfirm: _attemptRescan, + ); + }, + ); + }, + ), + ), + ], + ), + ), ), ], ), diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 78535c19b..386ea1cd8 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -101,6 +101,7 @@ class _SVG { String get downloadFolder => "assets/svg/folder-down.svg"; String get lock => "assets/svg/lock-keyhole.svg"; String get network => "assets/svg/network-wired.svg"; + String get networkWired => "assets/svg/network-wired-2.svg"; String get addressBook => "assets/svg/address-book.svg"; String get addressBook2 => "assets/svg/address-book2.svg"; String get arrowRotate3 => "assets/svg/rotate-exclamation.svg"; diff --git a/lib/widgets/conditional_parent.dart b/lib/widgets/conditional_parent.dart index 6db50c6e8..757c8f992 100644 --- a/lib/widgets/conditional_parent.dart +++ b/lib/widgets/conditional_parent.dart @@ -4,13 +4,13 @@ class ConditionalParent extends StatelessWidget { const ConditionalParent({ Key? key, required this.condition, - required this.child, required this.builder, + required this.child, }) : super(key: key); final bool condition; - final Widget child; final Widget Function(Widget) builder; + final Widget child; @override Widget build(BuildContext context) { diff --git a/pubspec.yaml b/pubspec.yaml index 29d25cb84..b84135f4a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -237,6 +237,7 @@ flutter: - assets/svg/rotate-exclamation.svg - assets/svg/folder-down.svg - assets/svg/network-wired.svg + - assets/svg/network-wired-2.svg - assets/svg/address-book.svg - assets/svg/address-book2.svg - assets/svg/arrow-right.svg From e0a8f32d69e04c5a6f66670f1f9f1ac0358e746d Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 2 Nov 2022 15:03:14 -0600 Subject: [PATCH 107/426] desktop wallet network settings expanding node cards --- .../sub_widgets/network_info_button.dart | 2 +- .../custom_buttons/blue_text_button.dart | 65 ++-- lib/widgets/node_card.dart | 360 ++++++++++++++---- 3 files changed, 329 insertions(+), 98 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart index 37cd414c7..5dc8c4723 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart @@ -157,7 +157,7 @@ class _NetworkInfoButtonState extends ConsumerState<NetworkInfoButton> { showDialog<void>( context: context, builder: (context) => DesktopDialog( - maxHeight: 600, + maxHeight: MediaQuery.of(context).size.height - 64, maxWidth: 580, child: Column( mainAxisSize: MainAxisSize.min, diff --git a/lib/widgets/custom_buttons/blue_text_button.dart b/lib/widgets/custom_buttons/blue_text_button.dart index 18757ab93..aa7f75b1f 100644 --- a/lib/widgets/custom_buttons/blue_text_button.dart +++ b/lib/widgets/custom_buttons/blue_text_button.dart @@ -5,11 +5,16 @@ import 'package:stackwallet/providers/ui/color_theme_provider.dart'; import 'package:stackwallet/utilities/text_styles.dart'; class BlueTextButton extends ConsumerStatefulWidget { - const BlueTextButton({Key? key, required this.text, this.onTap}) - : super(key: key); + const BlueTextButton({ + Key? key, + required this.text, + this.onTap, + this.enabled = true, + }) : super(key: key); final String text; final VoidCallback? onTap; + final bool enabled; @override ConsumerState<BlueTextButton> createState() => _BlueTextButtonState(); @@ -17,38 +22,42 @@ class BlueTextButton extends ConsumerStatefulWidget { class _BlueTextButtonState extends ConsumerState<BlueTextButton> with SingleTickerProviderStateMixin { - late AnimationController controller; - late Animation<dynamic> animation; + AnimationController? controller; + Animation<dynamic>? animation; late Color color; @override void initState() { - color = ref.read(colorThemeProvider.state).state.buttonTextBorderless; - controller = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 100), - ); - animation = ColorTween( - begin: ref.read(colorThemeProvider.state).state.buttonTextBorderless, - end: ref - .read(colorThemeProvider.state) - .state - .buttonTextBorderless - .withOpacity(0.4), - ).animate(controller); + if (widget.enabled) { + color = ref.read(colorThemeProvider.state).state.buttonTextBorderless; + controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 100), + ); + animation = ColorTween( + begin: ref.read(colorThemeProvider.state).state.buttonTextBorderless, + end: ref + .read(colorThemeProvider.state) + .state + .buttonTextBorderless + .withOpacity(0.4), + ).animate(controller!); - animation.addListener(() { - setState(() { - color = animation.value as Color; + animation!.addListener(() { + setState(() { + color = animation!.value as Color; + }); }); - }); + } else { + color = ref.read(colorThemeProvider.state).state.textSubtitle1; + } super.initState(); } @override void dispose() { - controller.dispose(); + controller?.dispose(); super.dispose(); } @@ -59,11 +68,13 @@ class _BlueTextButtonState extends ConsumerState<BlueTextButton> text: TextSpan( text: widget.text, style: STextStyles.link2(context).copyWith(color: color), - recognizer: TapGestureRecognizer() - ..onTap = () { - widget.onTap?.call(); - controller.forward().then((value) => controller.reverse()); - }, + recognizer: widget.enabled + ? (TapGestureRecognizer() + ..onTap = () { + widget.onTap?.call(); + controller?.forward().then((value) => controller?.reverse()); + }) + : null, ), ); } diff --git a/lib/widgets/node_card.dart b/lib/widgets/node_card.dart index 1f0287013..bf9d2746e 100644 --- a/lib/widgets/node_card.dart +++ b/lib/widgets/node_card.dart @@ -1,15 +1,31 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/electrumx_rpc/electrumx.dart'; +import 'package:stackwallet/models/node_model.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; +import 'package:stackwallet/utilities/enums/sync_type_enum.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/test_epic_box_connection.dart'; +import 'package:stackwallet/utilities/test_monero_node_connection.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/expandable.dart'; import 'package:stackwallet/widgets/node_options_sheet.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:tuple/tuple.dart'; class NodeCard extends ConsumerStatefulWidget { const NodeCard({ @@ -30,6 +46,125 @@ class NodeCard extends ConsumerStatefulWidget { class _NodeCardState extends ConsumerState<NodeCard> { String _status = "Disconnected"; late final String nodeId; + bool _advancedIsExpanded = true; + + Future<void> _notifyWalletsOfUpdatedNode(WidgetRef ref) async { + final managers = ref + .read(walletsChangeNotifierProvider) + .managers + .where((e) => e.coin == widget.coin); + final prefs = ref.read(prefsChangeNotifierProvider); + + switch (prefs.syncType) { + case SyncingType.currentWalletOnly: + for (final manager in managers) { + if (manager.isActiveWallet) { + manager.updateNode(true); + } else { + manager.updateNode(false); + } + } + break; + case SyncingType.selectedWalletsAtStartup: + final List<String> walletIdsToSync = prefs.walletIdsSyncOnStartup; + for (final manager in managers) { + if (walletIdsToSync.contains(manager.walletId)) { + manager.updateNode(true); + } else { + manager.updateNode(false); + } + } + break; + case SyncingType.allWalletsOnStartup: + for (final manager in managers) { + manager.updateNode(true); + } + break; + } + } + + Future<bool> _testConnection( + NodeModel node, + BuildContext context, + WidgetRef ref, + ) async { + bool testPassed = false; + + switch (widget.coin) { + case Coin.epicCash: + try { + final String uriString = "${node.host}:${node.port}/v1/version"; + + testPassed = await testEpicBoxNodeConnection(Uri.parse(uriString)); + } catch (e, s) { + Logging.instance.log("$e\n$s", level: LogLevel.Warning); + } + break; + + case Coin.monero: + case Coin.wownero: + try { + final uri = Uri.parse(node.host); + if (uri.scheme.startsWith("http")) { + final String path = uri.path.isEmpty ? "/json_rpc" : uri.path; + + String uriString = "${uri.scheme}://${uri.host}:${node.port}$path"; + + testPassed = await testMoneroNodeConnection(Uri.parse(uriString)); + } + } catch (e, s) { + Logging.instance.log("$e\n$s", level: LogLevel.Warning); + } + + break; + + case Coin.bitcoin: + case Coin.litecoin: + case Coin.dogecoin: + case Coin.firo: + case Coin.bitcoinTestNet: + case Coin.firoTestNet: + case Coin.dogecoinTestNet: + case Coin.bitcoincash: + case Coin.litecoinTestNet: + case Coin.namecoin: + case Coin.bitcoincashTestnet: + final client = ElectrumX( + host: node.host, + port: node.port, + useSSL: node.useSSL, + failovers: [], + prefs: ref.read(prefsChangeNotifierProvider), + ); + + try { + testPassed = await client.ping(); + } catch (_) { + testPassed = false; + } + + break; + } + + if (testPassed) { + // showFloatingFlushBar( + // type: FlushBarType.success, + // message: "Server ping success", + // context: context, + // ); + } else { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + iconAsset: Assets.svg.circleAlert, + message: "Could not connect to node", + context: context, + ), + ); + } + + return testPassed; + } @override void initState() { @@ -50,91 +185,176 @@ class _NodeCardState extends ConsumerState<NodeCard> { _status = "Disconnected"; } + final isDesktop = Util.isDesktop; + return RoundedWhiteContainer( padding: const EdgeInsets.all(0), - child: RawMaterialButton( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - showModalBottomSheet<dynamic>( - backgroundColor: Colors.transparent, - context: context, - builder: (_) => NodeOptionsSheet( - nodeId: nodeId, - coin: widget.coin, - popBackToRoute: widget.popBackToRoute, + borderColor: isDesktop + ? Theme.of(context).extension<StackColors>()!.background + : null, + child: ConditionalParent( + condition: !isDesktop, + builder: (child) { + return RawMaterialButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), + onPressed: () { + showModalBottomSheet<void>( + backgroundColor: Colors.transparent, + context: context, + builder: (_) => NodeOptionsSheet( + nodeId: nodeId, + coin: widget.coin, + popBackToRoute: widget.popBackToRoute, + ), + ); + }, + child: child, ); }, - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - Container( - width: 24, - height: 24, - decoration: BoxDecoration( - color: _node.name == DefaultNodes.defaultName - ? Theme.of(context) - .extension<StackColors>()! - .buttonBackSecondary - : Theme.of(context) - .extension<StackColors>()! - .infoItemIcons - .withOpacity(0.2), - borderRadius: BorderRadius.circular(100), + child: ConditionalParent( + condition: isDesktop, + builder: (child) { + return Expandable( + onExpandChanged: (state) { + setState(() { + _advancedIsExpanded = state == ExpandableState.expanded; + }); + }, + header: child, + body: Padding( + padding: const EdgeInsets.only( + bottom: 24, ), - child: Center( - child: SvgPicture.asset( - Assets.svg.node, - height: 11, - width: 14, + child: Row( + children: [ + const SizedBox( + width: 66, + ), + BlueTextButton( + text: "Connect", + enabled: _status == "Disconnected", + onTap: () async { + final canConnect = + await _testConnection(_node, context, ref); + if (!canConnect) { + return; + } + + await ref + .read(nodeServiceChangeNotifierProvider) + .setPrimaryNodeFor( + coin: widget.coin, + node: _node, + shouldNotifyListeners: true, + ); + + await _notifyWalletsOfUpdatedNode(ref); + }, + ), + const SizedBox( + width: 48, + ), + BlueTextButton( + text: "Details", + onTap: () { + Navigator.of(context).pushNamed( + NodeDetailsView.routeName, + arguments: Tuple3( + widget.coin, + widget.nodeId, + widget.popBackToRoute, + ), + ); + }, + ), + ], + ), + ), + ); + }, + child: Padding( + padding: EdgeInsets.all(isDesktop ? 16 : 12), + child: Row( + children: [ + Container( + width: isDesktop ? 40 : 24, + height: isDesktop ? 40 : 24, + decoration: BoxDecoration( color: _node.name == DefaultNodes.defaultName ? Theme.of(context) .extension<StackColors>()! - .accentColorDark + .buttonBackSecondary : Theme.of(context) .extension<StackColors>()! - .infoItemIcons, + .infoItemIcons + .withOpacity(0.2), + borderRadius: BorderRadius.circular(100), + ), + child: Center( + child: SvgPicture.asset( + Assets.svg.node, + height: isDesktop ? 18 : 11, + width: isDesktop ? 20 : 14, + color: _node.name == DefaultNodes.defaultName + ? Theme.of(context) + .extension<StackColors>()! + .accentColorDark + : Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + ), ), ), - ), - const SizedBox( - width: 12, - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - _node.name, - style: STextStyles.titleBold12(context), + const SizedBox( + width: 12, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _node.name, + style: STextStyles.titleBold12(context), + ), + const SizedBox( + height: 2, + ), + Text( + _status, + style: STextStyles.label(context), + ), + ], + ), + const Spacer(), + if (!isDesktop) + SvgPicture.asset( + Assets.svg.network, + color: _status == "Connected" + ? Theme.of(context) + .extension<StackColors>()! + .accentColorGreen + : Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + width: 20, + height: 20, ), - const SizedBox( - height: 2, - ), - Text( - _status, - style: STextStyles.label(context), - ), - ], - ), - const Spacer(), - SvgPicture.asset( - Assets.svg.network, - color: _status == "Connected" - ? Theme.of(context) + if (isDesktop) + SvgPicture.asset( + _advancedIsExpanded + ? Assets.svg.chevronDown + : Assets.svg.chevronUp, + width: 12, + height: 6, + color: Theme.of(context) .extension<StackColors>()! - .accentColorGreen - : Theme.of(context) - .extension<StackColors>()! - .buttonBackSecondary, - width: 20, - height: 20, - ), - ], + .textSubtitle1, + ), + ], + ), ), ), ), From d8eb43f4e8656d56a3cc7c48eca03a6ef44b7330 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 2 Nov 2022 16:13:14 -0600 Subject: [PATCH 108/426] tx details mobile fix --- .../transaction_details_view.dart | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart index d1e415b26..6f23f2e01 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart @@ -25,6 +25,7 @@ import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; @@ -260,11 +261,19 @@ class _TransactionDetailsViewState bottom: 32, ) : const EdgeInsets.all(0), - child: RoundedWhiteContainer( - borderColor: isDesktop - ? Theme.of(context).extension<StackColors>()!.background - : null, - padding: const EdgeInsets.all(0), + child: ConditionalParent( + condition: isDesktop, + builder: (child) { + return RoundedWhiteContainer( + borderColor: isDesktop + ? Theme.of(context) + .extension<StackColors>()! + .background + : null, + padding: const EdgeInsets.all(0), + child: child, + ); + }, child: SingleChildScrollView( primary: isDesktop ? false : null, child: Padding( From 0c4cb961d6241b2318000ec0cbbccb4cbd3bccc1 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Wed, 2 Nov 2022 17:18:22 -0600 Subject: [PATCH 109/426] WIP: currency popup --- .../currency_settings/currency_dialog.dart | 371 ++++++++++++++++++ .../currency_settings/currency_settings.dart | 117 ++++++ 2 files changed, 488 insertions(+) create mode 100644 lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_dialog.dart create mode 100644 lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart diff --git a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_dialog.dart b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_dialog.dart new file mode 100644 index 000000000..aabc81cd1 --- /dev/null +++ b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_dialog.dart @@ -0,0 +1,371 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/providers/global/base_currencies_provider.dart'; +import 'package:stackwallet/providers/global/prefs_provider.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.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/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; + +class CurrencyDialog extends ConsumerStatefulWidget { + const CurrencyDialog({Key? key}) : super(key: key); + + @override + ConsumerState<CurrencyDialog> createState() => _CurrencyDialog(); +} + +class _CurrencyDialog extends ConsumerState<CurrencyDialog> { + late String current; + late List<String> currenciesWithoutSelected; + + late final TextEditingController searchCurrencyController; + + late final FocusNode searchCurrencyFocusNode; + + void onTap(int index) { + if (currenciesWithoutSelected[index] == current || current.isEmpty) { + // ignore if already selected currency + return; + } + current = currenciesWithoutSelected[index]; + currenciesWithoutSelected.remove(current); + currenciesWithoutSelected.insert(0, current); + ref.read(prefsChangeNotifierProvider).currency = current; + } + + BorderRadius? _borderRadius(int index) { + if (index == 0 && currenciesWithoutSelected.length == 1) { + return BorderRadius.circular( + Constants.size.circularBorderRadius, + ); + } else if (index == 0) { + return BorderRadius.vertical( + top: Radius.circular( + Constants.size.circularBorderRadius, + ), + ); + } else if (index == currenciesWithoutSelected.length - 1) { + return BorderRadius.vertical( + bottom: Radius.circular( + Constants.size.circularBorderRadius, + ), + ); + } + return null; + } + + String filter = ""; + + List<String> _filtered() { + final currencyMap = ref.read(baseCurrenciesProvider).map; + return currenciesWithoutSelected.where((element) { + return element.toLowerCase().contains(filter.toLowerCase()) || + (currencyMap[element]?.toLowerCase().contains(filter.toLowerCase()) ?? + false); + }).toList(); + } + + @override + void initState() { + searchCurrencyController = TextEditingController(); + + searchCurrencyFocusNode = FocusNode(); + + super.initState(); + } + + @override + void dispose() { + searchCurrencyController.dispose(); + + searchCurrencyFocusNode.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + current = ref + .watch(prefsChangeNotifierProvider.select((value) => value.language)); + + currenciesWithoutSelected = ref + .watch(baseCurrenciesProvider.select((value) => value.map)) + .keys + .toList(); + if (current.isNotEmpty) { + currenciesWithoutSelected.remove(current); + currenciesWithoutSelected.insert(0, current); + } + currenciesWithoutSelected = _filtered(); + + return DesktopDialog( + maxHeight: 800, + maxWidth: 600, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(32), + child: Text( + "Select currency", + style: STextStyles.desktopH3(context), + textAlign: TextAlign.center, + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + flex: 24, + child: NestedScrollView( + floatHeaderSlivers: true, + headerSliverBuilder: (context, innerBoxIsScrolled) { + return [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor( + context), + sliver: SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 16, horizontal: 32), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: + Util.isDesktop ? false : true, + controller: searchCurrencyController, + focusNode: searchCurrencyFocusNode, + onChanged: (newString) { + setState(() => filter = newString); + }, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + searchCurrencyFocusNode, + context, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), + ), + suffixIcon: searchCurrencyController + .text.isNotEmpty + ? Padding( + padding: + const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + searchCurrencyController + .text = ""; + filter = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ]; + }, + body: Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor( + context, + ), + ), + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .popupBG, + borderRadius: _borderRadius(index), + ), + child: Padding( + padding: const EdgeInsets.all(4), + key: Key( + "desktopSettingsCurrencySelect_${currenciesWithoutSelected[index]}"), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32), + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: currenciesWithoutSelected[index] == + current + ? Theme.of(context) + .extension<StackColors>()! + .currencyListItemBG + : Theme.of(context) + .extension<StackColors>()! + .popupBG, + child: RawMaterialButton( + onPressed: () async { + onTap(index); + }, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + materialTapTargetSize: + MaterialTapTargetSize + .shrinkWrap, + value: true, + groupValue: + currenciesWithoutSelected[ + index] == + current, + onChanged: (_) { + onTap(index); + }, + ), + ), + const SizedBox( + width: 12, + ), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + currenciesWithoutSelected[ + index], + key: (currenciesWithoutSelected[ + index] == + current) + ? const Key( + "desktopSettingsSelectedCurrencyText") + : null, + style: + STextStyles.largeMedium14( + context), + ), + const SizedBox( + height: 2, + ), + Text( + ref.watch(baseCurrenciesProvider + .select((value) => + value.map))[ + currenciesWithoutSelected[ + index]] ?? + "", + key: (currenciesWithoutSelected[ + index] == + current) + ? const Key( + "desktopSelectedCurrencyTextDescription") + : null, + style: + STextStyles.itemSubtitle( + context), + ), + ], + ), + ], + ), + ), + ), + ), + ), + ), + ); + }, + childCount: currenciesWithoutSelected.length, + ), + ), + ], + ); + }, + ), + ), + ), + const Spacer(), + Padding( + padding: const EdgeInsets.all(32), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Save Changes", + onPressed: () {}, + ), + ) + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart new file mode 100644 index 000000000..b8ce76d25 --- /dev/null +++ b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter_riverpod/flutter_riverpod.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/rounded_white_container.dart'; + +import 'currency_dialog.dart'; + +class CurrencySettings extends ConsumerStatefulWidget { + const CurrencySettings({Key? key}) : super(key: key); + + static const String routeName = "/settingsMenuCurrency"; + + @override + ConsumerState<CurrencySettings> createState() => _CurrencySettings(); +} + +class _CurrencySettings extends ConsumerState<CurrencySettings> { + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + return Column( + children: [ + Padding( + padding: const EdgeInsets.only( + right: 30, + ), + child: RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset( + Assets.svg.circleDollarSign, + width: 48, + height: 48, + ), + Center( + child: Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.start, + text: TextSpan( + children: [ + TextSpan( + text: "Currency", + style: STextStyles.desktopTextSmall(context), + ), + TextSpan( + text: + "\n\nProtect your Stack Wallet with a strong password. Stack Wallet does not store " + "your password, and is therefore NOT able to restore it. Keep your password safe and secure.", + style: + STextStyles.desktopTextExtraExtraSmall(context), + ), + ], + ), + ), + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Padding( + padding: EdgeInsets.all( + 10, + ), + child: NewPasswordButton(), + ), + ], + ), + ], + ), + ), + ), + ], + ); + } +} + +class NewPasswordButton extends ConsumerWidget { + const NewPasswordButton({ + Key? key, + }) : super(key: key); + @override + Widget build(BuildContext context, WidgetRef ref) { + Future<void> chooseCurrency() async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return CurrencyDialog(); + }, + ); + } + + return SizedBox( + width: 200, + height: 48, + child: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + onPressed: () { + chooseCurrency(); + }, + child: Text( + "Change currency", + style: STextStyles.button(context), + ), + ), + ); + } +} From 939ccc69116ad71de4a97d2d69f28d353d957482 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Wed, 2 Nov 2022 17:19:12 -0600 Subject: [PATCH 110/426] created currency directory --- lib/pages_desktop_specific/home/desktop_settings_view.dart | 2 +- lib/route_generator.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages_desktop_specific/home/desktop_settings_view.dart b/lib/pages_desktop_specific/home/desktop_settings_view.dart index 9b3f897a9..63e1ee613 100644 --- a/lib/pages_desktop_specific/home/desktop_settings_view.dart +++ b/lib/pages_desktop_specific/home/desktop_settings_view.dart @@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/appearance_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart'; -import 'package:stackwallet/pages_desktop_specific/home/settings_menu/currency_settings.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/nodes_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/security_settings.dart'; diff --git a/lib/route_generator.dart b/lib/route_generator.dart index c5b764995..d5a38c0fd 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -94,7 +94,7 @@ import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_vie import 'package:stackwallet/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/appearance_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart'; -import 'package:stackwallet/pages_desktop_specific/home/settings_menu/currency_settings.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/nodes_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/security_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/settings_menu.dart'; From c1d81b52a5946f5acca3179e393745a757c81b25 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Wed, 2 Nov 2022 17:20:21 -0600 Subject: [PATCH 111/426] added flex and removed commented code --- .../backup_and_restore/create_auto_backup.dart | 14 -------------- .../language_settings/language_dialog.dart | 1 + 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart index 16bda2ff7..f3e502bcb 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart @@ -40,20 +40,6 @@ class _CreateAutoBackup extends State<CreateAutoBackup> { "Every 30 minutes", ]; - // List<DropdownMenuItem<String>> get dropdownItems { - // List<DropdownMenuItem<String>> menuItems = [ - // const DropdownMenuItem( - // value: "Every 10 minutes", - // child: Text("Every 10 minutes"), - // ), - // const DropdownMenuItem( - // value: "Every 20 minutes", - // child: Text("Every 20 minutes"), - // ), - // ]; - // return menuItems; - // } - @override void initState() { fileLocationController = TextEditingController(); diff --git a/lib/pages_desktop_specific/home/settings_menu/language_settings/language_dialog.dart b/lib/pages_desktop_specific/home/settings_menu/language_settings/language_dialog.dart index f68196710..d07c9729f 100644 --- a/lib/pages_desktop_specific/home/settings_menu/language_settings/language_dialog.dart +++ b/lib/pages_desktop_specific/home/settings_menu/language_settings/language_dialog.dart @@ -125,6 +125,7 @@ class _LanguageDialog extends ConsumerState<LanguageDialog> { ], ), Expanded( + flex: 24, child: NestedScrollView( floatHeaderSlivers: true, headerSliverBuilder: (context, innerBoxIsScrolled) { From 75d9ca6912654c2634b5c5366bfcd503db1177bd Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 2 Nov 2022 16:24:55 -0600 Subject: [PATCH 112/426] desktop network icon size fix --- .../wallet_network_settings_view.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart index fdb73fc88..322d67d74 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart @@ -481,8 +481,8 @@ class _WalletNetworkSettingsViewState child: Center( child: SvgPicture.asset( Assets.svg.radio, - height: 14, - width: 14, + height: isDesktop ? 19 : 14, + width: isDesktop ? 19 : 14, color: Theme.of(context) .extension<StackColors>()! .accentColorGreen, From 9186be7fb644cf9516388263df503fb12f1d544c Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 2 Nov 2022 17:29:26 -0600 Subject: [PATCH 113/426] WIP: desktop dialog transitions --- .../wallet_network_settings_view.dart | 21 ++- .../sub_widgets/network_info_button.dart | 126 +++++++++++++----- .../unlock_wallet_keys_desktop.dart | 59 +++++--- .../sub_widgets/wallet_keys_button.dart | 24 +++- lib/route_generator.dart | 89 +++++++++++-- 5 files changed, 248 insertions(+), 71 deletions(-) diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart index 322d67d74..accf244eb 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart @@ -10,6 +10,7 @@ import 'package:stackwallet/pages/settings_views/sub_widgets/nodes_list.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/sub_widgets/confirm_full_rescan.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/sub_widgets/rescanning_dialog.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart'; import 'package:stackwallet/services/coins/monero/monero_wallet.dart'; import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart'; @@ -875,14 +876,22 @@ class _WalletNetworkSettingsViewState child: BlueTextButton( text: "Rescan", onTap: () async { - await showDialog<dynamic>( - context: context, - builder: (context) { - return ConfirmFullRescanDialog( + await Navigator.of(context).push( + FadePageRoute<void>( + ConfirmFullRescanDialog( onConfirm: _attemptRescan, - ); - }, + ), + const RouteSettings(), + ), ); + // await showDialog<dynamic>( + // context: context, + // builder: (context) { + // return ConfirmFullRescanDialog( + // onConfirm: _attemptRescan, + // ); + // }, + // ); }, ), ), diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart index 5dc8c4723..c001f9bf3 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart @@ -6,6 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; @@ -154,44 +155,103 @@ class _NetworkInfoButtonState extends ConsumerState<NetworkInfoButton> { return GestureDetector( onTap: () { if (Util.isDesktop) { + // showDialog<void>( + // context: context, + // builder: (context) => DesktopDialog( + // maxHeight: MediaQuery.of(context).size.height - 64, + // maxWidth: 580, + // child: Column( + // mainAxisSize: MainAxisSize.min, + // children: [ + // Padding( + // padding: const EdgeInsets.only( + // left: 32, + // ), + // child: Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // Text( + // "Network", + // style: STextStyles.desktopH3(context), + // ), + // const DesktopDialogCloseButton(), + // ], + // ), + // ), + // Padding( + // padding: const EdgeInsets.only( + // top: 16, + // left: 32, + // right: 32, + // bottom: 32, + // ), + // child: WalletNetworkSettingsView( + // walletId: walletId, + // initialSyncStatus: _currentSyncStatus, + // initialNodeStatus: _currentNodeStatus, + // ), + // ), + // ], + // ), + // ), + // ); + showDialog<void>( context: context, - builder: (context) => DesktopDialog( - maxHeight: MediaQuery.of(context).size.height - 64, - maxWidth: 580, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, + builder: (context) => Navigator( + initialRoute: WalletNetworkSettingsView.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + FadePageRoute( + DesktopDialog( + maxHeight: MediaQuery.of(context).size.height - 64, + maxWidth: 580, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Network", + style: STextStyles.desktopH3(context), + ), + DesktopDialogCloseButton( + onPressedOverride: Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only( + top: 16, + left: 32, + right: 32, + bottom: 32, + ), + child: WalletNetworkSettingsView( + walletId: walletId, + initialSyncStatus: _currentSyncStatus, + initialNodeStatus: _currentNodeStatus, + ), + ), + ], + ), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Network", - style: STextStyles.desktopH3(context), - ), - const DesktopDialogCloseButton(), - ], + const RouteSettings( + name: WalletNetworkSettingsView.routeName, ), ), - Padding( - padding: const EdgeInsets.only( - top: 16, - left: 32, - right: 32, - bottom: 32, - ), - child: WalletNetworkSettingsView( - walletId: walletId, - initialSyncStatus: _currentSyncStatus, - initialNodeStatus: _currentNodeStatus, - ), - ), - ], - ), + ]; + }, ), ); } else { diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart index 37f047eed..e65820737 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart @@ -3,7 +3,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart'; import 'package:stackwallet/providers/providers.dart'; -import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -22,6 +21,8 @@ class UnlockWalletKeysDesktop extends ConsumerStatefulWidget { final String walletId; + static const String routeName = "/desktopUnlockWalletKeys"; + @override ConsumerState<UnlockWalletKeysDesktop> createState() => _UnlockWalletKeysDesktopState(); @@ -59,8 +60,13 @@ class _UnlockWalletKeysDesktopState children: [ Row( mainAxisAlignment: MainAxisAlignment.end, - children: const [ - DesktopDialogCloseButton(), + children: [ + DesktopDialogCloseButton( + onPressedOverride: Navigator.of( + context, + rootNavigator: true, + ).pop, + ), ], ), const SizedBox( @@ -175,7 +181,10 @@ class _UnlockWalletKeysDesktopState Expanded( child: SecondaryButton( label: "Cancel", - onPressed: Navigator.of(context).pop, + onPressed: Navigator.of( + context, + rootNavigator: true, + ).pop, ), ), const SizedBox( @@ -188,29 +197,35 @@ class _UnlockWalletKeysDesktopState onPressed: continueEnabled ? () async { // todo: check password - Navigator.of(context).pop(); + // Navigator.of(context).pop(); final words = await ref .read(walletsChangeNotifierProvider) .getManager(widget.walletId) .mnemonic; - await showDialog<void>( - context: context, - barrierDismissible: false, - builder: (context) => Navigator( - initialRoute: WalletKeysDesktopPopup.routeName, - onGenerateRoute: RouteGenerator.generateRoute, - onGenerateInitialRoutes: (_, __) { - return [ - RouteGenerator.generateRoute( - RouteSettings( - name: WalletKeysDesktopPopup.routeName, - arguments: words, - ), - ) - ]; - }, - ), + + await Navigator.of(context).pushReplacementNamed( + WalletKeysDesktopPopup.routeName, + arguments: words, ); + // + // await showDialog<void>( + // context: context, + // barrierDismissible: false, + // builder: (context) => Navigator( + // initialRoute: WalletKeysDesktopPopup.routeName, + // onGenerateRoute: RouteGenerator.generateRoute, + // onGenerateInitialRoutes: (_, __) { + // return [ + // RouteGenerator.generateRoute( + // RouteSettings( + // name: WalletKeysDesktopPopup.routeName, + // arguments: words, + // ), + // ) + // ]; + // }, + // ), + // ); } : null, ), diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_button.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_button.dart index d4921276d..649433d52 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_button.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_button.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart'; +import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -20,10 +21,29 @@ class WalletKeysButton extends StatelessWidget { showDialog<void>( context: context, barrierDismissible: false, - builder: (context) => UnlockWalletKeysDesktop( - walletId: walletId, + builder: (context) => Navigator( + initialRoute: UnlockWalletKeysDesktop.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + RouteGenerator.generateRoute( + RouteSettings( + name: UnlockWalletKeysDesktop.routeName, + arguments: walletId, + ), + ) + ]; + }, ), ); + + // showDialog<void>( + // context: context, + // barrierDismissible: false, + // builder: (context) => UnlockWalletKeysDesktop( + // walletId: walletId, + // ), + // ); }, child: Container( color: Colors.transparent, diff --git a/lib/route_generator.dart b/lib/route_generator.dart index d5a38c0fd..a6f23ffdc 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -106,6 +106,7 @@ import 'package:stackwallet/utilities/enums/add_wallet_type_enum.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:tuple/tuple.dart'; +import 'pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart'; import 'pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart'; class RouteGenerator { @@ -1085,29 +1086,67 @@ class RouteGenerator { case WalletKeysDesktopPopup.routeName: if (args is List<String>) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => WalletKeysDesktopPopup( + return FadePageRoute( + WalletKeysDesktopPopup( words: args, ), - settings: RouteSettings( + RouteSettings( name: settings.name, ), ); + // return getRoute( + // shouldUseMaterialRoute: useMaterialPageRoute, + // builder: (_) => WalletKeysDesktopPopup( + // words: args, + // ), + // settings: RouteSettings( + // name: settings.name, + // ), + // ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case UnlockWalletKeysDesktop.routeName: + if (args is String) { + return FadePageRoute( + UnlockWalletKeysDesktop( + walletId: args, + ), + RouteSettings( + name: settings.name, + ), + ); + // return getRoute( + // shouldUseMaterialRoute: useMaterialPageRoute, + // builder: (_) => WalletKeysDesktopPopup( + // words: args, + // ), + // settings: RouteSettings( + // name: settings.name, + // ), + // ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); case QRCodeDesktopPopupContent.routeName: if (args is String) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => QRCodeDesktopPopupContent( + return FadePageRoute( + QRCodeDesktopPopupContent( value: args, ), - settings: RouteSettings( + RouteSettings( name: settings.name, ), ); + // return getRoute( + // shouldUseMaterialRoute: useMaterialPageRoute, + // builder: (_) => QRCodeDesktopPopupContent( + // value: args, + // ), + // settings: RouteSettings( + // name: settings.name, + // ), + // ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1181,3 +1220,37 @@ class RouteGenerator { builder: (_) => errorView); } } + +class FadePageRoute<T> extends PageRoute<T> { + FadePageRoute(this.child, RouteSettings settings) : _settings = settings; + + final Widget child; + final RouteSettings _settings; + + @override + Color? get barrierColor => null; + + @override + String? get barrierLabel => null; + + @override + Widget buildPage( + BuildContext context, + Animation<double> animation, + Animation<double> secondaryAnimation, + ) { + return FadeTransition( + opacity: animation, + child: child, + ); + } + + @override + bool get maintainState => true; + + @override + Duration get transitionDuration => const Duration(milliseconds: 100); + + @override + RouteSettings get settings => _settings; +} From 9432ee89bf480cf25aab0fa23df24c84b688fd9b Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 2 Nov 2022 17:47:24 -0600 Subject: [PATCH 114/426] node card test fix --- test/widget_tests/node_card_test.dart | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/test/widget_tests/node_card_test.dart b/test/widget_tests/node_card_test.dart index 2728fc304..22e0661bf 100644 --- a/test/widget_tests/node_card_test.dart +++ b/test/widget_tests/node_card_test.dart @@ -1,15 +1,16 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/theme/light_colors.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; -import 'package:stackwallet/services/node_service.dart'; +import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/node_card.dart'; import 'package:stackwallet/widgets/node_options_sheet.dart'; @@ -190,13 +191,22 @@ void main() { await tester.tap(find.byType(NodeCard)); await tester.pumpAndSettle(); - expect(find.text("Connect"), findsOneWidget); - expect(find.text("Details"), findsOneWidget); - expect(find.byType(NodeOptionsSheet), findsOneWidget); - expect(find.byType(Text), findsNWidgets(7)); + if (Util.isDesktop) { + expect(find.text("Connect"), findsNothing); + expect(find.text("Details"), findsNothing); + + verify(nodeService.getPrimaryNodeFor(coin: Coin.bitcoin)).called(1); + verify(nodeService.getNodeById(id: "node id")).called(1); + } else { + expect(find.text("Connect"), findsOneWidget); + expect(find.text("Details"), findsOneWidget); + expect(find.byType(NodeOptionsSheet), findsOneWidget); + expect(find.byType(Text), findsNWidgets(7)); + + verify(nodeService.getPrimaryNodeFor(coin: Coin.bitcoin)).called(2); + verify(nodeService.getNodeById(id: "node id")).called(2); + } - verify(nodeService.getPrimaryNodeFor(coin: Coin.bitcoin)).called(2); - verify(nodeService.getNodeById(id: "node id")).called(2); verify(nodeService.addListener(any)).called(1); verifyNoMoreInteractions(nodeService); From 40a6e916f2222c10b82909c3443d6649081a7987 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 2 Nov 2022 17:47:38 -0600 Subject: [PATCH 115/426] currency dialog fix --- .../home/settings_menu/currency_settings/currency_dialog.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_dialog.dart b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_dialog.dart index aabc81cd1..bbe98c1af 100644 --- a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_dialog.dart +++ b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_dialog.dart @@ -96,7 +96,7 @@ class _CurrencyDialog extends ConsumerState<CurrencyDialog> { @override Widget build(BuildContext context) { current = ref - .watch(prefsChangeNotifierProvider.select((value) => value.language)); + .watch(prefsChangeNotifierProvider.select((value) => value.currency)); currenciesWithoutSelected = ref .watch(baseCurrenciesProvider.select((value) => value.map)) From 424852136ea9d4cb18f6a4af2062892e7f9353c0 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Wed, 2 Nov 2022 19:00:08 -0600 Subject: [PATCH 116/426] v1.5.13 build 84 --- .../home/settings_menu/currency_settings.dart | 102 ------------------ pubspec.yaml | 2 +- 2 files changed, 1 insertion(+), 103 deletions(-) delete mode 100644 lib/pages_desktop_specific/home/settings_menu/currency_settings.dart diff --git a/lib/pages_desktop_specific/home/settings_menu/currency_settings.dart b/lib/pages_desktop_specific/home/settings_menu/currency_settings.dart deleted file mode 100644 index 4327b320b..000000000 --- a/lib/pages_desktop_specific/home/settings_menu/currency_settings.dart +++ /dev/null @@ -1,102 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/src/widgets/framework.dart'; -import 'package:flutter_riverpod/flutter_riverpod.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/rounded_white_container.dart'; - -class CurrencySettings extends ConsumerStatefulWidget { - const CurrencySettings({Key? key}) : super(key: key); - - static const String routeName = "/settingsMenuCurrency"; - - @override - ConsumerState<CurrencySettings> createState() => _CurrencySettings(); -} - -class _CurrencySettings extends ConsumerState<CurrencySettings> { - @override - Widget build(BuildContext context) { - debugPrint("BUILD: $runtimeType"); - return Column( - children: [ - Padding( - padding: const EdgeInsets.only( - right: 30, - ), - child: RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SvgPicture.asset( - Assets.svg.circleDollarSign, - width: 48, - height: 48, - ), - Center( - child: Padding( - padding: const EdgeInsets.all(10), - child: RichText( - textAlign: TextAlign.start, - text: TextSpan( - children: [ - TextSpan( - text: "Currency", - style: STextStyles.desktopTextSmall(context), - ), - TextSpan( - text: - "\n\nProtect your Stack Wallet with a strong password. Stack Wallet does not store " - "your password, and is therefore NOT able to restore it. Keep your password safe and secure.", - style: - STextStyles.desktopTextExtraExtraSmall(context), - ), - ], - ), - ), - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: const [ - Padding( - padding: EdgeInsets.all( - 10, - ), - child: NewPasswordButton(), - ), - ], - ), - ], - ), - ), - ), - ], - ); - } -} - -class NewPasswordButton extends ConsumerWidget { - const NewPasswordButton({ - Key? key, - }) : super(key: key); - @override - Widget build(BuildContext context, WidgetRef ref) { - return SizedBox( - width: 200, - height: 48, - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () {}, - child: Text( - "Set up new password", - style: STextStyles.button(context), - ), - ), - ); - } -} diff --git a/pubspec.yaml b/pubspec.yaml index b84135f4a..5be35c66e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.5.12+84 +version: 1.5.13+84 environment: sdk: ">=2.17.0 <3.0.0" From 0504f2336c135682ffc8d02d744fcae04d1f5cde Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Wed, 2 Nov 2022 20:07:27 -0600 Subject: [PATCH 117/426] debug popup --- .../advanced_settings/advanced_settings.dart | 25 +- .../advanced_settings/debug_info_dialog.dart | 308 +++++++++++++----- 2 files changed, 245 insertions(+), 88 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart b/lib/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart index 1632576fd..8e89e3f67 100644 --- a/lib/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart @@ -10,6 +10,8 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'debug_info_dialog.dart'; + class AdvancedSettings extends ConsumerStatefulWidget { const AdvancedSettings({Key? key}) : super(key: key); @@ -226,16 +228,16 @@ class ShowLogsButton extends ConsumerWidget { }) : super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { - // Future<void> viewDebugLogs() async { - // await showDialog<dynamic>( - // context: context, - // useSafeArea: false, - // barrierDismissible: true, - // builder: (context) { - // return const DebugInfoDialog(); - // }, - // ); - // } + Future<void> viewDebugLogs() async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return const DebugInfoDialog(); + }, + ); + } return SizedBox( width: 101, @@ -245,8 +247,7 @@ class ShowLogsButton extends ConsumerWidget { .extension<StackColors>()! .getPrimaryEnabledButtonColor(context), onPressed: () { - // - // viewDebugLogs(); + viewDebugLogs(); }, child: Text( "Show logs", diff --git a/lib/pages_desktop_specific/home/settings_menu/advanced_settings/debug_info_dialog.dart b/lib/pages_desktop_specific/home/settings_menu/advanced_settings/debug_info_dialog.dart index 342d9180a..0406a059f 100644 --- a/lib/pages_desktop_specific/home/settings_menu/advanced_settings/debug_info_dialog.dart +++ b/lib/pages_desktop_specific/home/settings_menu/advanced_settings/debug_info_dialog.dart @@ -1,29 +1,33 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/models/isar/models/log.dart'; +import 'package:stackwallet/providers/global/debug_service_provider.dart'; +import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/log_level_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.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/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; -// import '../../../utilities/assets.dart'; -// import '../../../utilities/util.dart'; -// import '../../../widgets/icon_widgets/x_icon.dart'; -// import '../../../widgets/stack_text_field.dart'; -// import '../../../widgets/textfield_icon_button.dart'; - -class DebugInfoDialog extends StatefulWidget { +class DebugInfoDialog extends ConsumerStatefulWidget { const DebugInfoDialog({Key? key}) : super(key: key); @override - State<StatefulWidget> createState() => _DebugInfoDialog(); + ConsumerState<DebugInfoDialog> createState() => _DebugInfoDialog(); } -class _DebugInfoDialog extends State<DebugInfoDialog> { - final _searchController = TextEditingController(); - final _searchFocusNode = FocusNode(); +class _DebugInfoDialog extends ConsumerState<DebugInfoDialog> { + late final TextEditingController searchDebugController; + late final FocusNode searchDebugFocusNode; final scrollController = ScrollController(); @@ -62,15 +66,19 @@ class _DebugInfoDialog extends State<DebugInfoDialog> { @override void initState() { - // ref.read(debugServiceProvider).updateRecentLogs(); + searchDebugController = TextEditingController(); + searchDebugFocusNode = FocusNode(); + + ref.read(debugServiceProvider).updateRecentLogs(); super.initState(); } @override void dispose() { - _searchController.dispose(); + searchDebugFocusNode.dispose(); + searchDebugController.dispose(); + scrollController.dispose(); - _searchFocusNode.dispose(); super.dispose(); } @@ -78,7 +86,7 @@ class _DebugInfoDialog extends State<DebugInfoDialog> { @override Widget build(BuildContext context) { return DesktopDialog( - maxHeight: 800, + maxHeight: 850, maxWidth: 600, child: Column( children: [ @@ -96,68 +104,216 @@ class _DebugInfoDialog extends State<DebugInfoDialog> { const DesktopDialogCloseButton(), ], ), - Row( - children: [ - // ClipRRect( - // borderRadius: BorderRadius.circular( - // Constants.size.circularBorderRadius, - // ), - // child: TextField( - // key: const Key("desktopSettingDebugInfo"), - // autocorrect: Util.isDesktop ? false : true, - // enableSuggestions: Util.isDesktop ? false : true, - // controller: _searchController, - // focusNode: _searchFocusNode, - // // onChanged: (newString) { - // // setState(() => _searchTerm = newString); - // // }, - // style: STextStyles.field(context), - // decoration: standardInputDecoration( - // "Search", - // _searchFocusNode, - // context, - // ).copyWith( - // prefixIcon: Padding( - // padding: const EdgeInsets.symmetric( - // horizontal: 10, - // vertical: 16, - // ), - // child: SvgPicture.asset( - // Assets.svg.search, - // width: 16, - // height: 16, - // ), - // ), - // suffixIcon: _searchController.text.isNotEmpty - // ? Padding( - // padding: const EdgeInsets.only(right: 0), - // child: UnconstrainedBox( - // child: Row( - // children: [ - // TextFieldIconButton( - // child: const XIcon(), - // onTap: () async { - // setState(() { - // _searchController.text = ""; - // _searchTerm = ""; - // }); - // }, - // ), - // ], - // ), - // ), - // ) - // : null, - // ), - // ), - // ), - ], + Expanded( + flex: 24, + child: NestedScrollView( + floatHeaderSlivers: true, + headerSliverBuilder: (context, innerBoxIsScrolled) { + return [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor( + context), + sliver: SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 16, horizontal: 32), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: + Util.isDesktop ? false : true, + controller: searchDebugController, + focusNode: searchDebugFocusNode, + onChanged: (newString) { + setState(() => _searchTerm = newString); + }, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + searchDebugFocusNode, + context, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), + ), + suffixIcon: searchDebugController + .text.isNotEmpty + ? Padding( + padding: + const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + searchDebugController + .text = ""; + _searchTerm = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + ), + const SizedBox( + height: 12, + ), + ], + ), + ), + ), + ), + ]; + }, + body: Builder( + builder: (context) { + final logs = filtered( + ref.watch(debugServiceProvider + .select((value) => value.recentLogs)), + _searchTerm) + .reversed + .toList(growable: false); + return CustomScrollView( + reverse: true, + // shrinkWrap: true, + controller: scrollController, + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor( + context, + ), + ), + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final log = logs[index]; + + return Container( + key: Key( + "log_${log.id}_${log.timestampInMillisUTC}"), + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .popupBG, + borderRadius: _borderRadius(index, logs.length), + ), + child: Padding( + padding: const EdgeInsets.all(4), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32), + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .popupBG, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + " [${log.logLevel.name}]", + style: STextStyles.baseXS(context) + .copyWith( + fontSize: 8, + color: (log.logLevel == + LogLevel.Info + ? Theme.of(context) + .extension< + StackColors>()! + .topNavIconGreen + : (log.logLevel == + LogLevel.Warning + ? Theme.of(context) + .extension< + StackColors>()! + .topNavIconYellow + : (log.logLevel == + LogLevel.Error + ? Colors.orange + : Theme.of(context) + .extension< + StackColors>()! + .topNavIconRed))), + ), + ), + Text( + "[${DateTime.fromMillisecondsSinceEpoch(log.timestampInMillisUTC, isUtc: true)}]: ", + style: STextStyles.baseXS(context) + .copyWith( + fontSize: 12, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + ), + ), + ], + ), + Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const SizedBox( + width: 20, + ), + Flexible( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + SelectableText( + log.message, + style: STextStyles.baseXS( + context) + .copyWith( + fontSize: 11.5), + ), + ], + ), + ), + ], + ), + ], + ), + ), + ), + ), + ); + }, + childCount: logs.length, + ), + ), + ], + ); + }, + ), + ), ), - // Column( - // children: [ - // - // ], - // ), const Spacer(), Padding( padding: const EdgeInsets.all(32), From c711bb94195ef84038d637b98d29e2b44ca541e4 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Thu, 3 Nov 2022 09:41:10 -0600 Subject: [PATCH 118/426] added search bar to desktop nodes settings --- .../home/settings_menu/nodes_settings.dart | 211 +++++++++++------- 1 file changed, 130 insertions(+), 81 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart b/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart index bb60061d8..5f1e70143 100644 --- a/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart @@ -11,6 +11,9 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; +import '../../../utilities/util.dart'; +import '../../../widgets/stack_text_field.dart'; + class NodesSettings extends ConsumerStatefulWidget { const NodesSettings({Key? key}) : super(key: key); @@ -23,15 +26,27 @@ class NodesSettings extends ConsumerStatefulWidget { class _NodesSettings extends ConsumerState<NodesSettings> { List<Coin> _coins = [...Coin.values]; + late final TextEditingController searchNodeController; + late final FocusNode searchNodeFocusNode; + + String filter = ""; + @override void initState() { _coins = _coins.toList(); _coins.remove(Coin.firoTestNet); + + searchNodeController = TextEditingController(); + searchNodeFocusNode = FocusNode(); + super.initState(); } @override void dispose() { + searchNodeController.dispose(); + searchNodeFocusNode.dispose(); + super.dispose(); } @@ -85,88 +100,122 @@ class _NodesSettings extends ConsumerState<NodesSettings> { ), ], ), - //TODO: add search bar - SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ...coins.map( - (coin) { - final count = ref - .watch(nodeServiceChangeNotifierProvider - .select((value) => value.getNodesFor(coin))) - .length; - - return Padding( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - // side: BorderSide( - // color: Theme.of(context) - // .extension<StackColors>()! - // .shadow), - ), - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - onPressed: () { - Navigator.of(context).pushNamed( - CoinNodesView.routeName, - arguments: coin, - ); - }, - child: Padding( - padding: const EdgeInsets.all( - 12.0, - ), - child: Row( - children: [ - Row( - children: [ - SvgPicture.asset( - Assets.svg.iconFor(coin: coin), - width: 24, - height: 24, - ), - const SizedBox( - width: 12, - ), - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "${coin.prettyName} nodes", - style: STextStyles.titleBold12( - context), - ), - Text( - count > 1 - ? "$count nodes" - : "Default", - style: STextStyles.label(context), - ), - ], - ), - ], - ), - Expanded( - child: SvgPicture.asset( - Assets.svg.chevronRight, - alignment: Alignment.centerRight, - ), - ), - ], - ), - ), - ), - ); - }, + Padding( + padding: const EdgeInsets.all(10.0), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: searchNodeController, + focusNode: searchNodeFocusNode, + onChanged: (newString) { + setState(() => filter = newString); + }, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + searchNodeFocusNode, + context, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), + ), ), - ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(10.0), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ...coins.map( + (coin) { + final count = ref + .watch(nodeServiceChangeNotifierProvider + .select((value) => value.getNodesFor(coin))) + .length; + + return Padding( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + onPressed: () { + Navigator.of(context).pushNamed( + CoinNodesView.routeName, + arguments: coin, + ); + }, + child: Padding( + padding: const EdgeInsets.all( + 12.0, + ), + child: Row( + children: [ + Row( + children: [ + SvgPicture.asset( + Assets.svg.iconFor(coin: coin), + width: 24, + height: 24, + ), + const SizedBox( + width: 12, + ), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "${coin.prettyName} nodes", + style: STextStyles.titleBold12( + context), + ), + Text( + count > 1 + ? "$count nodes" + : "Default", + style: + STextStyles.label(context), + ), + ], + ), + ], + ), + Expanded( + child: SvgPicture.asset( + Assets.svg.chevronRight, + alignment: Alignment.centerRight, + ), + ), + ], + ), + ), + ), + ); + }, + ), + ], + ), ), ), ], From 3950c025d88d42d4d61e3e043e895858f73e6703 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Thu, 3 Nov 2022 10:01:16 -0600 Subject: [PATCH 119/426] v1.5.13 build 85 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 5be35c66e..64c678637 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.5.13+84 +version: 1.5.13+85 environment: sdk: ">=2.17.0 <3.0.0" From dc4e7f4baebc2082f33bb07cae2f0abff2f5dcb2 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 3 Nov 2022 11:04:57 -0600 Subject: [PATCH 120/426] shared currency settings code --- .../global_settings_view/currency_view.dart | 115 ++- .../currency_settings/currency_dialog.dart | 742 +++++++++--------- .../currency_settings/currency_settings.dart | 38 +- 3 files changed, 496 insertions(+), 399 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/currency_view.dart b/lib/pages/settings_views/global_settings_view/currency_view.dart index e884393bd..4f2c3258c 100644 --- a/lib/pages/settings_views/global_settings_view/currency_view.dart +++ b/lib/pages/settings_views/global_settings_view/currency_view.dart @@ -7,13 +7,17 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; -import 'package:stackwallet/utilities/util.dart'; +import '../../../widgets/rounded_white_container.dart'; class BaseCurrencySettingsView extends ConsumerStatefulWidget { const BaseCurrencySettingsView({Key? key}) : super(key: key); @@ -102,31 +106,90 @@ class _CurrencyViewState extends ConsumerState<BaseCurrencySettingsView> { currenciesWithoutSelected.insert(0, current); } currenciesWithoutSelected = _filtered(); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "Currency", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.only( - top: 12, - left: 16, - right: 16, - ), + final isDesktop = Util.isDesktop; + + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Currency", + style: STextStyles.navBarTitle(context), + ), + ), + body: Padding( + padding: const EdgeInsets.only( + top: 12, + left: 16, + right: 16, + ), + child: child, + ), + ); + }, + child: ConditionalParent( + condition: isDesktop, + builder: (child) { + return Padding( + padding: const EdgeInsets.only( + top: 16, + bottom: 32, + left: 32, + right: 32, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(20), + borderColor: + Theme.of(context).extension<StackColors>()!.background, + child: child, + ), + ), + const SizedBox( + height: 16, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + desktopMed: true, + onPressed: Navigator.of(context).pop, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Save changes", + desktopMed: true, + onPressed: Navigator.of(context).pop, + ), + ), + ], + ), + ], + ), + ); + }, child: NestedScrollView( floatHeaderSlivers: true, headerSliverBuilder: (context, innerBoxIsScrolled) { diff --git a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_dialog.dart b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_dialog.dart index bbe98c1af..602589cea 100644 --- a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_dialog.dart +++ b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_dialog.dart @@ -1,371 +1,371 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/providers/global/base_currencies_provider.dart'; -import 'package:stackwallet/providers/global/prefs_provider.dart'; -import 'package:stackwallet/utilities/assets.dart'; -import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/utilities/theme/stack_colors.dart'; -import 'package:stackwallet/utilities/util.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/icon_widgets/x_icon.dart'; -import 'package:stackwallet/widgets/rounded_container.dart'; -import 'package:stackwallet/widgets/stack_text_field.dart'; -import 'package:stackwallet/widgets/textfield_icon_button.dart'; - -class CurrencyDialog extends ConsumerStatefulWidget { - const CurrencyDialog({Key? key}) : super(key: key); - - @override - ConsumerState<CurrencyDialog> createState() => _CurrencyDialog(); -} - -class _CurrencyDialog extends ConsumerState<CurrencyDialog> { - late String current; - late List<String> currenciesWithoutSelected; - - late final TextEditingController searchCurrencyController; - - late final FocusNode searchCurrencyFocusNode; - - void onTap(int index) { - if (currenciesWithoutSelected[index] == current || current.isEmpty) { - // ignore if already selected currency - return; - } - current = currenciesWithoutSelected[index]; - currenciesWithoutSelected.remove(current); - currenciesWithoutSelected.insert(0, current); - ref.read(prefsChangeNotifierProvider).currency = current; - } - - BorderRadius? _borderRadius(int index) { - if (index == 0 && currenciesWithoutSelected.length == 1) { - return BorderRadius.circular( - Constants.size.circularBorderRadius, - ); - } else if (index == 0) { - return BorderRadius.vertical( - top: Radius.circular( - Constants.size.circularBorderRadius, - ), - ); - } else if (index == currenciesWithoutSelected.length - 1) { - return BorderRadius.vertical( - bottom: Radius.circular( - Constants.size.circularBorderRadius, - ), - ); - } - return null; - } - - String filter = ""; - - List<String> _filtered() { - final currencyMap = ref.read(baseCurrenciesProvider).map; - return currenciesWithoutSelected.where((element) { - return element.toLowerCase().contains(filter.toLowerCase()) || - (currencyMap[element]?.toLowerCase().contains(filter.toLowerCase()) ?? - false); - }).toList(); - } - - @override - void initState() { - searchCurrencyController = TextEditingController(); - - searchCurrencyFocusNode = FocusNode(); - - super.initState(); - } - - @override - void dispose() { - searchCurrencyController.dispose(); - - searchCurrencyFocusNode.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - current = ref - .watch(prefsChangeNotifierProvider.select((value) => value.currency)); - - currenciesWithoutSelected = ref - .watch(baseCurrenciesProvider.select((value) => value.map)) - .keys - .toList(); - if (current.isNotEmpty) { - currenciesWithoutSelected.remove(current); - currenciesWithoutSelected.insert(0, current); - } - currenciesWithoutSelected = _filtered(); - - return DesktopDialog( - maxHeight: 800, - maxWidth: 600, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.all(32), - child: Text( - "Select currency", - style: STextStyles.desktopH3(context), - textAlign: TextAlign.center, - ), - ), - const DesktopDialogCloseButton(), - ], - ), - Expanded( - flex: 24, - child: NestedScrollView( - floatHeaderSlivers: true, - headerSliverBuilder: (context, innerBoxIsScrolled) { - return [ - SliverOverlapAbsorber( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor( - context), - sliver: SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 16, horizontal: 32), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: - Util.isDesktop ? false : true, - controller: searchCurrencyController, - focusNode: searchCurrencyFocusNode, - onChanged: (newString) { - setState(() => filter = newString); - }, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - searchCurrencyFocusNode, - context, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, - ), - ), - suffixIcon: searchCurrencyController - .text.isNotEmpty - ? Padding( - padding: - const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - searchCurrencyController - .text = ""; - filter = ""; - }); - }, - ), - ], - ), - ), - ) - : null, - ), - ), - ), - ), - ], - ), - ), - ), - ), - ]; - }, - body: Builder( - builder: (context) { - return CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor( - context, - ), - ), - SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - return Container( - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .popupBG, - borderRadius: _borderRadius(index), - ), - child: Padding( - padding: const EdgeInsets.all(4), - key: Key( - "desktopSettingsCurrencySelect_${currenciesWithoutSelected[index]}"), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32), - child: RoundedContainer( - padding: const EdgeInsets.all(0), - color: currenciesWithoutSelected[index] == - current - ? Theme.of(context) - .extension<StackColors>()! - .currencyListItemBG - : Theme.of(context) - .extension<StackColors>()! - .popupBG, - child: RawMaterialButton( - onPressed: () async { - onTap(index); - }, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Row( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 20, - child: Radio( - activeColor: Theme.of(context) - .extension<StackColors>()! - .radioButtonIconEnabled, - materialTapTargetSize: - MaterialTapTargetSize - .shrinkWrap, - value: true, - groupValue: - currenciesWithoutSelected[ - index] == - current, - onChanged: (_) { - onTap(index); - }, - ), - ), - const SizedBox( - width: 12, - ), - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - currenciesWithoutSelected[ - index], - key: (currenciesWithoutSelected[ - index] == - current) - ? const Key( - "desktopSettingsSelectedCurrencyText") - : null, - style: - STextStyles.largeMedium14( - context), - ), - const SizedBox( - height: 2, - ), - Text( - ref.watch(baseCurrenciesProvider - .select((value) => - value.map))[ - currenciesWithoutSelected[ - index]] ?? - "", - key: (currenciesWithoutSelected[ - index] == - current) - ? const Key( - "desktopSelectedCurrencyTextDescription") - : null, - style: - STextStyles.itemSubtitle( - context), - ), - ], - ), - ], - ), - ), - ), - ), - ), - ), - ); - }, - childCount: currenciesWithoutSelected.length, - ), - ), - ], - ); - }, - ), - ), - ), - const Spacer(), - Padding( - padding: const EdgeInsets.all(32), - child: Row( - children: [ - Expanded( - child: SecondaryButton( - label: "Cancel", - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: PrimaryButton( - label: "Save Changes", - onPressed: () {}, - ), - ) - ], - ), - ), - ], - ), - ); - } -} +// import 'package:flutter/material.dart'; +// import 'package:flutter_riverpod/flutter_riverpod.dart'; +// import 'package:flutter_svg/svg.dart'; +// import 'package:stackwallet/providers/global/base_currencies_provider.dart'; +// import 'package:stackwallet/providers/global/prefs_provider.dart'; +// import 'package:stackwallet/utilities/assets.dart'; +// import 'package:stackwallet/utilities/constants.dart'; +// import 'package:stackwallet/utilities/text_styles.dart'; +// import 'package:stackwallet/utilities/theme/stack_colors.dart'; +// import 'package:stackwallet/utilities/util.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/icon_widgets/x_icon.dart'; +// import 'package:stackwallet/widgets/rounded_container.dart'; +// import 'package:stackwallet/widgets/stack_text_field.dart'; +// import 'package:stackwallet/widgets/textfield_icon_button.dart'; +// +// class CurrencyDialog extends ConsumerStatefulWidget { +// const CurrencyDialog({Key? key}) : super(key: key); +// +// @override +// ConsumerState<CurrencyDialog> createState() => _CurrencyDialog(); +// } +// +// class _CurrencyDialog extends ConsumerState<CurrencyDialog> { +// late String current; +// late List<String> currenciesWithoutSelected; +// +// late final TextEditingController searchCurrencyController; +// +// late final FocusNode searchCurrencyFocusNode; +// +// void onTap(int index) { +// if (currenciesWithoutSelected[index] == current || current.isEmpty) { +// // ignore if already selected currency +// return; +// } +// current = currenciesWithoutSelected[index]; +// currenciesWithoutSelected.remove(current); +// currenciesWithoutSelected.insert(0, current); +// ref.read(prefsChangeNotifierProvider).currency = current; +// } +// +// BorderRadius? _borderRadius(int index) { +// if (index == 0 && currenciesWithoutSelected.length == 1) { +// return BorderRadius.circular( +// Constants.size.circularBorderRadius, +// ); +// } else if (index == 0) { +// return BorderRadius.vertical( +// top: Radius.circular( +// Constants.size.circularBorderRadius, +// ), +// ); +// } else if (index == currenciesWithoutSelected.length - 1) { +// return BorderRadius.vertical( +// bottom: Radius.circular( +// Constants.size.circularBorderRadius, +// ), +// ); +// } +// return null; +// } +// +// String filter = ""; +// +// List<String> _filtered() { +// final currencyMap = ref.read(baseCurrenciesProvider).map; +// return currenciesWithoutSelected.where((element) { +// return element.toLowerCase().contains(filter.toLowerCase()) || +// (currencyMap[element]?.toLowerCase().contains(filter.toLowerCase()) ?? +// false); +// }).toList(); +// } +// +// @override +// void initState() { +// searchCurrencyController = TextEditingController(); +// +// searchCurrencyFocusNode = FocusNode(); +// +// super.initState(); +// } +// +// @override +// void dispose() { +// searchCurrencyController.dispose(); +// +// searchCurrencyFocusNode.dispose(); +// +// super.dispose(); +// } +// +// @override +// Widget build(BuildContext context) { +// current = ref +// .watch(prefsChangeNotifierProvider.select((value) => value.currency)); +// +// currenciesWithoutSelected = ref +// .watch(baseCurrenciesProvider.select((value) => value.map)) +// .keys +// .toList(); +// if (current.isNotEmpty) { +// currenciesWithoutSelected.remove(current); +// currenciesWithoutSelected.insert(0, current); +// } +// currenciesWithoutSelected = _filtered(); +// +// return DesktopDialog( +// maxHeight: 800, +// maxWidth: 600, +// child: Column( +// children: [ +// Row( +// mainAxisAlignment: MainAxisAlignment.spaceBetween, +// children: [ +// Padding( +// padding: const EdgeInsets.all(32), +// child: Text( +// "Select currency", +// style: STextStyles.desktopH3(context), +// textAlign: TextAlign.center, +// ), +// ), +// const DesktopDialogCloseButton(), +// ], +// ), +// Expanded( +// flex: 24, +// child: NestedScrollView( +// floatHeaderSlivers: true, +// headerSliverBuilder: (context, innerBoxIsScrolled) { +// return [ +// SliverOverlapAbsorber( +// handle: NestedScrollView.sliverOverlapAbsorberHandleFor( +// context), +// sliver: SliverToBoxAdapter( +// child: Padding( +// padding: const EdgeInsets.symmetric( +// vertical: 16, horizontal: 32), +// child: Column( +// children: [ +// Padding( +// padding: const EdgeInsets.only(bottom: 16), +// child: ClipRRect( +// borderRadius: BorderRadius.circular( +// Constants.size.circularBorderRadius, +// ), +// child: TextField( +// autocorrect: Util.isDesktop ? false : true, +// enableSuggestions: +// Util.isDesktop ? false : true, +// controller: searchCurrencyController, +// focusNode: searchCurrencyFocusNode, +// onChanged: (newString) { +// setState(() => filter = newString); +// }, +// style: STextStyles.field(context), +// decoration: standardInputDecoration( +// "Search", +// searchCurrencyFocusNode, +// context, +// ).copyWith( +// prefixIcon: Padding( +// padding: const EdgeInsets.symmetric( +// horizontal: 10, +// vertical: 16, +// ), +// child: SvgPicture.asset( +// Assets.svg.search, +// width: 16, +// height: 16, +// ), +// ), +// suffixIcon: searchCurrencyController +// .text.isNotEmpty +// ? Padding( +// padding: +// const EdgeInsets.only(right: 0), +// child: UnconstrainedBox( +// child: Row( +// children: [ +// TextFieldIconButton( +// child: const XIcon(), +// onTap: () async { +// setState(() { +// searchCurrencyController +// .text = ""; +// filter = ""; +// }); +// }, +// ), +// ], +// ), +// ), +// ) +// : null, +// ), +// ), +// ), +// ), +// ], +// ), +// ), +// ), +// ), +// ]; +// }, +// body: Builder( +// builder: (context) { +// return CustomScrollView( +// slivers: [ +// SliverOverlapInjector( +// handle: NestedScrollView.sliverOverlapAbsorberHandleFor( +// context, +// ), +// ), +// SliverList( +// delegate: SliverChildBuilderDelegate( +// (context, index) { +// return Container( +// decoration: BoxDecoration( +// color: Theme.of(context) +// .extension<StackColors>()! +// .popupBG, +// borderRadius: _borderRadius(index), +// ), +// child: Padding( +// padding: const EdgeInsets.all(4), +// key: Key( +// "desktopSettingsCurrencySelect_${currenciesWithoutSelected[index]}"), +// child: Padding( +// padding: const EdgeInsets.symmetric( +// horizontal: 32), +// child: RoundedContainer( +// padding: const EdgeInsets.all(0), +// color: currenciesWithoutSelected[index] == +// current +// ? Theme.of(context) +// .extension<StackColors>()! +// .currencyListItemBG +// : Theme.of(context) +// .extension<StackColors>()! +// .popupBG, +// child: RawMaterialButton( +// onPressed: () async { +// onTap(index); +// }, +// shape: RoundedRectangleBorder( +// borderRadius: BorderRadius.circular( +// Constants.size.circularBorderRadius, +// ), +// ), +// child: Padding( +// padding: const EdgeInsets.all(12.0), +// child: Row( +// crossAxisAlignment: +// CrossAxisAlignment.start, +// children: [ +// SizedBox( +// width: 20, +// height: 20, +// child: Radio( +// activeColor: Theme.of(context) +// .extension<StackColors>()! +// .radioButtonIconEnabled, +// materialTapTargetSize: +// MaterialTapTargetSize +// .shrinkWrap, +// value: true, +// groupValue: +// currenciesWithoutSelected[ +// index] == +// current, +// onChanged: (_) { +// onTap(index); +// }, +// ), +// ), +// const SizedBox( +// width: 12, +// ), +// Column( +// crossAxisAlignment: +// CrossAxisAlignment.start, +// children: [ +// Text( +// currenciesWithoutSelected[ +// index], +// key: (currenciesWithoutSelected[ +// index] == +// current) +// ? const Key( +// "desktopSettingsSelectedCurrencyText") +// : null, +// style: +// STextStyles.largeMedium14( +// context), +// ), +// const SizedBox( +// height: 2, +// ), +// Text( +// ref.watch(baseCurrenciesProvider +// .select((value) => +// value.map))[ +// currenciesWithoutSelected[ +// index]] ?? +// "", +// key: (currenciesWithoutSelected[ +// index] == +// current) +// ? const Key( +// "desktopSelectedCurrencyTextDescription") +// : null, +// style: +// STextStyles.itemSubtitle( +// context), +// ), +// ], +// ), +// ], +// ), +// ), +// ), +// ), +// ), +// ), +// ); +// }, +// childCount: currenciesWithoutSelected.length, +// ), +// ), +// ], +// ); +// }, +// ), +// ), +// ), +// const Spacer(), +// Padding( +// padding: const EdgeInsets.all(32), +// child: Row( +// children: [ +// Expanded( +// child: SecondaryButton( +// label: "Cancel", +// onPressed: () { +// Navigator.of(context).pop(); +// }, +// ), +// ), +// const SizedBox( +// width: 16, +// ), +// Expanded( +// child: PrimaryButton( +// label: "Save Changes", +// onPressed: () {}, +// ), +// ) +// ], +// ), +// ), +// ], +// ), +// ); +// } +// } diff --git a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart index b8ce76d25..cf51940e0 100644 --- a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart @@ -2,12 +2,14 @@ import 'package:flutter/material.dart'; import 'package:flutter/src/widgets/framework.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/currency_view.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/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; -import 'currency_dialog.dart'; +import '../../../../widgets/desktop/desktop_dialog_close_button.dart'; class CurrencySettings extends ConsumerStatefulWidget { const CurrencySettings({Key? key}) : super(key: key); @@ -87,12 +89,44 @@ class NewPasswordButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { Future<void> chooseCurrency() async { + // await showDialog<dynamic>( + // context: context, + // useSafeArea: false, + // barrierDismissible: true, + // builder: (context) { + // return CurrencyDialog(); + // }, + // ); await showDialog<dynamic>( context: context, useSafeArea: false, barrierDismissible: true, builder: (context) { - return CurrencyDialog(); + return DesktopDialog( + maxHeight: 800, + maxWidth: 600, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(32), + child: Text( + "Select currency", + style: STextStyles.desktopH3(context), + textAlign: TextAlign.center, + ), + ), + const DesktopDialogCloseButton(), + ], + ), + const Expanded( + child: BaseCurrencySettingsView(), + ), + ], + ), + ); }, ); } From 66ead82462e508b2645a44d3d900d37524604730 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 3 Nov 2022 12:26:24 -0600 Subject: [PATCH 121/426] node settings desktop stuff and some code cleanup --- .../currency_settings/currency_dialog.dart | 371 ------------------ .../currency_settings/currency_settings.dart | 92 +++-- .../language_settings/language_settings.dart | 26 +- .../home/settings_menu/nodes_settings.dart | 153 ++++---- 4 files changed, 140 insertions(+), 502 deletions(-) delete mode 100644 lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_dialog.dart diff --git a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_dialog.dart b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_dialog.dart deleted file mode 100644 index 602589cea..000000000 --- a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_dialog.dart +++ /dev/null @@ -1,371 +0,0 @@ -// import 'package:flutter/material.dart'; -// import 'package:flutter_riverpod/flutter_riverpod.dart'; -// import 'package:flutter_svg/svg.dart'; -// import 'package:stackwallet/providers/global/base_currencies_provider.dart'; -// import 'package:stackwallet/providers/global/prefs_provider.dart'; -// import 'package:stackwallet/utilities/assets.dart'; -// import 'package:stackwallet/utilities/constants.dart'; -// import 'package:stackwallet/utilities/text_styles.dart'; -// import 'package:stackwallet/utilities/theme/stack_colors.dart'; -// import 'package:stackwallet/utilities/util.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/icon_widgets/x_icon.dart'; -// import 'package:stackwallet/widgets/rounded_container.dart'; -// import 'package:stackwallet/widgets/stack_text_field.dart'; -// import 'package:stackwallet/widgets/textfield_icon_button.dart'; -// -// class CurrencyDialog extends ConsumerStatefulWidget { -// const CurrencyDialog({Key? key}) : super(key: key); -// -// @override -// ConsumerState<CurrencyDialog> createState() => _CurrencyDialog(); -// } -// -// class _CurrencyDialog extends ConsumerState<CurrencyDialog> { -// late String current; -// late List<String> currenciesWithoutSelected; -// -// late final TextEditingController searchCurrencyController; -// -// late final FocusNode searchCurrencyFocusNode; -// -// void onTap(int index) { -// if (currenciesWithoutSelected[index] == current || current.isEmpty) { -// // ignore if already selected currency -// return; -// } -// current = currenciesWithoutSelected[index]; -// currenciesWithoutSelected.remove(current); -// currenciesWithoutSelected.insert(0, current); -// ref.read(prefsChangeNotifierProvider).currency = current; -// } -// -// BorderRadius? _borderRadius(int index) { -// if (index == 0 && currenciesWithoutSelected.length == 1) { -// return BorderRadius.circular( -// Constants.size.circularBorderRadius, -// ); -// } else if (index == 0) { -// return BorderRadius.vertical( -// top: Radius.circular( -// Constants.size.circularBorderRadius, -// ), -// ); -// } else if (index == currenciesWithoutSelected.length - 1) { -// return BorderRadius.vertical( -// bottom: Radius.circular( -// Constants.size.circularBorderRadius, -// ), -// ); -// } -// return null; -// } -// -// String filter = ""; -// -// List<String> _filtered() { -// final currencyMap = ref.read(baseCurrenciesProvider).map; -// return currenciesWithoutSelected.where((element) { -// return element.toLowerCase().contains(filter.toLowerCase()) || -// (currencyMap[element]?.toLowerCase().contains(filter.toLowerCase()) ?? -// false); -// }).toList(); -// } -// -// @override -// void initState() { -// searchCurrencyController = TextEditingController(); -// -// searchCurrencyFocusNode = FocusNode(); -// -// super.initState(); -// } -// -// @override -// void dispose() { -// searchCurrencyController.dispose(); -// -// searchCurrencyFocusNode.dispose(); -// -// super.dispose(); -// } -// -// @override -// Widget build(BuildContext context) { -// current = ref -// .watch(prefsChangeNotifierProvider.select((value) => value.currency)); -// -// currenciesWithoutSelected = ref -// .watch(baseCurrenciesProvider.select((value) => value.map)) -// .keys -// .toList(); -// if (current.isNotEmpty) { -// currenciesWithoutSelected.remove(current); -// currenciesWithoutSelected.insert(0, current); -// } -// currenciesWithoutSelected = _filtered(); -// -// return DesktopDialog( -// maxHeight: 800, -// maxWidth: 600, -// child: Column( -// children: [ -// Row( -// mainAxisAlignment: MainAxisAlignment.spaceBetween, -// children: [ -// Padding( -// padding: const EdgeInsets.all(32), -// child: Text( -// "Select currency", -// style: STextStyles.desktopH3(context), -// textAlign: TextAlign.center, -// ), -// ), -// const DesktopDialogCloseButton(), -// ], -// ), -// Expanded( -// flex: 24, -// child: NestedScrollView( -// floatHeaderSlivers: true, -// headerSliverBuilder: (context, innerBoxIsScrolled) { -// return [ -// SliverOverlapAbsorber( -// handle: NestedScrollView.sliverOverlapAbsorberHandleFor( -// context), -// sliver: SliverToBoxAdapter( -// child: Padding( -// padding: const EdgeInsets.symmetric( -// vertical: 16, horizontal: 32), -// child: Column( -// children: [ -// Padding( -// padding: const EdgeInsets.only(bottom: 16), -// child: ClipRRect( -// borderRadius: BorderRadius.circular( -// Constants.size.circularBorderRadius, -// ), -// child: TextField( -// autocorrect: Util.isDesktop ? false : true, -// enableSuggestions: -// Util.isDesktop ? false : true, -// controller: searchCurrencyController, -// focusNode: searchCurrencyFocusNode, -// onChanged: (newString) { -// setState(() => filter = newString); -// }, -// style: STextStyles.field(context), -// decoration: standardInputDecoration( -// "Search", -// searchCurrencyFocusNode, -// context, -// ).copyWith( -// prefixIcon: Padding( -// padding: const EdgeInsets.symmetric( -// horizontal: 10, -// vertical: 16, -// ), -// child: SvgPicture.asset( -// Assets.svg.search, -// width: 16, -// height: 16, -// ), -// ), -// suffixIcon: searchCurrencyController -// .text.isNotEmpty -// ? Padding( -// padding: -// const EdgeInsets.only(right: 0), -// child: UnconstrainedBox( -// child: Row( -// children: [ -// TextFieldIconButton( -// child: const XIcon(), -// onTap: () async { -// setState(() { -// searchCurrencyController -// .text = ""; -// filter = ""; -// }); -// }, -// ), -// ], -// ), -// ), -// ) -// : null, -// ), -// ), -// ), -// ), -// ], -// ), -// ), -// ), -// ), -// ]; -// }, -// body: Builder( -// builder: (context) { -// return CustomScrollView( -// slivers: [ -// SliverOverlapInjector( -// handle: NestedScrollView.sliverOverlapAbsorberHandleFor( -// context, -// ), -// ), -// SliverList( -// delegate: SliverChildBuilderDelegate( -// (context, index) { -// return Container( -// decoration: BoxDecoration( -// color: Theme.of(context) -// .extension<StackColors>()! -// .popupBG, -// borderRadius: _borderRadius(index), -// ), -// child: Padding( -// padding: const EdgeInsets.all(4), -// key: Key( -// "desktopSettingsCurrencySelect_${currenciesWithoutSelected[index]}"), -// child: Padding( -// padding: const EdgeInsets.symmetric( -// horizontal: 32), -// child: RoundedContainer( -// padding: const EdgeInsets.all(0), -// color: currenciesWithoutSelected[index] == -// current -// ? Theme.of(context) -// .extension<StackColors>()! -// .currencyListItemBG -// : Theme.of(context) -// .extension<StackColors>()! -// .popupBG, -// child: RawMaterialButton( -// onPressed: () async { -// onTap(index); -// }, -// shape: RoundedRectangleBorder( -// borderRadius: BorderRadius.circular( -// Constants.size.circularBorderRadius, -// ), -// ), -// child: Padding( -// padding: const EdgeInsets.all(12.0), -// child: Row( -// crossAxisAlignment: -// CrossAxisAlignment.start, -// children: [ -// SizedBox( -// width: 20, -// height: 20, -// child: Radio( -// activeColor: Theme.of(context) -// .extension<StackColors>()! -// .radioButtonIconEnabled, -// materialTapTargetSize: -// MaterialTapTargetSize -// .shrinkWrap, -// value: true, -// groupValue: -// currenciesWithoutSelected[ -// index] == -// current, -// onChanged: (_) { -// onTap(index); -// }, -// ), -// ), -// const SizedBox( -// width: 12, -// ), -// Column( -// crossAxisAlignment: -// CrossAxisAlignment.start, -// children: [ -// Text( -// currenciesWithoutSelected[ -// index], -// key: (currenciesWithoutSelected[ -// index] == -// current) -// ? const Key( -// "desktopSettingsSelectedCurrencyText") -// : null, -// style: -// STextStyles.largeMedium14( -// context), -// ), -// const SizedBox( -// height: 2, -// ), -// Text( -// ref.watch(baseCurrenciesProvider -// .select((value) => -// value.map))[ -// currenciesWithoutSelected[ -// index]] ?? -// "", -// key: (currenciesWithoutSelected[ -// index] == -// current) -// ? const Key( -// "desktopSelectedCurrencyTextDescription") -// : null, -// style: -// STextStyles.itemSubtitle( -// context), -// ), -// ], -// ), -// ], -// ), -// ), -// ), -// ), -// ), -// ), -// ); -// }, -// childCount: currenciesWithoutSelected.length, -// ), -// ), -// ], -// ); -// }, -// ), -// ), -// ), -// const Spacer(), -// Padding( -// padding: const EdgeInsets.all(32), -// child: Row( -// children: [ -// Expanded( -// child: SecondaryButton( -// label: "Cancel", -// onPressed: () { -// Navigator.of(context).pop(); -// }, -// ), -// ), -// const SizedBox( -// width: 16, -// ), -// Expanded( -// child: PrimaryButton( -// label: "Save Changes", -// onPressed: () {}, -// ), -// ) -// ], -// ), -// ), -// ], -// ), -// ); -// } -// } diff --git a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart index cf51940e0..dab613f63 100644 --- a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/src/widgets/framework.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/currency_view.dart'; @@ -7,10 +6,9 @@ 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/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; -import '../../../../widgets/desktop/desktop_dialog_close_button.dart'; - class CurrencySettings extends ConsumerStatefulWidget { const CurrencySettings({Key? key}) : super(key: key); @@ -86,51 +84,51 @@ class NewPasswordButton extends ConsumerWidget { const NewPasswordButton({ Key? key, }) : super(key: key); + Future<void> chooseCurrency(BuildContext context) async { + // await showDialog<dynamic>( + // context: context, + // useSafeArea: false, + // barrierDismissible: true, + // builder: (context) { + // return CurrencyDialog(); + // }, + // ); + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return DesktopDialog( + maxHeight: 800, + maxWidth: 600, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(32), + child: Text( + "Select currency", + style: STextStyles.desktopH3(context), + textAlign: TextAlign.center, + ), + ), + const DesktopDialogCloseButton(), + ], + ), + const Expanded( + child: BaseCurrencySettingsView(), + ), + ], + ), + ); + }, + ); + } + @override Widget build(BuildContext context, WidgetRef ref) { - Future<void> chooseCurrency() async { - // await showDialog<dynamic>( - // context: context, - // useSafeArea: false, - // barrierDismissible: true, - // builder: (context) { - // return CurrencyDialog(); - // }, - // ); - await showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return DesktopDialog( - maxHeight: 800, - maxWidth: 600, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.all(32), - child: Text( - "Select currency", - style: STextStyles.desktopH3(context), - textAlign: TextAlign.center, - ), - ), - const DesktopDialogCloseButton(), - ], - ), - const Expanded( - child: BaseCurrencySettingsView(), - ), - ], - ), - ); - }, - ); - } - return SizedBox( width: 200, height: 48, @@ -139,7 +137,7 @@ class NewPasswordButton extends ConsumerWidget { .extension<StackColors>()! .getPrimaryEnabledButtonColor(context), onPressed: () { - chooseCurrency(); + chooseCurrency(context); }, child: Text( "Change currency", diff --git a/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart b/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart index 97b807b3b..0f66d7dd5 100644 --- a/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/src/widgets/framework.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/language_settings/language_dialog.dart'; @@ -86,19 +85,20 @@ class ChangeLanguageButton extends ConsumerWidget { const ChangeLanguageButton({ Key? key, }) : super(key: key); + + Future<void> chooseLanguage(BuildContext context) async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return const LanguageDialog(); + }, + ); + } + @override Widget build(BuildContext context, WidgetRef ref) { - Future<void> chooseLanguage() async { - await showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return LanguageDialog(); - }, - ); - } - return SizedBox( width: 200, height: 48, @@ -107,7 +107,7 @@ class ChangeLanguageButton extends ConsumerWidget { .extension<StackColors>()! .getPrimaryEnabledButtonColor(context), onPressed: () { - chooseLanguage(); + chooseLanguage(context); }, child: Text( "Change language", diff --git a/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart b/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart index 5f1e70143..abba6cccc 100644 --- a/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/src/widgets/framework.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/coin_nodes_view.dart'; @@ -9,10 +8,10 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.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/utilities/util.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; - -import '../../../utilities/util.dart'; -import '../../../widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; class NodesSettings extends ConsumerStatefulWidget { const NodesSettings({Key? key}) : super(key: key); @@ -62,6 +61,7 @@ class _NodesSettings extends ConsumerState<NodesSettings> { debugPrint("BUILD: $runtimeType"); return Column( + mainAxisSize: MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.only( @@ -70,6 +70,7 @@ class _NodesSettings extends ConsumerState<NodesSettings> { child: RoundedWhiteContainer( child: Column( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ SvgPicture.asset( Assets.svg.circleNode, @@ -78,6 +79,7 @@ class _NodesSettings extends ConsumerState<NodesSettings> { ), Column( crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.all(10), @@ -137,84 +139,93 @@ class _NodesSettings extends ConsumerState<NodesSettings> { ), Padding( padding: const EdgeInsets.all(10.0), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ...coins.map( - (coin) { - final count = ref - .watch(nodeServiceChangeNotifierProvider - .select((value) => value.getNodesFor(coin))) - .length; + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + borderColor: + Theme.of(context).extension<StackColors>()!.background, + child: ListView.separated( + primary: false, + shrinkWrap: true, + itemBuilder: (context, index) { + final coin = coins[index]; + final count = ref + .watch(nodeServiceChangeNotifierProvider + .select((value) => value.getNodesFor(coin))) + .length; - return Padding( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), + return Padding( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + onPressed: () { + showDialog<void>( + context: context, + builder: (context) => CoinNodesView( + coin: coin, ), - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - onPressed: () { - Navigator.of(context).pushNamed( - CoinNodesView.routeName, - arguments: coin, - ); - }, - child: Padding( - padding: const EdgeInsets.all( - 12.0, - ), - child: Row( + ); + }, + child: Padding( + padding: const EdgeInsets.all( + 12.0, + ), + child: Row( + children: [ + Row( children: [ - Row( + SvgPicture.asset( + Assets.svg.iconFor(coin: coin), + width: 24, + height: 24, + ), + const SizedBox( + width: 12, + ), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, children: [ - SvgPicture.asset( - Assets.svg.iconFor(coin: coin), - width: 24, - height: 24, + Text( + "${coin.prettyName} nodes", + style: STextStyles.titleBold12( + context), ), - const SizedBox( - width: 12, - ), - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "${coin.prettyName} nodes", - style: STextStyles.titleBold12( - context), - ), - Text( - count > 1 - ? "$count nodes" - : "Default", - style: - STextStyles.label(context), - ), - ], + Text( + count > 1 + ? "$count nodes" + : "Default", + style: STextStyles.label(context), ), ], ), - Expanded( - child: SvgPicture.asset( - Assets.svg.chevronRight, - alignment: Alignment.centerRight, - ), - ), ], ), - ), + Expanded( + child: SvgPicture.asset( + Assets.svg.chevronRight, + alignment: Alignment.centerRight, + ), + ), + ], ), - ); - }, - ), - ], + ), + ), + ); + }, + separatorBuilder: (context, index) => Container( + height: 1, + color: Theme.of(context) + .extension<StackColors>()! + .background, + ), + itemCount: coins.length, ), ), ), From e5f69700f7fcbaa4d380d1e8cf0f3ba67c6167cf Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Thu, 3 Nov 2022 14:28:24 -0600 Subject: [PATCH 122/426] conditional for desktop syncing pref settings --- .../syncing_options_view.dart | 701 +++++++++--------- .../syncing_preferences_settings.dart | 9 + 2 files changed, 345 insertions(+), 365 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart b/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart index bada67353..a65a03a87 100644 --- a/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart +++ b/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart @@ -6,6 +6,8 @@ import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/sync_type_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -16,383 +18,352 @@ class SyncingOptionsView extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Syncing", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Column( - children: [ - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: const EdgeInsets.all(4), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - final state = ref - .read(prefsChangeNotifierProvider) - .syncType; - if (state != SyncingType.currentWalletOnly) { - ref - .read(prefsChangeNotifierProvider) - .syncType = - SyncingType.currentWalletOnly; + return ConditionalParent( + condition: !Util.isDesktop, + builder: (child) { + return Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Syncing", + style: STextStyles.navBarTitle(context), + ), + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: child, + ), + ), + ); + }, + ), + ), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Column( + children: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.all(4), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + final state = + ref.read(prefsChangeNotifierProvider).syncType; + if (state != SyncingType.currentWalletOnly) { + ref.read(prefsChangeNotifierProvider).syncType = + SyncingType.currentWalletOnly; - // disable auto sync on all wallets that aren't active/current - ref - .read(walletsChangeNotifierProvider) - .managers - .forEach((e) { - if (!e.isActiveWallet) { - e.shouldAutoSync = false; - } - }); - } - }, - child: Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 20, - child: Radio( - activeColor: Theme.of(context) - .extension<StackColors>()! - .radioButtonIconEnabled, - value: - SyncingType.currentWalletOnly, - groupValue: ref.watch( - prefsChangeNotifierProvider - .select((value) => - value.syncType), - ), - onChanged: (value) { - if (value is SyncingType) { - ref - .read( - prefsChangeNotifierProvider) - .syncType = value; - } - }, - ), - ), - const SizedBox( - width: 12, - ), - Flexible( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Sync only currently open wallet", - style: STextStyles.titleBold12( - context), - textAlign: TextAlign.left, - ), - Text( - "Sync only the wallet that you are using", - style: STextStyles.itemSubtitle( - context), - textAlign: TextAlign.left, - ), - ], - ), - ), - ], - ), + // disable auto sync on all wallets that aren't active/current + ref + .read(walletsChangeNotifierProvider) + .managers + .forEach((e) { + if (!e.isActiveWallet) { + e.shouldAutoSync = false; + } + }); + } + }, + child: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: SyncingType.currentWalletOnly, + groupValue: ref.watch( + prefsChangeNotifierProvider + .select((value) => value.syncType), ), + onChanged: (value) { + if (value is SyncingType) { + ref + .read(prefsChangeNotifierProvider) + .syncType = value; + } + }, ), ), - ), - Padding( - padding: const EdgeInsets.all(4.0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - final state = ref - .read(prefsChangeNotifierProvider) - .syncType; - if (state != - SyncingType.allWalletsOnStartup) { - ref - .read(prefsChangeNotifierProvider) - .syncType = - SyncingType.allWalletsOnStartup; - - // enable auto sync on all wallets - ref - .read(walletsChangeNotifierProvider) - .managers - .forEach( - (e) => e.shouldAutoSync = true); - } - }, - child: Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 20, - child: Radio( - activeColor: Theme.of(context) - .extension<StackColors>()! - .radioButtonIconEnabled, - value: - SyncingType.allWalletsOnStartup, - groupValue: ref.watch( - prefsChangeNotifierProvider - .select((value) => - value.syncType), - ), - onChanged: (value) { - if (value is SyncingType) { - ref - .read( - prefsChangeNotifierProvider) - .syncType = value; - } - }, - ), - ), - const SizedBox( - width: 12, - ), - Flexible( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Sync all wallets at startup", - style: STextStyles.titleBold12( - context), - textAlign: TextAlign.left, - ), - Text( - "All of your wallets will start syncing when you open the app", - style: STextStyles.itemSubtitle( - context), - textAlign: TextAlign.left, - ), - ], - ), - ), - ], - ), - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(4), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - final state = ref - .read(prefsChangeNotifierProvider) - .syncType; - if (state != - SyncingType.selectedWalletsAtStartup) { - ref - .read(prefsChangeNotifierProvider) - .syncType = - SyncingType.selectedWalletsAtStartup; - - final ids = ref - .read(prefsChangeNotifierProvider) - .walletIdsSyncOnStartup; - - // enable auto sync on selected wallets only - ref - .read(walletsChangeNotifierProvider) - .managers - .forEach((e) => e.shouldAutoSync = - ids.contains(e.walletId)); - } - }, - child: Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.all(8), - child: Row( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 20, - child: Radio( - activeColor: Theme.of(context) - .extension<StackColors>()! - .radioButtonIconEnabled, - value: SyncingType - .selectedWalletsAtStartup, - groupValue: ref.watch( - prefsChangeNotifierProvider - .select((value) => - value.syncType), - ), - onChanged: (value) { - if (value is SyncingType) { - ref - .read( - prefsChangeNotifierProvider) - .syncType = value; - } - }, - ), - ), - const SizedBox( - width: 12, - ), - Flexible( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Sync only selected wallets at startup", - style: STextStyles.titleBold12( - context), - textAlign: TextAlign.left, - ), - Text( - "Only the wallets you select will start syncing when you open the app", - style: STextStyles.itemSubtitle( - context), - textAlign: TextAlign.left, - ), - ], - ), - ), - ], - ), - ), - ), - ), - ), - if (ref.watch(prefsChangeNotifierProvider - .select((value) => value.syncType)) != - SyncingType.selectedWalletsAtStartup) const SizedBox( - height: 12, + width: 12, ), - if (ref.watch(prefsChangeNotifierProvider - .select((value) => value.syncType)) == - SyncingType.selectedWalletsAtStartup) - Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.only( - left: 12.0, - right: 12, - bottom: 12, - ), - child: Row( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - const SizedBox( - width: 12 + 20, - height: 12, - ), - Flexible( - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants - .size.circularBorderRadius, - ), - ), - onPressed: () { - Navigator.of(context).pushNamed( - WalletSyncingOptionsView - .routeName); - }, - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Select wallets...", - style: - STextStyles.link2(context), - textAlign: TextAlign.left, - ), - ], - ), - ), - ), - ], - ), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Sync only currently open wallet", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + Text( + "Sync only the wallet that you are using", + style: STextStyles.itemSubtitle(context), + textAlign: TextAlign.left, + ), + ], ), ), + ], + ), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(4.0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + final state = + ref.read(prefsChangeNotifierProvider).syncType; + if (state != SyncingType.allWalletsOnStartup) { + ref.read(prefsChangeNotifierProvider).syncType = + SyncingType.allWalletsOnStartup; + + // enable auto sync on all wallets + ref + .read(walletsChangeNotifierProvider) + .managers + .forEach((e) => e.shouldAutoSync = true); + } + }, + child: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: SyncingType.allWalletsOnStartup, + groupValue: ref.watch( + prefsChangeNotifierProvider + .select((value) => value.syncType), + ), + onChanged: (value) { + if (value is SyncingType) { + ref + .read(prefsChangeNotifierProvider) + .syncType = value; + } + }, + ), + ), + const SizedBox( + width: 12, + ), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Sync all wallets at startup", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + Text( + "All of your wallets will start syncing when you open the app", + style: STextStyles.itemSubtitle(context), + textAlign: TextAlign.left, + ), + ], + ), + ), + ], + ), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(4), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + final state = + ref.read(prefsChangeNotifierProvider).syncType; + if (state != SyncingType.selectedWalletsAtStartup) { + ref.read(prefsChangeNotifierProvider).syncType = + SyncingType.selectedWalletsAtStartup; + + final ids = ref + .read(prefsChangeNotifierProvider) + .walletIdsSyncOnStartup; + + // enable auto sync on selected wallets only + ref + .read(walletsChangeNotifierProvider) + .managers + .forEach((e) => + e.shouldAutoSync = ids.contains(e.walletId)); + } + }, + child: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.all(8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: SyncingType.selectedWalletsAtStartup, + groupValue: ref.watch( + prefsChangeNotifierProvider + .select((value) => value.syncType), + ), + onChanged: (value) { + if (value is SyncingType) { + ref + .read(prefsChangeNotifierProvider) + .syncType = value; + } + }, + ), + ), + const SizedBox( + width: 12, + ), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Sync only selected wallets at startup", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + Text( + "Only the wallets you select will start syncing when you open the app", + style: STextStyles.itemSubtitle(context), + textAlign: TextAlign.left, + ), + ], + ), + ), + ], + ), + ), + ), + ), + ), + if (ref.watch(prefsChangeNotifierProvider + .select((value) => value.syncType)) != + SyncingType.selectedWalletsAtStartup) + const SizedBox( + height: 12, + ), + if (ref.watch(prefsChangeNotifierProvider + .select((value) => value.syncType)) == + SyncingType.selectedWalletsAtStartup) + Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.only( + left: 12.0, + right: 12, + bottom: 12, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + width: 12 + 20, + height: 12, + ), + Flexible( + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + Navigator.of(context).pushNamed( + WalletSyncingOptionsView.routeName); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Select wallets...", + style: STextStyles.link2(context), + textAlign: TextAlign.left, + ), + ], + ), + ), + ), ], ), ), - ], - ), - ), + ), + ], ), - ); - }, + ), + ], ), ), ); diff --git a/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart b/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart index 9b6c6c85c..7f6aac260 100644 --- a/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart @@ -7,6 +7,8 @@ import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; +import '../../../pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart'; + class SyncingPreferencesSettings extends ConsumerStatefulWidget { const SyncingPreferencesSettings({Key? key}) : super(key: key); @@ -62,6 +64,13 @@ class _SyncingPreferencesSettings ), ], ), + + ///TODO: ONLY SHOW SYNC OPTIONS ON BUTTON PRESS + Column( + children: [ + SyncingOptionsView(), + ], + ), Column( crossAxisAlignment: CrossAxisAlignment.start, children: const [ From 563492d4e85c9557ccb1e7fe4d5a8fb524dfe823 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Thu, 3 Nov 2022 17:11:01 -0600 Subject: [PATCH 123/426] conditional and padding for wallet syncing options --- .../syncing_options_view.dart | 625 ++++++++++-------- .../wallet_syncing_options_view.dart | 69 +- 2 files changed, 376 insertions(+), 318 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart b/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart index a65a03a87..3681009a9 100644 --- a/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart +++ b/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart @@ -9,6 +9,8 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class SyncingOptionsView extends ConsumerWidget { @@ -18,8 +20,9 @@ class SyncingOptionsView extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final isDesktop = Util.isDesktop; return ConditionalParent( - condition: !Util.isDesktop, + condition: !isDesktop, builder: (child) { return Scaffold( backgroundColor: @@ -54,317 +57,353 @@ class SyncingOptionsView extends ConsumerWidget { ), ); }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: Column( - children: [ - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: const EdgeInsets.all(4), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - final state = - ref.read(prefsChangeNotifierProvider).syncType; - if (state != SyncingType.currentWalletOnly) { - ref.read(prefsChangeNotifierProvider).syncType = - SyncingType.currentWalletOnly; - - // disable auto sync on all wallets that aren't active/current - ref - .read(walletsChangeNotifierProvider) - .managers - .forEach((e) { - if (!e.isActiveWallet) { - e.shouldAutoSync = false; - } - }); - } - }, - child: Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 20, - child: Radio( - activeColor: Theme.of(context) - .extension<StackColors>()! - .radioButtonIconEnabled, - value: SyncingType.currentWalletOnly, - groupValue: ref.watch( - prefsChangeNotifierProvider - .select((value) => value.syncType), - ), - onChanged: (value) { - if (value is SyncingType) { - ref - .read(prefsChangeNotifierProvider) - .syncType = value; - } - }, - ), - ), - const SizedBox( - width: 12, - ), - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Sync only currently open wallet", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - Text( - "Sync only the wallet that you are using", - style: STextStyles.itemSubtitle(context), - textAlign: TextAlign.left, - ), - ], - ), - ), - ], - ), - ), + child: Column( + children: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.all(4), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), ), - ), - Padding( - padding: const EdgeInsets.all(4.0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - final state = - ref.read(prefsChangeNotifierProvider).syncType; - if (state != SyncingType.allWalletsOnStartup) { - ref.read(prefsChangeNotifierProvider).syncType = - SyncingType.allWalletsOnStartup; + onPressed: () { + final state = + ref.read(prefsChangeNotifierProvider).syncType; + if (state != SyncingType.currentWalletOnly) { + ref.read(prefsChangeNotifierProvider).syncType = + SyncingType.currentWalletOnly; - // enable auto sync on all wallets - ref - .read(walletsChangeNotifierProvider) - .managers - .forEach((e) => e.shouldAutoSync = true); - } - }, - child: Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 20, - child: Radio( - activeColor: Theme.of(context) - .extension<StackColors>()! - .radioButtonIconEnabled, - value: SyncingType.allWalletsOnStartup, - groupValue: ref.watch( - prefsChangeNotifierProvider - .select((value) => value.syncType), - ), - onChanged: (value) { - if (value is SyncingType) { - ref - .read(prefsChangeNotifierProvider) - .syncType = value; - } - }, - ), - ), - const SizedBox( - width: 12, - ), - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Sync all wallets at startup", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - Text( - "All of your wallets will start syncing when you open the app", - style: STextStyles.itemSubtitle(context), - textAlign: TextAlign.left, - ), - ], - ), - ), - ], - ), - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(4), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - final state = - ref.read(prefsChangeNotifierProvider).syncType; - if (state != SyncingType.selectedWalletsAtStartup) { - ref.read(prefsChangeNotifierProvider).syncType = - SyncingType.selectedWalletsAtStartup; - - final ids = ref - .read(prefsChangeNotifierProvider) - .walletIdsSyncOnStartup; - - // enable auto sync on selected wallets only - ref - .read(walletsChangeNotifierProvider) - .managers - .forEach((e) => - e.shouldAutoSync = ids.contains(e.walletId)); - } - }, - child: Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.all(8), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 20, - child: Radio( - activeColor: Theme.of(context) - .extension<StackColors>()! - .radioButtonIconEnabled, - value: SyncingType.selectedWalletsAtStartup, - groupValue: ref.watch( - prefsChangeNotifierProvider - .select((value) => value.syncType), - ), - onChanged: (value) { - if (value is SyncingType) { - ref - .read(prefsChangeNotifierProvider) - .syncType = value; - } - }, - ), - ), - const SizedBox( - width: 12, - ), - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Sync only selected wallets at startup", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - Text( - "Only the wallets you select will start syncing when you open the app", - style: STextStyles.itemSubtitle(context), - textAlign: TextAlign.left, - ), - ], - ), - ), - ], - ), - ), - ), - ), - ), - if (ref.watch(prefsChangeNotifierProvider - .select((value) => value.syncType)) != - SyncingType.selectedWalletsAtStartup) - const SizedBox( - height: 12, - ), - if (ref.watch(prefsChangeNotifierProvider - .select((value) => value.syncType)) == - SyncingType.selectedWalletsAtStartup) - Container( + // disable auto sync on all wallets that aren't active/current + ref + .read(walletsChangeNotifierProvider) + .managers + .forEach((e) { + if (!e.isActiveWallet) { + e.shouldAutoSync = false; + } + }); + } + }, + child: Container( color: Colors.transparent, child: Padding( - padding: const EdgeInsets.only( - left: 12.0, - right: 12, - bottom: 12, - ), + padding: const EdgeInsets.all(8.0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: SyncingType.currentWalletOnly, + groupValue: ref.watch( + prefsChangeNotifierProvider + .select((value) => value.syncType), + ), + onChanged: (value) { + if (value is SyncingType) { + ref + .read(prefsChangeNotifierProvider) + .syncType = value; + } + }, + ), + ), const SizedBox( - width: 12 + 20, - height: 12, + width: 12, ), Flexible( - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Sync only currently open wallet", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, ), - ), - onPressed: () { - Navigator.of(context).pushNamed( - WalletSyncingOptionsView.routeName); - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Select wallets...", - style: STextStyles.link2(context), - textAlign: TextAlign.left, - ), - ], - ), + Text( + "Sync only the wallet that you are using", + style: STextStyles.itemSubtitle(context), + textAlign: TextAlign.left, + ), + ], ), ), ], ), ), ), - ], - ), + ), + ), + Padding( + padding: const EdgeInsets.all(4.0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + final state = + ref.read(prefsChangeNotifierProvider).syncType; + if (state != SyncingType.allWalletsOnStartup) { + ref.read(prefsChangeNotifierProvider).syncType = + SyncingType.allWalletsOnStartup; + + // enable auto sync on all wallets + ref + .read(walletsChangeNotifierProvider) + .managers + .forEach((e) => e.shouldAutoSync = true); + } + }, + child: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: SyncingType.allWalletsOnStartup, + groupValue: ref.watch( + prefsChangeNotifierProvider + .select((value) => value.syncType), + ), + onChanged: (value) { + if (value is SyncingType) { + ref + .read(prefsChangeNotifierProvider) + .syncType = value; + } + }, + ), + ), + const SizedBox( + width: 12, + ), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Sync all wallets at startup", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + Text( + "All of your wallets will start syncing when you open the app", + style: STextStyles.itemSubtitle(context), + textAlign: TextAlign.left, + ), + ], + ), + ), + ], + ), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(4), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + final state = + ref.read(prefsChangeNotifierProvider).syncType; + if (state != SyncingType.selectedWalletsAtStartup) { + ref.read(prefsChangeNotifierProvider).syncType = + SyncingType.selectedWalletsAtStartup; + + final ids = ref + .read(prefsChangeNotifierProvider) + .walletIdsSyncOnStartup; + + // enable auto sync on selected wallets only + ref + .read(walletsChangeNotifierProvider) + .managers + .forEach((e) => + e.shouldAutoSync = ids.contains(e.walletId)); + } + }, + child: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.all(8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: SyncingType.selectedWalletsAtStartup, + groupValue: ref.watch( + prefsChangeNotifierProvider + .select((value) => value.syncType), + ), + onChanged: (value) { + if (value is SyncingType) { + ref + .read(prefsChangeNotifierProvider) + .syncType = value; + } + }, + ), + ), + const SizedBox( + width: 12, + ), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Sync only selected wallets at startup", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + Text( + "Only the wallets you select will start syncing when you open the app", + style: STextStyles.itemSubtitle(context), + textAlign: TextAlign.left, + ), + ], + ), + ), + ], + ), + ), + ), + ), + ), + if (ref.watch(prefsChangeNotifierProvider + .select((value) => value.syncType)) != + SyncingType.selectedWalletsAtStartup) + const SizedBox( + height: 12, + ), + if (ref.watch(prefsChangeNotifierProvider + .select((value) => value.syncType)) == + SyncingType.selectedWalletsAtStartup) + Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.only( + left: 12.0, + right: 12, + bottom: 12, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + width: 12 + 20, + height: 12, + ), + Flexible( + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + !isDesktop + ? Navigator.of(context).pushNamed( + WalletSyncingOptionsView.routeName) + : showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return DesktopDialog( + maxWidth: 600, + maxHeight: 800, + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Padding( + padding: + const EdgeInsets.all( + 32), + child: Text( + "Select wallets to sync", + style: STextStyles + .desktopH3(context), + textAlign: + TextAlign.center, + ), + ), + const DesktopDialogCloseButton(), + ], + ), + const Expanded( + child: + WalletSyncingOptionsView(), + ), + ], + ), + ); + }); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Select wallets...", + style: STextStyles.link2(context), + textAlign: TextAlign.left, + ), + ], + ), + ), + ), + ], + ), + ), + ), + ], ), - ], - ), + ), + ], ), ); } diff --git a/lib/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart b/lib/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart index 1e302cf12..c615d5f94 100644 --- a/lib/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart +++ b/lib/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart @@ -10,7 +10,9 @@ import 'package:stackwallet/utilities/enums/sync_type_enum.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/animated_text.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -25,30 +27,47 @@ class WalletSyncingOptionsView extends ConsumerWidget { final managers = ref .watch(walletsChangeNotifierProvider.select((value) => value.managers)); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - Navigator.of(context).pop(); - }, - ), - title: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - "Sync only selected wallets at startup", - style: STextStyles.navBarTitle(context), + final isDesktop = Util.isDesktop; + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).pop(); + }, + ), + title: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + "Sync only selected wallets at startup", + style: STextStyles.navBarTitle(context), + ), + ), ), - ), - ), - body: LayoutBuilder(builder: (context, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, + body: Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: child, ), - child: SingleChildScrollView( + ); + }, + child: ConditionalParent( + condition: isDesktop, + builder: (child) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 32), + child: child, + ); + }, + child: LayoutBuilder(builder: (context, constraints) { + return SingleChildScrollView( child: ConstrainedBox( constraints: BoxConstraints( minHeight: constraints.maxHeight - 24, @@ -208,9 +227,9 @@ class WalletSyncingOptionsView extends ConsumerWidget { ), ), ), - ), - ); - }), + ); + }), + ), ); } } From bd04f1d9f9133d1a871ce57d8ae65f2a98669bf1 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Thu, 3 Nov 2022 17:26:56 -0600 Subject: [PATCH 124/426] added border to wallet selection --- .../wallet_syncing_options_view.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart b/lib/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart index c615d5f94..9724356a1 100644 --- a/lib/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart +++ b/lib/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart @@ -90,6 +90,11 @@ class WalletSyncingOptionsView extends ConsumerWidget { ), RoundedWhiteContainer( padding: const EdgeInsets.all(0), + borderColor: !isDesktop + ? Colors.transparent + : Theme.of(context) + .extension<StackColors>()! + .background, child: Column( children: [ ...managers.map( From 14d7c443f237e06a957fcd719413baae7c3cd072 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Thu, 3 Nov 2022 17:58:31 -0600 Subject: [PATCH 125/426] v1.5.14 build 86 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 64c678637..9ba6d4eb9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.5.13+85 +version: 1.5.14+86 environment: sdk: ">=2.17.0 <3.0.0" From bed25b37f720a756c76200372e742c2cf2173ac7 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 3 Nov 2022 17:53:19 -0600 Subject: [PATCH 126/426] desktop node management ui --- .../add_edit_node_view.dart | 653 +++++++++++------- .../manage_nodes_views/coin_nodes_view.dart | 33 +- .../manage_nodes_views/node_details_view.dart | 333 ++++++--- .../home/settings_menu/nodes_settings.dart | 19 +- lib/utilities/theme/stack_colors.dart | 14 + lib/widgets/desktop/delete_button.dart | 98 +++ 6 files changed, 762 insertions(+), 388 deletions(-) create mode 100644 lib/widgets/desktop/delete_button.dart diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index 77349f399..87aee413e 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -8,7 +8,6 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; -import 'package:stackwallet/providers/global/node_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -20,15 +19,18 @@ import 'package:stackwallet/utilities/test_epic_box_connection.dart'; import 'package:stackwallet/utilities/test_monero_node_connection.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.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/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; import 'package:uuid/uuid.dart'; -import 'package:stackwallet/utilities/util.dart'; - enum AddEditNodeViewType { add, edit } class AddEditNodeView extends ConsumerStatefulWidget { @@ -59,6 +61,7 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> { late final AddEditNodeViewType viewType; late final Coin coin; late final String? nodeId; + late final bool isDesktop; late bool saveEnabled; late bool testConnectionEnabled; @@ -162,8 +165,198 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> { return testPassed; } + Future<void> attemptSave() async { + final canConnect = await _testConnection(showFlushBar: false); + + bool? shouldSave; + + if (!canConnect) { + await showDialog<dynamic>( + context: context, + useSafeArea: true, + barrierDismissible: true, + builder: (_) => isDesktop + ? DesktopDialog( + maxWidth: 440, + maxHeight: 300, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only( + top: 32, + ), + child: Row( + children: [ + const SizedBox( + width: 32, + ), + Text( + "Server currently unreachable", + style: STextStyles.desktopH3(context), + ), + ], + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + top: 16, + bottom: 32, + ), + child: Column( + children: [ + const Spacer(), + Text( + "Would you like to save this node anyways?", + style: STextStyles.desktopTextMedium(context), + ), + const Spacer( + flex: 2, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + desktopMed: true, + onPressed: () => Navigator.of( + context, + rootNavigator: true, + ).pop(false), + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Save", + desktopMed: true, + onPressed: () => Navigator.of( + context, + rootNavigator: true, + ).pop(true), + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ) + : StackDialog( + title: "Server currently unreachable", + message: "Would you like to save this node anyways?", + leftButton: TextButton( + onPressed: () async { + Navigator.of(context).pop(false); + }, + child: Text( + "Cancel", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + rightButton: TextButton( + onPressed: () async { + Navigator.of(context).pop(true); + }, + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + child: Text( + "Save", + style: STextStyles.button(context), + ), + ), + ), + ).then((value) { + if (value is bool && value) { + shouldSave = true; + } else { + shouldSave = false; + } + }); + } + + if (!canConnect && !shouldSave!) { + // return without saving + return; + } + + final formData = ref.read(nodeFormDataProvider); + + // strip unused path + String address = formData.host!; + if (coin == Coin.monero || coin == Coin.wownero || coin == Coin.epicCash) { + if (address.startsWith("http")) { + final uri = Uri.parse(address); + address = "${uri.scheme}://${uri.host}"; + } + } + + switch (viewType) { + case AddEditNodeViewType.add: + NodeModel node = NodeModel( + host: address, + port: formData.port!, + name: formData.name!, + id: const Uuid().v1(), + useSSL: formData.useSSL!, + loginName: formData.login, + enabled: true, + coinName: coin.name, + isFailover: formData.isFailover!, + isDown: false, + ); + + await ref.read(nodeServiceChangeNotifierProvider).add( + node, + formData.password, + true, + ); + if (mounted) { + Navigator.of(context) + .popUntil(ModalRoute.withName(widget.routeOnSuccessOrDelete)); + } + break; + case AddEditNodeViewType.edit: + NodeModel node = NodeModel( + host: address, + port: formData.port!, + name: formData.name!, + id: nodeId!, + useSSL: formData.useSSL!, + loginName: formData.login, + enabled: true, + coinName: coin.name, + isFailover: formData.isFailover!, + isDown: false, + ); + + await ref.read(nodeServiceChangeNotifierProvider).add( + node, + formData.password, + true, + ); + if (mounted) { + Navigator.of(context) + .popUntil(ModalRoute.withName(widget.routeOnSuccessOrDelete)); + } + break; + } + } + @override void initState() { + isDesktop = Util.isDesktop; ref.refresh(nodeFormDataProvider); viewType = widget.viewType; @@ -196,279 +389,203 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> { .select((value) => value.getNodeById(id: nodeId!))) : null; - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - viewType == AddEditNodeViewType.edit ? "Edit node" : "Add node", - style: STextStyles.navBarTitle(context), - ), - actions: [ - if (viewType == AddEditNodeViewType.edit) - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("deleteNodeAppBarButtonKey"), - size: 36, - shadows: const [], - color: Theme.of(context).extension<StackColors>()!.background, - icon: SvgPicture.asset( - Assets.svg.trash, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - width: 20, - height: 20, - ), - onPressed: () async { - Navigator.popUntil(context, - ModalRoute.withName(widget.routeOnSuccessOrDelete)); + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + viewType == AddEditNodeViewType.edit ? "Edit node" : "Add node", + style: STextStyles.navBarTitle(context), + ), + actions: [ + if (viewType == AddEditNodeViewType.edit) + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("deleteNodeAppBarButtonKey"), + size: 36, + shadows: const [], + color: + Theme.of(context).extension<StackColors>()!.background, + icon: SvgPicture.asset( + Assets.svg.trash, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + width: 20, + height: 20, + ), + onPressed: () async { + Navigator.popUntil(context, + ModalRoute.withName(widget.routeOnSuccessOrDelete)); - await ref.read(nodeServiceChangeNotifierProvider).delete( - nodeId!, - true, - ); - }, + await ref.read(nodeServiceChangeNotifierProvider).delete( + nodeId!, + true, + ); + }, + ), ), ), - ), - ], - ), - body: Padding( - padding: const EdgeInsets.only( - top: 12, - left: 12, - right: 12, - bottom: 12, + ], ), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(4), - child: ConstrainedBox( - constraints: - BoxConstraints(minHeight: constraints.maxHeight - 8), - child: IntrinsicHeight( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - NodeForm( - node: node, - secureStore: widget.secureStore, - readOnly: false, - coin: widget.coin, - onChanged: (canSave, canTest) { - if (canSave != saveEnabled && - canTest != testConnectionEnabled) { - setState(() { - saveEnabled = canSave; - testConnectionEnabled = canTest; - }); - } else if (canSave != saveEnabled) { - setState(() { - saveEnabled = canSave; - }); - } else if (canTest != testConnectionEnabled) { - setState(() { - testConnectionEnabled = canTest; - }); - } - }, - ), - const Spacer(), - TextButton( - onPressed: testConnectionEnabled - ? () async { - await _testConnection(); - } - : null, - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Test connection", - style: STextStyles.button(context).copyWith( - color: testConnectionEnabled - ? Theme.of(context) - .extension<StackColors>()! - .textDark - : Theme.of(context) - .extension<StackColors>()! - .textWhite, - ), - ), - ), - const SizedBox(height: 16), - TextButton( - style: saveEnabled - ? Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context) - : Theme.of(context) - .extension<StackColors>()! - .getPrimaryDisabledButtonColor(context), - onPressed: saveEnabled - ? () async { - final canConnect = await _testConnection( - showFlushBar: false); - - bool? shouldSave; - - if (!canConnect) { - await showDialog<dynamic>( - context: context, - useSafeArea: true, - barrierDismissible: true, - builder: (_) => StackDialog( - title: "Server currently unreachable", - message: - "Would you like to save this node anyways?", - leftButton: TextButton( - onPressed: () async { - Navigator.of(context).pop(false); - }, - child: Text( - "Cancel", - style: STextStyles.button(context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .accentColorDark), - ), - ), - rightButton: TextButton( - onPressed: () async { - Navigator.of(context).pop(true); - }, - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor( - context), - child: Text( - "Save", - style: STextStyles.button(context), - ), - ), - ), - ).then((value) { - if (value is bool && value) { - shouldSave = true; - } else { - shouldSave = false; - } - }); - } - - if (!canConnect && !shouldSave!) { - // return without saving - return; - } - - final formData = - ref.read(nodeFormDataProvider); - - // strip unused path - String address = formData.host!; - if (coin == Coin.monero || - coin == Coin.wownero || - coin == Coin.epicCash) { - if (address.startsWith("http")) { - final uri = Uri.parse(address); - address = "${uri.scheme}://${uri.host}"; - } - } - - switch (viewType) { - case AddEditNodeViewType.add: - NodeModel node = NodeModel( - host: address, - port: formData.port!, - name: formData.name!, - id: const Uuid().v1(), - useSSL: formData.useSSL!, - loginName: formData.login, - enabled: true, - coinName: coin.name, - isFailover: formData.isFailover!, - isDown: false, - ); - - await ref - .read( - nodeServiceChangeNotifierProvider) - .add( - node, - formData.password, - true, - ); - if (mounted) { - Navigator.of(context).popUntil( - ModalRoute.withName( - widget.routeOnSuccessOrDelete)); - } - break; - case AddEditNodeViewType.edit: - NodeModel node = NodeModel( - host: address, - port: formData.port!, - name: formData.name!, - id: nodeId!, - useSSL: formData.useSSL!, - loginName: formData.login, - enabled: true, - coinName: coin.name, - isFailover: formData.isFailover!, - isDown: false, - ); - - await ref - .read( - nodeServiceChangeNotifierProvider) - .add( - node, - formData.password, - true, - ); - if (mounted) { - Navigator.of(context).popUntil( - ModalRoute.withName( - widget.routeOnSuccessOrDelete)); - } - break; - } - } - : null, - child: Text( - "Save", - style: STextStyles.button(context), - ), - ), - ], + body: Padding( + padding: const EdgeInsets.only( + top: 12, + left: 12, + right: 12, + bottom: 12, + ), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(4), + child: ConstrainedBox( + constraints: + BoxConstraints(minHeight: constraints.maxHeight - 8), + child: IntrinsicHeight( + child: child, ), ), ), + ); + }, + ), + ), + ), + child: ConditionalParent( + condition: isDesktop, + builder: (child) => DesktopDialog( + maxWidth: 580, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + const SizedBox( + width: 8, + ), + const AppBarBackButton( + iconSize: 24, + size: 40, + ), + Text( + "Add new node", + style: STextStyles.desktopH3(context), + ) + ], ), - ); - }, + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + top: 16, + bottom: 32, + ), + child: child, + ), + ], + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + NodeForm( + node: node, + secureStore: widget.secureStore, + readOnly: false, + coin: widget.coin, + onChanged: (canSave, canTest) { + if (canSave != saveEnabled && + canTest != testConnectionEnabled) { + setState(() { + saveEnabled = canSave; + testConnectionEnabled = canTest; + }); + } else if (canSave != saveEnabled) { + setState(() { + saveEnabled = canSave; + }); + } else if (canTest != testConnectionEnabled) { + setState(() { + testConnectionEnabled = canTest; + }); + } + }, + ), + if (!isDesktop) const Spacer(), + if (isDesktop) + const SizedBox( + height: 78, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Test connection", + enabled: testConnectionEnabled, + desktopMed: true, + onPressed: testConnectionEnabled + ? () async { + await _testConnection(); + } + : null, + ), + ), + if (isDesktop) + const SizedBox( + width: 16, + ), + if (isDesktop) + Expanded( + child: PrimaryButton( + label: "Save", + enabled: saveEnabled, + desktopMed: true, + onPressed: saveEnabled ? attemptSave : null, + ), + ), + ], + ), + if (!isDesktop) + const SizedBox( + height: 16, + ), + if (!isDesktop) + TextButton( + style: saveEnabled + ? Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context) + : Theme.of(context) + .extension<StackColors>()! + .getPrimaryDisabledButtonColor(context), + onPressed: saveEnabled ? attemptSave : null, + child: Text( + "Save", + style: STextStyles.button(context), + ), + ), + ], ), ), ); diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/coin_nodes_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/coin_nodes_view.dart index e3743d54e..a93b64be3 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/coin_nodes_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/coin_nodes_view.dart @@ -9,6 +9,7 @@ import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:tuple/tuple.dart'; @@ -17,11 +18,13 @@ class CoinNodesView extends ConsumerStatefulWidget { const CoinNodesView({ Key? key, required this.coin, + this.rootNavigator = false, }) : super(key: key); static const String routeName = "/coinNodes"; final Coin coin; + final bool rootNavigator; @override ConsumerState<CoinNodesView> createState() => _CoinNodesViewState(); @@ -63,12 +66,17 @@ class _CoinNodesViewState extends ConsumerState<CoinNodesView> { textAlign: TextAlign.center, ), Expanded( - child: const DesktopDialogCloseButton(), + child: DesktopDialogCloseButton( + onPressedOverride: Navigator.of( + context, + rootNavigator: widget.rootNavigator, + ).pop, + ), ), ], ), Padding( - padding: EdgeInsets.only( + padding: const EdgeInsets.only( left: 32, right: 32, ), @@ -83,14 +91,19 @@ class _CoinNodesViewState extends ConsumerState<CoinNodesView> { ), textAlign: TextAlign.left, ), - RichText( - text: TextSpan( - text: 'Add new nodes', - style: - STextStyles.desktopTextExtraSmall(context).copyWith( - color: Colors.blueAccent, - ), - ), + BlueTextButton( + text: "Add new node", + onTap: () { + Navigator.of(context).pushNamed( + AddEditNodeView.routeName, + arguments: Tuple4( + AddEditNodeViewType.add, + widget.coin, + null, + CoinNodesView.routeName, + ), + ); + }, ), ], ), diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart index c5f797e37..c5e666ce2 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart @@ -17,7 +17,13 @@ import 'package:stackwallet/utilities/test_epic_box_connection.dart'; import 'package:stackwallet/utilities/test_monero_node_connection.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/delete_button.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:tuple/tuple.dart'; class NodeDetailsView extends ConsumerStatefulWidget { @@ -48,6 +54,8 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> { late final String nodeId; late final String popRouteName; + bool _desktopReadOnly = true; + @override initState() { secureStore = widget.secureStore; @@ -126,130 +134,239 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> { } if (testPassed) { - showFloatingFlushBar( - type: FlushBarType.success, - message: "Server ping success", - context: context, + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "Server ping success", + context: context, + ), ); } else { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Server unreachable", - context: context, + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Server unreachable", + context: context, + ), ); } } @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "Node details", - style: STextStyles.navBarTitle(context), - ), - actions: [ - if (!nodeId.startsWith("default")) - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("nodeDetailsEditNodeAppBarButtonKey"), - size: 36, - shadows: const [], - color: Theme.of(context).extension<StackColors>()!.background, - icon: SvgPicture.asset( - Assets.svg.pencil, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - width: 20, - height: 20, + final isDesktop = Util.isDesktop; + + final node = ref.watch(nodeServiceChangeNotifierProvider + .select((value) => value.getNodeById(id: nodeId))); + + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Node details", + style: STextStyles.navBarTitle(context), + ), + actions: [ + if (!nodeId.startsWith("default")) + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("nodeDetailsEditNodeAppBarButtonKey"), + size: 36, + shadows: const [], + color: + Theme.of(context).extension<StackColors>()!.background, + icon: SvgPicture.asset( + Assets.svg.pencil, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + width: 20, + height: 20, + ), + onPressed: () { + Navigator.of(context).pushNamed( + AddEditNodeView.routeName, + arguments: Tuple4( + AddEditNodeViewType.edit, + coin, + nodeId, + popRouteName, + ), + ); + }, ), - onPressed: () { - Navigator.of(context).pushNamed( - AddEditNodeView.routeName, - arguments: Tuple4( - AddEditNodeViewType.edit, - coin, - nodeId, - popRouteName, - ), - ); - }, ), ), - ), - ], - ), - body: Padding( - padding: const EdgeInsets.only( - top: 12, - left: 12, - right: 12, + ], ), - child: LayoutBuilder( - builder: (context, constraints) { - final node = ref.watch(nodeServiceChangeNotifierProvider - .select((value) => value.getNodeById(id: nodeId))); - - return SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(4), - child: ConstrainedBox( - constraints: - BoxConstraints(minHeight: constraints.maxHeight - 8), - child: IntrinsicHeight( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - NodeForm( - node: node, - secureStore: secureStore, - readOnly: true, - coin: coin, - ), - const Spacer(), - TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - onPressed: () async { - await _testConnection(ref, context); - }, - child: Text( - "Test connection", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - ), - ), - const SizedBox(height: 16), - ], + body: Padding( + padding: const EdgeInsets.only( + top: 12, + left: 12, + right: 12, + ), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(4), + child: ConstrainedBox( + constraints: + BoxConstraints(minHeight: constraints.maxHeight - 8), + child: IntrinsicHeight( + child: child, ), ), ), + ); + }, + ), + ), + ), + child: ConditionalParent( + condition: isDesktop, + builder: (child) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + const SizedBox( + width: 8, + ), + const AppBarBackButton( + iconSize: 24, + size: 40, + ), + Text( + "Node details", + style: STextStyles.desktopH3(context), + ) + ], ), - ); - }, + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + top: 16, + bottom: 32, + ), + child: child, + ), + ], + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + NodeForm( + node: node, + secureStore: secureStore, + readOnly: isDesktop ? _desktopReadOnly : true, + coin: coin, + ), + if (!isDesktop) const Spacer(), + if (isDesktop) + const SizedBox( + height: 22, + ), + if (isDesktop) + SizedBox( + height: 56, + child: _desktopReadOnly + ? null + : Row( + children: [ + Expanded( + child: DeleteButton( + label: "Delete node", + desktopMed: true, + onPressed: () async { + Navigator.of(context).pop(); + + await ref + .read(nodeServiceChangeNotifierProvider) + .delete( + node!.id, + true, + ); + }, + ), + ), + const SizedBox( + width: 16, + ), + const Spacer(), + ], + ), + ), + if (isDesktop && !_desktopReadOnly) + const SizedBox( + height: 45, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Test connection", + desktopMed: true, + onPressed: () async { + await _testConnection(ref, context); + }, + ), + ), + if (isDesktop) + const SizedBox( + width: 16, + ), + if (isDesktop) + Expanded( + child: !nodeId.startsWith("default") + ? PrimaryButton( + label: _desktopReadOnly ? "Edit" : "Save", + desktopMed: true, + onPressed: () async { + final shouldSave = _desktopReadOnly == false; + setState(() { + _desktopReadOnly = !_desktopReadOnly; + }); + + if (shouldSave) { + // todo save node + } + }, + ) + : Container(), + ), + ], + ), + if (!isDesktop) + const SizedBox( + height: 16, + ), + ], ), ), ); diff --git a/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart b/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart index abba6cccc..1d0317037 100644 --- a/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart @@ -4,6 +4,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/coin_nodes_view.dart'; import 'package:stackwallet/providers/global/node_service_provider.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; +import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -167,8 +168,22 @@ class _NodesSettings extends ConsumerState<NodesSettings> { onPressed: () { showDialog<void>( context: context, - builder: (context) => CoinNodesView( - coin: coin, + builder: (context) => Navigator( + initialRoute: CoinNodesView.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + FadePageRoute( + CoinNodesView( + coin: coin, + rootNavigator: true, + ), + const RouteSettings( + name: CoinNodesView.routeName, + ), + ), + ]; + }, ), ); }, diff --git a/lib/utilities/theme/stack_colors.dart b/lib/utilities/theme/stack_colors.dart index 8a241db06..8249dccf4 100644 --- a/lib/utilities/theme/stack_colors.dart +++ b/lib/utilities/theme/stack_colors.dart @@ -1468,6 +1468,20 @@ class StackColors extends ThemeExtension<StackColors> { } } + ButtonStyle? getDeleteEnabledButtonColor(BuildContext context) => + Theme.of(context).textButtonTheme.style?.copyWith( + backgroundColor: MaterialStateProperty.all<Color>( + textFieldErrorBG, + ), + ); + + ButtonStyle? getDeleteDisabledButtonColor(BuildContext context) => + Theme.of(context).textButtonTheme.style?.copyWith( + backgroundColor: MaterialStateProperty.all<Color>( + buttonBackSecondaryDisabled, + ), + ); + ButtonStyle? getPrimaryEnabledButtonColor(BuildContext context) => Theme.of(context).textButtonTheme.style?.copyWith( backgroundColor: MaterialStateProperty.all<Color>( diff --git a/lib/widgets/desktop/delete_button.dart b/lib/widgets/desktop/delete_button.dart new file mode 100644 index 000000000..e64c85f34 --- /dev/null +++ b/lib/widgets/desktop/delete_button.dart @@ -0,0 +1,98 @@ +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/utilities/util.dart'; +import 'package:stackwallet/widgets/desktop/custom_text_button.dart'; + +class DeleteButton extends StatelessWidget { + const DeleteButton({ + Key? key, + this.width, + this.height, + this.label, + this.onPressed, + this.enabled = true, + this.desktopMed = false, + }) : super(key: key); + + final double? width; + final double? height; + final String? label; + final VoidCallback? onPressed; + final bool enabled; + final bool desktopMed; + + TextStyle getStyle(bool isDesktop, BuildContext context) { + if (isDesktop) { + if (desktopMed) { + return STextStyles.desktopTextExtraSmall(context).copyWith( + color: enabled + ? Theme.of(context).extension<StackColors>()!.accentColorRed + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondaryDisabled, + ); + } else { + return enabled + ? STextStyles.desktopButtonSecondaryEnabled(context).copyWith( + color: + Theme.of(context).extension<StackColors>()!.accentColorRed) + : STextStyles.desktopButtonSecondaryDisabled(context); + } + } else { + return STextStyles.button(context).copyWith( + color: enabled + ? Theme.of(context).extension<StackColors>()!.accentColorRed + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondaryDisabled, + ); + } + } + + @override + Widget build(BuildContext context) { + final isDesktop = Util.isDesktop; + + return CustomTextButtonBase( + height: desktopMed ? 56 : height, + width: width, + textButton: TextButton( + onPressed: enabled ? onPressed : null, + style: enabled + ? Theme.of(context) + .extension<StackColors>()! + .getDeleteEnabledButtonColor(context) + : Theme.of(context) + .extension<StackColors>()! + .getDeleteDisabledButtonColor(context), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + Assets.svg.trash, + width: 20, + height: 20, + color: enabled + ? Theme.of(context).extension<StackColors>()!.accentColorRed + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondaryDisabled, + ), + if (label != null) + const SizedBox( + width: 10, + ), + if (label != null) + Text( + label!, + style: getStyle(isDesktop, context), + ), + ], + ), + ), + ); + } +} From 23d0ab8734de658e361e56654375048a62998e94 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 3 Nov 2022 18:11:29 -0600 Subject: [PATCH 127/426] stack privacy calls fix for small or narrow mobile screens --- lib/pages/stack_privacy_calls.dart | 489 +++++++++++------------------ 1 file changed, 175 insertions(+), 314 deletions(-) diff --git a/lib/pages/stack_privacy_calls.dart b/lib/pages/stack_privacy_calls.dart index fd6f60def..3f819492e 100644 --- a/lib/pages/stack_privacy_calls.dart +++ b/lib/pages/stack_privacy_calls.dart @@ -14,6 +14,7 @@ import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.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'; @@ -70,180 +71,195 @@ class _StackPrivacyCalls extends ConsumerState<StackPrivacyCalls> { ), ), body: SafeArea( - child: Padding( - padding: EdgeInsets.fromLTRB(0, isDesktop ? 0 : 40, 0, 0), - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: isDesktop ? 480 : double.infinity, + child: ConditionalParent( + condition: !isDesktop, + builder: (child) => LayoutBuilder( + builder: (context, constraints) => SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: child, + ), + ), ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - "Choose your Stack experience", - style: isDesktop - ? STextStyles.desktopH2(context) - : STextStyles.pageTitleH1(context), - ), - SizedBox( - height: isDesktop ? 16 : 8, - ), - Text( - !widget.isSettings - ? "You can change it later in Settings" - : "", - style: isDesktop - ? STextStyles.desktopSubtitleH2(context) - : STextStyles.subtitle(context), - ), - SizedBox( - height: isDesktop ? 32 : 36, - ), - Padding( - padding: EdgeInsets.symmetric( - horizontal: isDesktop ? 0 : 16, + ), + child: Padding( + padding: EdgeInsets.fromLTRB(0, isDesktop ? 0 : 40, 0, 0), + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: isDesktop ? 480 : double.infinity, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Choose your Stack experience", + style: isDesktop + ? STextStyles.desktopH2(context) + : STextStyles.pageTitleH1(context), ), - child: PrivacyToggle( - externalCallsEnabled: isEasy, - onChanged: (externalCalls) { - isEasy = externalCalls; - setState(() { - infoToggle = isEasy; - }); - }, + SizedBox( + height: isDesktop ? 16 : 8, ), - ), - SizedBox( - height: isDesktop ? 16 : 36, - ), - Padding( - padding: isDesktop - ? const EdgeInsets.all(0) - : const EdgeInsets.all(16.0), - child: RoundedWhiteContainer( - child: Center( - child: RichText( - textAlign: TextAlign.left, - text: TextSpan( - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - : STextStyles.label(context).copyWith( - fontSize: 12.0, - ), - children: infoToggle - ? [ - const TextSpan( - text: - "Exchange data preloaded for a seamless experience."), - const TextSpan( - text: - "\n\nCoinGecko enabled: (24 hour price change shown in-app, total wallet value shown in USD or other currency)."), - TextSpan( - text: - "\n\nRecommended for most crypto users.", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall600( - context) - : TextStyle( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - fontWeight: FontWeight.w600, - ), + Text( + !widget.isSettings + ? "You can change it later in Settings" + : "", + style: isDesktop + ? STextStyles.desktopSubtitleH2(context) + : STextStyles.subtitle(context), + ), + SizedBox( + height: isDesktop ? 32 : 36, + ), + Padding( + padding: EdgeInsets.symmetric( + horizontal: isDesktop ? 0 : 16, + ), + child: PrivacyToggle( + externalCallsEnabled: isEasy, + onChanged: (externalCalls) { + isEasy = externalCalls; + setState(() { + infoToggle = isEasy; + }); + }, + ), + ), + SizedBox( + height: isDesktop ? 16 : 36, + ), + Padding( + padding: isDesktop + ? const EdgeInsets.all(0) + : const EdgeInsets.all(16.0), + child: RoundedWhiteContainer( + child: Center( + child: RichText( + textAlign: TextAlign.left, + text: TextSpan( + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.label(context).copyWith( + fontSize: 12.0, ), - ] - : [ - const TextSpan( + children: infoToggle + ? [ + const TextSpan( + text: + "Exchange data preloaded for a seamless experience."), + const TextSpan( + text: + "\n\nCoinGecko enabled: (24 hour price change shown in-app, total wallet value shown in USD or other currency)."), + TextSpan( text: - "Exchange data not preloaded (slower experience)."), - const TextSpan( + "\n\nRecommended for most crypto users.", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall600( + context) + : TextStyle( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + fontWeight: FontWeight.w600, + ), + ), + ] + : [ + const TextSpan( + text: + "Exchange data not preloaded (slower experience)."), + const TextSpan( + text: + "\n\nCoinGecko disabled (price changes not shown, no wallet value shown in other currencies)."), + TextSpan( text: - "\n\nCoinGecko disabled (price changes not shown, no wallet value shown in other currencies)."), - TextSpan( - text: - "\n\nRecommended for the privacy conscious.", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall600( - context) - : TextStyle( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - fontWeight: FontWeight.w600, - ), - ), - ], + "\n\nRecommended for the privacy conscious.", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall600( + context) + : TextStyle( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + fontWeight: FontWeight.w600, + ), + ), + ], + ), ), ), ), ), - ), - if (!isDesktop) - const Spacer( - flex: 4, - ), - if (isDesktop) - const SizedBox( - height: 32, - ), - Padding( - padding: isDesktop - ? const EdgeInsets.all(0) - : const EdgeInsets.symmetric( - horizontal: 16, - vertical: 16, - ), - child: Row( - children: [ - Expanded( - child: PrimaryButton( - label: - !widget.isSettings ? "Continue" : "Save changes", - onPressed: () { - ref - .read(prefsChangeNotifierProvider) - .externalCalls = isEasy; + if (!isDesktop) + const Spacer( + flex: 4, + ), + if (isDesktop) + const SizedBox( + height: 32, + ), + Padding( + padding: isDesktop + ? const EdgeInsets.all(0) + : const EdgeInsets.symmetric( + horizontal: 16, + vertical: 16, + ), + child: Row( + children: [ + Expanded( + child: PrimaryButton( + label: + !widget.isSettings ? "Continue" : "Save changes", + onPressed: () { + ref + .read(prefsChangeNotifierProvider) + .externalCalls = isEasy; - DB.instance - .put<dynamic>( - boxName: DB.boxNamePrefs, - key: "externalCalls", - value: isEasy) - .then((_) { - if (isEasy) { - unawaited( - ExchangeDataLoadingService().loadAll(ref)); - ref - .read(priceAnd24hChangeNotifierProvider) - .start(true); - } - }); - if (!widget.isSettings) { - if (isDesktop) { - Navigator.of(context).pushNamed( - CreatePasswordView.routeName, - ); + DB.instance + .put<dynamic>( + boxName: DB.boxNamePrefs, + key: "externalCalls", + value: isEasy) + .then((_) { + if (isEasy) { + unawaited( + ExchangeDataLoadingService().loadAll(ref)); + ref + .read(priceAnd24hChangeNotifierProvider) + .start(true); + } + }); + if (!widget.isSettings) { + if (isDesktop) { + Navigator.of(context).pushNamed( + CreatePasswordView.routeName, + ); + } else { + Navigator.of(context).pushNamed( + CreatePinView.routeName, + ); + } } else { - Navigator.of(context).pushNamed( - CreatePinView.routeName, - ); + Navigator.pop(context); } - } else { - Navigator.pop(context); - } - }, + }, + ), ), - ), - ], + ], + ), ), - ), - if (isDesktop) - const SizedBox( - height: kDesktopAppBarHeight, - ), - ], + if (isDesktop) + const SizedBox( + height: kDesktopAppBarHeight, + ), + ], + ), ), ), ), @@ -494,158 +510,3 @@ class _PrivacyToggleState extends State<PrivacyToggle> { ); } } - -// class ContinueButton extends ConsumerWidget { -// const ContinueButton({ -// Key? key, -// required this.isDesktop, -// required this.onPressed, -// required this.label, -// }) : super(key: key); -// -// final String label; -// final bool isDesktop; -// final VoidCallback onPressed; -// -// @override -// Widget build(BuildContext context, WidgetRef ref) { -// if (isDesktop) { -// return SizedBox( -// width: 328, -// height: 70, -// child: TextButton( -// style: Theme.of(context) -// .extension<StackColors>()! -// .getPrimaryEnabledButtonColor(context), -// onPressed: onPressed, -// child: Text( -// label, -// style: STextStyles.button(context).copyWith(fontSize: 20), -// ), -// ), -// ); -// } else { -// return TextButton( -// style: Theme.of(context) -// .extension<StackColors>()! -// .getPrimaryEnabledButtonColor(context), -// onPressed: onPressed, -// child: Text( -// label, -// style: STextStyles.button(context), -// ), -// ); -// } -// } -// } - -// class CustomRadio extends StatefulWidget { -// CustomRadio(this.upperCall, {Key? key}) : super(key: key); -// -// Function upperCall; -// -// @override -// createState() { -// return CustomRadioState(); -// } -// } -// -// class CustomRadioState extends State<CustomRadio> { -// List<RadioModel> sampleData = <RadioModel>[]; -// -// @override -// void initState() { -// super.initState(); -// sampleData.add( -// RadioModel(true, Assets.svg.personaEasy, 'Easy Crypto', 'Recommended')); -// sampleData.add(RadioModel( -// false, Assets.svg.personaIncognito, 'Incognito', 'Privacy conscious')); -// } -// -// @override -// Widget build(BuildContext context) { -// return Row( -// mainAxisAlignment: MainAxisAlignment.center, -// children: [ -// InkWell( -// onTap: () { -// setState(() { -// // if (!sampleData[0].isSelected) { -// widget.upperCall.call(true); -// // } -// for (var element in sampleData) { -// element.isSelected = false; -// } -// sampleData[0].isSelected = true; -// }); -// }, -// child: RadioItem(sampleData[0]), -// ), -// InkWell( -// onTap: () { -// setState(() { -// // if (!sampleData[1].isSelected) { -// widget.upperCall.call(false); -// // } -// for (var element in sampleData) { -// element.isSelected = false; -// } -// sampleData[1].isSelected = true; -// }); -// }, -// child: RadioItem(sampleData[1]), -// ) -// ], -// ); -// } -// } -// -// class RadioItem extends StatelessWidget { -// final RadioModel _item; -// const RadioItem(this._item, {Key? key}) : super(key: key); -// @override -// Widget build(BuildContext context) { -// return Container( -// margin: const EdgeInsets.all(15.0), -// child: RoundedWhiteContainer( -// borderColor: _item.isSelected ? const Color(0xFF0056D2) : null, -// child: Center( -// child: Column( -// children: [ -// SvgPicture.asset( -// _item.svg, -// // color: Theme.of(context).extension<StackColors>()!.textWhite, -// width: 140, -// height: 140, -// ), -// RichText( -// textAlign: TextAlign.center, -// text: TextSpan( -// style: STextStyles.label(context).copyWith(fontSize: 12.0), -// children: [ -// TextSpan( -// text: _item.topText, -// style: TextStyle( -// color: Theme.of(context) -// .extension<StackColors>()! -// .textDark, -// fontWeight: FontWeight.bold)), -// TextSpan(text: "\n${_item.bottomText}"), -// ], -// ), -// ), -// ], -// )), -// ), -// ); -// } -// } -// -// class RadioModel { -// bool isSelected; -// final String svg; -// final String topText; -// final String bottomText; -// -// RadioModel(this.isSelected, this.svg, this.topText, this.bottomText); -// } From 9231c3a2a59a3322ca0e8ed1bb6eaed1cbeff611 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 3 Nov 2022 18:11:51 -0600 Subject: [PATCH 128/426] all transactions search field fix for mobile --- .../all_transactions_view.dart | 131 ++++++++++-------- 1 file changed, 71 insertions(+), 60 deletions(-) diff --git a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart index 8604ae721..d41877a9a 100644 --- a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart +++ b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart @@ -21,6 +21,7 @@ import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.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_dialog.dart'; @@ -302,68 +303,78 @@ class _TransactionDetailsViewState extends ConsumerState<AllTransactionsView> { padding: const EdgeInsets.all(4), child: Row( children: [ - SizedBox( - width: isDesktop ? 570 : null, - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + ConditionalParent( + condition: isDesktop, + builder: (child) => SizedBox( + width: 570, + child: child, + ), + child: ConditionalParent( + condition: !isDesktop, + builder: (child) => Expanded( + child: child, ), - child: TextField( - autocorrect: !isDesktop, - enableSuggestions: !isDesktop, - controller: _searchController, - focusNode: searchFieldFocusNode, - onChanged: (value) { - setState(() { - _searchString = value; - }); - }, - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context), - decoration: standardInputDecoration( - "Search...", - searchFieldFocusNode, - context, - desktopMed: isDesktop, - ).copyWith( - prefixIcon: Padding( - padding: EdgeInsets.symmetric( - horizontal: isDesktop ? 12 : 10, - vertical: isDesktop ? 18 : 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: isDesktop ? 20 : 16, - height: isDesktop ? 20 : 16, - ), - ), - suffixIcon: _searchController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchController.text = ""; - _searchString = ""; - }); - }, - ), - ], - ), - ), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: _searchController, + focusNode: searchFieldFocusNode, + onChanged: (value) { + setState(() { + _searchString = value; + }); + }, + style: isDesktop + ? STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveText, + height: 1.8, ) - : null, + : STextStyles.field(context), + decoration: standardInputDecoration( + "Search...", + searchFieldFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + prefixIcon: Padding( + padding: EdgeInsets.symmetric( + horizontal: isDesktop ? 12 : 10, + vertical: isDesktop ? 18 : 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: isDesktop ? 20 : 16, + height: isDesktop ? 20 : 16, + ), + ), + suffixIcon: _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + _searchString = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), ), ), ), From a6c380592e7e9a335ef033a13d9d0b5e2d21d6b1 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Fri, 4 Nov 2022 14:18:54 -0600 Subject: [PATCH 129/426] added conditional for desktop manual and restore backup --- .../create_backup_view.dart | 821 +++++++++--------- .../restore_from_file_view.dart | 537 ++++++------ 2 files changed, 695 insertions(+), 663 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart index 9242c0482..b710aacf4 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart @@ -15,6 +15,7 @@ import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/progress_bar.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; @@ -93,426 +94,436 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "Create backup", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (!Platform.isAndroid) - Consumer(builder: (context, ref, __) { - return Container( - color: Colors.transparent, - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - onTap: Platform.isAndroid - ? null - : () async { - try { - await stackFileSystem.prepareStorage(); + final isDesktop = Util.isDesktop; - if (mounted) { - await stackFileSystem - .pickDir(context); - } + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Create backup", + style: STextStyles.navBarTitle(context), + ), + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: child, + ), + ), + ); + }, + ), + ), + ); + }, + child: ConditionalParent( + condition: isDesktop, + builder: (child) { + return Column( + children: [ + Text( + "Choose file location", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of(context).extension<StackColors>()!.textDark3), + ), + // child, + ], + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (!Platform.isAndroid) + Consumer(builder: (context, ref, __) { + return Container( + color: Colors.transparent, + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + onTap: Platform.isAndroid + ? null + : () async { + try { + await stackFileSystem.prepareStorage(); - if (mounted) { - setState(() { - fileLocationController.text = - stackFileSystem.dirPath ?? ""; - }); - } - } catch (e, s) { - Logging.instance.log("$e\n$s", - level: LogLevel.Error); - } - }, - controller: fileLocationController, - style: STextStyles.field(context), - decoration: InputDecoration( - hintText: "Save to...", - hintStyle: STextStyles.fieldLabel(context), - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox( - width: 16, - ), - SvgPicture.asset( - Assets.svg.folder, - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, - width: 16, - height: 16, - ), - const SizedBox( - width: 12, - ), - ], - ), - ), - ), - key: const Key( - "createBackupSaveToFileLocationTextFieldKey"), - readOnly: true, - toolbarOptions: const ToolbarOptions( - copy: true, - cut: false, - paste: false, - selectAll: false, - ), - onChanged: (newValue) { - // ref.read(addressEntryDataProvider(widget.id)).address = newValue; - }, + if (mounted) { + await stackFileSystem.pickDir(context); + } + + if (mounted) { + setState(() { + fileLocationController.text = + stackFileSystem.dirPath ?? ""; + }); + } + } catch (e, s) { + Logging.instance + .log("$e\n$s", level: LogLevel.Error); + } + }, + controller: fileLocationController, + style: STextStyles.field(context), + decoration: InputDecoration( + hintText: "Save to...", + hintStyle: STextStyles.fieldLabel(context), + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox( + width: 16, ), - ); - }), - if (!Platform.isAndroid) + SvgPicture.asset( + Assets.svg.folder, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 16, + height: 16, + ), + const SizedBox( + width: 12, + ), + ], + ), + ), + ), + key: + const Key("createBackupSaveToFileLocationTextFieldKey"), + readOnly: true, + toolbarOptions: const ToolbarOptions( + copy: true, + cut: false, + paste: false, + selectAll: false, + ), + onChanged: (newValue) { + // ref.read(addressEntryDataProvider(widget.id)).address = newValue; + }, + ), + ); + }), + if (!Platform.isAndroid) + const SizedBox( + height: 8, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("createBackupPasswordFieldKey1"), + focusNode: passwordFocusNode, + controller: passwordController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Create passphrase", + passwordFocusNode, + context, + ).copyWith( + suffixIcon: UnconstrainedBox( + child: Row( + children: [ const SizedBox( - height: 8, + width: 16, ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("createBackupPasswordFieldKey1"), - focusNode: passwordFocusNode, - controller: passwordController, - style: STextStyles.field(context), - obscureText: hidePassword, - enableSuggestions: false, - autocorrect: false, - decoration: standardInputDecoration( - "Create passphrase", - passwordFocusNode, - context, - ).copyWith( - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox( - width: 16, - ), - GestureDetector( - key: const Key( - "createBackupPasswordFieldShowPasswordButtonKey"), - onTap: () async { - setState(() { - hidePassword = !hidePassword; - }); - }, - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, - width: 16, - height: 16, - ), - ), - const SizedBox( - width: 12, - ), - ], - ), - ), - ), - onChanged: (newValue) { - if (newValue.isEmpty) { - setState(() { - passwordFeedback = ""; - }); - return; - } - final result = zxcvbn.evaluate(newValue); - String suggestionsAndTips = ""; - for (var sug - in result.feedback.suggestions!.toSet()) { - suggestionsAndTips += "$sug\n"; - } - suggestionsAndTips += result.feedback.warning!; - String feedback = - // "Password Strength: ${((result.score! / 4.0) * 100).toInt()}%\n" - suggestionsAndTips; - - passwordStrength = result.score! / 4; - - // hack fix to format back string returned from zxcvbn - if (feedback.contains("phrasesNo need")) { - feedback = feedback.replaceFirst( - "phrasesNo need", "phrases\nNo need"); - } - - if (feedback.endsWith("\n")) { - feedback = - feedback.substring(0, feedback.length - 2); - } - + GestureDetector( + key: const Key( + "createBackupPasswordFieldShowPasswordButtonKey"), + onTap: () async { setState(() { - passwordFeedback = feedback; + hidePassword = !hidePassword; }); }, - ), - ), - if (passwordFocusNode.hasFocus || - passwordRepeatFocusNode.hasFocus || - passwordController.text.isNotEmpty) - Padding( - padding: EdgeInsets.only( - left: 12, - right: 12, - top: passwordFeedback.isNotEmpty ? 4 : 0, - ), - child: passwordFeedback.isNotEmpty - ? Text( - passwordFeedback, - style: STextStyles.infoSmall(context), - ) - : null, - ), - if (passwordFocusNode.hasFocus || - passwordRepeatFocusNode.hasFocus || - passwordController.text.isNotEmpty) - Padding( - padding: const EdgeInsets.only( - left: 12, - right: 12, - top: 10, - ), - child: ProgressBar( - key: const Key("createStackBackUpProgressBar"), - width: MediaQuery.of(context).size.width - 32 - 24, - height: 5, - fillColor: passwordStrength < 0.51 - ? Theme.of(context) - .extension<StackColors>()! - .accentColorRed - : passwordStrength < 1 - ? Theme.of(context) - .extension<StackColors>()! - .accentColorYellow - : Theme.of(context) - .extension<StackColors>()! - .accentColorGreen, - backgroundColor: Theme.of(context) + child: SvgPicture.asset( + hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash, + color: Theme.of(context) .extension<StackColors>()! - .buttonBackSecondary, - percent: passwordStrength < 0.25 - ? 0.03 - : passwordStrength, + .textDark3, + width: 16, + height: 16, ), ), - const SizedBox( - height: 10, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + const SizedBox( + width: 12, ), - child: TextField( - key: const Key("createBackupPasswordFieldKey2"), - focusNode: passwordRepeatFocusNode, - controller: passwordRepeatController, - style: STextStyles.field(context), - obscureText: hidePassword, - enableSuggestions: false, - autocorrect: false, - decoration: standardInputDecoration( - "Confirm passphrase", - passwordRepeatFocusNode, - context, - ).copyWith( - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox( - width: 16, - ), - GestureDetector( - key: const Key( - "createBackupPasswordFieldShowPasswordButtonKey"), - onTap: () async { - setState(() { - hidePassword = !hidePassword; - }); - }, - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, - width: 16, - height: 16, - ), - ), - const SizedBox( - width: 12, - ), - ], - ), - ), - ), - onChanged: (newValue) { - setState(() {}); - // TODO: ? check if passwords match? - }, - ), - ), - const SizedBox( - height: 16, - ), - const Spacer(), - TextButton( - style: shouldEnableCreate - ? Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context) - : Theme.of(context) - .extension<StackColors>()! - .getPrimaryDisabledButtonColor(context), - onPressed: !shouldEnableCreate - ? null - : () async { - final String pathToSave = - fileLocationController.text; - final String passphrase = - passwordController.text; - final String repeatPassphrase = - passwordRepeatController.text; - - if (pathToSave.isEmpty) { - unawaited(showFloatingFlushBar( - type: FlushBarType.warning, - message: "Directory not chosen", - context: context, - )); - return; - } - if (!(await Directory(pathToSave).exists())) { - unawaited(showFloatingFlushBar( - type: FlushBarType.warning, - message: "Directory does not exist", - context: context, - )); - return; - } - if (passphrase.isEmpty) { - unawaited(showFloatingFlushBar( - type: FlushBarType.warning, - message: "A passphrase is required", - context: context, - )); - return; - } - if (passphrase != repeatPassphrase) { - unawaited(showFloatingFlushBar( - type: FlushBarType.warning, - message: "Passphrase does not match", - context: context, - )); - return; - } - - unawaited(showDialog<dynamic>( - context: context, - barrierDismissible: false, - builder: (_) => const StackDialog( - title: "Encrypting backup", - message: "This shouldn't take long", - ), - )); - // make sure the dialog is able to be displayed for at least 1 second - await Future<void>.delayed( - const Duration(seconds: 1)); - - final DateTime now = DateTime.now(); - final String fileToSave = - "$pathToSave/stackbackup_${now.year}_${now.month}_${now.day}_${now.hour}_${now.minute}_${now.second}.swb"; - - final backup = - await SWB.createStackWalletJSON(); - - bool result = - await SWB.encryptStackWalletWithPassphrase( - fileToSave, - passphrase, - jsonEncode(backup), - ); - - if (mounted) { - // pop encryption progress dialog - Navigator.of(context).pop(); - - if (result) { - await showDialog<dynamic>( - context: context, - barrierDismissible: false, - builder: (_) => Platform.isAndroid - ? StackOkDialog( - title: "Backup saved to:", - message: fileToSave, - ) - : const StackOkDialog( - title: - "Backup creation succeeded"), - ); - passwordController.text = ""; - passwordRepeatController.text = ""; - setState(() {}); - } else { - await showDialog<dynamic>( - context: context, - barrierDismissible: false, - builder: (_) => const StackOkDialog( - title: "Backup creation failed"), - ); - } - } - }, - child: Text( - "Create backup", - style: STextStyles.button(context), - ), - ), - ], + ], + ), ), ), + onChanged: (newValue) { + if (newValue.isEmpty) { + setState(() { + passwordFeedback = ""; + }); + return; + } + final result = zxcvbn.evaluate(newValue); + String suggestionsAndTips = ""; + for (var sug in result.feedback.suggestions!.toSet()) { + suggestionsAndTips += "$sug\n"; + } + suggestionsAndTips += result.feedback.warning!; + String feedback = + // "Password Strength: ${((result.score! / 4.0) * 100).toInt()}%\n" + suggestionsAndTips; + + passwordStrength = result.score! / 4; + + // hack fix to format back string returned from zxcvbn + if (feedback.contains("phrasesNo need")) { + feedback = feedback.replaceFirst( + "phrasesNo need", "phrases\nNo need"); + } + + if (feedback.endsWith("\n")) { + feedback = feedback.substring(0, feedback.length - 2); + } + + setState(() { + passwordFeedback = feedback; + }); + }, ), - ); - }, + ), + if (passwordFocusNode.hasFocus || + passwordRepeatFocusNode.hasFocus || + passwordController.text.isNotEmpty) + Padding( + padding: EdgeInsets.only( + left: 12, + right: 12, + top: passwordFeedback.isNotEmpty ? 4 : 0, + ), + child: passwordFeedback.isNotEmpty + ? Text( + passwordFeedback, + style: STextStyles.infoSmall(context), + ) + : null, + ), + if (passwordFocusNode.hasFocus || + passwordRepeatFocusNode.hasFocus || + passwordController.text.isNotEmpty) + Padding( + padding: const EdgeInsets.only( + left: 12, + right: 12, + top: 10, + ), + child: ProgressBar( + key: const Key("createStackBackUpProgressBar"), + width: MediaQuery.of(context).size.width - 32 - 24, + height: 5, + fillColor: passwordStrength < 0.51 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorRed + : passwordStrength < 1 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorYellow + : Theme.of(context) + .extension<StackColors>()! + .accentColorGreen, + backgroundColor: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + percent: passwordStrength < 0.25 ? 0.03 : passwordStrength, + ), + ), + const SizedBox( + height: 10, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("createBackupPasswordFieldKey2"), + focusNode: passwordRepeatFocusNode, + controller: passwordRepeatController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Confirm passphrase", + passwordRepeatFocusNode, + context, + ).copyWith( + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox( + width: 16, + ), + GestureDetector( + key: const Key( + "createBackupPasswordFieldShowPasswordButtonKey"), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 16, + height: 16, + ), + ), + const SizedBox( + width: 12, + ), + ], + ), + ), + ), + onChanged: (newValue) { + setState(() {}); + // TODO: ? check if passwords match? + }, + ), + ), + const SizedBox( + height: 16, + ), + const Spacer(), + TextButton( + style: shouldEnableCreate + ? Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context) + : Theme.of(context) + .extension<StackColors>()! + .getPrimaryDisabledButtonColor(context), + onPressed: !shouldEnableCreate + ? null + : () async { + final String pathToSave = fileLocationController.text; + final String passphrase = passwordController.text; + final String repeatPassphrase = + passwordRepeatController.text; + + if (pathToSave.isEmpty) { + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "Directory not chosen", + context: context, + )); + return; + } + if (!(await Directory(pathToSave).exists())) { + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "Directory does not exist", + context: context, + )); + return; + } + if (passphrase.isEmpty) { + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "A passphrase is required", + context: context, + )); + return; + } + if (passphrase != repeatPassphrase) { + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "Passphrase does not match", + context: context, + )); + return; + } + + unawaited(showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => const StackDialog( + title: "Encrypting backup", + message: "This shouldn't take long", + ), + )); + // make sure the dialog is able to be displayed for at least 1 second + await Future<void>.delayed(const Duration(seconds: 1)); + + final DateTime now = DateTime.now(); + final String fileToSave = + "$pathToSave/stackbackup_${now.year}_${now.month}_${now.day}_${now.hour}_${now.minute}_${now.second}.swb"; + + final backup = await SWB.createStackWalletJSON(); + + bool result = await SWB.encryptStackWalletWithPassphrase( + fileToSave, + passphrase, + jsonEncode(backup), + ); + + if (mounted) { + // pop encryption progress dialog + Navigator.of(context).pop(); + + if (result) { + await showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => Platform.isAndroid + ? StackOkDialog( + title: "Backup saved to:", + message: fileToSave, + ) + : const StackOkDialog( + title: "Backup creation succeeded"), + ); + passwordController.text = ""; + passwordRepeatController.text = ""; + setState(() {}); + } else { + await showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => const StackOkDialog( + title: "Backup creation failed"), + ); + } + } + }, + child: Text( + "Create backup", + style: STextStyles.button(context), + ), + ), + ], ), ), ); 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 232be9028..16c3ea8e3 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 @@ -15,13 +15,13 @@ import 'package:stackwallet/utilities/enums/flush_bar_type.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/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:tuple/tuple.dart'; -import 'package:stackwallet/utilities/util.dart'; - class RestoreFromFileView extends ConsumerStatefulWidget { const RestoreFromFileView({Key? key}) : super(key: key); @@ -65,275 +65,296 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "Restore from file", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - onTap: () async { - try { - await stackFileSystem.prepareStorage(); - if (mounted) { - await stackFileSystem.openFile(context); - } + final isDesktop = Util.isDesktop; - if (mounted) { + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Restore from file", + style: STextStyles.navBarTitle(context), + ), + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: child, + ), + ), + ); + }, + ), + ), + ); + }, + child: ConditionalParent( + condition: isDesktop, + builder: (child) { + return Column( + children: [ + Text( + "Choose file location", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + ), + // child, + ], + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + onTap: () async { + try { + await stackFileSystem.prepareStorage(); + if (mounted) { + await stackFileSystem.openFile(context); + } + + if (mounted) { + setState(() { + fileLocationController.text = + stackFileSystem.filePath ?? ""; + }); + } + } catch (e, s) { + Logging.instance.log("$e\n$s", level: LogLevel.Error); + } + }, + controller: fileLocationController, + style: STextStyles.field(context), + decoration: InputDecoration( + hintText: "Choose file...", + hintStyle: STextStyles.fieldLabel(context), + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox( + width: 16, + ), + SvgPicture.asset( + Assets.svg.folder, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 16, + height: 16, + ), + const SizedBox( + width: 12, + ), + ], + ), + ), + ), + key: const Key("restoreFromFileLocationTextFieldKey"), + readOnly: true, + toolbarOptions: const ToolbarOptions( + copy: true, + cut: false, + paste: false, + selectAll: false, + ), + onChanged: (newValue) {}, + ), + const SizedBox( + height: 8, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("restoreFromFilePasswordFieldKey"), + focusNode: passwordFocusNode, + controller: passwordController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Enter password", + passwordFocusNode, + context, + ).copyWith( + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox( + width: 16, + ), + GestureDetector( + key: const Key( + "restoreFromFilePasswordFieldShowPasswordButtonKey"), + onTap: () async { setState(() { - fileLocationController.text = - stackFileSystem.filePath ?? ""; + hidePassword = !hidePassword; }); - } - } catch (e, s) { - Logging.instance - .log("$e\n$s", level: LogLevel.Error); - } - }, - controller: fileLocationController, - style: STextStyles.field(context), - decoration: InputDecoration( - hintText: "Choose file...", - hintStyle: STextStyles.fieldLabel(context), - suffixIcon: UnconstrainedBox( - child: Row( + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 16, + height: 16, + ), + ), + const SizedBox( + width: 12, + ), + ], + ), + ), + ), + onChanged: (newValue) { + setState(() {}); + }, + ), + ), + const SizedBox( + height: 16, + ), + const Spacer(), + TextButton( + style: passwordController.text.isEmpty || + fileLocationController.text.isEmpty + ? Theme.of(context) + .extension<StackColors>()! + .getPrimaryDisabledButtonColor(context) + : Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + onPressed: passwordController.text.isEmpty || + fileLocationController.text.isEmpty + ? null + : () async { + final String fileToRestore = + fileLocationController.text; + final String passphrase = passwordController.text; + + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75)); + } + + if (!(await File(fileToRestore).exists())) { + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Backup file does not exist", + context: context, + ); + return; + } + + bool shouldPop = false; + showDialog<dynamic>( + barrierDismissible: false, + context: context, + builder: (_) => WillPopScope( + onWillPop: () async { + return shouldPop; + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, children: [ - const SizedBox( - width: 16, - ), - SvgPicture.asset( - Assets.svg.folder, - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, - width: 16, - height: 16, + Material( + color: Colors.transparent, + child: Center( + child: Text( + "Decrypting Stack backup file", + style: STextStyles.pageTitleH2(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textWhite, + ), + ), + ), ), const SizedBox( - width: 12, + height: 64, + ), + const Center( + child: LoadingIndicator( + width: 100, + ), ), ], ), ), - ), - key: const Key("restoreFromFileLocationTextFieldKey"), - readOnly: true, - toolbarOptions: const ToolbarOptions( - copy: true, - cut: false, - paste: false, - selectAll: false, - ), - onChanged: (newValue) {}, - ), - const SizedBox( - height: 8, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("restoreFromFilePasswordFieldKey"), - focusNode: passwordFocusNode, - controller: passwordController, - style: STextStyles.field(context), - obscureText: hidePassword, - enableSuggestions: false, - autocorrect: false, - decoration: standardInputDecoration( - "Enter password", - passwordFocusNode, - context, - ).copyWith( - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox( - width: 16, - ), - GestureDetector( - key: const Key( - "restoreFromFilePasswordFieldShowPasswordButtonKey"), - onTap: () async { - setState(() { - hidePassword = !hidePassword; - }); - }, - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, - width: 16, - height: 16, - ), - ), - const SizedBox( - width: 12, - ), - ], + ); + + final String? jsonString = await compute( + SWB.decryptStackWalletWithPassphrase, + Tuple2(fileToRestore, passphrase), + debugLabel: "stack wallet decryption compute", + ); + + if (mounted) { + // pop LoadingIndicator + shouldPop = true; + Navigator.of(context).pop(); + + passwordController.text = ""; + + if (jsonString == null) { + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Failed to decrypt backup file", + context: context, + ); + return; + } + + Navigator.of(context).push( + RouteGenerator.getRoute( + builder: (_) => StackRestoreProgressView( + jsonString: jsonString, ), ), - ), - onChanged: (newValue) { - setState(() {}); - }, - ), - ), - const SizedBox( - height: 16, - ), - const Spacer(), - TextButton( - style: passwordController.text.isEmpty || - fileLocationController.text.isEmpty - ? Theme.of(context) - .extension<StackColors>()! - .getPrimaryDisabledButtonColor(context) - : Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: passwordController.text.isEmpty || - fileLocationController.text.isEmpty - ? null - : () async { - final String fileToRestore = - fileLocationController.text; - final String passphrase = - passwordController.text; - - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration(milliseconds: 75)); - } - - if (!(await File(fileToRestore).exists())) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Backup file does not exist", - context: context, - ); - return; - } - - bool shouldPop = false; - showDialog<dynamic>( - barrierDismissible: false, - context: context, - builder: (_) => WillPopScope( - onWillPop: () async { - return shouldPop; - }, - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Material( - color: Colors.transparent, - child: Center( - child: Text( - "Decrypting Stack backup file", - style: STextStyles.pageTitleH2( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textWhite, - ), - ), - ), - ), - const SizedBox( - height: 64, - ), - const Center( - child: LoadingIndicator( - width: 100, - ), - ), - ], - ), - ), - ); - - final String? jsonString = await compute( - SWB.decryptStackWalletWithPassphrase, - Tuple2(fileToRestore, passphrase), - debugLabel: "stack wallet decryption compute", - ); - - if (mounted) { - // pop LoadingIndicator - shouldPop = true; - Navigator.of(context).pop(); - - passwordController.text = ""; - - if (jsonString == null) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Failed to decrypt backup file", - context: context, - ); - return; - } - - Navigator.of(context).push( - RouteGenerator.getRoute( - builder: (_) => StackRestoreProgressView( - jsonString: jsonString, - ), - ), - ); - } - }, - child: Text( - "Restore", - style: STextStyles.button(context), - ), - ), - ], - ), + ); + } + }, + child: Text( + "Restore", + style: STextStyles.button(context), ), ), - ); - }, - ), - ), - ); + ], + ), + )); } } From 2c935d65b6380a34a174b32db287190e52dd20e1 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Fri, 4 Nov 2022 14:19:53 -0600 Subject: [PATCH 130/426] WIP: showing textfields for backups --- .../backup_and_restore_settings.dart | 341 +++++++++--------- 1 file changed, 179 insertions(+), 162 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart index 8b74f8d40..d5ad6e6c7 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart @@ -2,6 +2,8 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/restore_backup_dialog.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -24,193 +26,208 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - return ListView( - shrinkWrap: true, - scrollDirection: Axis.vertical, - children: [ - Padding( - padding: const EdgeInsets.only( - right: 30, - ), - child: RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SvgPicture.asset( - Assets.svg.backupAuto, - width: 48, - height: 48, - ), - Center( - child: Padding( - padding: const EdgeInsets.all(10), - child: RichText( - textAlign: TextAlign.start, - text: TextSpan( + + return LayoutBuilder(builder: (context, constraints) { + return SingleChildScrollView( + scrollDirection: Axis.vertical, + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only( + right: 30, + ), + child: RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - TextSpan( - text: "Auto Backup", - style: STextStyles.desktopTextSmall(context), + SvgPicture.asset( + Assets.svg.backupAuto, + width: 48, + height: 48, ), - TextSpan( - text: - "\n\nAuto backup is a custom Stack Wallet feature that offers a convenient backup of your data." - "To ensure maximum security, we recommend using a unique password that you haven't used anywhere " - "else on the internet before. Your password is not stored.", - style: - STextStyles.desktopTextExtraExtraSmall(context), + Center( + child: Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.start, + text: TextSpan( + children: [ + TextSpan( + text: "Auto Backup", + style: + STextStyles.desktopTextSmall(context), + ), + TextSpan( + text: + "\n\nAuto backup is a custom Stack Wallet feature that offers a convenient backup of your data." + "To ensure maximum security, we recommend using a unique password that you haven't used anywhere " + "else on the internet before. Your password is not stored.", + style: STextStyles + .desktopTextExtraExtraSmall(context), + ), + TextSpan( + text: + "\n\nFor more information, please see our website ", + style: STextStyles + .desktopTextExtraExtraSmall(context), + ), + TextSpan( + text: "stackwallet.com", + style: STextStyles.richLink(context) + .copyWith(fontSize: 14), + recognizer: TapGestureRecognizer() + ..onTap = () { + launchUrl( + Uri.parse( + "https://stackwallet.com/"), + mode: + LaunchMode.externalApplication, + ); + }, + ), + ], + ), + ), + ), ), - TextSpan( - text: - "\n\nFor more information, please see our website ", - style: - STextStyles.desktopTextExtraExtraSmall(context), - ), - TextSpan( - text: "stackwallet.com", - style: STextStyles.richLink(context) - .copyWith(fontSize: 14), - recognizer: TapGestureRecognizer() - ..onTap = () { - launchUrl( - Uri.parse("https://stackwallet.com/"), - mode: LaunchMode.externalApplication, - ); - }, + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Padding( + padding: EdgeInsets.all( + 10, + ), + child: AutoBackupButton(), + ), + ], ), ], ), ), ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: const [ - Padding( - padding: EdgeInsets.all( - 10, - ), - child: AutoBackupButton(), + const SizedBox( + height: 25, + ), + Padding( + padding: const EdgeInsets.only( + right: 30, ), - ], - ), - ], - ), - ), - ), - const SizedBox( - height: 25, - ), - Padding( - padding: const EdgeInsets.only( - right: 30, - ), - child: RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SvgPicture.asset( - Assets.svg.backupAdd, - width: 48, - height: 48, - alignment: Alignment.topLeft, - ), - Center( - child: Padding( - padding: const EdgeInsets.all(10), - child: RichText( - textAlign: TextAlign.start, - text: TextSpan( + child: RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - TextSpan( - text: "Manual Backup", - style: STextStyles.desktopTextSmall(context), + SvgPicture.asset( + Assets.svg.backupAdd, + width: 48, + height: 48, + alignment: Alignment.topLeft, ), - TextSpan( - text: - "\n\nCreate manual backup to easily transfer your data between devices. " - "You will create a backup file that can be later used in the Restore option. " - "Use a strong password to encrypt your data.", - style: - STextStyles.desktopTextExtraExtraSmall(context), + Center( + child: Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.start, + text: TextSpan( + children: [ + TextSpan( + text: "Manual Backup", + style: + STextStyles.desktopTextSmall(context), + ), + TextSpan( + text: + "\n\nCreate manual backup to easily transfer your data between devices. " + "You will create a backup file that can be later used in the Restore option. " + "Use a strong password to encrypt your data.", + style: STextStyles + .desktopTextExtraExtraSmall(context), + ), + ], + ), + ), + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Padding( + padding: EdgeInsets.all( + 10, + ), + child: CreateBackupView(), + ), + ], ), ], ), ), ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: const [ - Padding( - padding: EdgeInsets.all( - 10, - ), - child: ManualBackupButton(), + const SizedBox( + height: 25, + ), + Padding( + padding: const EdgeInsets.only( + right: 30, ), - ], - ), - ], - ), - ), - ), - const SizedBox( - height: 25, - ), - Padding( - padding: const EdgeInsets.only( - right: 30, - ), - child: RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SvgPicture.asset( - Assets.svg.backupRestore, - width: 48, - height: 48, - alignment: Alignment.topLeft, - ), - Center( - child: Padding( - padding: const EdgeInsets.all(10), - child: RichText( - textAlign: TextAlign.start, - text: TextSpan( + child: RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - TextSpan( - text: "Restore Backup", - style: STextStyles.desktopTextSmall(context), + SvgPicture.asset( + Assets.svg.backupRestore, + width: 48, + height: 48, + alignment: Alignment.topLeft, ), - TextSpan( - text: - "\n\nUse your Stack Wallet backup file to restore your wallets, address book " - "and wallet preferences.", - style: - STextStyles.desktopTextExtraExtraSmall(context), + Center( + child: Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.start, + text: TextSpan( + children: [ + TextSpan( + text: "Restore Backup", + style: + STextStyles.desktopTextSmall(context), + ), + TextSpan( + text: + "\n\nUse your Stack Wallet backup file to restore your wallets, address book " + "and wallet preferences.", + style: STextStyles + .desktopTextExtraExtraSmall(context), + ), + ], + ), + ), + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Padding( + padding: EdgeInsets.all( + 10, + ), + child: RestoreFromFileView(), + ), + ], ), ], ), ), ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: const [ - Padding( - padding: EdgeInsets.all( - 10, - ), - child: RestoreBackupButton(), - ), - ], - ), - ], + ], + ), ), - ), - ), - ], - ); + )); + }); } } From 905e396a17f06813224dc2278df8a0ef37fdba20 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Fri, 4 Nov 2022 14:21:09 -0600 Subject: [PATCH 131/426] implemented clear logs on press --- .../advanced_settings/debug_info_dialog.dart | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/advanced_settings/debug_info_dialog.dart b/lib/pages_desktop_specific/home/settings_menu/advanced_settings/debug_info_dialog.dart index 0406a059f..cf687e3e7 100644 --- a/lib/pages_desktop_specific/home/settings_menu/advanced_settings/debug_info_dialog.dart +++ b/lib/pages_desktop_specific/home/settings_menu/advanced_settings/debug_info_dialog.dart @@ -1,10 +1,14 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/models/isar/models/log.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/providers/global/debug_service_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/enums/log_level_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -105,7 +109,7 @@ class _DebugInfoDialog extends ConsumerState<DebugInfoDialog> { ], ), Expanded( - flex: 24, + // flex: 24, child: NestedScrollView( floatHeaderSlivers: true, headerSliverBuilder: (context, innerBoxIsScrolled) { @@ -314,7 +318,7 @@ class _DebugInfoDialog extends ConsumerState<DebugInfoDialog> { ), ), ), - const Spacer(), + // const Spacer(), Padding( padding: const EdgeInsets.all(32), child: Row( @@ -322,7 +326,18 @@ class _DebugInfoDialog extends ConsumerState<DebugInfoDialog> { Expanded( child: SecondaryButton( label: "Clear logs", - onPressed: () {}, + onPressed: () async { + await ref.read(debugServiceProvider).deleteAllMessages(); + await ref.read(debugServiceProvider).updateRecentLogs(); + + if (mounted) { + Navigator.pop(context); + unawaited(showFloatingFlushBar( + type: FlushBarType.info, + context: context, + message: 'Logs cleared!')); + } + }, ), ), const SizedBox( From ac0d4191c6a2a6fd62da1a02b9fde9679be8aee9 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Fri, 4 Nov 2022 16:34:36 -0600 Subject: [PATCH 132/426] added button widgets and some containers to restore dialog --- assets/svg/framed-address-book.svg | 4 + assets/svg/framed-gear.svg | 4 + .../backup_and_restore_settings.dart | 154 ++++-------- .../restore_backup_dialog.dart | 230 ++++++++++++------ lib/utilities/assets.dart | 2 + pubspec.yaml | 2 + 6 files changed, 217 insertions(+), 179 deletions(-) create mode 100644 assets/svg/framed-address-book.svg create mode 100644 assets/svg/framed-gear.svg diff --git a/assets/svg/framed-address-book.svg b/assets/svg/framed-address-book.svg new file mode 100644 index 000000000..157117097 --- /dev/null +++ b/assets/svg/framed-address-book.svg @@ -0,0 +1,4 @@ +<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="40" height="40" rx="8" fill="#E0E3E3"/> +<path d="M26 8H12.5C10.843 8 9.5 9.34297 9.5 11V29C9.5 30.657 10.843 32 12.5 32H26C27.657 32 29 30.657 29 29V11C29 9.34297 27.6547 8 26 8ZM19.25 14C20.907 14 22.25 15.343 22.25 17C22.25 18.657 20.907 20 19.25 20C17.5934 20 16.25 18.657 16.25 17C16.25 15.343 17.5953 14 19.25 14ZM23.75 26H14.75C14.3375 26 14 25.6625 14 25.25C14 23.1781 15.6781 21.5 17.75 21.5H20.75C22.8209 21.5 24.5 23.1791 24.5 25.25C24.5 25.6625 24.1625 26 23.75 26ZM31.25 11H30.5V15.5H31.25C31.6625 15.5 32 15.1625 32 14.75V11.75C32 11.3356 31.6625 11 31.25 11ZM31.25 17H30.5V21.5H31.25C31.6625 21.5 32 21.1625 32 20.75V17.75C32 17.3375 31.6625 17 31.25 17ZM31.25 23H30.5V27.5H31.25C31.6642 27.5 32 27.1642 32 26.75V23.75C32 23.3375 31.6625 23 31.25 23Z" fill="#232323"/> +</svg> diff --git a/assets/svg/framed-gear.svg b/assets/svg/framed-gear.svg new file mode 100644 index 000000000..749d9803d --- /dev/null +++ b/assets/svg/framed-gear.svg @@ -0,0 +1,4 @@ +<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="40" height="40" rx="8" fill="#E0E3E3"/> +<path d="M30.6765 16.1586C30.8183 16.5281 30.698 16.9449 30.4058 17.2156L28.5452 18.9086C28.5925 19.2652 28.6183 19.6305 28.6183 19.9613C28.6183 20.3695 28.5925 20.7348 28.5452 21.0914L30.4058 22.7844C30.698 23.0551 30.8183 23.4676 30.6765 23.8414C30.4874 24.3527 30.2597 24.8469 30.0019 25.3152L29.7999 25.6633C29.5163 26.1359 29.1984 26.5828 28.8503 27.0082C28.5925 27.3133 28.1757 27.4207 27.7976 27.3004L25.4042 26.5355C24.8284 26.9781 24.1538 27.3477 23.5136 27.6313L22.9765 30.0848C22.8906 30.4715 22.5898 30.7465 22.1945 30.8496C21.6015 30.9484 20.9913 31 20.3296 31C19.7452 31 19.1351 30.9484 18.5421 30.8496C18.1468 30.7465 17.846 30.4715 17.7601 30.0848L17.223 27.6313C16.5441 27.3477 15.9081 26.9781 15.3323 26.5355L12.9407 27.3004C12.5609 27.4207 12.1419 27.3133 11.8875 27.0082C11.5391 26.5828 11.2211 26.1359 10.9375 25.6633L10.7364 25.3152C10.4756 24.8469 10.2487 24.3527 10.0584 23.8414C9.91914 23.4719 10.0364 23.0551 10.3312 22.7844L12.19 21.0914C12.1428 20.7348 12.1183 20.3695 12.1183 20C12.1183 19.6305 12.1428 19.2652 12.19 18.9086L10.3312 17.2156C10.0364 16.9449 9.91914 16.5324 10.0584 16.1586C10.2487 15.6473 10.476 15.1531 10.7364 14.6848L10.9371 14.3367C11.2207 13.8641 11.5391 13.4172 11.8875 12.9939C12.1419 12.6867 12.5609 12.5802 12.9407 12.7013L15.3323 13.4645C15.9081 13.0202 16.5441 12.6506 17.223 12.37L17.7601 9.91652C17.846 9.52637 18.1468 9.21656 18.5421 9.15082C19.1351 9.05161 19.7452 9 20.3296 9C20.9913 9 21.6015 9.05161 22.1945 9.15082C22.5898 9.21656 22.8906 9.52637 22.9765 9.91652L23.5136 12.37C24.1538 12.6506 24.8284 13.0202 25.4042 13.4645L27.7976 12.7013C28.1757 12.5802 28.5925 12.6867 28.8503 12.9939C29.1984 13.4172 29.5163 13.8641 29.7999 14.3367L30.0019 14.6848C30.2597 15.1531 30.4874 15.6473 30.6765 16.1586ZM20.3683 23.4375C22.2675 23.4375 23.8058 21.8992 23.8058 19.9613C23.8058 18.1008 22.2675 16.5238 20.3683 16.5238C18.4691 16.5238 16.9308 18.1008 16.9308 19.9613C16.9308 21.8992 18.4691 23.4375 20.3683 23.4375Z" fill="#232323"/> +</svg> diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart index d5ad6e6c7..b59206f17 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart @@ -5,10 +5,9 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart'; -import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/restore_backup_dialog.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/desktop/primary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -23,6 +22,20 @@ class BackupRestoreSettings extends ConsumerStatefulWidget { } class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { + late bool createBackup = false; + late bool restoreBackup = false; + + Future<void> enableAutoBackup(BuildContext context) async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return const EnableBackupDialog(); + }, + ); + } + @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); @@ -97,12 +110,19 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { ), Column( crossAxisAlignment: CrossAxisAlignment.start, - children: const [ + children: [ Padding( padding: EdgeInsets.all( 10, ), - child: AutoBackupButton(), + child: PrimaryButton( + desktopMed: true, + width: 200, + label: "Enable auto backup", + onPressed: () { + enableAutoBackup(context); + }, + ), ), ], ), @@ -154,12 +174,23 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { ), Column( crossAxisAlignment: CrossAxisAlignment.start, - children: const [ + children: [ Padding( padding: EdgeInsets.all( 10, ), - child: CreateBackupView(), + child: createBackup + ? const CreateBackupView() + : PrimaryButton( + desktopMed: true, + width: 200, + label: "Create manual backup", + onPressed: () { + setState(() { + createBackup = true; + }); + }, + ), ), ], ), @@ -173,6 +204,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { Padding( padding: const EdgeInsets.only( right: 30, + bottom: 40, ), child: RoundedWhiteContainer( child: Column( @@ -210,12 +242,23 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { ), Column( crossAxisAlignment: CrossAxisAlignment.start, - children: const [ + children: [ Padding( padding: EdgeInsets.all( 10, ), - child: RestoreFromFileView(), + child: restoreBackup + ? RestoreFromFileView() + : PrimaryButton( + desktopMed: true, + width: 200, + label: "Restore backup", + onPressed: () { + setState(() { + restoreBackup = true; + }); + }, + ), ), ], ), @@ -230,98 +273,3 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { }); } } - -class AutoBackupButton extends ConsumerWidget { - const AutoBackupButton({ - Key? key, - }) : super(key: key); - @override - Widget build(BuildContext context, WidgetRef ref) { - Future<void> enableAutoBackup() async { - await showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return const EnableBackupDialog(); - }, - ); - } - - return SizedBox( - width: 200, - height: 48, - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () { - enableAutoBackup(); - }, - child: Text( - "Enable auto backup", - style: STextStyles.button(context), - ), - ), - ); - } -} - -class ManualBackupButton extends ConsumerWidget { - const ManualBackupButton({ - Key? key, - }) : super(key: key); - @override - Widget build(BuildContext context, WidgetRef ref) { - return SizedBox( - width: 200, - height: 48, - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () {}, - child: Text( - "Create manual backup", - style: STextStyles.button(context), - ), - ), - ); - } -} - -class RestoreBackupButton extends ConsumerWidget { - const RestoreBackupButton({ - Key? key, - }) : super(key: key); - @override - Widget build(BuildContext context, WidgetRef ref) { - Future<void> restoreBackup() async { - await showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return const RestoreBackupDialog(); - }, - ); - } - - return SizedBox( - width: 200, - height: 48, - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () { - restoreBackup(); - }, - child: Text( - "Restore", - style: STextStyles.button(context), - ), - ), - ); - } -} diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/restore_backup_dialog.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/restore_backup_dialog.dart index 07d49274a..7f944847d 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/restore_backup_dialog.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/restore_backup_dialog.dart @@ -1,10 +1,12 @@ 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/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_white_container.dart'; class RestoreBackupDialog extends StatelessWidget { const RestoreBackupDialog({Key? key}) : super(key: key); @@ -12,82 +14,158 @@ class RestoreBackupDialog extends StatelessWidget { @override Widget build(BuildContext context) { return DesktopDialog( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.all(32), - child: Text( - "Restoring Stack Wallet", - style: STextStyles.desktopH3(context), - textAlign: TextAlign.center, + maxHeight: 750, + maxWidth: 600, + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(32), + child: Text( + "Restoring Stack Wallet", + style: STextStyles.desktopH3(context), + textAlign: TextAlign.center, + ), + ), + const DesktopDialogCloseButton(), + ], + ), + const SizedBox( + height: 30, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Row( + children: [ + Text( + "Settings", + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + ), + textAlign: TextAlign.left, + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, vertical: 12), + child: RoundedWhiteContainer( + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + SvgPicture.asset( + Assets.svg.framedAddressBook, + width: 40, + height: 40, + ), + const SizedBox(width: 12), + Text( + "Address Book", + style: + STextStyles.desktopTextSmall(context), + ), + ], + ), + + ///TODO: CHECKMARK ANIMATION + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, vertical: 12), + child: RoundedWhiteContainer( + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + SvgPicture.asset( + Assets.svg.framedGear, + width: 40, + height: 40, + ), + const SizedBox(width: 12), + Text( + "Preferences", + style: + STextStyles.desktopTextSmall(context), + ), + ], + ), + + ///TODO: CHECKMARK ANIMATION + ], + ), + ), + ), + const SizedBox( + height: 30, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Row( + children: [ + Text( + "Wallets", + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + ), + textAlign: TextAlign.left, + ), + ], + ), + ), + const Spacer(), + Padding( + padding: const EdgeInsets.all(32), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + SecondaryButton( + desktopMed: true, + width: 200, + label: "Cancel restore process", + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ), + ), + ], + ), ), ), - const DesktopDialogCloseButton(), - ], - ), - const SizedBox( - height: 30, - ), - Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - ), - child: Row( - children: [ - Text( - "Settings", - style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: - Theme.of(context).extension<StackColors>()!.textDark3, - ), - textAlign: TextAlign.left, - ), - ], - ), - ), - // RoundedWhiteContainer( - // child: Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // Row(), - // ], - // ), - // ), - const Spacer(), - Padding( - padding: const EdgeInsets.all(32), - child: Row( - children: [ - Expanded( - child: SecondaryButton( - label: "Cancel", - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: PrimaryButton( - label: "Continue", - onPressed: () { - // Navigator.of(context).pop(); - // onConfirm.call(); - }, - ), - ) - ], - ), - ), - ], - ), - ); + ); + }, + )); } } diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 386ea1cd8..b0c6b3bf9 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -59,6 +59,8 @@ class _SVG { String txExchangeFailed(BuildContext context) => "assets/svg/${Theme.of(context).extension<StackColors>()!.themeType.name}/tx-exchange-icon-failed.svg"; + String get framedGear => "assets/svg/framed-gear.svg"; + String get framedAddressBook => "assets/svg/framed-address-book.svg"; String get themeLight => "assets/svg/light/light-mode.svg"; String get themeDark => "assets/svg/dark/dark-theme.svg"; String get circleNode => "assets/svg/node-circle.svg"; diff --git a/pubspec.yaml b/pubspec.yaml index 9ba6d4eb9..1e5c80e76 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -338,6 +338,8 @@ flutter: - assets/svg/message-question-1.svg - assets/svg/drd-icon.svg - assets/svg/box-auto.svg + - assets/svg/framed-address-book.svg + - assets/svg/framed-gear.svg # exchange icons - assets/svg/exchange_icons/change_now_logo_1.svg - assets/svg/exchange_icons/simpleswap-icon.svg From 21f18326d80ba4a7572536f4f051528dee2b03a3 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 4 Nov 2022 10:08:29 -0600 Subject: [PATCH 133/426] update swb lib dependency --- crypto_plugins/flutter_libmonero | 2 +- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index 51f74f05d..277d922c3 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit 51f74f05d465a92e0118cf7c2bcfb049df21af42 +Subproject commit 277d922c3b1d637c1ccda25f51395c618d293015 diff --git a/pubspec.lock b/pubspec.lock index 20992d827..2f7770d3a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1378,8 +1378,8 @@ packages: dependency: "direct main" description: path: "." - ref: b7b184ec36466f2a24104a7056de88881cb0c1e9 - resolved-ref: b7b184ec36466f2a24104a7056de88881cb0c1e9 + ref: "011dc9ce3d29f5fdeeaf711d58b5122f055c146d" + resolved-ref: "011dc9ce3d29f5fdeeaf711d58b5122f055c146d" url: "https://github.com/cypherstack/stack_wallet_backup.git" source: git version: "0.0.1" diff --git a/pubspec.yaml b/pubspec.yaml index 1e5c80e76..86f24330b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -54,7 +54,7 @@ dependencies: stack_wallet_backup: git: url: https://github.com/cypherstack/stack_wallet_backup.git - ref: b7b184ec36466f2a24104a7056de88881cb0c1e9 + ref: 011dc9ce3d29f5fdeeaf711d58b5122f055c146d # Utility plugins # provider: ^6.0.1 From 4dd8ae23c5ab89d3baccec1053c347ca6bef7d17 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 4 Nov 2022 13:32:02 -0600 Subject: [PATCH 134/426] WIP: desktop password --- lib/main.dart | 29 +++--- .../create_password/create_password_view.dart | 20 +++-- .../desktop_login_view.dart | 47 ++++++++++ .../storage_crypto_handler_provider.dart | 4 + lib/utilities/desktop_password_service.dart | 89 +++++++++++++++++++ 5 files changed, 174 insertions(+), 15 deletions(-) create mode 100644 lib/pages_desktop_specific/desktop_login_view.dart create mode 100644 lib/providers/desktop/storage_crypto_handler_provider.dart create mode 100644 lib/utilities/desktop_password_service.dart diff --git a/lib/main.dart b/lib/main.dart index 58a287b31..77a8b1441 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -30,7 +30,8 @@ import 'package:stackwallet/pages/loading_view.dart'; import 'package:stackwallet/pages/pinpad_views/create_pin_view.dart'; import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart'; -import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_login_view.dart'; +import 'package:stackwallet/providers/desktop/storage_crypto_handler_provider.dart'; import 'package:stackwallet/providers/global/auto_swb_service_provider.dart'; import 'package:stackwallet/providers/global/base_currencies_provider.dart'; // import 'package:stackwallet/providers/global/has_authenticated_start_state_provider.dart'; @@ -207,6 +208,7 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme> late final Completer<void> loadingCompleter; bool didLoad = false; + bool _desktopHasPassword = false; Future<void> load() async { try { @@ -218,6 +220,11 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme> await DB.instance.init(); await _prefs.init(); + if (Util.isDesktop) { + _desktopHasPassword = + await ref.read(storageCryptoHandlerProvider).hasPassword(); + } + _notificationsService = ref.read(notificationsProvider); _nodeService = ref.read(nodeServiceChangeNotifierProvider); _tradesService = ref.read(tradesServiceProvider); @@ -545,21 +552,23 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme> builder: (BuildContext context, AsyncSnapshot<void> snapshot) { if (snapshot.connectionState == ConnectionState.done) { // FlutterNativeSplash.remove(); - if (_wallets.hasWallets || _prefs.hasPin) { - // return HomeView(); - + if (Util.isDesktop && + (_wallets.hasWallets || _desktopHasPassword)) { String? startupWalletId; if (ref.read(prefsChangeNotifierProvider).gotoWalletOnStartup) { startupWalletId = ref.read(prefsChangeNotifierProvider).startupWalletId; } - // TODO proper desktop auth view - if (Util.isDesktop) { - Future<void>.delayed(Duration.zero).then((value) => - Navigator.of(context).pushNamedAndRemoveUntil( - DesktopHomeView.routeName, (route) => false)); - return Container(); + return DesktopLoginView(startupWalletId: startupWalletId); + } else if (!Util.isDesktop && + (_wallets.hasWallets || _prefs.hasPin)) { + // return HomeView(); + + String? startupWalletId; + if (ref.read(prefsChangeNotifierProvider).gotoWalletOnStartup) { + startupWalletId = + ref.read(prefsChangeNotifierProvider).startupWalletId; } return LockscreenView( diff --git a/lib/pages_desktop_specific/create_password/create_password_view.dart b/lib/pages_desktop_specific/create_password/create_password_view.dart index 2391a22f6..0a8429058 100644 --- a/lib/pages_desktop_specific/create_password/create_password_view.dart +++ b/lib/pages_desktop_specific/create_password/create_password_view.dart @@ -1,10 +1,12 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; +import 'package:stackwallet/providers/desktop/storage_crypto_handler_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; @@ -18,7 +20,7 @@ import 'package:stackwallet/widgets/progress_bar.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:zxcvbn/zxcvbn.dart'; -class CreatePasswordView extends StatefulWidget { +class CreatePasswordView extends ConsumerStatefulWidget { const CreatePasswordView({ Key? key, this.secureStore = const SecureStorageWrapper( @@ -31,10 +33,10 @@ class CreatePasswordView extends StatefulWidget { final FlutterSecureStorageInterface secureStore; @override - State<CreatePasswordView> createState() => _CreatePasswordViewState(); + ConsumerState<CreatePasswordView> createState() => _CreatePasswordViewState(); } -class _CreatePasswordViewState extends State<CreatePasswordView> { +class _CreatePasswordViewState extends ConsumerState<CreatePasswordView> { late final TextEditingController passwordController; late final TextEditingController passwordRepeatController; @@ -76,8 +78,16 @@ class _CreatePasswordViewState extends State<CreatePasswordView> { return; } - await widget.secureStore - .write(key: "stackDesktopPassword", value: passphrase); + try { + await ref.read(storageCryptoHandlerProvider).initFromNew(passphrase); + } catch (e) { + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "Error: $e", + context: context, + )); + return; + } if (mounted) { unawaited(Navigator.of(context) diff --git a/lib/pages_desktop_specific/desktop_login_view.dart b/lib/pages_desktop_specific/desktop_login_view.dart new file mode 100644 index 000000000..c986bffde --- /dev/null +++ b/lib/pages_desktop_specific/desktop_login_view.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; + +class DesktopLoginView extends StatefulWidget { + const DesktopLoginView({ + Key? key, + this.startupWalletId, + }) : super(key: key); + + static const String routeName = "/desktopLogin"; + + final String? startupWalletId; + + @override + State<DesktopLoginView> createState() => _DesktopLoginViewState(); +} + +class _DesktopLoginViewState extends State<DesktopLoginView> { + @override + Widget build(BuildContext context) { + return Material( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "Login", + style: STextStyles.desktopH3(context), + ), + PrimaryButton( + label: "Login", + onPressed: () { + // todo auth + + Navigator.of(context).pushNamedAndRemoveUntil( + DesktopHomeView.routeName, + (route) => false, + ); + }, + ) + ], + ), + ); + } +} diff --git a/lib/providers/desktop/storage_crypto_handler_provider.dart b/lib/providers/desktop/storage_crypto_handler_provider.dart new file mode 100644 index 000000000..5b15ccaf3 --- /dev/null +++ b/lib/providers/desktop/storage_crypto_handler_provider.dart @@ -0,0 +1,4 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/utilities/desktop_password_service.dart'; + +final storageCryptoHandlerProvider = Provider<DPS>((ref) => DPS()); diff --git a/lib/utilities/desktop_password_service.dart b/lib/utilities/desktop_password_service.dart new file mode 100644 index 000000000..da537b3c2 --- /dev/null +++ b/lib/utilities/desktop_password_service.dart @@ -0,0 +1,89 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:stack_wallet_backup/secure_storage.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:stackwallet/utilities/logger.dart'; + +const String _kKeyBlobKey = "swbKeyBlobKeyStringID"; + +String _getMessageFromException(Object exception) { + if (exception is IncorrectPassphrase) { + return exception.errMsg(); + } + if (exception is BadDecryption) { + return exception.errMsg(); + } + if (exception is InvalidLength) { + return exception.errMsg(); + } + if (exception is EncodingError) { + return exception.errMsg(); + } + + return exception.toString(); +} + +class DPS { + StorageCryptoHandler? _handler; + final SecureStorageWrapper secureStorageWrapper; + + StorageCryptoHandler get handler { + if (_handler == null) { + throw Exception( + "DPS: attempted to access handler without proper authentication"); + } + return _handler!; + } + + DPS({ + this.secureStorageWrapper = const SecureStorageWrapper( + FlutterSecureStorage(), + ), + }); + + Future<void> initFromNew(String passphrase) async { + if (_handler != null) { + throw Exception("DPS: attempted to re initialize with new passphrase"); + } + + try { + _handler = await StorageCryptoHandler.fromNewPassphrase(passphrase); + await secureStorageWrapper.write( + key: _kKeyBlobKey, + value: await _handler!.getKeyBlob(), + ); + } catch (e, s) { + Logging.instance.log( + "${_getMessageFromException(e)}\n$s", + level: LogLevel.Error, + ); + rethrow; + } + } + + Future<void> initFromExisting(String passphrase) async { + if (_handler != null) { + throw Exception( + "DPS: attempted to re initialize with existing passphrase"); + } + final keyBlob = await secureStorageWrapper.read(key: _kKeyBlobKey); + + if (keyBlob == null) { + throw Exception( + "DPS: failed to find keyBlob while attempting to initialize with existing passphrase"); + } + + try { + _handler = await StorageCryptoHandler.fromExisting(passphrase, keyBlob); + } catch (e, s) { + Logging.instance.log( + "${_getMessageFromException(e)}\n$s", + level: LogLevel.Error, + ); + rethrow; + } + } + + Future<bool> hasPassword() async { + return (await secureStorageWrapper.read(key: _kKeyBlobKey)) != null; + } +} From 039a9a68f6e96ae9b2033ac8279005a03f93fa97 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 4 Nov 2022 13:55:13 -0600 Subject: [PATCH 135/426] bch clean up linter warnings and unused variables --- .../coins/bitcoincash/bitcoincash_wallet.dart | 209 ++++++++++-------- 1 file changed, 119 insertions(+), 90 deletions(-) diff --git a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart index 3a5cebdec..c01ff6248 100644 --- a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart +++ b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart @@ -6,7 +6,7 @@ import 'dart:typed_data'; import 'package:bech32/bech32.dart'; import 'package:bip32/bip32.dart' as bip32; import 'package:bip39/bip39.dart' as bip39; -import 'package:bitbox/bitbox.dart' as Bitbox; +import 'package:bitbox/bitbox.dart' as bitbox; import 'package:bitcoindart/bitcoindart.dart'; import 'package:bs58check/bs58check.dart' as bs58check; import 'package:crypto/crypto.dart'; @@ -258,7 +258,7 @@ class BitcoinCashWallet extends CoinServiceAPI { } Future<void> updateStoredChainHeight({required int newHeight}) async { - DB.instance.put<dynamic>( + await DB.instance.put<dynamic>( boxName: walletId, key: "storedChainHeight", value: newHeight); } @@ -266,8 +266,8 @@ class BitcoinCashWallet extends CoinServiceAPI { Uint8List? decodeBase58; Segwit? decodeBech32; try { - if (Bitbox.Address.detectFormat(address) == 0) { - address = Bitbox.Address.toLegacyAddress(address); + if (bitbox.Address.detectFormat(address) == 0) { + address = bitbox.Address.toLegacyAddress(address); } } catch (e, s) {} try { @@ -609,7 +609,9 @@ class BitcoinCashWallet extends CoinServiceAPI { // get address tx counts final counts = await _getBatchTxCount(addresses: txCountCallArgs); - print("Counts $counts"); + if (kDebugMode) { + print("Counts $counts"); + } // check and add appropriate addresses for (int k = 0; k < txCountBatchSize; k++) { int count = counts["${_id}_$k"]!; @@ -745,31 +747,35 @@ class BitcoinCashWallet extends CoinServiceAPI { // notify on new incoming transaction for (final tx in unconfirmedTxnsToNotifyPending) { if (tx.txType == "Received") { - NotificationApi.showNotification( - title: "Incoming transaction", - body: walletName, - walletId: walletId, - iconAssetName: Assets.svg.iconFor(coin: coin), - date: DateTime.now(), - shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS, - coinName: coin.name, - txid: tx.txid, - confirmations: tx.confirmations, - requiredConfirmations: MINIMUM_CONFIRMATIONS, + unawaited( + NotificationApi.showNotification( + title: "Incoming transaction", + body: walletName, + walletId: walletId, + iconAssetName: Assets.svg.iconFor(coin: coin), + date: DateTime.now(), + shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS, + coinName: coin.name, + txid: tx.txid, + confirmations: tx.confirmations, + requiredConfirmations: MINIMUM_CONFIRMATIONS, + ), ); await txTracker.addNotifiedPending(tx.txid); } else if (tx.txType == "Sent") { - NotificationApi.showNotification( - title: "Sending transaction", - body: walletName, - walletId: walletId, - iconAssetName: Assets.svg.iconFor(coin: coin), - date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), - shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS, - coinName: coin.name, - txid: tx.txid, - confirmations: tx.confirmations, - requiredConfirmations: MINIMUM_CONFIRMATIONS, + unawaited( + NotificationApi.showNotification( + title: "Sending transaction", + body: walletName, + walletId: walletId, + iconAssetName: Assets.svg.iconFor(coin: coin), + date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), + shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS, + coinName: coin.name, + txid: tx.txid, + confirmations: tx.confirmations, + requiredConfirmations: MINIMUM_CONFIRMATIONS, + ), ); await txTracker.addNotifiedPending(tx.txid); } @@ -778,26 +784,30 @@ class BitcoinCashWallet extends CoinServiceAPI { // notify on confirmed for (final tx in unconfirmedTxnsToNotifyConfirmed) { if (tx.txType == "Received") { - NotificationApi.showNotification( - title: "Incoming transaction confirmed", - body: walletName, - walletId: walletId, - iconAssetName: Assets.svg.iconFor(coin: coin), - date: DateTime.now(), - shouldWatchForUpdates: false, - coinName: coin.name, + unawaited( + NotificationApi.showNotification( + title: "Incoming transaction confirmed", + body: walletName, + walletId: walletId, + iconAssetName: Assets.svg.iconFor(coin: coin), + date: DateTime.now(), + shouldWatchForUpdates: false, + coinName: coin.name, + ), ); await txTracker.addNotifiedConfirmed(tx.txid); } else if (tx.txType == "Sent") { - NotificationApi.showNotification( - title: "Outgoing transaction confirmed", - body: walletName, - walletId: walletId, - iconAssetName: Assets.svg.iconFor(coin: coin), - date: DateTime.now(), - shouldWatchForUpdates: false, - coinName: coin.name, + unawaited( + NotificationApi.showNotification( + title: "Outgoing transaction confirmed", + body: walletName, + walletId: walletId, + iconAssetName: Assets.svg.iconFor(coin: coin), + date: DateTime.now(), + shouldWatchForUpdates: false, + coinName: coin.name, + ), ); await txTracker.addNotifiedConfirmed(tx.txid); } @@ -862,7 +872,7 @@ class BitcoinCashWallet extends CoinServiceAPI { if (currentHeight != storedHeight) { if (currentHeight != -1) { // -1 failed to fetch current height - updateStoredChainHeight(newHeight: currentHeight); + await updateStoredChainHeight(newHeight: currentHeight); } GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId)); @@ -1147,10 +1157,12 @@ class BitcoinCashWallet extends CoinServiceAPI { bool validateAddress(String address) { try { // 0 for bitcoincash: address scheme, 1 for legacy address - final format = Bitbox.Address.detectFormat(address); - print("format $format"); + final format = bitbox.Address.detectFormat(address); + if (kDebugMode) { + print("format $format"); + } return true; - } catch (e, s) { + } catch (e) { return false; } } @@ -1226,7 +1238,7 @@ class BitcoinCashWallet extends CoinServiceAPI { ); if (shouldRefresh) { - refresh(); + unawaited(refresh()); } } @@ -1522,12 +1534,14 @@ class BitcoinCashWallet extends CoinServiceAPI { break; } - print("Array key is ${jsonEncode(arrayKey)}"); + if (kDebugMode) { + print("Array key is ${jsonEncode(arrayKey)}"); + } final internalChainArray = DB.instance.get<dynamic>(boxName: walletId, key: arrayKey); if (derivePathType == DerivePathType.bip44) { - if (Bitbox.Address.detectFormat(internalChainArray.last as String) == 1) { - return Bitbox.Address.toCashAddress(internalChainArray.last as String); + if (bitbox.Address.detectFormat(internalChainArray.last as String) == 1) { + return bitbox.Address.toCashAddress(internalChainArray.last as String); } } return internalChainArray.last as String; @@ -1642,7 +1656,9 @@ class BitcoinCashWallet extends CoinServiceAPI { batches[batchNumber] = {}; } final scripthash = _convertToScriptHash(allAddresses[i], _network); - print("SCRIPT_HASH_FOR_ADDRESS ${allAddresses[i]} IS $scripthash"); + if (kDebugMode) { + print("SCRIPT_HASH_FOR_ADDRESS ${allAddresses[i]} IS $scripthash"); + } batches[batchNumber]!.addAll({ scripthash: [scripthash] }); @@ -1818,20 +1834,28 @@ class BitcoinCashWallet extends CoinServiceAPI { }) async { try { final Map<String, List<dynamic>> args = {}; - print("Address $addresses"); + if (kDebugMode) { + print("Address $addresses"); + } for (final entry in addresses.entries) { args[entry.key] = [_convertToScriptHash(entry.value, _network)]; } - print("Args ${jsonEncode(args)}"); + if (kDebugMode) { + print("Args ${jsonEncode(args)}"); + } final response = await electrumXClient.getBatchHistory(args: args); - print("Response ${jsonEncode(response)}"); + if (kDebugMode) { + print("Response ${jsonEncode(response)}"); + } final Map<String, int> result = {}; for (final entry in response.entries) { result[entry.key] = entry.value.length; } - print("result ${jsonEncode(result)}"); + if (kDebugMode) { + print("result ${jsonEncode(result)}"); + } return result; } catch (e, s) { Logging.instance.log( @@ -1995,8 +2019,8 @@ class BitcoinCashWallet extends CoinServiceAPI { /// Returns the scripthash or throws an exception on invalid bch address String _convertToScriptHash(String bchAddress, NetworkType network) { try { - if (Bitbox.Address.detectFormat(bchAddress) == 0) { - bchAddress = Bitbox.Address.toLegacyAddress(bchAddress); + if (bitbox.Address.detectFormat(bchAddress) == 0) { + bchAddress = bitbox.Address.toLegacyAddress(bchAddress); } final output = Address.addressToOutputScript(bchAddress, network); final hash = sha256.convert(output.toList(growable: false)).toString(); @@ -2073,8 +2097,8 @@ class BitcoinCashWallet extends CoinServiceAPI { List<String> allAddressesOld = await _fetchAllOwnAddresses(); List<String> allAddresses = []; for (String address in allAddressesOld) { - if (Bitbox.Address.detectFormat(address) == 1) { - allAddresses.add(Bitbox.Address.toCashAddress(address)); + if (bitbox.Address.detectFormat(address) == 1) { + allAddresses.add(bitbox.Address.toCashAddress(address)); } else { allAddresses.add(address); } @@ -2085,8 +2109,8 @@ class BitcoinCashWallet extends CoinServiceAPI { as List<dynamic>; List<dynamic> changeAddressesP2PKH = []; for (var address in changeAddressesP2PKHOld) { - if (Bitbox.Address.detectFormat(address as String) == 1) { - changeAddressesP2PKH.add(Bitbox.Address.toCashAddress(address)); + if (bitbox.Address.detectFormat(address as String) == 1) { + changeAddressesP2PKH.add(bitbox.Address.toCashAddress(address)); } else { changeAddressesP2PKH.add(address); } @@ -2108,21 +2132,26 @@ class BitcoinCashWallet extends CoinServiceAPI { unconfirmedCachedTransactions .removeWhere((key, value) => value.confirmedStatus); - print("CACHED_TRANSACTIONS_IS $cachedTransactions"); + if (kDebugMode) { + print("CACHED_TRANSACTIONS_IS $cachedTransactions"); + } if (cachedTransactions != null) { for (final tx in allTxHashes.toList(growable: false)) { final txHeight = tx["height"] as int; if (txHeight > 0 && txHeight < latestTxnBlockHeight - MINIMUM_CONFIRMATIONS) { if (unconfirmedCachedTransactions[tx["tx_hash"] as String] == null) { - print(cachedTransactions.findTransaction(tx["tx_hash"] as String)); - print(unconfirmedCachedTransactions[tx["tx_hash"] as String]); + if (kDebugMode) { + print( + cachedTransactions.findTransaction(tx["tx_hash"] as String)); + print(unconfirmedCachedTransactions[tx["tx_hash"] as String]); + } final cachedTx = cachedTransactions.findTransaction(tx["tx_hash"] as String); if (!(cachedTx != null && addressType(address: cachedTx.address) == DerivePathType.bip44 && - Bitbox.Address.detectFormat(cachedTx.address) == 1)) { + bitbox.Address.detectFormat(cachedTx.address) == 1)) { allTxHashes.remove(tx); } } @@ -2782,8 +2811,8 @@ class BitcoinCashWallet extends CoinServiceAPI { final n = output["n"]; if (n != null && n == utxosToUse[i].vout) { String address = output["scriptPubKey"]["addresses"][0] as String; - if (Bitbox.Address.detectFormat(address) == 0) { - address = Bitbox.Address.toLegacyAddress(address); + if (bitbox.Address.detectFormat(address) == 0) { + address = bitbox.Address.toLegacyAddress(address); } if (!addressTxid.containsKey(address)) { addressTxid[address] = <String>[]; @@ -2814,8 +2843,8 @@ class BitcoinCashWallet extends CoinServiceAPI { ); for (int i = 0; i < p2pkhLength; i++) { String address = addressesP2PKH[i]; - if (Bitbox.Address.detectFormat(address) == 0) { - address = Bitbox.Address.toLegacyAddress(address); + if (bitbox.Address.detectFormat(address) == 0) { + address = bitbox.Address.toLegacyAddress(address); } // receives @@ -2950,36 +2979,36 @@ class BitcoinCashWallet extends CoinServiceAPI { required List<String> recipients, required List<int> satoshiAmounts, }) async { - final builder = Bitbox.Bitbox.transactionBuilder(); + final builder = bitbox.Bitbox.transactionBuilder(); // retrieve address' utxos from the rest api - List<Bitbox.Utxo> _utxos = + List<bitbox.Utxo> _utxos = []; // await Bitbox.Address.utxo(address) as List<Bitbox.Utxo>; - utxosToUse.forEach((element) { - _utxos.add(Bitbox.Utxo( + for (var element in utxosToUse) { + _utxos.add(bitbox.Utxo( element.txid, element.vout, - Bitbox.BitcoinCash.fromSatoshi(element.value), + bitbox.BitcoinCash.fromSatoshi(element.value), element.value, 0, MINIMUM_CONFIRMATIONS + 1)); - }); - Logger.print("bch utxos: ${_utxos}"); + } + Logger.print("bch utxos: $_utxos"); // placeholder for input signatures - final signatures = <Map>[]; + final List<Map<dynamic, dynamic>> signatures = []; // placeholder for total input balance - int totalBalance = 0; + // int totalBalance = 0; // iterate through the list of address _utxos and use them as inputs for the // withdrawal transaction - _utxos.forEach((Bitbox.Utxo utxo) { + for (var utxo in _utxos) { // add the utxo as an input for the transaction builder.addInput(utxo.txid, utxo.vout); final ec = utxoSigningData[utxo.txid]["keyPair"] as ECPair; - final bitboxEC = Bitbox.ECPair.fromWIF(ec.toWIF()); + final bitboxEC = bitbox.ECPair.fromWIF(ec.toWIF()); // add a signature to the list to be used later signatures.add({ @@ -2988,15 +3017,15 @@ class BitcoinCashWallet extends CoinServiceAPI { "original_amount": utxo.satoshis }); - totalBalance += utxo.satoshis; - }); + // totalBalance += utxo.satoshis; + } // calculate the fee based on number of inputs and one expected output - final fee = - Bitbox.BitcoinCash.getByteCount(signatures.length, recipients.length); + // final fee = + // bitbox.BitcoinCash.getByteCount(signatures.length, recipients.length); // calculate how much balance will be left over to spend after the fee - final sendAmount = totalBalance - fee; + // final sendAmount = totalBalance - fee; // add the output based on the address provided in the testing data for (int i = 0; i < recipients.length; i++) { @@ -3006,12 +3035,12 @@ class BitcoinCashWallet extends CoinServiceAPI { } // sign all inputs - signatures.forEach((signature) { + for (var signature in signatures) { builder.sign( signature["vin"] as int, - signature["key_pair"] as Bitbox.ECPair, + signature["key_pair"] as bitbox.ECPair, signature["original_amount"] as int); - }); + } // build the transaction final tx = builder.build(); @@ -3038,7 +3067,7 @@ class BitcoinCashWallet extends CoinServiceAPI { ); // clear cache - _cachedElectrumXClient.clearSharedTransactionCache(coin: coin); + await _cachedElectrumXClient.clearSharedTransactionCache(coin: coin); // back up data await _rescanBackup(); From ccd94fcf86601e03d995d170611b5888694d38a0 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 4 Nov 2022 14:10:30 -0600 Subject: [PATCH 136/426] bch compare address type to constant names instead of int literals --- .../coins/bitcoincash/bitcoincash_wallet.dart | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart index c01ff6248..0a72cea99 100644 --- a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart +++ b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart @@ -266,7 +266,8 @@ class BitcoinCashWallet extends CoinServiceAPI { Uint8List? decodeBase58; Segwit? decodeBech32; try { - if (bitbox.Address.detectFormat(address) == 0) { + if (bitbox.Address.detectFormat(address) == + bitbox.Address.formatCashAddr) { address = bitbox.Address.toLegacyAddress(address); } } catch (e, s) {} @@ -1540,7 +1541,8 @@ class BitcoinCashWallet extends CoinServiceAPI { final internalChainArray = DB.instance.get<dynamic>(boxName: walletId, key: arrayKey); if (derivePathType == DerivePathType.bip44) { - if (bitbox.Address.detectFormat(internalChainArray.last as String) == 1) { + if (bitbox.Address.detectFormat(internalChainArray.last as String) == + bitbox.Address.formatLegacy) { return bitbox.Address.toCashAddress(internalChainArray.last as String); } } @@ -2019,7 +2021,8 @@ class BitcoinCashWallet extends CoinServiceAPI { /// Returns the scripthash or throws an exception on invalid bch address String _convertToScriptHash(String bchAddress, NetworkType network) { try { - if (bitbox.Address.detectFormat(bchAddress) == 0) { + if (bitbox.Address.detectFormat(bchAddress) == + bitbox.Address.formatCashAddr) { bchAddress = bitbox.Address.toLegacyAddress(bchAddress); } final output = Address.addressToOutputScript(bchAddress, network); @@ -2097,7 +2100,7 @@ class BitcoinCashWallet extends CoinServiceAPI { List<String> allAddressesOld = await _fetchAllOwnAddresses(); List<String> allAddresses = []; for (String address in allAddressesOld) { - if (bitbox.Address.detectFormat(address) == 1) { + if (bitbox.Address.detectFormat(address) == bitbox.Address.formatLegacy) { allAddresses.add(bitbox.Address.toCashAddress(address)); } else { allAddresses.add(address); @@ -2109,7 +2112,8 @@ class BitcoinCashWallet extends CoinServiceAPI { as List<dynamic>; List<dynamic> changeAddressesP2PKH = []; for (var address in changeAddressesP2PKHOld) { - if (bitbox.Address.detectFormat(address as String) == 1) { + if (bitbox.Address.detectFormat(address as String) == + bitbox.Address.formatLegacy) { changeAddressesP2PKH.add(bitbox.Address.toCashAddress(address)); } else { changeAddressesP2PKH.add(address); @@ -2151,7 +2155,8 @@ class BitcoinCashWallet extends CoinServiceAPI { if (!(cachedTx != null && addressType(address: cachedTx.address) == DerivePathType.bip44 && - bitbox.Address.detectFormat(cachedTx.address) == 1)) { + bitbox.Address.detectFormat(cachedTx.address) == + bitbox.Address.formatLegacy)) { allTxHashes.remove(tx); } } @@ -2811,7 +2816,8 @@ class BitcoinCashWallet extends CoinServiceAPI { final n = output["n"]; if (n != null && n == utxosToUse[i].vout) { String address = output["scriptPubKey"]["addresses"][0] as String; - if (bitbox.Address.detectFormat(address) == 0) { + if (bitbox.Address.detectFormat(address) == + bitbox.Address.formatCashAddr) { address = bitbox.Address.toLegacyAddress(address); } if (!addressTxid.containsKey(address)) { @@ -2843,7 +2849,8 @@ class BitcoinCashWallet extends CoinServiceAPI { ); for (int i = 0; i < p2pkhLength; i++) { String address = addressesP2PKH[i]; - if (bitbox.Address.detectFormat(address) == 0) { + if (bitbox.Address.detectFormat(address) == + bitbox.Address.formatCashAddr) { address = bitbox.Address.toLegacyAddress(address); } From 74b075328f856de25dca0a5f54d43f59627a07a0 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 4 Nov 2022 17:22:22 -0600 Subject: [PATCH 137/426] temp bch send fix --- .../coins/bitcoincash/bitcoincash_wallet.dart | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart index 0a72cea99..09578c1ca 100644 --- a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart +++ b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart @@ -1162,7 +1162,17 @@ class BitcoinCashWallet extends CoinServiceAPI { if (kDebugMode) { print("format $format"); } - return true; + + if (format == bitbox.Address.formatCashAddr) { + String addr = address; + if (address.contains(":")) { + addr = address.split(":").last; + } + + return addr.startsWith("q"); + } else { + return address.startsWith("1"); + } } catch (e) { return false; } From a5d7723deee4e932f3070cc3ec63dd9f8c5d92c4 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Fri, 4 Nov 2022 17:49:36 -0600 Subject: [PATCH 138/426] added buttons and restore dialog --- .../create_backup_view.dart | 36 +++++++++++-- .../restore_from_file_view.dart | 52 ++++++++++++++++--- 2 files changed, 77 insertions(+), 11 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart index b710aacf4..2d2ed4960 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart @@ -17,6 +17,8 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/progress_bar.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; @@ -142,14 +144,38 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { condition: isDesktop, builder: (child) { return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Choose file location", - style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: - Theme.of(context).extension<StackColors>()!.textDark3), + Padding( + padding: const EdgeInsets.all(10.0), + child: Text( + "Choose file location", + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + ), ), // child, + const SizedBox(height: 20), + Row( + children: [ + PrimaryButton( + desktopMed: true, + width: 200, + label: "Create backup", + onPressed: () {}, + ), + const SizedBox(width: 16), + SecondaryButton( + desktopMed: true, + width: 200, + label: "Cancel", + onPressed: () {}, + ), + ], + ), ], ); }, 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 16c3ea8e3..9f2796415 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 @@ -8,6 +8,7 @@ import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/stack_file_system.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/sub_views/stack_restore_progress_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/restore_backup_dialog.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -18,6 +19,8 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:tuple/tuple.dart'; @@ -42,6 +45,17 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { bool hidePassword = true; + Future<void> restoreBackupPopup(BuildContext context) async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return const RestoreBackupDialog(); + }, + ); + } + @override void initState() { stackFileSystem = StackFileSystem(); @@ -114,15 +128,41 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { condition: isDesktop, builder: (child) { return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Choose file location", - style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark3), + Padding( + padding: const EdgeInsets.all(10.0), + child: Text( + "Choose file location", + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + textAlign: TextAlign.left, + ), ), // child, + const SizedBox(height: 20), + Row( + children: [ + PrimaryButton( + desktopMed: true, + width: 200, + label: "Restore", + onPressed: () { + restoreBackupPopup(context); + }, + ), + const SizedBox(width: 16), + SecondaryButton( + desktopMed: true, + width: 200, + label: "Cancel", + onPressed: () {}, + ), + ], + ), ], ); }, From e06910a34a68451d00cb337dcf7d194eb19d34cd Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Fri, 4 Nov 2022 17:50:51 -0600 Subject: [PATCH 139/426] WIP: theme change --- .../settings_menu/appearance_settings.dart | 216 ++++++++++-------- 1 file changed, 125 insertions(+), 91 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart b/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart index d524453e2..b5f239ab1 100644 --- a/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart @@ -23,6 +23,19 @@ class AppearanceOptionSettings extends ConsumerStatefulWidget { class _AppearanceOptionSettings extends ConsumerState<AppearanceOptionSettings> { + // late bool isLight; + + // @override + // void initState() { + // + // super.initState(); + // } + // + // @override + // void dispose() { + // super.dispose(); + // } + @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); @@ -127,13 +140,7 @@ class _AppearanceOptionSettings ], ), ), - const Padding( - padding: EdgeInsets.only( - left: 10, - right: 10, - ), - child: ThemeToggle(), - ), + ThemeToggle(), ], ), ), @@ -169,95 +176,38 @@ class _ThemeToggle extends State<ThemeToggle> { elevation: 0, hoverColor: Colors.transparent, shape: RoundedRectangleBorder( + side: BorderSide( + color: + Theme.of(context).extension<StackColors>()!.infoItemIcons, + width: 2, + ), + // side: !externalCallsEnabled + // ? BorderSide.none + // : BorderSide( + // color: Theme.of(context) + // .extension<StackColors>()! + // .infoItemIcons, + // width: 2, + // ), borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius * 2, ), ), onPressed: () {}, //onPressed - child: Stack( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only( - left: 24, - ), - child: SvgPicture.asset( - Assets.svg.themeLight, - ), - ), - Padding( - padding: const EdgeInsets.only( - left: 50, - top: 12, - ), - child: Text( - "Light", - style: - STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ), - ), - ) - ], - ), - // if (externalCallsEnabled) - Positioned( - bottom: 0, - left: 6, - child: SvgPicture.asset( - Assets.svg.checkCircle, - width: 20, - height: 20, - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - ), - ), - // if (!externalCallsEnabled) - // Positioned( - // top: 4, - // right: 4, - // child: Container( - // width: 20, - // height: 20, - // decoration: BoxDecoration( - // borderRadius: BorderRadius.circular(1000), - // color: Theme.of(context) - // .extension<StackColors>()! - // .textFieldDefaultBG, - // ), - // ), - // ), - ], - ), - ), - ), - const SizedBox( - width: 1, - ), - Expanded( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: RawMaterialButton( - elevation: 0, - hoverColor: Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius * 2, - ), - ), - onPressed: () {}, //onPressed + child: Padding( + padding: const EdgeInsets.all(8.0), child: Stack( children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SvgPicture.asset( - Assets.svg.themeDark, + Padding( + padding: const EdgeInsets.only( + left: 24, + ), + child: SvgPicture.asset( + Assets.svg.themeLight, + ), ), Padding( padding: const EdgeInsets.only( @@ -265,7 +215,7 @@ class _ThemeToggle extends State<ThemeToggle> { top: 12, ), child: Text( - "Dark", + "Light", style: STextStyles.desktopTextExtraSmall(context) .copyWith( color: Theme.of(context) @@ -273,13 +223,13 @@ class _ThemeToggle extends State<ThemeToggle> { .textDark, ), ), - ), + ) ], ), // if (externalCallsEnabled) Positioned( bottom: 0, - left: 0, + left: 6, child: SvgPicture.asset( Assets.svg.checkCircle, width: 20, @@ -291,8 +241,8 @@ class _ThemeToggle extends State<ThemeToggle> { ), // if (!externalCallsEnabled) // Positioned( - // top: 4, - // right: 4, + // bottom: 0, + // left: 6, // child: Container( // width: 20, // height: 20, @@ -309,6 +259,90 @@ class _ThemeToggle extends State<ThemeToggle> { ), ), ), + const SizedBox( + width: 1, + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: RawMaterialButton( + elevation: 0, + hoverColor: Colors.transparent, + shape: RoundedRectangleBorder( + // side: !externalCallsEnabled + // ? BorderSide.none + // : BorderSide( + // color: Theme.of(context) + // .extension<StackColors>()! + // .infoItemIcons, + // width: 2, + // ), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius * 2, + ), + ), + onPressed: () {}, //onPressed + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset( + Assets.svg.themeDark, + ), + Padding( + padding: const EdgeInsets.only( + left: 45, + top: 12, + ), + child: Text( + "Dark", + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + ), + ], + ), + // if (externalCallsEnabled) + // Positioned( + // bottom: 0, + // left: 0, + // child: SvgPicture.asset( + // Assets.svg.checkCircle, + // width: 20, + // height: 20, + // color: Theme.of(context) + // .extension<StackColors>()! + // .infoItemIcons, + // ), + // ), + // if (!externalCallsEnabled) + Positioned( + bottom: 0, + left: 0, + child: Container( + width: 20, + height: 20, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(1000), + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + ), + ), + ), + ], + ), + ), + ), + ), + ), ], ); } From 7dbc9d270b1e3d62fed837179241b9350a7234d9 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 4 Nov 2022 18:18:25 -0600 Subject: [PATCH 140/426] temp bch send fix testnet and fix tests --- lib/services/coins/bitcoincash/bitcoincash_wallet.dart | 4 ++++ test/services/coins/bitcoincash/bitcoincash_wallet_test.dart | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart index 09578c1ca..b96aa160c 100644 --- a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart +++ b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart @@ -1163,6 +1163,10 @@ class BitcoinCashWallet extends CoinServiceAPI { print("format $format"); } + if (_coin == Coin.bitcoincashTestnet) { + return true; + } + if (format == bitbox.Address.formatCashAddr) { String addr = address; if (address.contains(":")) { diff --git a/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart b/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart index 4c392fe81..50ff8f741 100644 --- a/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart +++ b/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart @@ -1,4 +1,3 @@ -import 'package:bitcoindart/bitcoindart.dart'; import 'package:decimal/decimal.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hive/hive.dart'; @@ -140,7 +139,7 @@ void main() { test("invalid mainnet bitcoincash legacy/p2pkh address", () { expect( mainnetWallet?.validateAddress("mhqpGtwhcR6gFuuRjLTpHo41919QfuGy8Y"), - true); + false); expect(secureStore?.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); From b4d97e86cc1c6d3a00e62e0d614729aa1ea3c324 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 4 Nov 2022 18:26:23 -0600 Subject: [PATCH 141/426] long address fix --- .../transaction_details_view.dart | 141 +++++++++--------- 1 file changed, 73 insertions(+), 68 deletions(-) diff --git a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart index 6f23f2e01..1c2fb8e5d 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart @@ -471,75 +471,80 @@ class _TransactionDetailsViewState MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + _transaction.txType.toLowerCase() == + "sent" + ? "Sent to" + : "Receiving address", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle( + context), + ), + const SizedBox( + height: 8, + ), _transaction.txType.toLowerCase() == - "sent" - ? "Sent to" - : "Receiving address", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - : STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 8, - ), - _transaction.txType.toLowerCase() == - "received" - ? FutureBuilder( - future: fetchContactNameFor( - _transaction.address), - builder: (builderContext, - AsyncSnapshot<String> - snapshot) { - String addressOrContactName = - _transaction.address; - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - addressOrContactName = - snapshot.data!; - } - return SelectableText( - addressOrContactName, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of( - context) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles - .itemSubtitle12( - context), - ); - }, - ) - : SelectableText( - _transaction.address, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles.itemSubtitle12( - context), - ), - ], + "received" + ? FutureBuilder( + future: fetchContactNameFor( + _transaction.address), + builder: (builderContext, + AsyncSnapshot<String> + snapshot) { + String addressOrContactName = + _transaction.address; + if (snapshot.connectionState == + ConnectionState + .done && + snapshot.hasData) { + addressOrContactName = + snapshot.data!; + } + return SelectableText( + addressOrContactName, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textDark, + ) + : STextStyles + .itemSubtitle12( + context), + ); + }, + ) + : SelectableText( + _transaction.address, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textDark, + ) + : STextStyles + .itemSubtitle12( + context), + ), + ], + ), ), if (isDesktop) IconCopyButton( From e87aa64e1b79c9671f9ba888cd4188c78fca2fb1 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Sat, 5 Nov 2022 09:40:28 -0600 Subject: [PATCH 142/426] isFavorite bandaid fix for https://github.com/cypherstack/stack_wallet/issues/203 --- lib/services/coins/bitcoin/bitcoin_wallet.dart | 7 ++++--- lib/services/coins/bitcoincash/bitcoincash_wallet.dart | 7 ++++--- lib/services/coins/dogecoin/dogecoin_wallet.dart | 7 ++++--- lib/services/coins/epiccash/epiccash_wallet.dart | 7 ++++--- lib/services/coins/firo/firo_wallet.dart | 7 ++++--- lib/services/coins/litecoin/litecoin_wallet.dart | 7 ++++--- lib/services/coins/monero/monero_wallet.dart | 7 ++++--- lib/services/coins/namecoin/namecoin_wallet.dart | 7 ++++--- lib/services/coins/wownero/wownero_wallet.dart | 7 ++++--- 9 files changed, 36 insertions(+), 27 deletions(-) diff --git a/lib/services/coins/bitcoin/bitcoin_wallet.dart b/lib/services/coins/bitcoin/bitcoin_wallet.dart index da3bdfed0..391beb909 100644 --- a/lib/services/coins/bitcoin/bitcoin_wallet.dart +++ b/lib/services/coins/bitcoin/bitcoin_wallet.dart @@ -174,9 +174,10 @@ class BitcoinWallet extends CoinServiceAPI { return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite") as bool; } catch (e, s) { - Logging.instance - .log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error); - rethrow; + Logging.instance.log( + "isFavorite fetch failed (returning false by default): $e\n$s", + level: LogLevel.Error); + return false; } } diff --git a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart index b96aa160c..fa88b3f2f 100644 --- a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart +++ b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart @@ -3376,9 +3376,10 @@ class BitcoinCashWallet extends CoinServiceAPI { return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite") as bool; } catch (e, s) { - Logging.instance - .log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error); - rethrow; + Logging.instance.log( + "isFavorite fetch failed (returning false by default): $e\n$s", + level: LogLevel.Error); + return false; } } diff --git a/lib/services/coins/dogecoin/dogecoin_wallet.dart b/lib/services/coins/dogecoin/dogecoin_wallet.dart index 0235a0c02..fbb551dcd 100644 --- a/lib/services/coins/dogecoin/dogecoin_wallet.dart +++ b/lib/services/coins/dogecoin/dogecoin_wallet.dart @@ -2983,9 +2983,10 @@ class DogecoinWallet extends CoinServiceAPI { return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite") as bool; } catch (e, s) { - Logging.instance - .log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error); - rethrow; + Logging.instance.log( + "isFavorite fetch failed (returning false by default): $e\n$s", + level: LogLevel.Error); + return false; } } diff --git a/lib/services/coins/epiccash/epiccash_wallet.dart b/lib/services/coins/epiccash/epiccash_wallet.dart index 7ccb7feaf..b98854e61 100644 --- a/lib/services/coins/epiccash/epiccash_wallet.dart +++ b/lib/services/coins/epiccash/epiccash_wallet.dart @@ -558,9 +558,10 @@ class EpicCashWallet extends CoinServiceAPI { return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite") as bool; } catch (e, s) { - Logging.instance - .log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error); - rethrow; + Logging.instance.log( + "isFavorite fetch failed (returning false by default): $e\n$s", + level: LogLevel.Error); + return false; } } diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index 61ef2e9de..2a2102e7c 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -821,9 +821,10 @@ class FiroWallet extends CoinServiceAPI { return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite") as bool; } catch (e, s) { - Logging.instance - .log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error); - rethrow; + Logging.instance.log( + "isFavorite fetch failed (returning false by default): $e\n$s", + level: LogLevel.Error); + return false; } } diff --git a/lib/services/coins/litecoin/litecoin_wallet.dart b/lib/services/coins/litecoin/litecoin_wallet.dart index 0ab3a92a8..c07cca1f3 100644 --- a/lib/services/coins/litecoin/litecoin_wallet.dart +++ b/lib/services/coins/litecoin/litecoin_wallet.dart @@ -174,9 +174,10 @@ class LitecoinWallet extends CoinServiceAPI { return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite") as bool; } catch (e, s) { - Logging.instance - .log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error); - rethrow; + Logging.instance.log( + "isFavorite fetch failed (returning false by default): $e\n$s", + level: LogLevel.Error); + return false; } } diff --git a/lib/services/coins/monero/monero_wallet.dart b/lib/services/coins/monero/monero_wallet.dart index b0ebac4e6..9bcc3515f 100644 --- a/lib/services/coins/monero/monero_wallet.dart +++ b/lib/services/coins/monero/monero_wallet.dart @@ -1376,9 +1376,10 @@ class MoneroWallet extends CoinServiceAPI { return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite") as bool; } catch (e, s) { - Logging.instance - .log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error); - rethrow; + Logging.instance.log( + "isFavorite fetch failed (returning false by default): $e\n$s", + level: LogLevel.Error); + return false; } } diff --git a/lib/services/coins/namecoin/namecoin_wallet.dart b/lib/services/coins/namecoin/namecoin_wallet.dart index e9cae1ab2..893db69e0 100644 --- a/lib/services/coins/namecoin/namecoin_wallet.dart +++ b/lib/services/coins/namecoin/namecoin_wallet.dart @@ -170,9 +170,10 @@ class NamecoinWallet extends CoinServiceAPI { return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite") as bool; } catch (e, s) { - Logging.instance - .log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error); - rethrow; + Logging.instance.log( + "isFavorite fetch failed (returning false by default): $e\n$s", + level: LogLevel.Error); + return false; } } diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart index d3aba5bbb..342e5d84a 100644 --- a/lib/services/coins/wownero/wownero_wallet.dart +++ b/lib/services/coins/wownero/wownero_wallet.dart @@ -1382,9 +1382,10 @@ class WowneroWallet extends CoinServiceAPI { return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite") as bool; } catch (e, s) { - Logging.instance - .log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error); - rethrow; + Logging.instance.log( + "isFavorite fetch failed (returning false by default): $e\n$s", + level: LogLevel.Error); + return false; } } From 1e61a779ecd8620aa44c472dcd94ab49ba00d73a Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 7 Nov 2022 08:15:44 -0700 Subject: [PATCH 143/426] desktop support view route added --- lib/pages_desktop_specific/home/desktop_home_view.dart | 7 +++++-- .../home/support_and_about_view/desktop_support_view.dart | 0 lib/route_generator.dart | 7 +++++++ 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 lib/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart diff --git a/lib/pages_desktop_specific/home/desktop_home_view.dart b/lib/pages_desktop_specific/home/desktop_home_view.dart index 6aa104081..e9f6f2b4b 100644 --- a/lib/pages_desktop_specific/home/desktop_home_view.dart +++ b/lib/pages_desktop_specific/home/desktop_home_view.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_menu.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_settings_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -37,8 +38,10 @@ class _DesktopHomeViewState extends ConsumerState<DesktopHomeView> { onGenerateRoute: RouteGenerator.generateRoute, initialRoute: DesktopSettingsView.routeName, ), - Container( - color: Colors.blue, + const Navigator( + key: Key("desktopSupportHomeKey"), + onGenerateRoute: RouteGenerator.generateRoute, + initialRoute: DesktopSupportView.routeName, ), Container( color: Colors.pink, diff --git a/lib/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart b/lib/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart new file mode 100644 index 000000000..e69de29bb diff --git a/lib/route_generator.dart b/lib/route_generator.dart index a6f23ffdc..47a84f07c 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -99,6 +99,7 @@ import 'package:stackwallet/pages_desktop_specific/home/settings_menu/nodes_sett import 'package:stackwallet/pages_desktop_specific/home/settings_menu/security_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/settings_menu.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart'; +import 'package:stackwallet/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; @@ -1084,6 +1085,12 @@ class RouteGenerator { builder: (_) => const AdvancedSettings(), settings: RouteSettings(name: settings.name)); + case DesktopSupportView.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const DesktopSupportView(), + settings: RouteSettings(name: settings.name)); + case WalletKeysDesktopPopup.routeName: if (args is List<String>) { return FadePageRoute( From dbb2b309ca243e650660bd13af1551cfe8f94f0c Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 7 Nov 2022 09:15:02 -0700 Subject: [PATCH 144/426] blue link text added to desktop --- .../global_settings_view/support_view.dart | 535 +++++++++++------- .../desktop_support_view.dart | 50 ++ 2 files changed, 366 insertions(+), 219 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/support_view.dart b/lib/pages/settings_views/global_settings_view/support_view.dart index fdfa6f404..20aeedf61 100644 --- a/lib/pages/settings_views/global_settings_view/support_view.dart +++ b/lib/pages/settings_views/global_settings_view/support_view.dart @@ -4,7 +4,10 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -18,269 +21,363 @@ class SupportView extends StatelessWidget { @override Widget build(BuildContext context) { + final isDesktop = Util.isDesktop; + debugPrint("BUILD: $runtimeType"); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Support", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - child: Text( - "If you need support or want to report a bug, reach out to us on any of our socials!", - style: STextStyles.smallMed12(context), - ), + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, ), - const SizedBox( - height: 12, + title: Text( + "Support", + style: STextStyles.navBarTitle(context), ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + child: Text( + "If you need support or want to report a bug, reach out to us on any of our socials!", + style: STextStyles.smallMed12(context), + ), + ), + isDesktop + ? const SizedBox( + height: 24, + ) + : const SizedBox( + height: 12, ), - onPressed: () { + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + if (!isDesktop) { launchUrl( Uri.parse("https://t.me/stackwallet"), mode: LaunchMode.externalApplication, ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.socials.telegram, - width: iconSize, - height: iconSize, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - ), - const SizedBox( - width: 12, - ), - Text( - "Telegram", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - ], - ), + } + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + SvgPicture.asset( + Assets.socials.telegram, + width: iconSize, + height: iconSize, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + const SizedBox( + width: 12, + ), + Text( + "Telegram", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + ], + ), + BlueTextButton( + text: isDesktop ? "@stackwallet" : "", + onTap: () { + launchUrl( + Uri.parse("https://t.me/stackwallet"), + mode: LaunchMode.externalApplication, + ); + }, + ), + ], ), ), ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), + ), + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - onPressed: () { + ), + onPressed: () { + if (!isDesktop) { launchUrl( Uri.parse("https://discord.gg/RZMG3yUm"), mode: LaunchMode.externalApplication, ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.socials.discord, - width: iconSize, - height: iconSize, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - ), - const SizedBox( - width: 12, - ), - Text( - "Discord", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - ], - ), + } + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + SvgPicture.asset( + Assets.socials.discord, + width: iconSize, + height: iconSize, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + const SizedBox( + width: 12, + ), + Text( + "Discord", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + ], + ), + BlueTextButton( + text: isDesktop ? "Stack Wallet" : "", + onTap: () { + launchUrl( + Uri.parse( + "https://discord.gg/RZMG3yUm"), //expired link? + mode: LaunchMode.externalApplication, + ); + }, + ), + ], ), ), ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), + ), + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - onPressed: () { + ), + onPressed: () { + if (!isDesktop) { launchUrl( Uri.parse("https://www.reddit.com/r/stackwallet/"), mode: LaunchMode.externalApplication, ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.socials.reddit, - width: iconSize, - height: iconSize, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - ), - const SizedBox( - width: 12, - ), - Text( - "Reddit", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - ], - ), + } + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + SvgPicture.asset( + Assets.socials.reddit, + width: iconSize, + height: iconSize, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + const SizedBox( + width: 12, + ), + Text( + "Reddit", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + ], + ), + BlueTextButton( + text: isDesktop ? "r/stackwallet" : "", + onTap: () { + launchUrl( + Uri.parse("https://www.reddit.com/r/stackwallet/"), + mode: LaunchMode.externalApplication, + ); + }, + ), + ], ), ), ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), + ), + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - onPressed: () { + ), + onPressed: () { + if (!isDesktop) { launchUrl( Uri.parse("https://twitter.com/stack_wallet"), mode: LaunchMode.externalApplication, ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.socials.twitter, - width: iconSize, - height: iconSize, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - ), - const SizedBox( - width: 12, - ), - Text( - "Twitter", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - ], - ), + } + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + SvgPicture.asset( + Assets.socials.twitter, + width: iconSize, + height: iconSize, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + const SizedBox( + width: 12, + ), + Text( + "Twitter", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + ], + ), + BlueTextButton( + text: isDesktop ? "@stack_wallet" : "", + onTap: () { + launchUrl( + Uri.parse("https://twitter.com/stack_wallet"), + mode: LaunchMode.externalApplication, + ); + }, + ), + ], ), ), ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), + ), + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - onPressed: () { + ), + onPressed: () { + if (!isDesktop) { launchUrl( Uri.parse("mailto://support@stackwallet.com"), mode: LaunchMode.externalApplication, ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.envelope, - width: iconSize, - height: iconSize, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - ), - const SizedBox( - width: 12, - ), - Text( - "Email", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - ], - ), + } + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + SvgPicture.asset( + Assets.svg.envelope, + width: iconSize, + height: iconSize, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + const SizedBox( + width: 12, + ), + Text( + "Email", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + ], + ), + BlueTextButton( + text: isDesktop ? "support@stackwallet.com" : "", + onTap: () { + launchUrl( + Uri.parse("mailto://support@stackwallet.com"), + mode: LaunchMode.externalApplication, + ); + }, + ), + ], ), ), ), - ], - ), + ), + ], ), ); } diff --git a/lib/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart b/lib/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart index e69de29bb..8e9d709d1 100644 --- a/lib/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart +++ b/lib/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; + +import '../../../pages/settings_views/global_settings_view/support_view.dart'; + +class DesktopSupportView extends ConsumerStatefulWidget { + const DesktopSupportView({Key? key}) : super(key: key); + + static const String routeName = "/desktopSupportView"; + + @override + ConsumerState<DesktopSupportView> createState() => _DesktopSupportView(); +} + +class _DesktopSupportView extends ConsumerState<DesktopSupportView> { + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + return DesktopScaffold( + background: Theme.of(context).extension<StackColors>()!.background, + appBar: DesktopAppBar( + isCompactHeight: true, + leading: Row( + children: [ + const SizedBox( + width: 24, + height: 24, + ), + Text( + "Support", + style: STextStyles.desktopH3(context), + ) + ], + ), + ), + body: Column( + children: const [ + Padding( + padding: EdgeInsets.fromLTRB(24, 10, 377, 270), + child: SupportView(), + ), + ], + ), + ); + } +} From 786831bcef80f10660f8f73d8708e1c57dffd497 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 7 Nov 2022 08:31:27 -0600 Subject: [PATCH 145/426] alphabetically sort contacts --- lib/services/address_book_service.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/services/address_book_service.dart b/lib/services/address_book_service.dart index 6f7d2b9bd..f51eefbba 100644 --- a/lib/services/address_book_service.dart +++ b/lib/services/address_book_service.dart @@ -20,10 +20,13 @@ class AddressBookService extends ChangeNotifier { List<Contact> get contacts { final keys = List<String>.from( DB.instance.keys<dynamic>(boxName: DB.boxNameAddressBook)); - return keys + final _contacts = keys .map((id) => Contact.fromJson(Map<String, dynamic>.from(DB.instance .get<dynamic>(boxName: DB.boxNameAddressBook, key: id) as Map))) .toList(growable: false); + _contacts + .sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); + return _contacts; } Future<List<Contact>>? _addressBookEntries; From f6bad974e6915338ce94c22ea356833b8cb8c8c8 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 7 Nov 2022 10:14:47 -0600 Subject: [PATCH 146/426] address book tests updated --- test/address_book_service_test.dart | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/address_book_service_test.dart b/test/address_book_service_test.dart index 1059f7fc3..c5effd223 100644 --- a/test/address_book_service_test.dart +++ b/test/address_book_service_test.dart @@ -94,19 +94,19 @@ void main() { test("get contacts", () { final service = AddressBookService(); expect(service.contacts.toString(), - [contactA, contactB, contactC].toString()); + [contactC, contactB, contactA].toString()); }); test("get addressBookEntries", () async { final service = AddressBookService(); expect((await service.addressBookEntries).toString(), - [contactA, contactB, contactC].toString()); + [contactC, contactB, contactA].toString()); }); test("search contacts", () async { final service = AddressBookService(); final results = await service.search("j"); - expect(results.toString(), [contactA, contactB].toString()); + expect(results.toString(), [contactB, contactA].toString()); final results2 = await service.search("ja"); expect(results2.toString(), [contactB].toString()); @@ -118,7 +118,7 @@ void main() { expect(results4.toString(), <Contact>[].toString()); final results5 = await service.search(""); - expect(results5.toString(), [contactA, contactB, contactC].toString()); + expect(results5.toString(), [contactC, contactB, contactA].toString()); final results6 = await service.search("epic address"); expect(results6.toString(), [contactC].toString()); @@ -140,7 +140,7 @@ void main() { expect(result, false); expect(service.contacts.length, 3); expect(service.contacts.toString(), - [contactA, contactB, contactC].toString()); + [contactC, contactB, contactA].toString()); }); test("edit contact", () async { @@ -149,14 +149,14 @@ void main() { expect(await service.editContact(editedContact), true); expect(service.contacts.length, 3); expect(service.contacts.toString(), - [contactA, editedContact, contactC].toString()); + [contactC, contactA, editedContact].toString()); }); test("remove existing contact", () async { final service = AddressBookService(); await service.removeContact(contactB.id); expect(service.contacts.length, 2); - expect(service.contacts.toString(), [contactA, contactC].toString()); + expect(service.contacts.toString(), [contactC, contactA].toString()); }); test("remove non existing contact", () async { @@ -164,7 +164,7 @@ void main() { await service.removeContact("some id"); expect(service.contacts.length, 3); expect(service.contacts.toString(), - [contactA, contactB, contactC].toString()); + [contactC, contactB, contactA].toString()); }); tearDown(() async { From bb260e3a23b7e0c1ddd4509d16a95db3a51305c0 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 7 Nov 2022 10:24:08 -0600 Subject: [PATCH 147/426] hacky fix (due to current persistence design) to get sent transactions showing up right away for electrumx coins --- .../send_view/confirm_transaction_view.dart | 4 +- .../coins/bitcoin/bitcoin_wallet.dart | 50 +++++++++++++++++++ .../coins/bitcoincash/bitcoincash_wallet.dart | 50 +++++++++++++++++++ lib/services/coins/coin_service.dart | 5 +- .../coins/dogecoin/dogecoin_wallet.dart | 50 +++++++++++++++++++ .../coins/epiccash/epiccash_wallet.dart | 8 +++ lib/services/coins/firo/firo_wallet.dart | 50 +++++++++++++++++++ .../coins/litecoin/litecoin_wallet.dart | 50 +++++++++++++++++++ lib/services/coins/manager.dart | 3 ++ lib/services/coins/monero/monero_wallet.dart | 8 +++ .../coins/namecoin/namecoin_wallet.dart | 50 +++++++++++++++++++ .../coins/wownero/wownero_wallet.dart | 8 +++ .../services/coins/fake_coin_service_api.dart | 6 +++ 13 files changed, 339 insertions(+), 3 deletions(-) diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index 81d5a3da2..26d1231f0 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -87,13 +87,13 @@ class _ConfirmTransactionViewState txid = await manager.confirmSend(txData: transactionInfo); } - unawaited(manager.refresh()); - // save note await ref .read(notesServiceChangeNotifierProvider(walletId)) .editOrAddNote(txid: txid, note: note); + unawaited(manager.refresh()); + // pop back to wallet if (mounted) { Navigator.of(context).popUntil(ModalRoute.withName(routeOnSuccessName)); diff --git a/lib/services/coins/bitcoin/bitcoin_wallet.dart b/lib/services/coins/bitcoin/bitcoin_wallet.dart index 391beb909..d0920075d 100644 --- a/lib/services/coins/bitcoin/bitcoin_wallet.dart +++ b/lib/services/coins/bitcoin/bitcoin_wallet.dart @@ -10,6 +10,7 @@ import 'package:bitcoindart/bitcoindart.dart'; import 'package:bs58check/bs58check.dart' as bs58check; import 'package:crypto/crypto.dart'; import 'package:decimal/decimal.dart'; +import 'package:devicelocale/devicelocale.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; @@ -1283,6 +1284,54 @@ class BitcoinWallet extends CoinServiceAPI { _transactionData ??= _fetchTransactionData(); Future<TransactionData>? _transactionData; + TransactionData? cachedTxData; + + // hack to add tx to txData before refresh completes + // required based on current app architecture where we don't properly store + // transactions locally in a good way + @override + Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async { + final priceData = + await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; + final locale = await Devicelocale.currentLocale; + final String worthNow = Format.localizedStringAsFixed( + value: + ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / + Decimal.fromInt(Constants.satsPerCoin)) + .toDecimal(scaleOnInfinitePrecision: 2), + decimalPlaces: 2, + locale: locale!); + + final tx = models.Transaction( + txid: txData["txid"] as String, + confirmedStatus: false, + timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, + txType: "Sent", + amount: txData["recipientAmt"] as int, + worthNow: worthNow, + worthAtBlockTimestamp: worthNow, + fees: txData["fee"] as int, + inputSize: 0, + outputSize: 0, + inputs: [], + outputs: [], + address: txData["address"] as String, + height: -1, + confirmations: 0, + ); + + if (cachedTxData == null) { + final data = await _fetchTransactionData(); + _transactionData = Future(() => data); + } + + final transactions = cachedTxData!.getAllTransactions(); + transactions[tx.txid] = tx; + cachedTxData = models.TransactionData.fromMap(transactions); + _transactionData = Future(() => cachedTxData!); + } + @override bool validateAddress(String address) { return Address.validateAddress(address, _network); @@ -2661,6 +2710,7 @@ class BitcoinWallet extends CoinServiceAPI { await DB.instance.put<dynamic>( boxName: walletId, key: 'latest_tx_model', value: txModel); + cachedTxData = txModel; return txModel; } diff --git a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart index fa88b3f2f..98a31ee0c 100644 --- a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart +++ b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart @@ -11,6 +11,7 @@ import 'package:bitcoindart/bitcoindart.dart'; import 'package:bs58check/bs58check.dart' as bs58check; import 'package:crypto/crypto.dart'; import 'package:decimal/decimal.dart'; +import 'package:devicelocale/devicelocale.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; @@ -1154,6 +1155,54 @@ class BitcoinCashWallet extends CoinServiceAPI { _transactionData ??= _fetchTransactionData(); Future<TransactionData>? _transactionData; + TransactionData? cachedTxData; + + // hack to add tx to txData before refresh completes + // required based on current app architecture where we don't properly store + // transactions locally in a good way + @override + Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async { + final priceData = + await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; + final locale = await Devicelocale.currentLocale; + final String worthNow = Format.localizedStringAsFixed( + value: + ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / + Decimal.fromInt(Constants.satsPerCoin)) + .toDecimal(scaleOnInfinitePrecision: 2), + decimalPlaces: 2, + locale: locale!); + + final tx = models.Transaction( + txid: txData["txid"] as String, + confirmedStatus: false, + timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, + txType: "Sent", + amount: txData["recipientAmt"] as int, + worthNow: worthNow, + worthAtBlockTimestamp: worthNow, + fees: txData["fee"] as int, + inputSize: 0, + outputSize: 0, + inputs: [], + outputs: [], + address: txData["address"] as String, + height: -1, + confirmations: 0, + ); + + if (cachedTxData == null) { + final data = await _fetchTransactionData(); + _transactionData = Future(() => data); + } + + final transactions = cachedTxData!.getAllTransactions(); + transactions[tx.txid] = tx; + cachedTxData = models.TransactionData.fromMap(transactions); + _transactionData = Future(() => cachedTxData!); + } + @override bool validateAddress(String address) { try { @@ -2449,6 +2498,7 @@ class BitcoinCashWallet extends CoinServiceAPI { await DB.instance.put<dynamic>( boxName: walletId, key: 'latest_tx_model', value: txModel); + cachedTxData = txModel; return txModel; } diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart index c36fa9eee..655865494 100644 --- a/lib/services/coins/coin_service.dart +++ b/lib/services/coins/coin_service.dart @@ -9,8 +9,8 @@ import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart'; import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/monero/monero_wallet.dart'; -import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart'; import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart'; +import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/prefs.dart'; @@ -277,4 +277,7 @@ abstract class CoinServiceAPI { Future<int> estimateFeeFor(int satoshiAmount, int feeRate); Future<bool> generateNewAddress(); + + // used for electrumx coins + Future<void> updateSentCachedTxData(Map<String, dynamic> txData); } diff --git a/lib/services/coins/dogecoin/dogecoin_wallet.dart b/lib/services/coins/dogecoin/dogecoin_wallet.dart index fbb551dcd..67be291a2 100644 --- a/lib/services/coins/dogecoin/dogecoin_wallet.dart +++ b/lib/services/coins/dogecoin/dogecoin_wallet.dart @@ -10,6 +10,7 @@ import 'package:bitcoindart/bitcoindart.dart'; import 'package:bs58check/bs58check.dart' as bs58check; import 'package:crypto/crypto.dart'; import 'package:decimal/decimal.dart'; +import 'package:devicelocale/devicelocale.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; @@ -1051,6 +1052,54 @@ class DogecoinWallet extends CoinServiceAPI { _transactionData ??= _fetchTransactionData(); Future<TransactionData>? _transactionData; + TransactionData? cachedTxData; + + // hack to add tx to txData before refresh completes + // required based on current app architecture where we don't properly store + // transactions locally in a good way + @override + Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async { + final priceData = + await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; + final locale = await Devicelocale.currentLocale; + final String worthNow = Format.localizedStringAsFixed( + value: + ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / + Decimal.fromInt(Constants.satsPerCoin)) + .toDecimal(scaleOnInfinitePrecision: 2), + decimalPlaces: 2, + locale: locale!); + + final tx = models.Transaction( + txid: txData["txid"] as String, + confirmedStatus: false, + timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, + txType: "Sent", + amount: txData["recipientAmt"] as int, + worthNow: worthNow, + worthAtBlockTimestamp: worthNow, + fees: txData["fee"] as int, + inputSize: 0, + outputSize: 0, + inputs: [], + outputs: [], + address: txData["address"] as String, + height: -1, + confirmations: 0, + ); + + if (cachedTxData == null) { + final data = await _fetchTransactionData(); + _transactionData = Future(() => data); + } + + final transactions = cachedTxData!.getAllTransactions(); + transactions[tx.txid] = tx; + cachedTxData = models.TransactionData.fromMap(transactions); + _transactionData = Future(() => cachedTxData!); + } + @override bool validateAddress(String address) { return Address.validateAddress(address, _network); @@ -2273,6 +2322,7 @@ class DogecoinWallet extends CoinServiceAPI { await DB.instance.put<dynamic>( boxName: walletId, key: 'latest_tx_model', value: txModel); + cachedTxData = txModel; return txModel; } diff --git a/lib/services/coins/epiccash/epiccash_wallet.dart b/lib/services/coins/epiccash/epiccash_wallet.dart index b98854e61..3702a9158 100644 --- a/lib/services/coins/epiccash/epiccash_wallet.dart +++ b/lib/services/coins/epiccash/epiccash_wallet.dart @@ -2259,6 +2259,14 @@ class EpicCashWallet extends CoinServiceAPI { _transactionData ??= _fetchTransactionData(); Future<TransactionData>? _transactionData; + // not used in epic + TransactionData? cachedTxData; + + @override + Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async { + // not used in epic + } + @override Future<List<UtxoObject>> get unspentOutputs => throw UnimplementedError(); diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index 2a2102e7c..d19c4f1ab 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -908,6 +908,52 @@ class FiroWallet extends CoinServiceAPI { Future<models.TransactionData> get _txnData => _transactionData ??= _fetchTransactionData(); + models.TransactionData? cachedTxData; + + // hack to add tx to txData before refresh completes + // required based on current app architecture where we don't properly store + // transactions locally in a good way + @override + Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async { + final currentPrice = await firoPrice; + final locale = await Devicelocale.currentLocale; + final String worthNow = Format.localizedStringAsFixed( + value: + ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / + Decimal.fromInt(Constants.satsPerCoin)) + .toDecimal(scaleOnInfinitePrecision: 2), + decimalPlaces: 2, + locale: locale!); + + final tx = models.Transaction( + txid: txData["txid"] as String, + confirmedStatus: false, + timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, + txType: "Sent", + amount: txData["recipientAmt"] as int, + worthNow: worthNow, + worthAtBlockTimestamp: worthNow, + fees: txData["fee"] as int, + inputSize: 0, + outputSize: 0, + inputs: [], + outputs: [], + address: txData["address"] as String, + height: -1, + confirmations: 0, + ); + + if (cachedTxData == null) { + final data = await _fetchTransactionData(); + _transactionData = Future(() => data); + } + + final transactions = cachedTxData!.getAllTransactions(); + transactions[tx.txid] = tx; + cachedTxData = models.TransactionData.fromMap(transactions); + _transactionData = Future(() => cachedTxData!); + } + /// Holds wallet lelantus transaction data Future<models.TransactionData>? _lelantusTransactionData; Future<models.TransactionData> get lelantusTransactionData => @@ -1110,6 +1156,9 @@ class FiroWallet extends CoinServiceAPI { final txHash = await _electrumXClient.broadcastTransaction( rawTx: txData["hex"] as String); Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info); + txData["txid"] = txHash; + // dirty ui update hack + await updateSentCachedTxData(txData as Map<String, dynamic>); return txHash; } catch (e, s) { Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s", @@ -3465,6 +3514,7 @@ class FiroWallet extends CoinServiceAPI { await DB.instance.put<dynamic>( boxName: walletId, key: 'latest_tx_model', value: txModel); + cachedTxData = txModel; return txModel; } diff --git a/lib/services/coins/litecoin/litecoin_wallet.dart b/lib/services/coins/litecoin/litecoin_wallet.dart index c07cca1f3..4551325f7 100644 --- a/lib/services/coins/litecoin/litecoin_wallet.dart +++ b/lib/services/coins/litecoin/litecoin_wallet.dart @@ -10,6 +10,7 @@ import 'package:bitcoindart/bitcoindart.dart'; import 'package:bs58check/bs58check.dart' as bs58check; import 'package:crypto/crypto.dart'; import 'package:decimal/decimal.dart'; +import 'package:devicelocale/devicelocale.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; @@ -1285,6 +1286,54 @@ class LitecoinWallet extends CoinServiceAPI { _transactionData ??= _fetchTransactionData(); Future<TransactionData>? _transactionData; + TransactionData? cachedTxData; + + // hack to add tx to txData before refresh completes + // required based on current app architecture where we don't properly store + // transactions locally in a good way + @override + Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async { + final priceData = + await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; + final locale = await Devicelocale.currentLocale; + final String worthNow = Format.localizedStringAsFixed( + value: + ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / + Decimal.fromInt(Constants.satsPerCoin)) + .toDecimal(scaleOnInfinitePrecision: 2), + decimalPlaces: 2, + locale: locale!); + + final tx = models.Transaction( + txid: txData["txid"] as String, + confirmedStatus: false, + timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, + txType: "Sent", + amount: txData["recipientAmt"] as int, + worthNow: worthNow, + worthAtBlockTimestamp: worthNow, + fees: txData["fee"] as int, + inputSize: 0, + outputSize: 0, + inputs: [], + outputs: [], + address: txData["address"] as String, + height: -1, + confirmations: 0, + ); + + if (cachedTxData == null) { + final data = await _fetchTransactionData(); + _transactionData = Future(() => data); + } + + final transactions = cachedTxData!.getAllTransactions(); + transactions[tx.txid] = tx; + cachedTxData = models.TransactionData.fromMap(transactions); + _transactionData = Future(() => cachedTxData!); + } + @override bool validateAddress(String address) { return Address.validateAddress(address, _network, _network.bech32!); @@ -2673,6 +2722,7 @@ class LitecoinWallet extends CoinServiceAPI { await DB.instance.put<dynamic>( boxName: walletId, key: 'latest_tx_model', value: txModel); + cachedTxData = txModel; return txModel; } diff --git a/lib/services/coins/manager.dart b/lib/services/coins/manager.dart index c8329ec28..8054fe168 100644 --- a/lib/services/coins/manager.dart +++ b/lib/services/coins/manager.dart @@ -108,6 +108,9 @@ class Manager with ChangeNotifier { try { final txid = await _currentWallet.confirmSend(txData: txData); + txData["txid"] = txid; + await _currentWallet.updateSentCachedTxData(txData); + notifyListeners(); return txid; } catch (e) { diff --git a/lib/services/coins/monero/monero_wallet.dart b/lib/services/coins/monero/monero_wallet.dart index 9bcc3515f..662d4077b 100644 --- a/lib/services/coins/monero/monero_wallet.dart +++ b/lib/services/coins/monero/monero_wallet.dart @@ -1190,6 +1190,14 @@ class MoneroWallet extends CoinServiceAPI { _transactionData ??= _fetchTransactionData(); Future<TransactionData>? _transactionData; + // not used in monero + TransactionData? cachedTxData; + + @override + Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async { + // not used in monero + } + Future<TransactionData> _fetchTransactionData() async { final transactions = walletBase?.transactionHistory!.transactions; diff --git a/lib/services/coins/namecoin/namecoin_wallet.dart b/lib/services/coins/namecoin/namecoin_wallet.dart index 893db69e0..8a4b26012 100644 --- a/lib/services/coins/namecoin/namecoin_wallet.dart +++ b/lib/services/coins/namecoin/namecoin_wallet.dart @@ -10,6 +10,7 @@ import 'package:bitcoindart/bitcoindart.dart'; import 'package:bs58check/bs58check.dart' as bs58check; import 'package:crypto/crypto.dart'; import 'package:decimal/decimal.dart'; +import 'package:devicelocale/devicelocale.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; @@ -1276,6 +1277,54 @@ class NamecoinWallet extends CoinServiceAPI { _transactionData ??= _fetchTransactionData(); Future<TransactionData>? _transactionData; + TransactionData? cachedTxData; + + // hack to add tx to txData before refresh completes + // required based on current app architecture where we don't properly store + // transactions locally in a good way + @override + Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async { + final priceData = + await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; + final locale = await Devicelocale.currentLocale; + final String worthNow = Format.localizedStringAsFixed( + value: + ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / + Decimal.fromInt(Constants.satsPerCoin)) + .toDecimal(scaleOnInfinitePrecision: 2), + decimalPlaces: 2, + locale: locale!); + + final tx = models.Transaction( + txid: txData["txid"] as String, + confirmedStatus: false, + timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, + txType: "Sent", + amount: txData["recipientAmt"] as int, + worthNow: worthNow, + worthAtBlockTimestamp: worthNow, + fees: txData["fee"] as int, + inputSize: 0, + outputSize: 0, + inputs: [], + outputs: [], + address: txData["address"] as String, + height: -1, + confirmations: 0, + ); + + if (cachedTxData == null) { + final data = await _fetchTransactionData(); + _transactionData = Future(() => data); + } + + final transactions = cachedTxData!.getAllTransactions(); + transactions[tx.txid] = tx; + cachedTxData = models.TransactionData.fromMap(transactions); + _transactionData = Future(() => cachedTxData!); + } + @override bool validateAddress(String address) { return Address.validateAddress(address, _network, namecoin.bech32!); @@ -2673,6 +2722,7 @@ class NamecoinWallet extends CoinServiceAPI { await DB.instance.put<dynamic>( boxName: walletId, key: 'latest_tx_model', value: txModel); + cachedTxData = txModel; return txModel; } diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart index 342e5d84a..72f43eac8 100644 --- a/lib/services/coins/wownero/wownero_wallet.dart +++ b/lib/services/coins/wownero/wownero_wallet.dart @@ -1195,6 +1195,14 @@ class WowneroWallet extends CoinServiceAPI { _transactionData ??= _fetchTransactionData(); Future<TransactionData>? _transactionData; + // not used in wownero + TransactionData? cachedTxData; + + @override + Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async { + // not used in wownero + } + Future<TransactionData> _fetchTransactionData() async { final transactions = walletBase?.transactionHistory!.transactions; diff --git a/test/services/coins/fake_coin_service_api.dart b/test/services/coins/fake_coin_service_api.dart index a3ae28a4b..c5f300c16 100644 --- a/test/services/coins/fake_coin_service_api.dart +++ b/test/services/coins/fake_coin_service_api.dart @@ -182,4 +182,10 @@ class FakeCoinServiceAPI extends CoinServiceAPI { // TODO: implement generateNewAddress throw UnimplementedError(); } + + @override + Future<void> updateSentCachedTxData(Map<String, dynamic> txData) { + // TODO: implement updateSentCachedTxData + throw UnimplementedError(); + } } From 6223df54320afea50d258580d4780b820491d35d Mon Sep 17 00:00:00 2001 From: sneurlax <sneurlax@gmail.com> Date: Mon, 7 Nov 2022 11:21:10 -0600 Subject: [PATCH 148/426] specify tests for 14 word seeds and add more error checking code to test and update ref to flutter_libmonero enabling tests (mocked storage, etc) --- crypto_plugins/flutter_libmonero | 2 +- .../coins/wownero/wownero_wallet_test.dart | 45 +++++++++++-------- .../wownero/wownero_wallet_test_data.dart | 4 +- 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index 277d922c3..d92fea3e6 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit 277d922c3b1d637c1ccda25f51395c618d293015 +Subproject commit d92fea3e6b915b79697deafbd5710fb4c15bfc76 diff --git a/test/services/coins/wownero/wownero_wallet_test.dart b/test/services/coins/wownero/wownero_wallet_test.dart index e64dd772c..83b4844c7 100644 --- a/test/services/coins/wownero/wownero_wallet_test.dart +++ b/test/services/coins/wownero/wownero_wallet_test.dart @@ -83,13 +83,13 @@ void main() async { _walletInfoSource = await Hive.openBox<WalletInfo>(WalletInfo.boxName); walletService = wownero.createWowneroWalletService(_walletInfoSource); - group("Wownero tests", () { + group("Wownero 14 word tests", () { setUp(() async { try { final dirPath = await pathForWalletDir(name: name, type: type); path = await pathForWallet(name: name, type: type); credentials = wownero.createWowneroRestoreWalletFromSeedCredentials( - name: name, height: 465760, mnemonic: testMnemonic); + name: name, height: 465760, mnemonic: testMnemonic14); walletInfo = WalletInfo.external( id: WalletBase.idFor(name, type), @@ -116,27 +116,36 @@ void main() async { } }); - test("Test mainnet address generation from seed", () async { + test("Test mainnet address generation from 14 word seed", () async { final wallet = await _walletCreationService.restoreFromSeed(credentials); walletInfo.address = wallet.walletAddresses.address; - await _walletInfoSource.add(walletInfo); + bool hasThrown = false; + try { + await _walletInfoSource.add(walletInfo); + walletBase?.close(); + walletBase = wallet as WowneroWalletBase; + + expect(walletInfo.address, mainnetTestData14[0][0]); + expect( + await walletBase!.getTransactionAddress(0, 0), mainnetTestData14[0][0]); + expect( + await walletBase!.getTransactionAddress(0, 1), mainnetTestData14[0][1]); + expect( + await walletBase!.getTransactionAddress(0, 2), mainnetTestData14[0][2]); + expect( + await walletBase!.getTransactionAddress(1, 0), mainnetTestData14[1][0]); + expect( + await walletBase!.getTransactionAddress(1, 1), mainnetTestData14[1][1]); + expect( + await walletBase!.getTransactionAddress(1, 2), mainnetTestData14[1][2]); + } catch (_) { + hasThrown = true; + } + expect(hasThrown, false); + walletBase?.close(); walletBase = wallet as WowneroWalletBase; - - expect(walletInfo.address, mainnetTestData[0][0]); - expect( - await walletBase!.getTransactionAddress(0, 0), mainnetTestData[0][0]); - expect( - await walletBase!.getTransactionAddress(0, 1), mainnetTestData[0][1]); - expect( - await walletBase!.getTransactionAddress(0, 2), mainnetTestData[0][2]); - expect( - await walletBase!.getTransactionAddress(1, 0), mainnetTestData[1][0]); - expect( - await walletBase!.getTransactionAddress(1, 1), mainnetTestData[1][1]); - expect( - await walletBase!.getTransactionAddress(1, 2), mainnetTestData[1][2]); }); }); } diff --git a/test/services/coins/wownero/wownero_wallet_test_data.dart b/test/services/coins/wownero/wownero_wallet_test_data.dart index b0d93a448..8ab825dfa 100644 --- a/test/services/coins/wownero/wownero_wallet_test_data.dart +++ b/test/services/coins/wownero/wownero_wallet_test_data.dart @@ -1,6 +1,6 @@ -String testMnemonic = +String testMnemonic14 = 'weather cruise school such silly profit clerk wage reduce obtain ill sand episode shadow'; -var mainnetTestData = [ +var mainnetTestData14 = [ [ 'Wo3jmHvTMLwE6h29fpgcb8PbJSpaKuqM7XTXVfiiu8bLCZsJvrQCbQSJR48Vo3BWNQKsMsXZ4VixndXTH25QtorC27NCjmsEi', 'WW3K54QzmMFB1uTZh3LVvgQYqANLmX1FkJHLJ4sU1E7BQmp8nGizyBnjNXSgsjCa4BQ3Rw3GG5jw1ByUkaUjSywm2KmHAbFvK', From c88971ebd6eaf7e278591eef2925c1fe6b8a2081 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 7 Nov 2022 11:46:17 -0600 Subject: [PATCH 149/426] firo pub/priv balance send from choice on exchange flow --- .../confirm_change_now_send.dart | 13 +- lib/pages/exchange_view/send_from_view.dart | 559 ++++++++++++------ 2 files changed, 404 insertions(+), 168 deletions(-) diff --git a/lib/pages/exchange_view/confirm_change_now_send.dart b/lib/pages/exchange_view/confirm_change_now_send.dart index d77ad6b8c..e99cf2df4 100644 --- a/lib/pages/exchange_view/confirm_change_now_send.dart +++ b/lib/pages/exchange_view/confirm_change_now_send.dart @@ -10,6 +10,7 @@ import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; import 'package:stackwallet/providers/exchange/trade_sent_from_stack_lookup_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/route_generator.dart'; +import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -27,6 +28,7 @@ class ConfirmChangeNowSendView extends ConsumerStatefulWidget { required this.walletId, this.routeOnSuccessName = WalletView.routeName, required this.trade, + this.shouldSendPublicFiroFunds, }) : super(key: key); static const String routeName = "/confirmChangeNowSend"; @@ -35,6 +37,7 @@ class ConfirmChangeNowSendView extends ConsumerStatefulWidget { final String walletId; final String routeOnSuccessName; final Trade trade; + final bool? shouldSendPublicFiroFunds; @override ConsumerState<ConfirmChangeNowSendView> createState() => @@ -63,7 +66,15 @@ class _ConfirmChangeNowSendViewState ref.read(walletsChangeNotifierProvider).getManager(walletId); try { - final txid = await manager.confirmSend(txData: transactionInfo); + late final String txid; + + if (widget.shouldSendPublicFiroFunds == true) { + txid = await (manager.wallet as FiroWallet) + .confirmSendPublic(txData: transactionInfo); + } else { + txid = await manager.confirmSend(txData: transactionInfo); + } + unawaited(manager.refresh()); // save note diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart index 20fc81903..c87175955 100644 --- a/lib/pages/exchange_view/send_from_view.dart +++ b/lib/pages/exchange_view/send_from_view.dart @@ -10,6 +10,8 @@ import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages/send_view/sub_widgets/building_transaction_dialog.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/route_generator.dart'; +import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; +import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -18,7 +20,9 @@ import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/animated_text.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/expandable.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; @@ -162,6 +166,130 @@ class _SendFromCardState extends ConsumerState<SendFromCard> { late final String address; late final Trade trade; + Future<void> _send(Manager manager, {bool? shouldSendPublicFiroFunds}) async { + final _amount = Format.decimalAmountToSatoshis(amount); + + try { + bool wasCancelled = false; + + unawaited( + showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return BuildingTransactionDialog( + onCancel: () { + wasCancelled = true; + + Navigator.of(context).pop(); + }, + ); + }, + ), + ); + + late Map<String, dynamic> txData; + + // if not firo then do normal send + if (shouldSendPublicFiroFunds == null) { + txData = await manager.prepareSend( + address: address, + satoshiAmount: _amount, + args: { + "feeRate": FeeRateType.average, + // ref.read(feeRateTypeStateProvider) + }, + ); + } else { + final firoWallet = manager.wallet as FiroWallet; + // otherwise do firo send based on balance selected + if (shouldSendPublicFiroFunds) { + txData = await firoWallet.prepareSendPublic( + address: address, + satoshiAmount: _amount, + args: { + "feeRate": FeeRateType.average, + // ref.read(feeRateTypeStateProvider) + }, + ); + } else { + txData = await firoWallet.prepareSend( + address: address, + satoshiAmount: _amount, + args: { + "feeRate": FeeRateType.average, + // ref.read(feeRateTypeStateProvider) + }, + ); + } + } + + if (!wasCancelled) { + // pop building dialog + + if (mounted) { + Navigator.of(context).pop(); + } + + txData["note"] = + "${trade.payInCurrency.toUpperCase()}/${trade.payOutCurrency.toUpperCase()} exchange"; + txData["address"] = address; + + if (mounted) { + await Navigator.of(context).push( + RouteGenerator.getRoute( + shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, + builder: (_) => ConfirmChangeNowSendView( + transactionInfo: txData, + walletId: walletId, + routeOnSuccessName: HomeView.routeName, + trade: trade, + shouldSendPublicFiroFunds: shouldSendPublicFiroFunds, + ), + settings: const RouteSettings( + name: ConfirmChangeNowSendView.routeName, + ), + ), + ); + } + } + } catch (e) { + // if (mounted) { + // pop building dialog + Navigator.of(context).pop(); + + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return StackDialog( + title: "Transaction failed", + message: e.toString(), + rightButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Ok", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, + ), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ); + }, + ); + // } + } + } + @override void initState() { walletId = widget.walletId; @@ -182,181 +310,278 @@ class _SendFromCardState extends ConsumerState<SendFromCard> { final coin = manager.coin; + final isFiro = coin == Coin.firoTestNet || coin == Coin.firo; + return RoundedWhiteContainer( padding: const EdgeInsets.all(0), - child: MaterialButton( - splashColor: Theme.of(context).extension<StackColors>()!.highlight, - key: Key("walletsSheetItemButtonKey_$walletId"), - padding: const EdgeInsets.all(8), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + child: ConditionalParent( + condition: isFiro, + builder: (child) => Expandable( + header: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.all(12), + child: child, + ), + ), + body: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MaterialButton( + splashColor: + Theme.of(context).extension<StackColors>()!.highlight, + key: Key("walletsSheetItemButtonFiroPrivateKey_$walletId"), + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () => _send( + manager, + shouldSendPublicFiroFunds: false, + ), + child: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.only( + top: 6, + left: 16, + right: 16, + bottom: 6, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Use private balance", + style: STextStyles.itemSubtitle(context), + ), + FutureBuilder( + future: (manager.wallet as FiroWallet) + .availablePrivateBalance(), + builder: (builderContext, + AsyncSnapshot<Decimal> snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + return Text( + "${Format.localizedStringAsFixed( + value: snapshot.data!, + locale: locale, + decimalPlaces: Constants.decimalPlaces, + )} ${coin.ticker}", + style: STextStyles.itemSubtitle(context), + ); + } else { + return AnimatedText( + stringsToLoopThrough: const [ + "Loading balance", + "Loading balance.", + "Loading balance..", + "Loading balance..." + ], + style: STextStyles.itemSubtitle(context), + ); + } + }, + ), + ], + ), + SvgPicture.asset( + Assets.svg.chevronRight, + height: 14, + width: 7, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemLabel, + ), + ], + ), + ), + ), + ), + MaterialButton( + splashColor: + Theme.of(context).extension<StackColors>()!.highlight, + key: Key("walletsSheetItemButtonFiroPublicKey_$walletId"), + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () => _send( + manager, + shouldSendPublicFiroFunds: true, + ), + child: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.only( + top: 6, + left: 16, + right: 16, + bottom: 6, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Use public balance", + style: STextStyles.itemSubtitle(context), + ), + FutureBuilder( + future: (manager.wallet as FiroWallet) + .availablePublicBalance(), + builder: (builderContext, + AsyncSnapshot<Decimal> snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + return Text( + "${Format.localizedStringAsFixed( + value: snapshot.data!, + locale: locale, + decimalPlaces: Constants.decimalPlaces, + )} ${coin.ticker}", + style: STextStyles.itemSubtitle(context), + ); + } else { + return AnimatedText( + stringsToLoopThrough: const [ + "Loading balance", + "Loading balance.", + "Loading balance..", + "Loading balance..." + ], + style: STextStyles.itemSubtitle(context), + ); + } + }, + ), + ], + ), + SvgPicture.asset( + Assets.svg.chevronRight, + height: 14, + width: 7, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemLabel, + ), + ], + ), + ), + ), + ), + const SizedBox( + height: 6, + ), + ], ), ), - onPressed: () async { - final _amount = Format.decimalAmountToSatoshis(amount); - - try { - bool wasCancelled = false; - - unawaited(showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: false, - builder: (context) { - return BuildingTransactionDialog( - onCancel: () { - wasCancelled = true; - - Navigator.of(context).pop(); - }, - ); - }, - )); - - final txData = await manager.prepareSend( - address: address, - satoshiAmount: _amount, - args: { - "feeRate": FeeRateType.average, - // ref.read(feeRateTypeStateProvider) - }, - ); - - if (!wasCancelled) { - // pop building dialog - - if (mounted) { - Navigator.of(context).pop(); - } - - txData["note"] = - "${trade.payInCurrency.toUpperCase()}/${trade.payOutCurrency.toUpperCase()} exchange"; - txData["address"] = address; - - if (mounted) { - await Navigator.of(context).push( - RouteGenerator.getRoute( - shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: (_) => ConfirmChangeNowSendView( - transactionInfo: txData, - walletId: walletId, - routeOnSuccessName: HomeView.routeName, - trade: trade, - ), - settings: const RouteSettings( - name: ConfirmChangeNowSendView.routeName, - ), + child: ConditionalParent( + condition: !isFiro, + builder: (child) => MaterialButton( + splashColor: Theme.of(context).extension<StackColors>()!.highlight, + key: Key("walletsSheetItemButtonKey_$walletId"), + padding: const EdgeInsets.all(8), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () => _send(manager), + child: child, + ), + child: Row( + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .colorForCoin(manager.coin) + .withOpacity(0.5), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - ); - } - } - } catch (e) { - // if (mounted) { - // pop building dialog - Navigator.of(context).pop(); - - await showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return StackDialog( - title: "Transaction failed", - message: e.toString(), - rightButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Ok", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .buttonTextSecondary, + ), + child: Padding( + padding: const EdgeInsets.all(6), + child: SvgPicture.asset( + Assets.svg.iconFor(coin: coin), + width: 24, + height: 24, + ), + ), + ), + const SizedBox( + width: 12, + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + manager.walletName, + style: STextStyles.titleBold12(context), + ), + if (!isFiro) + const SizedBox( + height: 2, ), - ), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ); - }, - ); - // } - } - }, - child: Row( - children: [ - Container( - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .colorForCoin(manager.coin) - .withOpacity(0.5), - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + if (!isFiro) + FutureBuilder( + future: manager.totalBalance, + builder: + (builderContext, AsyncSnapshot<Decimal> snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + return Text( + "${Format.localizedStringAsFixed( + value: snapshot.data!, + locale: locale, + decimalPlaces: coin == Coin.monero + ? Constants.decimalPlacesMonero + : coin == Coin.wownero + ? Constants.decimalPlacesWownero + : Constants.decimalPlaces, + )} ${coin.ticker}", + style: STextStyles.itemSubtitle(context), + ); + } else { + return AnimatedText( + stringsToLoopThrough: const [ + "Loading balance", + "Loading balance.", + "Loading balance..", + "Loading balance..." + ], + style: STextStyles.itemSubtitle(context), + ); + } + }, + ), + ], ), ), - child: Padding( - padding: const EdgeInsets.all(6), - child: SvgPicture.asset( - Assets.svg.iconFor(coin: coin), - width: 24, - height: 24, - ), - ), - ), - const SizedBox( - width: 12, - ), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - manager.walletName, - style: STextStyles.titleBold12(context), - ), - const SizedBox( - height: 2, - ), - FutureBuilder( - future: manager.totalBalance, - builder: (builderContext, AsyncSnapshot<Decimal> snapshot) { - if (snapshot.connectionState == ConnectionState.done && - snapshot.hasData) { - return Text( - "${Format.localizedStringAsFixed( - value: snapshot.data!, - locale: locale, - decimalPlaces: coin == Coin.monero - ? Constants.decimalPlacesMonero - : coin == Coin.wownero - ? Constants.decimalPlacesWownero - : Constants.decimalPlaces, - )} ${coin.ticker}", - style: STextStyles.itemSubtitle(context), - ); - } else { - return AnimatedText( - stringsToLoopThrough: const [ - "Loading balance", - "Loading balance.", - "Loading balance..", - "Loading balance..." - ], - style: STextStyles.itemSubtitle(context), - ); - } - }, - ), - ], - ), - ), - ], + ], + ), ), ), ); From 9fe9ee3a1236011d2e6c96147af82b1aa8874390 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 7 Nov 2022 10:42:55 -0700 Subject: [PATCH 150/426] desktop about view route added --- lib/pages_desktop_specific/home/desktop_home_view.dart | 7 +++++-- .../home/support_and_about_view/desktop_about_view.dart | 0 lib/route_generator.dart | 7 +++++++ 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 lib/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart diff --git a/lib/pages_desktop_specific/home/desktop_home_view.dart b/lib/pages_desktop_specific/home/desktop_home_view.dart index e9f6f2b4b..14d2dae03 100644 --- a/lib/pages_desktop_specific/home/desktop_home_view.dart +++ b/lib/pages_desktop_specific/home/desktop_home_view.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_menu.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_settings_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -43,8 +44,10 @@ class _DesktopHomeViewState extends ConsumerState<DesktopHomeView> { onGenerateRoute: RouteGenerator.generateRoute, initialRoute: DesktopSupportView.routeName, ), - Container( - color: Colors.pink, + const Navigator( + key: Key("desktopAboutHomeKey"), + onGenerateRoute: RouteGenerator.generateRoute, + initialRoute: DesktopAboutView.routeName, ), ]; diff --git a/lib/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart b/lib/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart new file mode 100644 index 000000000..e69de29bb diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 47a84f07c..40f11dc57 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -99,6 +99,7 @@ import 'package:stackwallet/pages_desktop_specific/home/settings_menu/nodes_sett import 'package:stackwallet/pages_desktop_specific/home/settings_menu/security_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/settings_menu.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart'; +import 'package:stackwallet/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; @@ -1091,6 +1092,12 @@ class RouteGenerator { builder: (_) => const DesktopSupportView(), settings: RouteSettings(name: settings.name)); + case DesktopAboutView.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const DesktopAboutView(), + settings: RouteSettings(name: settings.name)); + case WalletKeysDesktopPopup.routeName: if (args is List<String>) { return FadePageRoute( From 48a0e3a5ca89528a1bdca69e8c027967c7957e1d Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 7 Nov 2022 12:31:49 -0700 Subject: [PATCH 151/426] desktop about view added --- .../desktop_about_view.dart | 655 ++++++++++++++++++ 1 file changed, 655 insertions(+) diff --git a/lib/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart b/lib/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart index e69de29bb..86fcf78e0 100644 --- a/lib/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart +++ b/lib/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart @@ -0,0 +1,655 @@ +import 'dart:convert'; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_libepiccash/git_versions.dart' as EPIC_VERSIONS; +import 'package:flutter_libmonero/git_versions.dart' as MONERO_VERSIONS; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:http/http.dart'; +import 'package:lelantus/git_versions.dart' as FIRO_VERSIONS; +import 'package:package_info_plus/package_info_plus.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_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:url_launcher/url_launcher.dart'; + +const kGithubAPI = "https://api.github.com"; +const kGithubSearch = "/search/commits"; +const kGithubHead = "/repos"; + +enum CommitStatus { isHead, isOldCommit, notACommit, notLoaded } + +Future<bool> doesCommitExist( + String organization, + String project, + String commit, +) async { + Logging.instance.log("doesCommitExist", level: LogLevel.Info); + final Client client = Client(); + try { + final uri = Uri.parse( + "$kGithubAPI$kGithubHead/$organization/$project/commits/$commit"); + + final commitQuery = await client.get( + uri, + headers: {'Content-Type': 'application/json'}, + ); + + final response = jsonDecode(commitQuery.body.toString()); + Logging.instance.log("doesCommitExist $project $commit $response", + level: LogLevel.Info); + bool isThereCommit; + try { + isThereCommit = response['sha'] == commit; + Logging.instance + .log("isThereCommit $isThereCommit", level: LogLevel.Info); + return isThereCommit; + } catch (e, s) { + return false; + } + } catch (e, s) { + Logging.instance.log("$e $s", level: LogLevel.Error); + return false; + } +} + +Future<bool> isHeadCommit( + String organization, + String project, + String branch, + String commit, +) async { + Logging.instance.log("doesCommitExist", level: LogLevel.Info); + final Client client = Client(); + try { + final uri = Uri.parse( + "$kGithubAPI$kGithubHead/$organization/$project/commits/$branch"); + + final commitQuery = await client.get( + uri, + headers: {'Content-Type': 'application/json'}, + ); + + final response = jsonDecode(commitQuery.body.toString()); + Logging.instance.log("isHeadCommit $project $commit $branch $response", + level: LogLevel.Info); + bool isHead; + try { + isHead = response['sha'] == commit; + Logging.instance.log("isHead $isHead", level: LogLevel.Info); + return isHead; + } catch (e, s) { + return false; + } + } catch (e, s) { + Logging.instance.log("$e $s", level: LogLevel.Error); + return false; + } +} + +class DesktopAboutView extends ConsumerWidget { + const DesktopAboutView({Key? key}) : super(key: key); + + static const String routeName = "/desktopAboutView"; + + @override + Widget build(BuildContext context, WidgetRef ref) { + String firoCommit = FIRO_VERSIONS.getPluginVersion(); + String epicCashCommit = EPIC_VERSIONS.getPluginVersion(); + String moneroCommit = MONERO_VERSIONS.getPluginVersion(); + List<Future> futureFiroList = [ + doesCommitExist("cypherstack", "flutter_liblelantus", firoCommit), + isHeadCommit("cypherstack", "flutter_liblelantus", "main", firoCommit), + ]; + Future commitFiroFuture = Future.wait(futureFiroList); + List<Future> futureEpicList = [ + doesCommitExist("cypherstack", "flutter_libepiccash", epicCashCommit), + isHeadCommit( + "cypherstack", "flutter_libepiccash", "main", epicCashCommit), + ]; + Future commitEpicFuture = Future.wait(futureEpicList); + List<Future> futureMoneroList = [ + doesCommitExist("cypherstack", "flutter_libmonero", moneroCommit), + isHeadCommit("cypherstack", "flutter_libmonero", "main", moneroCommit), + ]; + Future commitMoneroFuture = Future.wait(futureMoneroList); + + debugPrint("BUILD: $runtimeType"); + return DesktopScaffold( + background: Theme.of(context).extension<StackColors>()!.background, + appBar: DesktopAppBar( + isCompactHeight: true, + leading: Row( + children: [ + const SizedBox( + width: 24, + height: 24, + ), + Text( + "About", + style: STextStyles.desktopH3(context), + ) + ], + ), + ), + body: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(24, 10, 24, 35), + child: RoundedWhiteContainer( + width: 929, + height: 411, + child: Padding( + padding: const EdgeInsets.only(left: 10, top: 10), + child: Column( + // mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + "Stack Wallet", + style: STextStyles.desktopH3(context), + textAlign: TextAlign.start, + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + RichText( + textAlign: TextAlign.start, + text: TextSpan( + style: STextStyles.label(context), + children: [ + TextSpan( + text: + "By using Stack Wallet, you agree to the ", + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + ), + TextSpan( + text: "Terms of service", + style: STextStyles.richLink(context) + .copyWith(fontSize: 14), + recognizer: TapGestureRecognizer() + ..onTap = () { + launchUrl( + Uri.parse( + "https://stackwallet.com/terms-of-service.html"), + mode: LaunchMode.externalApplication, + ); + }, + ), + TextSpan( + text: " and ", + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + ), + TextSpan( + text: "Privacy policy", + style: STextStyles.richLink(context) + .copyWith(fontSize: 14), + recognizer: TapGestureRecognizer() + ..onTap = () { + launchUrl( + Uri.parse( + "https://stackwallet.com/privacy-policy.html"), + mode: LaunchMode.externalApplication, + ); + }, + ), + ], + ), + ), + ], + ), + const SizedBox(height: 32), + Padding( + padding: const EdgeInsets.only(right: 10, bottom: 10), + child: Column( + children: [ + FutureBuilder( + future: PackageInfo.fromPlatform(), + builder: + (context, AsyncSnapshot<PackageInfo> snapshot) { + String version = ""; + String signature = ""; + String build = ""; + + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + version = snapshot.data!.version; + build = snapshot.data!.buildNumber; + signature = snapshot.data!.buildSignature; + } + + return Column( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Version", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textDark), + ), + const SizedBox( + height: 2, + ), + SelectableText( + version, + style: STextStyles.itemSubtitle( + context), + ), + ], + ), + const SizedBox( + width: 400, + ), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Build number", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textDark), + ), + const SizedBox( + height: 2, + ), + SelectableText( + build, + style: STextStyles.itemSubtitle( + context), + ), + ], + ), + ], + ), + const SizedBox(height: 32), + Row( + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Build signature", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textDark), + ), + const SizedBox( + height: 2, + ), + SelectableText( + signature, + style: STextStyles.itemSubtitle( + context), + ), + ], + ), + const SizedBox( + width: 350, + ), + FutureBuilder( + future: commitFiroFuture, + builder: (context, + AsyncSnapshot<dynamic> snapshot) { + bool commitExists = false; + bool isHead = false; + CommitStatus stateOfCommit = + CommitStatus.notLoaded; + + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + commitExists = + snapshot.data![0] as bool; + isHead = + snapshot.data![1] as bool; + if (commitExists && isHead) { + stateOfCommit = + CommitStatus.isHead; + } else if (commitExists) { + stateOfCommit = + CommitStatus.isOldCommit; + } else { + stateOfCommit = + CommitStatus.notACommit; + } + } + TextStyle indicationStyle = + STextStyles.itemSubtitle( + context); + switch (stateOfCommit) { + case CommitStatus.isHead: + indicationStyle = STextStyles + .itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .accentColorGreen); + break; + case CommitStatus.isOldCommit: + indicationStyle = STextStyles + .itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .accentColorYellow); + break; + case CommitStatus.notACommit: + indicationStyle = STextStyles + .itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .accentColorRed); + break; + default: + break; + } + return Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Firo Build Commit", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textDark), + ), + const SizedBox( + height: 2, + ), + SelectableText( + firoCommit, + style: indicationStyle, + ), + ], + ); + }), + ], + ), + const SizedBox(height: 35), + Row( + children: [ + FutureBuilder( + future: commitEpicFuture, + builder: (context, + AsyncSnapshot<dynamic> snapshot) { + bool commitExists = false; + bool isHead = false; + CommitStatus stateOfCommit = + CommitStatus.notLoaded; + + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + commitExists = + snapshot.data![0] as bool; + isHead = + snapshot.data![1] as bool; + if (commitExists && isHead) { + stateOfCommit = + CommitStatus.isHead; + } else if (commitExists) { + stateOfCommit = + CommitStatus.isOldCommit; + } else { + stateOfCommit = + CommitStatus.notACommit; + } + } + TextStyle indicationStyle = + STextStyles.itemSubtitle( + context); + switch (stateOfCommit) { + case CommitStatus.isHead: + indicationStyle = STextStyles + .itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .accentColorGreen); + break; + case CommitStatus.isOldCommit: + indicationStyle = STextStyles + .itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .accentColorYellow); + break; + case CommitStatus.notACommit: + indicationStyle = STextStyles + .itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .accentColorRed); + break; + default: + break; + } + return Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Epic Cash Build Commit", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textDark), + ), + const SizedBox( + height: 2, + ), + SelectableText( + epicCashCommit, + style: indicationStyle, + ), + ], + ); + }), + const SizedBox( + width: 105, + ), + FutureBuilder( + future: commitMoneroFuture, + builder: (context, + AsyncSnapshot<dynamic> snapshot) { + bool commitExists = false; + bool isHead = false; + CommitStatus stateOfCommit = + CommitStatus.notLoaded; + + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + commitExists = + snapshot.data![0] as bool; + isHead = + snapshot.data![1] as bool; + if (commitExists && isHead) { + stateOfCommit = + CommitStatus.isHead; + } else if (commitExists) { + stateOfCommit = + CommitStatus.isOldCommit; + } else { + stateOfCommit = + CommitStatus.notACommit; + } + } + TextStyle indicationStyle = + STextStyles.itemSubtitle( + context); + switch (stateOfCommit) { + case CommitStatus.isHead: + indicationStyle = STextStyles + .itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .accentColorGreen); + break; + case CommitStatus.isOldCommit: + indicationStyle = STextStyles + .itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .accentColorYellow); + break; + case CommitStatus.notACommit: + indicationStyle = STextStyles + .itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .accentColorRed); + break; + default: + break; + } + return Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Monero Build Commit", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textDark), + ), + const SizedBox( + height: 2, + ), + SelectableText( + moneroCommit, + style: indicationStyle, + ), + ], + ); + }), + ], + ), + const SizedBox(height: 35), + Row( + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Website", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textDark), + ), + const SizedBox( + height: 2, + ), + BlueTextButton( + text: "https://stackwallet.com", + onTap: () { + launchUrl( + Uri.parse( + "https://stackwallet.com"), + mode: LaunchMode + .externalApplication, + ); + }, + ), + ], + ) + ], + ) + ], + ); + }, + ) + ], + ), + ) + ], + ), + ), + ), + ), + ], + ), + ); + } +} From cb83515dbc774abc9c9e49fbd9f18f265ec699a1 Mon Sep 17 00:00:00 2001 From: sneurlax <sneurlax@gmail.com> Date: Mon, 7 Nov 2022 13:45:25 -0600 Subject: [PATCH 152/426] do not use wownero-seed (wow_seed) --- crypto_plugins/flutter_libmonero | 2 +- .../coins/wownero/wownero_wallet_test.dart | 107 ++++++++++++++++++ .../wownero/wownero_wallet_test_data.dart | 8 ++ 3 files changed, 116 insertions(+), 1 deletion(-) diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index d92fea3e6..371443607 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit d92fea3e6b915b79697deafbd5710fb4c15bfc76 +Subproject commit 371443607433a0ec509e2933ee21def29e9ad429 diff --git a/test/services/coins/wownero/wownero_wallet_test.dart b/test/services/coins/wownero/wownero_wallet_test.dart index 83b4844c7..945d8650f 100644 --- a/test/services/coins/wownero/wownero_wallet_test.dart +++ b/test/services/coins/wownero/wownero_wallet_test.dart @@ -83,8 +83,10 @@ void main() async { _walletInfoSource = await Hive.openBox<WalletInfo>(WalletInfo.boxName); walletService = wownero.createWowneroWalletService(_walletInfoSource); + /* group("Wownero 14 word tests", () { setUp(() async { + bool hasThrown = false; try { final dirPath = await pathForWalletDir(name: name, type: type); path = await pathForWallet(name: name, type: type); @@ -113,7 +115,9 @@ void main() async { } catch (e, s) { print(e); print(s); + hasThrown = true; } + expect(hasThrown, false); }); test("Test mainnet address generation from 14 word seed", () async { @@ -147,6 +151,109 @@ void main() async { walletBase?.close(); walletBase = wallet as WowneroWalletBase; }); + + // TODO delete left over wallet file with name: name + }); + */ + + group("Wownero 25 word tests", () { + setUp(() async { + bool hasThrown = false; + try { + final dirPath = await pathForWalletDir(name: name, type: type); + path = await pathForWallet(name: name, type: type); + credentials = wownero.createWowneroRestoreWalletFromSeedCredentials( + name: name, height: 465760, mnemonic: testMnemonic25); + + walletInfo = WalletInfo.external( + id: WalletBase.idFor(name, type), + name: name, + type: type, + isRecovery: false, + restoreHeight: credentials.height ?? 0, + date: DateTime.now(), + path: path, + address: "", + dirPath: dirPath); + credentials.walletInfo = walletInfo; + + _walletCreationService = WalletCreationService( + secureStorage: storage, + sharedPreferences: prefs, + walletService: walletService, + keyService: keysStorage, + ); + _walletCreationService.changeWalletType(); + } catch (e, s) { + print(e); + print(s); + hasThrown = true; + } + expect(hasThrown, false); + }); + + test("Test mainnet address generation from 25 word seed", () async { + bool hasThrown = false; + try { + name = 'namee${Random().nextInt(10000000)}'; + final dirPath = await pathForWalletDir(name: name, type: type); + path = await pathForWallet(name: name, type: type); + try { + credentials = wownero.createWowneroRestoreWalletFromSeedCredentials( + name: name, height: 465760, mnemonic: testMnemonic25); + } catch (e, s) { + print(e); + print(s); + hasThrown = true; + } + expect(hasThrown, false); + + walletInfo = WalletInfo.external( + id: WalletBase.idFor(name, type), + name: name, + type: type, + isRecovery: false, + restoreHeight: credentials.height ?? 0, + date: DateTime.now(), + path: path, + address: "", + dirPath: dirPath); + credentials.walletInfo = walletInfo; + + _walletCreationService = WalletCreationService( + secureStorage: storage, + sharedPreferences: prefs, + walletService: walletService, + keyService: keysStorage, + ); + _walletCreationService.changeWalletType(); + } catch (e, s) { + print(e); + print(s); + hasThrown = true; + } + expect(hasThrown, false); + + final wallet = await _walletCreationService.restoreFromSeed(credentials); + walletInfo.address = wallet.walletAddresses.address; + + hasThrown = false; + try { + await _walletInfoSource.add(walletInfo); + walletBase?.close(); + walletBase = wallet as WowneroWalletBase; + + expect(walletInfo.address, mainnetTestData25[0][0]); + } catch (_) { + hasThrown = true; + } + expect(hasThrown, false); + + walletBase?.close(); + walletBase = wallet as WowneroWalletBase; + }); + + // TODO delete left over wallet file with name: name }); } diff --git a/test/services/coins/wownero/wownero_wallet_test_data.dart b/test/services/coins/wownero/wownero_wallet_test_data.dart index 8ab825dfa..7f27fc486 100644 --- a/test/services/coins/wownero/wownero_wallet_test_data.dart +++ b/test/services/coins/wownero/wownero_wallet_test_data.dart @@ -12,3 +12,11 @@ var mainnetTestData14 = [ 'WW2KQLLt6gjC9gRsC4NGehbAZX6UPU7sK89UQFwSg3NKj3MXPwnjh5BiJVqYYNQb6JNsfa7oP7eDjLagtLa2H6YP11RhUNQqw' ] ]; + +String testMnemonic25 = + 'myth byline benches sadness nylon tamper guide giving match angled lurk rally makeup alarms river soapy dolphin woven ticket maul examine public luggage mammal alarms'; +var mainnetTestData25 = [ + [ + 'Wo3piMnt1ztjLktFJNsfs9ce6N1tyHk7DB93cNqTGPJ7To3RS7W2q5DdxgQAG5E6RQXQhchQD7ip8WWL3fD8Ww5K2XmAXYxta' + ] +]; From 6e5a0bad784831ba7c98db99b3291f6ab305ac48 Mon Sep 17 00:00:00 2001 From: sneurlax <sneurlax@gmail.com> Date: Mon, 7 Nov 2022 14:42:52 -0600 Subject: [PATCH 153/426] do not use wownero-seed (wow_seed) function for height, hardcoded POC --- crypto_plugins/flutter_libmonero | 2 +- lib/services/coins/wownero/wownero_wallet.dart | 7 ++++--- .../services/coins/wownero/wownero_wallet_test.dart | 13 ++----------- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index 371443607..da826f313 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit 371443607433a0ec509e2933ee21def29e9ad429 +Subproject commit da826f31352c695942bc9b821d1d0c82a9267ade diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart index d3aba5bbb..a069414b6 100644 --- a/lib/services/coins/wownero/wownero_wallet.dart +++ b/lib/services/coins/wownero/wownero_wallet.dart @@ -713,8 +713,8 @@ class WowneroWallet extends CoinServiceAPI { final wallet = await _walletCreationService?.create(credentials); // subtract a couple days to ensure we have a buffer for SWB - final bufferedCreateHeight = - getSeedHeightSync(wallet?.seed.trim() as String); + final bufferedCreateHeight = 0; + //final bufferedCreateHeight = getSeedHeightSync(wallet?.seed.trim() as String); // TODO use an alternative to wow_seed's get_seed_height await DB.instance.put<dynamic>( boxName: walletId, key: "restoreHeight", value: bufferedCreateHeight); @@ -969,7 +969,8 @@ class WowneroWallet extends CoinServiceAPI { await _secureStore.write( key: '${_walletId}_mnemonic', value: mnemonic.trim()); - height = getSeedHeightSync(mnemonic.trim()); + height = 0; + //height = getSeedHeightSync(mnemonic.trim()); // TODO use an alternative to wow_seed's get_seed_height await DB.instance .put<dynamic>(boxName: walletId, key: "restoreHeight", value: height); diff --git a/test/services/coins/wownero/wownero_wallet_test.dart b/test/services/coins/wownero/wownero_wallet_test.dart index 945d8650f..ca7540da5 100644 --- a/test/services/coins/wownero/wownero_wallet_test.dart +++ b/test/services/coins/wownero/wownero_wallet_test.dart @@ -91,7 +91,7 @@ void main() async { final dirPath = await pathForWalletDir(name: name, type: type); path = await pathForWallet(name: name, type: type); credentials = wownero.createWowneroRestoreWalletFromSeedCredentials( - name: name, height: 465760, mnemonic: testMnemonic14); + name: name, height: 465760, mnemonic: testMnemonic14); // TODO catch failure walletInfo = WalletInfo.external( id: WalletBase.idFor(name, type), @@ -163,7 +163,7 @@ void main() async { final dirPath = await pathForWalletDir(name: name, type: type); path = await pathForWallet(name: name, type: type); credentials = wownero.createWowneroRestoreWalletFromSeedCredentials( - name: name, height: 465760, mnemonic: testMnemonic25); + name: name, height: 465760, mnemonic: testMnemonic25); // TODO catch failure walletInfo = WalletInfo.external( id: WalletBase.idFor(name, type), @@ -198,15 +198,6 @@ void main() async { name = 'namee${Random().nextInt(10000000)}'; final dirPath = await pathForWalletDir(name: name, type: type); path = await pathForWallet(name: name, type: type); - try { - credentials = wownero.createWowneroRestoreWalletFromSeedCredentials( - name: name, height: 465760, mnemonic: testMnemonic25); - } catch (e, s) { - print(e); - print(s); - hasThrown = true; - } - expect(hasThrown, false); walletInfo = WalletInfo.external( id: WalletBase.idFor(name, type), From b41c4c37bd6c5756fe8be0f4011da685d1265bd8 Mon Sep 17 00:00:00 2001 From: sneurlax <sneurlax@gmail.com> Date: Mon, 7 Nov 2022 14:46:48 -0600 Subject: [PATCH 154/426] delineate divergence point more clearly --- lib/services/coins/wownero/wownero_wallet.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart index a069414b6..1153a881a 100644 --- a/lib/services/coins/wownero/wownero_wallet.dart +++ b/lib/services/coins/wownero/wownero_wallet.dart @@ -713,8 +713,12 @@ class WowneroWallet extends CoinServiceAPI { final wallet = await _walletCreationService?.create(credentials); // subtract a couple days to ensure we have a buffer for SWB + // 14 words + //final bufferedCreateHeight = getSeedHeightSync(wallet?.seed.trim() as String); + + // 25 words final bufferedCreateHeight = 0; - //final bufferedCreateHeight = getSeedHeightSync(wallet?.seed.trim() as String); // TODO use an alternative to wow_seed's get_seed_height + // TODO use an alternative to wow_seed's get_seed_height await DB.instance.put<dynamic>( boxName: walletId, key: "restoreHeight", value: bufferedCreateHeight); From c962f597fdc9c696edbbdac655172ab9f5e217e7 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 7 Nov 2022 14:46:36 -0600 Subject: [PATCH 155/426] added extra checks to BCH as well as test cases --- .../coins/bitcoincash/bitcoincash_wallet.dart | 58 ++++--- .../bitcoincash/bitcoincash_wallet_test.dart | 164 +++++++++++++++++- 2 files changed, 199 insertions(+), 23 deletions(-) diff --git a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart index 98a31ee0c..5b3b54663 100644 --- a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart +++ b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart @@ -208,9 +208,9 @@ class BitcoinCashWallet extends CoinServiceAPI { _getCurrentAddressForChain(0, DerivePathType.bip44); Future<String>? _currentReceivingAddressP2PKH; - Future<String> get currentReceivingAddressP2SH => - _currentReceivingAddressP2SH ??= - _getCurrentAddressForChain(0, DerivePathType.bip49); + // Future<String> get currentReceivingAddressP2SH => + // _currentReceivingAddressP2SH ??= + // _getCurrentAddressForChain(0, DerivePathType.bip49); Future<String>? _currentReceivingAddressP2SH; @override @@ -269,7 +269,11 @@ class BitcoinCashWallet extends CoinServiceAPI { try { if (bitbox.Address.detectFormat(address) == bitbox.Address.formatCashAddr) { - address = bitbox.Address.toLegacyAddress(address); + if (validateCashAddr(address)) { + address = bitbox.Address.toLegacyAddress(address); + } else { + throw ArgumentError('$address is not currently supported'); + } } } catch (e, s) {} try { @@ -294,11 +298,14 @@ class BitcoinCashWallet extends CoinServiceAPI { } catch (err) { // Bech32 decode fail } - if (_network.bech32 != decodeBech32!.hrp) { - throw ArgumentError('Invalid prefix or Network mismatch'); - } - if (decodeBech32.version != 0) { - throw ArgumentError('Invalid address version'); + + if (decodeBech32 != null) { + if (_network.bech32 != decodeBech32.hrp) { + throw ArgumentError('Invalid prefix or Network mismatch'); + } + if (decodeBech32.version != 0) { + throw ArgumentError('Invalid address version'); + } } } throw ArgumentError('$address has no matching Script'); @@ -1203,6 +1210,15 @@ class BitcoinCashWallet extends CoinServiceAPI { _transactionData = Future(() => cachedTxData!); } + bool validateCashAddr(String cashAddr) { + String addr = cashAddr; + if (cashAddr.contains(":")) { + addr = cashAddr.split(":").last; + } + + return addr.startsWith("q"); + } + @override bool validateAddress(String address) { try { @@ -1217,12 +1233,7 @@ class BitcoinCashWallet extends CoinServiceAPI { } if (format == bitbox.Address.formatCashAddr) { - String addr = address; - if (address.contains(":")) { - addr = address.split(":").last; - } - - return addr.startsWith("q"); + return validateCashAddr(address); } else { return address.startsWith("1"); } @@ -2085,7 +2096,8 @@ class BitcoinCashWallet extends CoinServiceAPI { String _convertToScriptHash(String bchAddress, NetworkType network) { try { if (bitbox.Address.detectFormat(bchAddress) == - bitbox.Address.formatCashAddr) { + bitbox.Address.formatCashAddr && + validateCashAddr(bchAddress)) { bchAddress = bitbox.Address.toLegacyAddress(bchAddress); } final output = Address.addressToOutputScript(bchAddress, network); @@ -2163,7 +2175,8 @@ class BitcoinCashWallet extends CoinServiceAPI { List<String> allAddressesOld = await _fetchAllOwnAddresses(); List<String> allAddresses = []; for (String address in allAddressesOld) { - if (bitbox.Address.detectFormat(address) == bitbox.Address.formatLegacy) { + if (bitbox.Address.detectFormat(address) == bitbox.Address.formatLegacy && + addressType(address: address) == DerivePathType.bip44) { allAddresses.add(bitbox.Address.toCashAddress(address)); } else { allAddresses.add(address); @@ -2882,7 +2895,12 @@ class BitcoinCashWallet extends CoinServiceAPI { String address = output["scriptPubKey"]["addresses"][0] as String; if (bitbox.Address.detectFormat(address) == bitbox.Address.formatCashAddr) { - address = bitbox.Address.toLegacyAddress(address); + if (validateCashAddr(address)) { + address = bitbox.Address.toLegacyAddress(address); + } else { + throw Exception( + "Unsupported address found during fetchBuildTxData(): $address"); + } } if (!addressTxid.containsKey(address)) { addressTxid[address] = <String>[]; @@ -2913,10 +2931,6 @@ class BitcoinCashWallet extends CoinServiceAPI { ); for (int i = 0; i < p2pkhLength; i++) { String address = addressesP2PKH[i]; - if (bitbox.Address.detectFormat(address) == - bitbox.Address.formatCashAddr) { - address = bitbox.Address.toLegacyAddress(address); - } // receives final receiveDerivation = receiveDerivations[address]; diff --git a/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart b/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart index 50ff8f741..077163809 100644 --- a/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart +++ b/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart @@ -60,7 +60,7 @@ void main() { }); }); - group("validate mainnet bitcoincash addresses", () { + group("mainnet bitcoincash addressType", () { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; @@ -136,6 +136,168 @@ void main() { verifyNoMoreInteractions(priceAPI); }); + test("P2PKH cashaddr with prefix", () { + expect( + mainnetWallet?.addressType( + address: + "bitcoincash:qrwjyc4pewj9utzrtnh0whkzkuvy5q8wg52n254x6k"), + DerivePathType.bip44); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("P2PKH cashaddr without prefix", () { + expect( + mainnetWallet?.addressType( + address: "qrwjyc4pewj9utzrtnh0whkzkuvy5q8wg52n254x6k"), + DerivePathType.bip44); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("Multisig cashaddr with prefix", () { + expect( + () => mainnetWallet?.addressType( + address: + "bitcoincash:pzpp3nchmzzf0gr69lj82ymurg5u3ds6kcwr5m07np"), + throwsArgumentError); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("Multisig cashaddr without prefix", () { + expect( + () => mainnetWallet?.addressType( + address: "pzpp3nchmzzf0gr69lj82ymurg5u3ds6kcwr5m07np"), + throwsArgumentError); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("Multisig/P2SH address", () { + expect( + mainnetWallet?.addressType( + address: "3DYuVEmuKWQFxJcF7jDPhwPiXLTiNnyMFb"), + DerivePathType.bip49); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + }); + + group("validate mainnet bitcoincash addresses", () { + MockElectrumX? client; + MockCachedElectrumX? cachedClient; + MockPriceAPI? priceAPI; + FakeSecureStorage? secureStore; + MockTransactionNotificationTracker? tracker; + + BitcoinCashWallet? mainnetWallet; + + setUp(() { + client = MockElectrumX(); + cachedClient = MockCachedElectrumX(); + priceAPI = MockPriceAPI(); + secureStore = FakeSecureStorage(); + tracker = MockTransactionNotificationTracker(); + + mainnetWallet = BitcoinCashWallet( + walletId: "validateAddressMainNet", + walletName: "validateAddressMainNet", + coin: Coin.bitcoincash, + client: client!, + cachedClient: cachedClient!, + tracker: tracker!, + priceAPI: priceAPI, + secureStore: secureStore, + ); + }); + + test("valid mainnet legacy/p2pkh address type", () { + expect( + mainnetWallet?.validateAddress("1DP3PUePwMa5CoZwzjznVKhzdLsZftjcAT"), + true); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("valid mainnet legacy/p2pkh cashaddr with prefix address type", () { + expect( + mainnetWallet?.validateAddress( + "bitcoincash:qrwjyc4pewj9utzrtnh0whkzkuvy5q8wg52n254x6k"), + true); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("valid mainnet legacy/p2pkh cashaddr without prefix address type", () { + expect( + mainnetWallet + ?.validateAddress("qrwjyc4pewj9utzrtnh0whkzkuvy5q8wg52n254x6k"), + true); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("invalid legacy/p2pkh address type", () { + expect( + mainnetWallet?.validateAddress("mhqpGtwhcR6gFuuRjLTpHo41919QfuGy8Y"), + false); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test( + "invalid cashaddr (is valid multisig but bitbox is broken for multisig)", + () { + expect( + mainnetWallet + ?.validateAddress("pzpp3nchmzzf0gr69lj82ymurg5u3ds6kcwr5m07np"), + false); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + + test("multisig address should fail for bitbox", () { + expect( + mainnetWallet?.validateAddress("3DYuVEmuKWQFxJcF7jDPhwPiXLTiNnyMFb"), + false); + expect(secureStore?.interactions, 0); + verifyNoMoreInteractions(client); + verifyNoMoreInteractions(cachedClient); + verifyNoMoreInteractions(tracker); + verifyNoMoreInteractions(priceAPI); + }); + test("invalid mainnet bitcoincash legacy/p2pkh address", () { expect( mainnetWallet?.validateAddress("mhqpGtwhcR6gFuuRjLTpHo41919QfuGy8Y"), From b3a343d28a29c6cc60e3f71d6d851b2aff27ebcc Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 7 Nov 2022 15:48:16 -0600 Subject: [PATCH 156/426] desktop theme toggle --- .../settings_menu/appearance_settings.dart | 391 ++++++++++-------- 1 file changed, 218 insertions(+), 173 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart b/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart index b5f239ab1..bf6fc81e2 100644 --- a/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart @@ -1,16 +1,19 @@ import 'package:flutter/material.dart'; -import 'package:flutter/src/widgets/framework.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/providers/global/prefs_provider.dart'; +import 'package:stackwallet/providers/ui/color_theme_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/color_theme.dart'; +import 'package:stackwallet/utilities/theme/dark_colors.dart'; +import 'package:stackwallet/utilities/theme/light_colors.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; -import '../../../providers/global/prefs_provider.dart'; -import '../../../utilities/constants.dart'; -import '../../../widgets/custom_buttons/draggable_switch_button.dart'; - class AppearanceOptionSettings extends ConsumerStatefulWidget { const AppearanceOptionSettings({Key? key}) : super(key: key); @@ -140,7 +143,10 @@ class _AppearanceOptionSettings ], ), ), - ThemeToggle(), + const Padding( + padding: EdgeInsets.all(10), + child: ThemeToggle(), + ), ], ), ), @@ -150,7 +156,7 @@ class _AppearanceOptionSettings } } -class ThemeToggle extends StatefulWidget { +class ThemeToggle extends ConsumerStatefulWidget { const ThemeToggle({ Key? key, }) : super(key: key); @@ -159,187 +165,226 @@ class ThemeToggle extends StatefulWidget { // final void Function(bool)? onChanged; @override - State<StatefulWidget> createState() => _ThemeToggle(); + ConsumerState<ThemeToggle> createState() => _ThemeToggle(); } -class _ThemeToggle extends State<ThemeToggle> { +class _ThemeToggle extends ConsumerState<ThemeToggle> { // late bool externalCallsEnabled; + late String _selectedTheme; + + @override + void initState() { + _selectedTheme = + DB.instance.get<dynamic>(boxName: DB.boxNameTheme, key: "colorScheme") + as String? ?? + "light"; + + super.initState(); + } + @override Widget build(BuildContext context) { return Row( - // mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: RawMaterialButton( - elevation: 0, - hoverColor: Colors.transparent, - shape: RoundedRectangleBorder( - side: BorderSide( - color: - Theme.of(context).extension<StackColors>()!.infoItemIcons, - width: 2, - ), - // side: !externalCallsEnabled - // ? BorderSide.none - // : BorderSide( - // color: Theme.of(context) - // .extension<StackColors>()! - // .infoItemIcons, - // width: 2, - // ), - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius * 2, - ), - ), - onPressed: () {}, //onPressed - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Stack( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only( - left: 24, - ), - child: SvgPicture.asset( - Assets.svg.themeLight, - ), - ), - Padding( - padding: const EdgeInsets.only( - left: 50, - top: 12, - ), - child: Text( - "Light", - style: STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ), - ), - ) - ], - ), - // if (externalCallsEnabled) - Positioned( - bottom: 0, - left: 6, - child: SvgPicture.asset( - Assets.svg.checkCircle, - width: 20, - height: 20, - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - ), - ), - // if (!externalCallsEnabled) - // Positioned( - // bottom: 0, - // left: 6, - // child: Container( - // width: 20, - // height: 20, - // decoration: BoxDecoration( - // borderRadius: BorderRadius.circular(1000), - // color: Theme.of(context) - // .extension<StackColors>()! - // .textFieldDefaultBG, - // ), - // ), - // ), - ], - ), + MaterialButton( + splashColor: Colors.transparent, + hoverColor: Colors.transparent, + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), ), - ), - const SizedBox( - width: 1, - ), - Expanded( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: RawMaterialButton( - elevation: 0, - hoverColor: Colors.transparent, - shape: RoundedRectangleBorder( - // side: !externalCallsEnabled - // ? BorderSide.none - // : BorderSide( - // color: Theme.of(context) - // .extension<StackColors>()! - // .infoItemIcons, - // width: 2, - // ), - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius * 2, - ), - ), - onPressed: () {}, //onPressed - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Stack( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SvgPicture.asset( - Assets.svg.themeDark, - ), - Padding( - padding: const EdgeInsets.only( - left: 45, - top: 12, - ), - child: Text( - "Dark", - style: STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ), - ), - ), - ], - ), - // if (externalCallsEnabled) - // Positioned( - // bottom: 0, - // left: 0, - // child: SvgPicture.asset( - // Assets.svg.checkCircle, - // width: 20, - // height: 20, - // color: Theme.of(context) - // .extension<StackColors>()! - // .infoItemIcons, - // ), - // ), - // if (!externalCallsEnabled) - Positioned( - bottom: 0, - left: 0, - child: Container( - width: 20, - height: 20, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(1000), - color: Theme.of(context) + onPressed: () { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.light.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + LightColors(), + ); + + setState(() { + _selectedTheme = "light"; + }); + }, + child: SizedBox( + width: 200, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + border: Border.all( + width: 2.5, + color: _selectedTheme == "light" + ? Theme.of(context) .extension<StackColors>()! - .textFieldDefaultBG, - ), + .infoItemIcons + : Theme.of(context).extension<StackColors>()!.popupBG, + ), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: SvgPicture.asset( + Assets.svg.themeLight, + ), + ), + const SizedBox( + height: 12, + ), + Row( + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: "light", + groupValue: _selectedTheme, + onChanged: (newValue) { + if (newValue is String && newValue == "light") { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.light.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + LightColors(), + ); + + setState(() { + _selectedTheme = "light"; + }); + } + }, + ), + ), + const SizedBox( + width: 14, + ), + Text( + "Light", + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, ), ), ], ), - ), + ], + ), + ), + ), + const SizedBox( + width: 20, + ), + MaterialButton( + splashColor: Colors.transparent, + hoverColor: Colors.transparent, + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.dark.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + DarkColors(), + ); + + setState(() { + _selectedTheme = "dark"; + }); + }, + child: SizedBox( + width: 200, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + border: Border.all( + width: 2.5, + color: _selectedTheme == "dark" + ? Theme.of(context) + .extension<StackColors>()! + .infoItemIcons + : Theme.of(context).extension<StackColors>()!.popupBG, + ), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: SvgPicture.asset( + Assets.svg.themeDark, + ), + ), + const SizedBox( + height: 12, + ), + Row( + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: "dark", + groupValue: _selectedTheme, + onChanged: (newValue) { + if (newValue is String && newValue == "dark") { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.dark.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + DarkColors(), + ); + + setState(() { + _selectedTheme = "dark"; + }); + } + }, + ), + ), + const SizedBox( + width: 14, + ), + Text( + "Dark", + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + ], + ), + ], ), ), ), From 3c627a5ddb36e93c229f9e96a9829df1664d700d Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 7 Nov 2022 15:57:04 -0600 Subject: [PATCH 157/426] support tweak --- .../desktop_support_view.dart | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart b/lib/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart index 8e9d709d1..ce3e3f3cc 100644 --- a/lib/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart +++ b/lib/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart @@ -1,12 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/support_view.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; -import '../../../pages/settings_views/global_settings_view/support_view.dart'; - class DesktopSupportView extends ConsumerStatefulWidget { const DesktopSupportView({Key? key}) : super(key: key); @@ -38,10 +37,18 @@ class _DesktopSupportView extends ConsumerState<DesktopSupportView> { ), ), body: Column( - children: const [ + crossAxisAlignment: CrossAxisAlignment.start, + children: [ Padding( - padding: EdgeInsets.fromLTRB(24, 10, 377, 270), - child: SupportView(), + padding: const EdgeInsets.fromLTRB(24, 10, 0, 0), + child: Row( + children: const [ + SizedBox( + width: 576, + child: SupportView(), + ), + ], + ), ), ], ), From 2f6b1278fe59971855e9772de7e60961abf3a29e Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 7 Nov 2022 16:30:17 -0600 Subject: [PATCH 158/426] swb desktop layout tweaks --- .../create_backup_view.dart | 4 +- .../restore_from_file_view.dart | 4 +- .../backup_and_restore_settings.dart | 192 ++++++++++-------- .../enable_backup_dialog.dart | 3 +- 4 files changed, 115 insertions(+), 88 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart index 2d2ed4960..30fcb7962 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart @@ -157,7 +157,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { .textDark3), ), ), - // child, + child, const SizedBox(height: 20), Row( children: [ @@ -442,7 +442,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { const SizedBox( height: 16, ), - const Spacer(), + if (!isDesktop) const Spacer(), TextButton( style: shouldEnableCreate ? Theme.of(context) 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 9f2796415..0c101d0b3 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 @@ -142,7 +142,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { textAlign: TextAlign.left, ), ), - // child, + child, const SizedBox(height: 20), Row( children: [ @@ -285,7 +285,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { const SizedBox( height: 16, ), - const Spacer(), + if (!isDesktop) const Spacer(), TextButton( style: passwordController.text.isEmpty || fileLocationController.text.isEmpty diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart index b59206f17..49debf22f 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart @@ -64,48 +64,56 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { height: 48, ), Center( - child: Padding( - padding: const EdgeInsets.all(10), - child: RichText( - textAlign: TextAlign.start, - text: TextSpan( - children: [ - TextSpan( - text: "Auto Backup", - style: - STextStyles.desktopTextSmall(context), + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.start, + text: TextSpan( + children: [ + TextSpan( + text: "Auto Backup", + style: STextStyles.desktopTextSmall( + context), + ), + TextSpan( + text: + "\n\nAuto backup is a custom Stack Wallet feature that offers a convenient backup of your data." + "To ensure maximum security, we recommend using a unique password that you haven't used anywhere " + "else on the internet before. Your password is not stored.", + style: STextStyles + .desktopTextExtraExtraSmall( + context), + ), + TextSpan( + text: + "\n\nFor more information, please see our website ", + style: STextStyles + .desktopTextExtraExtraSmall( + context), + ), + TextSpan( + text: "stackwallet.com", + style: STextStyles.richLink(context) + .copyWith(fontSize: 14), + recognizer: TapGestureRecognizer() + ..onTap = () { + launchUrl( + Uri.parse( + "https://stackwallet.com/"), + mode: LaunchMode + .externalApplication, + ); + }, + ), + ], + ), ), - TextSpan( - text: - "\n\nAuto backup is a custom Stack Wallet feature that offers a convenient backup of your data." - "To ensure maximum security, we recommend using a unique password that you haven't used anywhere " - "else on the internet before. Your password is not stored.", - style: STextStyles - .desktopTextExtraExtraSmall(context), - ), - TextSpan( - text: - "\n\nFor more information, please see our website ", - style: STextStyles - .desktopTextExtraExtraSmall(context), - ), - TextSpan( - text: "stackwallet.com", - style: STextStyles.richLink(context) - .copyWith(fontSize: 14), - recognizer: TapGestureRecognizer() - ..onTap = () { - launchUrl( - Uri.parse( - "https://stackwallet.com/"), - mode: - LaunchMode.externalApplication, - ); - }, - ), - ], + ), ), - ), + ], ), ), Column( @@ -148,39 +156,49 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { alignment: Alignment.topLeft, ), Center( - child: Padding( - padding: const EdgeInsets.all(10), - child: RichText( - textAlign: TextAlign.start, - text: TextSpan( - children: [ - TextSpan( - text: "Manual Backup", - style: - STextStyles.desktopTextSmall(context), + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.start, + text: TextSpan( + children: [ + TextSpan( + text: "Manual Backup", + style: STextStyles.desktopTextSmall( + context), + ), + TextSpan( + text: + "\n\nCreate manual backup to easily transfer your data between devices. " + "You will create a backup file that can be later used in the Restore option. " + "Use a strong password to encrypt your data.", + style: STextStyles + .desktopTextExtraExtraSmall( + context), + ), + ], + ), ), - TextSpan( - text: - "\n\nCreate manual backup to easily transfer your data between devices. " - "You will create a backup file that can be later used in the Restore option. " - "Use a strong password to encrypt your data.", - style: STextStyles - .desktopTextExtraExtraSmall(context), - ), - ], + ), ), - ), + ], ), ), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: EdgeInsets.all( + padding: const EdgeInsets.all( 10, ), child: createBackup - ? const CreateBackupView() + ? const SizedBox( + width: 512, + child: CreateBackupView(), + ) : PrimaryButton( desktopMed: true, width: 200, @@ -217,27 +235,34 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { alignment: Alignment.topLeft, ), Center( - child: Padding( - padding: const EdgeInsets.all(10), - child: RichText( - textAlign: TextAlign.start, - text: TextSpan( - children: [ - TextSpan( - text: "Restore Backup", - style: - STextStyles.desktopTextSmall(context), + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.start, + text: TextSpan( + children: [ + TextSpan( + text: "Restore Backup", + style: STextStyles.desktopTextSmall( + context), + ), + TextSpan( + text: + "\n\nUse your Stack Wallet backup file to restore your wallets, address book " + "and wallet preferences.", + style: STextStyles + .desktopTextExtraExtraSmall( + context), + ), + ], + ), ), - TextSpan( - text: - "\n\nUse your Stack Wallet backup file to restore your wallets, address book " - "and wallet preferences.", - style: STextStyles - .desktopTextExtraExtraSmall(context), - ), - ], + ), ), - ), + ], ), ), Column( @@ -248,7 +273,10 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { 10, ), child: restoreBackup - ? RestoreFromFileView() + ? const SizedBox( + width: 512, + child: RestoreFromFileView(), + ) : PrimaryButton( desktopMed: true, width: 200, diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart index 046d136a8..963fb4441 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart @@ -61,8 +61,7 @@ class EnableBackupDialog extends StatelessWidget { child: SecondaryButton( label: "Cancel", onPressed: () { - int count = 0; - Navigator.of(context).popUntil((_) => count++ >= 2); + Navigator.of(context).pop(); }, ), ), From 77aa3bc8e484534f893c3ee5f04820bca50918ad Mon Sep 17 00:00:00 2001 From: sneurlax <sneurlax@gmail.com> Date: Mon, 7 Nov 2022 16:45:26 -0600 Subject: [PATCH 159/426] use wownero-seed for 14 word seed, use wownero wallet2 for 25 word seed and update tests showing examples of both. TODO proper validation, must eg calculate and check checksums etc --- crypto_plugins/flutter_libmonero | 2 +- .../coins/wownero/wownero_wallet_test.dart | 40 ++++++++++--------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index da826f313..e95c19662 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit da826f31352c695942bc9b821d1d0c82a9267ade +Subproject commit e95c19662ccf17d83109ab7b651cfbc0521deb47 diff --git a/test/services/coins/wownero/wownero_wallet_test.dart b/test/services/coins/wownero/wownero_wallet_test.dart index ca7540da5..2099df364 100644 --- a/test/services/coins/wownero/wownero_wallet_test.dart +++ b/test/services/coins/wownero/wownero_wallet_test.dart @@ -83,7 +83,6 @@ void main() async { _walletInfoSource = await Hive.openBox<WalletInfo>(WalletInfo.boxName); walletService = wownero.createWowneroWalletService(_walletInfoSource); - /* group("Wownero 14 word tests", () { setUp(() async { bool hasThrown = false; @@ -91,7 +90,9 @@ void main() async { final dirPath = await pathForWalletDir(name: name, type: type); path = await pathForWallet(name: name, type: type); credentials = wownero.createWowneroRestoreWalletFromSeedCredentials( - name: name, height: 465760, mnemonic: testMnemonic14); // TODO catch failure + name: name, + height: 465760, + mnemonic: testMnemonic14); // TODO catch failure walletInfo = WalletInfo.external( id: WalletBase.idFor(name, type), @@ -128,21 +129,21 @@ void main() async { try { await _walletInfoSource.add(walletInfo); walletBase?.close(); - walletBase = wallet as WowneroWalletBase; + walletBase = wallet as WowneroWalletBase; expect(walletInfo.address, mainnetTestData14[0][0]); - expect( - await walletBase!.getTransactionAddress(0, 0), mainnetTestData14[0][0]); - expect( - await walletBase!.getTransactionAddress(0, 1), mainnetTestData14[0][1]); - expect( - await walletBase!.getTransactionAddress(0, 2), mainnetTestData14[0][2]); - expect( - await walletBase!.getTransactionAddress(1, 0), mainnetTestData14[1][0]); - expect( - await walletBase!.getTransactionAddress(1, 1), mainnetTestData14[1][1]); - expect( - await walletBase!.getTransactionAddress(1, 2), mainnetTestData14[1][2]); + expect(await walletBase!.getTransactionAddress(0, 0), + mainnetTestData14[0][0]); + expect(await walletBase!.getTransactionAddress(0, 1), + mainnetTestData14[0][1]); + expect(await walletBase!.getTransactionAddress(0, 2), + mainnetTestData14[0][2]); + expect(await walletBase!.getTransactionAddress(1, 0), + mainnetTestData14[1][0]); + expect(await walletBase!.getTransactionAddress(1, 1), + mainnetTestData14[1][1]); + expect(await walletBase!.getTransactionAddress(1, 2), + mainnetTestData14[1][2]); } catch (_) { hasThrown = true; } @@ -151,19 +152,21 @@ void main() async { walletBase?.close(); walletBase = wallet as WowneroWalletBase; }); - + // TODO delete left over wallet file with name: name }); - */ group("Wownero 25 word tests", () { setUp(() async { bool hasThrown = false; try { + name = 'namee${Random().nextInt(10000000)}'; final dirPath = await pathForWalletDir(name: name, type: type); path = await pathForWallet(name: name, type: type); credentials = wownero.createWowneroRestoreWalletFromSeedCredentials( - name: name, height: 465760, mnemonic: testMnemonic25); // TODO catch failure + name: name, + height: 465760, + mnemonic: testMnemonic25); // TODO catch failure walletInfo = WalletInfo.external( id: WalletBase.idFor(name, type), @@ -195,7 +198,6 @@ void main() async { test("Test mainnet address generation from 25 word seed", () async { bool hasThrown = false; try { - name = 'namee${Random().nextInt(10000000)}'; final dirPath = await pathForWalletDir(name: name, type: type); path = await pathForWallet(name: name, type: type); From fa0c982274159d3d7be179ad48a88cf50faef62a Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 8 Nov 2022 07:35:28 -0600 Subject: [PATCH 160/426] Return what we internally consider the "txid" for epic transactions from the epic confirmSend to be consistent with all other coins confirmSend return value. This should fix the epic notes issue. --- .../coins/epiccash/epiccash_wallet.dart | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/services/coins/epiccash/epiccash_wallet.dart b/lib/services/coins/epiccash/epiccash_wallet.dart index 3702a9158..6c71f39e4 100644 --- a/lib/services/coins/epiccash/epiccash_wallet.dart +++ b/lib/services/coins/epiccash/epiccash_wallet.dart @@ -833,10 +833,16 @@ class EpicCashWallet extends CoinServiceAPI { final txLogEntryFirst = txLogEntry[0]; Logger.print("TX_LOG_ENTRY_IS $txLogEntryFirst"); final wallet = await Hive.openBox<dynamic>(_walletId); - final slateToAddresses = (await wallet.get("slate_to_address")) as Map?; - slateToAddresses?[txLogEntryFirst['tx_slate_id']] = txData['addresss']; + final slateToAddresses = + (await wallet.get("slate_to_address")) as Map? ?? {}; + final slateId = txLogEntryFirst['tx_slate_id'] as String; + slateToAddresses[slateId] = txData['addresss']; await wallet.put('slate_to_address', slateToAddresses); - return txLogEntryFirst['tx_slate_id'] as String; + final slatesToCommits = await getSlatesToCommits(); + String? commitId = slatesToCommits[slateId]?['commitId'] as String?; + Logging.instance.log("sent commitId: $commitId", level: LogLevel.Info); + return commitId!; + // return txLogEntryFirst['tx_slate_id'] as String; } } catch (e, s) { Logging.instance.log("Error sending $e - $s", level: LogLevel.Error); @@ -2155,8 +2161,9 @@ class EpicCashWallet extends CoinServiceAPI { as String? ?? ""; String? commitId = slatesToCommits[slateId]?['commitId'] as String?; - Logging.instance - .log("commitId: $commitId $slateId", level: LogLevel.Info); + Logging.instance.log( + "commitId: $commitId, slateId: $slateId, id: ${tx["id"]}", + level: LogLevel.Info); bool isCancelled = tx["tx_type"] == "TxSentCancelled" || tx["tx_type"] == "TxReceivedCancelled"; From 7c3d40782cbd024911e4626f6a0aa15d85290b79 Mon Sep 17 00:00:00 2001 From: sneurlax <sneurlax@gmail.com> Date: Tue, 8 Nov 2022 09:55:15 -0600 Subject: [PATCH 161/426] add generation tests and update flutter_libmonero ref change seedWords to SeedWordsLength to match rest of codebase --- crypto_plugins/flutter_libmonero | 2 +- .../coins/wownero/wownero_wallet.dart | 20 +- .../coins/wownero/wownero_wallet_test.dart | 172 ++++++++++++++---- 3 files changed, 147 insertions(+), 47 deletions(-) diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index e95c19662..afdee4b88 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit e95c19662ccf17d83109ab7b651cfbc0521deb47 +Subproject commit afdee4b880202f39a2375afc320f0642e98a1827 diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart index 1153a881a..a0dc7bfe0 100644 --- a/lib/services/coins/wownero/wownero_wallet.dart +++ b/lib/services/coins/wownero/wownero_wallet.dart @@ -647,7 +647,7 @@ class WowneroWallet extends CoinServiceAPI { } //TODO: take in the default language when creating wallet. - Future<void> _generateNewWallet() async { + Future<void> _generateNewWallet({int seedWordsLength = 14}) async { Logging.instance .log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info); // TODO: ping wownero server and make sure the genesis hash matches @@ -687,6 +687,7 @@ class WowneroWallet extends CoinServiceAPI { credentials = wownero.createWowneroNewWalletCredentials( name: name, language: "English", + seedWordsLength: seedWordsLength ); walletInfo = WalletInfo.external( @@ -713,12 +714,12 @@ class WowneroWallet extends CoinServiceAPI { final wallet = await _walletCreationService?.create(credentials); // subtract a couple days to ensure we have a buffer for SWB - // 14 words - //final bufferedCreateHeight = getSeedHeightSync(wallet?.seed.trim() as String); - - // 25 words - final bufferedCreateHeight = 0; - // TODO use an alternative to wow_seed's get_seed_height + if (seedWordsLength == 14) { + final bufferedCreateHeight = getSeedHeightSync(wallet?.seed.trim() as String); + } else { + final bufferedCreateHeight = 0; + // TODO use an alternative to wow_seed's get_seed_height + } await DB.instance.put<dynamic>( boxName: walletId, key: "restoreHeight", value: bufferedCreateHeight); @@ -726,6 +727,7 @@ class WowneroWallet extends CoinServiceAPI { await _secureStore.write( key: '${_walletId}_mnemonic', value: wallet?.seed.trim()); + walletInfo.address = wallet?.walletAddresses.address; await DB.instance .add<WalletInfo>(boxName: WalletInfo.boxName, value: walletInfo); @@ -782,7 +784,7 @@ class WowneroWallet extends CoinServiceAPI { @override // TODO: implement initializeWallet - Future<bool> initializeNew() async { + Future<bool> initializeNew({int seedWordsLength = 14}) async { await _prefs.init(); // TODO: ping actual wownero network // try { @@ -800,7 +802,7 @@ class WowneroWallet extends CoinServiceAPI { prefs = await SharedPreferences.getInstance(); keysStorage = KeyService(storage!); - await _generateNewWallet(); + await _generateNewWallet(seedWordsLength: seedWordsLength); // var password; // try { // password = diff --git a/test/services/coins/wownero/wownero_wallet_test.dart b/test/services/coins/wownero/wownero_wallet_test.dart index 2099df364..660bc1438 100644 --- a/test/services/coins/wownero/wownero_wallet_test.dart +++ b/test/services/coins/wownero/wownero_wallet_test.dart @@ -46,7 +46,7 @@ dynamic _walletInfoSource; String path = ''; -String name = 'namee${Random().nextInt(10000000)}'; +String name = ''; int nettype = 0; WalletType type = WalletType.wownero; @@ -83,10 +83,75 @@ void main() async { _walletInfoSource = await Hive.openBox<WalletInfo>(WalletInfo.boxName); walletService = wownero.createWowneroWalletService(_walletInfoSource); - group("Wownero 14 word tests", () { + group("Wownero 14 word seed generation", () { setUp(() async { bool hasThrown = false; try { + name = 'namee${Random().nextInt(10000000)}'; + final dirPath = await pathForWalletDir(name: name, type: type); + path = await pathForWallet(name: name, type: type); + credentials = wownero.createWowneroNewWalletCredentials( + name: name, + language: "English", + seedWordsLength: 14); // TODO catch failure + + walletInfo = WalletInfo.external( + id: WalletBase.idFor(name, type), + name: name, + type: type, + isRecovery: false, + restoreHeight: credentials.height ?? 0, + date: DateTime.now(), + path: path, + address: "", + dirPath: dirPath); + credentials.walletInfo = walletInfo; + + _walletCreationService = WalletCreationService( + secureStorage: storage, + sharedPreferences: prefs, + walletService: walletService, + keyService: keysStorage, + ); + _walletCreationService.changeWalletType(); + } catch (e, s) { + print(e); + print(s); + hasThrown = true; + } + expect(hasThrown, false); + }); + + test("Wownero 14 word seed address generation", () async { + final wallet = await _walletCreationService.create(credentials); + // TODO validate mnemonic + walletInfo.address = wallet.walletAddresses.address; + + bool hasThrown = false; + try { + await _walletInfoSource.add(walletInfo); + walletBase?.close(); + walletBase = wallet as WowneroWalletBase; + + // TODO validate + //expect(walletInfo.address, mainnetTestData14[0][0]); + } catch (_) { + hasThrown = true; + } + expect(hasThrown, false); + + walletBase?.close(); + walletBase = wallet as WowneroWalletBase; + }); + + // TODO delete left over wallet file with name: name + }); + + group("Wownero 14 word seed restoration", () { + setUp(() async { + bool hasThrown = false; + try { + name = 'namee${Random().nextInt(10000000)}'; final dirPath = await pathForWalletDir(name: name, type: type); path = await pathForWallet(name: name, type: type); credentials = wownero.createWowneroRestoreWalletFromSeedCredentials( @@ -121,7 +186,7 @@ void main() async { expect(hasThrown, false); }); - test("Test mainnet address generation from 14 word seed", () async { + test("Wownero 14 word seed address generation", () async { final wallet = await _walletCreationService.restoreFromSeed(credentials); walletInfo.address = wallet.walletAddresses.address; @@ -156,7 +221,71 @@ void main() async { // TODO delete left over wallet file with name: name }); - group("Wownero 25 word tests", () { + group("Wownero 25 word seed generation", () { + setUp(() async { + bool hasThrown = false; + try { + name = 'namee${Random().nextInt(10000000)}'; + final dirPath = await pathForWalletDir(name: name, type: type); + path = await pathForWallet(name: name, type: type); + credentials = wownero.createWowneroNewWalletCredentials( + name: name, + language: "English", + seedWordsLength: 25); // TODO catch failure + + walletInfo = WalletInfo.external( + id: WalletBase.idFor(name, type), + name: name, + type: type, + isRecovery: false, + restoreHeight: credentials.height ?? 0, + date: DateTime.now(), + path: path, + address: "", + dirPath: dirPath); + credentials.walletInfo = walletInfo; + + _walletCreationService = WalletCreationService( + secureStorage: storage, + sharedPreferences: prefs, + walletService: walletService, + keyService: keysStorage, + ); + _walletCreationService.changeWalletType(); + } catch (e, s) { + print(e); + print(s); + hasThrown = true; + } + expect(hasThrown, false); + }); + + test("Wownero 25 word seed address generation", () async { + final wallet = await _walletCreationService.create(credentials); + // TODO validate mnemonic + walletInfo.address = wallet.walletAddresses.address; + + bool hasThrown = false; + try { + await _walletInfoSource.add(walletInfo); + walletBase?.close(); + walletBase = wallet as WowneroWalletBase; + + // TODO validate + //expect(walletInfo.address, mainnetTestData14[0][0]); + } catch (_) { + hasThrown = true; + } + expect(hasThrown, false); + + walletBase?.close(); + walletBase = wallet as WowneroWalletBase; + }); + + // TODO delete left over wallet file with name: name + }); + + group("Wownero 25 word seed restoration", () { setUp(() async { bool hasThrown = false; try { @@ -195,42 +324,11 @@ void main() async { expect(hasThrown, false); }); - test("Test mainnet address generation from 25 word seed", () async { - bool hasThrown = false; - try { - final dirPath = await pathForWalletDir(name: name, type: type); - path = await pathForWallet(name: name, type: type); - - walletInfo = WalletInfo.external( - id: WalletBase.idFor(name, type), - name: name, - type: type, - isRecovery: false, - restoreHeight: credentials.height ?? 0, - date: DateTime.now(), - path: path, - address: "", - dirPath: dirPath); - credentials.walletInfo = walletInfo; - - _walletCreationService = WalletCreationService( - secureStorage: storage, - sharedPreferences: prefs, - walletService: walletService, - keyService: keysStorage, - ); - _walletCreationService.changeWalletType(); - } catch (e, s) { - print(e); - print(s); - hasThrown = true; - } - expect(hasThrown, false); - + test("Wownero 25 word seed address generation", () async { final wallet = await _walletCreationService.restoreFromSeed(credentials); walletInfo.address = wallet.walletAddresses.address; - hasThrown = false; + bool hasThrown = false; try { await _walletInfoSource.add(walletInfo); walletBase?.close(); From f17785ffc7e81f187239881eed0e72da6d7fb8b2 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 8 Nov 2022 10:18:48 -0600 Subject: [PATCH 162/426] monero/wownero untrusted cert popup --- .../add_edit_node_view.dart | 24 +++- .../manage_nodes_views/node_details_view.dart | 24 +++- .../test_monero_node_connection.dart | 115 +++++++++++++++--- lib/widgets/node_card.dart | 24 +++- lib/widgets/node_options_sheet.dart | 24 +++- 5 files changed, 192 insertions(+), 19 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index 87aee413e..382e3f09e 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -110,7 +110,29 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> { ref.read(nodeFormDataProvider).useSSL = false; } - testPassed = await testMoneroNodeConnection(Uri.parse(uriString)); + final response = await testMoneroNodeConnection( + Uri.parse(uriString), + false, + ); + + if (response.cert != null) { + if (mounted) { + final shouldAllowBadCert = await showBadX509CertificateDialog( + response.cert!, + response.url!, + response.port!, + context, + ); + + if (shouldAllowBadCert) { + final response = await testMoneroNodeConnection( + Uri.parse(uriString), true); + testPassed = response.success; + } + } + } else { + testPassed = response.success; + } } } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Warning); diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart index c5e666ce2..f9b64c460 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart @@ -97,7 +97,29 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> { String uriString = "${uri.scheme}://${uri.host}:${node.port}$path"; - testPassed = await testMoneroNodeConnection(Uri.parse(uriString)); + final response = await testMoneroNodeConnection( + Uri.parse(uriString), + false, + ); + + if (response.cert != null) { + if (mounted) { + final shouldAllowBadCert = await showBadX509CertificateDialog( + response.cert!, + response.url!, + response.port!, + context, + ); + + if (shouldAllowBadCert) { + final response = await testMoneroNodeConnection( + Uri.parse(uriString), true); + testPassed = response.success; + } + } + } else { + testPassed = response.success; + } } } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Warning); diff --git a/lib/utilities/test_monero_node_connection.dart b/lib/utilities/test_monero_node_connection.dart index 7cb01e8b1..92b645141 100644 --- a/lib/utilities/test_monero_node_connection.dart +++ b/lib/utilities/test_monero_node_connection.dart @@ -1,26 +1,111 @@ import 'dart:convert'; +import 'dart:io'; -import 'package:http/http.dart' as http; +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; -Future<bool> testMoneroNodeConnection(Uri uri) async { +class MoneroNodeConnectionResponse { + final X509Certificate? cert; + final String? url; + final int? port; + final bool success; + + MoneroNodeConnectionResponse(this.cert, this.url, this.port, this.success); +} + +Future<MoneroNodeConnectionResponse> testMoneroNodeConnection( + Uri uri, + bool allowBadX509Certificate, +) async { + final client = HttpClient(); + MoneroNodeConnectionResponse? badCertResponse; try { - final client = http.Client(); - final response = await client - .post( - uri, - headers: {'Content-Type': 'application/json'}, - body: jsonEncode({"jsonrpc": "2.0", "id": "0", "method": "get_info"}), - ) - .timeout(const Duration(milliseconds: 1200), - onTimeout: () async => http.Response('Error', 408)); + client.badCertificateCallback = (cert, url, port) { + if (allowBadX509Certificate) { + return true; + } - final result = jsonDecode(response.body); + if (badCertResponse == null) { + badCertResponse = MoneroNodeConnectionResponse(cert, url, port, false); + } else { + return false; + } + + return false; + }; + + final request = await client.postUrl(uri); + + final body = utf8.encode( + jsonEncode({ + "jsonrpc": "2.0", + "id": "0", + "method": "get_info", + }), + ); + + request.headers.add( + 'Content-Length', + body.length.toString(), + preserveHeaderCase: true, + ); + request.headers.set( + 'Content-Type', + 'application/json', + preserveHeaderCase: true, + ); + + request.add(body); + + final response = await request.close(); + final result = await response.transform(utf8.decoder).join(); // TODO: json decoded without error so assume connection exists? // or we can check for certain values in the response to decide - return true; + return MoneroNodeConnectionResponse(null, null, null, true); } catch (e, s) { - Logging.instance.log("$e\n$s", level: LogLevel.Warning); - return false; + if (badCertResponse != null) { + return badCertResponse!; + } else { + Logging.instance.log("$e\n$s", level: LogLevel.Warning); + return MoneroNodeConnectionResponse(null, null, null, false); + } + } finally { + client.close(force: true); } } + +Future<bool> showBadX509CertificateDialog( + X509Certificate cert, + String url, + int port, + BuildContext context, +) async { + final result = await showDialog<bool>( + context: context, + barrierDismissible: false, + builder: (context) { + return StackDialog( + title: "Untrusted X509Certificate", + message: "SHA1: ${Format.uint8listToString(cert.sha1)}", + leftButton: SecondaryButton( + label: "Cancel", + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + rightButton: PrimaryButton( + label: "Trust", + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ); + }, + ); + + return result ?? false; +} diff --git a/lib/widgets/node_card.dart b/lib/widgets/node_card.dart index bf9d2746e..1da7e9012 100644 --- a/lib/widgets/node_card.dart +++ b/lib/widgets/node_card.dart @@ -110,7 +110,29 @@ class _NodeCardState extends ConsumerState<NodeCard> { String uriString = "${uri.scheme}://${uri.host}:${node.port}$path"; - testPassed = await testMoneroNodeConnection(Uri.parse(uriString)); + final response = await testMoneroNodeConnection( + Uri.parse(uriString), + false, + ); + + if (response.cert != null) { + if (mounted) { + final shouldAllowBadCert = await showBadX509CertificateDialog( + response.cert!, + response.url!, + response.port!, + context, + ); + + if (shouldAllowBadCert) { + final response = await testMoneroNodeConnection( + Uri.parse(uriString), true); + testPassed = response.success; + } + } + } else { + testPassed = response.success; + } } } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Warning); diff --git a/lib/widgets/node_options_sheet.dart b/lib/widgets/node_options_sheet.dart index a5345161c..7ffd290f3 100644 --- a/lib/widgets/node_options_sheet.dart +++ b/lib/widgets/node_options_sheet.dart @@ -93,7 +93,29 @@ class NodeOptionsSheet extends ConsumerWidget { String uriString = "${uri.scheme}://${uri.host}:${node.port}$path"; - testPassed = await testMoneroNodeConnection(Uri.parse(uriString)); + final response = await testMoneroNodeConnection( + Uri.parse(uriString), + false, + ); + + if (response.cert != null) { + // if (mounted) { + final shouldAllowBadCert = await showBadX509CertificateDialog( + response.cert!, + response.url!, + response.port!, + context, + ); + + if (shouldAllowBadCert) { + final response = + await testMoneroNodeConnection(Uri.parse(uriString), true); + testPassed = response.success; + } + // } + } else { + testPassed = response.success; + } } } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Warning); From a8c3d5f1042b6b70888acca8db0020928faa4684 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 8 Nov 2022 11:41:12 -0600 Subject: [PATCH 163/426] format sha1 string --- lib/utilities/test_monero_node_connection.dart | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/utilities/test_monero_node_connection.dart b/lib/utilities/test_monero_node_connection.dart index 92b645141..5e35f9a03 100644 --- a/lib/utilities/test_monero_node_connection.dart +++ b/lib/utilities/test_monero_node_connection.dart @@ -84,13 +84,23 @@ Future<bool> showBadX509CertificateDialog( int port, BuildContext context, ) async { + final chars = Format.uint8listToString(cert.sha1) + .toUpperCase() + .characters + .toList(growable: false); + + String sha1 = chars.sublist(0, 2).join(); + for (int i = 2; i < chars.length; i += 2) { + sha1 += ":${chars.sublist(i, i + 2).join()}"; + } + final result = await showDialog<bool>( context: context, barrierDismissible: false, builder: (context) { return StackDialog( title: "Untrusted X509Certificate", - message: "SHA1: ${Format.uint8listToString(cert.sha1)}", + message: "SHA1:\n$sha1", leftButton: SecondaryButton( label: "Cancel", onPressed: () { From e41f8088b02c8aeaac8caaebac27dcbe7cc1d893 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 8 Nov 2022 12:00:10 -0600 Subject: [PATCH 164/426] WIP: wownero 25 word seed option ui --- .../restore_options_view.dart | 36 +++++++++++++++---- .../restore_wallet_view.dart | 5 +++ .../coins/wownero/wownero_wallet.dart | 10 +++++- lib/utilities/constants.dart | 6 +--- 4 files changed, 45 insertions(+), 12 deletions(-) diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart index 76e74fa14..1ce5d713a 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart @@ -252,7 +252,11 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> { SizedBox( height: isDesktop ? 40 : 24, ), - if (coin == Coin.monero || coin == Coin.epicCash) + if (coin == Coin.monero || + coin == Coin.epicCash || + (coin == Coin.wownero && + ref.watch(mnemonicWordCountStateProvider.state).state == + 25)) Text( "Choose start date", style: isDesktop @@ -264,11 +268,19 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> { : STextStyles.smallMed12(context), textAlign: TextAlign.left, ), - if (coin == Coin.monero || coin == Coin.epicCash) + if (coin == Coin.monero || + coin == Coin.epicCash || + (coin == Coin.wownero && + ref.watch(mnemonicWordCountStateProvider.state).state == + 25)) SizedBox( height: isDesktop ? 16 : 8, ), - if (coin == Coin.monero || coin == Coin.epicCash) + if (coin == Coin.monero || + coin == Coin.epicCash || + (coin == Coin.wownero && + ref.watch(mnemonicWordCountStateProvider.state).state == + 25)) // if (!isDesktop) RestoreFromDatePicker( @@ -278,11 +290,19 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> { // if (isDesktop) // // TODO desktop date picker - if (coin == Coin.monero || coin == Coin.epicCash) + if (coin == Coin.monero || + coin == Coin.epicCash || + (coin == Coin.wownero && + ref.watch(mnemonicWordCountStateProvider.state).state == + 25)) const SizedBox( height: 8, ), - if (coin == Coin.monero || coin == Coin.epicCash) + if (coin == Coin.monero || + coin == Coin.epicCash || + (coin == Coin.wownero && + ref.watch(mnemonicWordCountStateProvider.state).state == + 25)) RoundedWhiteContainer( child: Center( child: Text( @@ -299,7 +319,11 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> { ), ), ), - if (coin == Coin.monero || coin == Coin.epicCash) + if (coin == Coin.monero || + coin == Coin.epicCash || + (coin == Coin.wownero && + ref.watch(mnemonicWordCountStateProvider.state).state == + 25)) SizedBox( height: isDesktop ? 24 : 16, ), diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart index def0724b5..a6b7e7e77 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart @@ -149,6 +149,7 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> { super.dispose(); } + // TODO: check for wownero wordlist? bool _isValidMnemonicWord(String word) { // TODO: get the actual language if (widget.coin == Coin.monero) { @@ -181,6 +182,10 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> { if (widget.coin == Coin.monero) { height = monero.getHeigthByDate(date: widget.restoreFromDate); } + // todo: wait until this implemented + // else if (widget.coin == Coin.wownero) { + // height = wownero.getHeightByDate(date: widget.restoreFromDate); + // } // TODO: make more robust estimate of date maybe using https://explorer.epic.tech/api-index if (widget.coin == Coin.epicCash) { diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart index 72f43eac8..788f2f9d8 100644 --- a/lib/services/coins/wownero/wownero_wallet.dart +++ b/lib/services/coins/wownero/wownero_wallet.dart @@ -942,6 +942,11 @@ class WowneroWallet extends CoinServiceAPI { required int maxNumberOfIndexesToCheck, required int height, }) async { + final int seedLength = mnemonic.trim().split(" ").length; + if (!(seedLength == 14 || seedLength == 25)) { + throw Exception("Invalid wownero mnemonic length found: $seedLength"); + } + await _prefs.init(); longMutex = true; final start = DateTime.now(); @@ -969,7 +974,10 @@ class WowneroWallet extends CoinServiceAPI { await _secureStore.write( key: '${_walletId}_mnemonic', value: mnemonic.trim()); - height = getSeedHeightSync(mnemonic.trim()); + // extract seed height from 14 word seed + if (seedLength == 14) { + height = getSeedHeightSync(mnemonic.trim()); + } await DB.instance .put<dynamic>(boxName: walletId, key: "restoreHeight", value: height); diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index 4fb3fb54b..e27fbaa3d 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -35,10 +35,6 @@ abstract class Constants { static const int pinLength = 4; - // enable testnet - // TODO: currently unused - static const bool allowTestnets = true; - // Enable Logger.print statements static const bool disableLogger = false; @@ -66,7 +62,7 @@ abstract class Constants { values.addAll([25]); break; case Coin.wownero: - values.addAll([14]); + values.addAll([14, 25]); break; } return values; From 43deb9f81fa99d65318a116c1c835a0dc9f96f9e Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 7 Nov 2022 16:17:54 -0700 Subject: [PATCH 165/426] desktop about ui fix --- .../desktop_about_view.dart | 933 ++++++++++-------- 1 file changed, 497 insertions(+), 436 deletions(-) diff --git a/lib/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart b/lib/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart index 86fcf78e0..18988cb68 100644 --- a/lib/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart +++ b/lib/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart @@ -140,262 +140,123 @@ class DesktopAboutView extends ConsumerWidget { children: [ Padding( padding: const EdgeInsets.fromLTRB(24, 10, 24, 35), - child: RoundedWhiteContainer( - width: 929, - height: 411, - child: Padding( - padding: const EdgeInsets.only(left: 10, top: 10), - child: Column( - // mainAxisAlignment: MainAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text( - "Stack Wallet", - style: STextStyles.desktopH3(context), - textAlign: TextAlign.start, - ), - ], - ), - const SizedBox(height: 16), - Row( - children: [ - RichText( - textAlign: TextAlign.start, - text: TextSpan( - style: STextStyles.label(context), + child: Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + width: 929, + height: 411, + child: Padding( + padding: const EdgeInsets.only(left: 10, top: 10), + child: Column( + // mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, children: [ - TextSpan( - text: - "By using Stack Wallet, you agree to the ", - style: STextStyles.desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark3), - ), - TextSpan( - text: "Terms of service", - style: STextStyles.richLink(context) - .copyWith(fontSize: 14), - recognizer: TapGestureRecognizer() - ..onTap = () { - launchUrl( - Uri.parse( - "https://stackwallet.com/terms-of-service.html"), - mode: LaunchMode.externalApplication, - ); - }, - ), - TextSpan( - text: " and ", - style: STextStyles.desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark3), - ), - TextSpan( - text: "Privacy policy", - style: STextStyles.richLink(context) - .copyWith(fontSize: 14), - recognizer: TapGestureRecognizer() - ..onTap = () { - launchUrl( - Uri.parse( - "https://stackwallet.com/privacy-policy.html"), - mode: LaunchMode.externalApplication, - ); - }, + Text( + "Stack Wallet", + style: STextStyles.desktopH3(context), + textAlign: TextAlign.start, ), ], ), - ), - ], - ), - const SizedBox(height: 32), - Padding( - padding: const EdgeInsets.only(right: 10, bottom: 10), - child: Column( - children: [ - FutureBuilder( - future: PackageInfo.fromPlatform(), - builder: - (context, AsyncSnapshot<PackageInfo> snapshot) { - String version = ""; - String signature = ""; - String build = ""; + const SizedBox(height: 16), + Row( + children: [ + RichText( + textAlign: TextAlign.start, + text: TextSpan( + style: STextStyles.label(context), + children: [ + TextSpan( + text: + "By using Stack Wallet, you agree to the ", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + ), + TextSpan( + text: "Terms of service", + style: STextStyles.richLink(context) + .copyWith(fontSize: 14), + recognizer: TapGestureRecognizer() + ..onTap = () { + launchUrl( + Uri.parse( + "https://stackwallet.com/terms-of-service.html"), + mode: + LaunchMode.externalApplication, + ); + }, + ), + TextSpan( + text: " and ", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + ), + TextSpan( + text: "Privacy policy", + style: STextStyles.richLink(context) + .copyWith(fontSize: 14), + recognizer: TapGestureRecognizer() + ..onTap = () { + launchUrl( + Uri.parse( + "https://stackwallet.com/privacy-policy.html"), + mode: + LaunchMode.externalApplication, + ); + }, + ), + ], + ), + ), + ], + ), + const SizedBox(height: 32), + Padding( + padding: + const EdgeInsets.only(right: 10, bottom: 10), + child: Column( + children: [ + FutureBuilder( + future: PackageInfo.fromPlatform(), + builder: (context, + AsyncSnapshot<PackageInfo> snapshot) { + String version = ""; + String signature = ""; + String build = ""; - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - version = snapshot.data!.version; - build = snapshot.data!.buildNumber; - signature = snapshot.data!.buildSignature; - } + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + version = snapshot.data!.version; + build = snapshot.data!.buildNumber; + signature = snapshot.data!.buildSignature; + } - return Column( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Version", - style: STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark), - ), - const SizedBox( - height: 2, - ), - SelectableText( - version, - style: STextStyles.itemSubtitle( - context), - ), - ], - ), - const SizedBox( - width: 400, - ), - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Build number", - style: STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark), - ), - const SizedBox( - height: 2, - ), - SelectableText( - build, - style: STextStyles.itemSubtitle( - context), - ), - ], - ), - ], - ), - const SizedBox(height: 32), - Row( - children: [ - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Build signature", - style: STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark), - ), - const SizedBox( - height: 2, - ), - SelectableText( - signature, - style: STextStyles.itemSubtitle( - context), - ), - ], - ), - const SizedBox( - width: 350, - ), - FutureBuilder( - future: commitFiroFuture, - builder: (context, - AsyncSnapshot<dynamic> snapshot) { - bool commitExists = false; - bool isHead = false; - CommitStatus stateOfCommit = - CommitStatus.notLoaded; - - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - commitExists = - snapshot.data![0] as bool; - isHead = - snapshot.data![1] as bool; - if (commitExists && isHead) { - stateOfCommit = - CommitStatus.isHead; - } else if (commitExists) { - stateOfCommit = - CommitStatus.isOldCommit; - } else { - stateOfCommit = - CommitStatus.notACommit; - } - } - TextStyle indicationStyle = - STextStyles.itemSubtitle( - context); - switch (stateOfCommit) { - case CommitStatus.isHead: - indicationStyle = STextStyles - .itemSubtitle(context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .accentColorGreen); - break; - case CommitStatus.isOldCommit: - indicationStyle = STextStyles - .itemSubtitle(context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .accentColorYellow); - break; - case CommitStatus.notACommit: - indicationStyle = STextStyles - .itemSubtitle(context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .accentColorRed); - break; - default: - break; - } - return Column( + return Column( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Firo Build Commit", + "Version", style: STextStyles .desktopTextExtraExtraSmall( context) @@ -410,84 +271,22 @@ class DesktopAboutView extends ConsumerWidget { height: 2, ), SelectableText( - firoCommit, - style: indicationStyle, + version, + style: + STextStyles.itemSubtitle( + context), ), ], - ); - }), - ], - ), - const SizedBox(height: 35), - Row( - children: [ - FutureBuilder( - future: commitEpicFuture, - builder: (context, - AsyncSnapshot<dynamic> snapshot) { - bool commitExists = false; - bool isHead = false; - CommitStatus stateOfCommit = - CommitStatus.notLoaded; - - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - commitExists = - snapshot.data![0] as bool; - isHead = - snapshot.data![1] as bool; - if (commitExists && isHead) { - stateOfCommit = - CommitStatus.isHead; - } else if (commitExists) { - stateOfCommit = - CommitStatus.isOldCommit; - } else { - stateOfCommit = - CommitStatus.notACommit; - } - } - TextStyle indicationStyle = - STextStyles.itemSubtitle( - context); - switch (stateOfCommit) { - case CommitStatus.isHead: - indicationStyle = STextStyles - .itemSubtitle(context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .accentColorGreen); - break; - case CommitStatus.isOldCommit: - indicationStyle = STextStyles - .itemSubtitle(context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .accentColorYellow); - break; - case CommitStatus.notACommit: - indicationStyle = STextStyles - .itemSubtitle(context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .accentColorRed); - break; - default: - break; - } - return Column( + ), + const SizedBox( + width: 400, + ), + Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Epic Cash Build Commit", + "Build number", style: STextStyles .desktopTextExtraExtraSmall( context) @@ -502,82 +301,24 @@ class DesktopAboutView extends ConsumerWidget { height: 2, ), SelectableText( - epicCashCommit, - style: indicationStyle, + build, + style: + STextStyles.itemSubtitle( + context), ), ], - ); - }), - const SizedBox( - width: 105, - ), - FutureBuilder( - future: commitMoneroFuture, - builder: (context, - AsyncSnapshot<dynamic> snapshot) { - bool commitExists = false; - bool isHead = false; - CommitStatus stateOfCommit = - CommitStatus.notLoaded; - - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - commitExists = - snapshot.data![0] as bool; - isHead = - snapshot.data![1] as bool; - if (commitExists && isHead) { - stateOfCommit = - CommitStatus.isHead; - } else if (commitExists) { - stateOfCommit = - CommitStatus.isOldCommit; - } else { - stateOfCommit = - CommitStatus.notACommit; - } - } - TextStyle indicationStyle = - STextStyles.itemSubtitle( - context); - switch (stateOfCommit) { - case CommitStatus.isHead: - indicationStyle = STextStyles - .itemSubtitle(context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .accentColorGreen); - break; - case CommitStatus.isOldCommit: - indicationStyle = STextStyles - .itemSubtitle(context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .accentColorYellow); - break; - case CommitStatus.notACommit: - indicationStyle = STextStyles - .itemSubtitle(context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .accentColorRed); - break; - default: - break; - } - return Column( + ), + ], + ), + const SizedBox(height: 32), + Row( + children: [ + Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Monero Build Commit", + "Build signature", style: STextStyles .desktopTextExtraExtraSmall( context) @@ -592,60 +333,380 @@ class DesktopAboutView extends ConsumerWidget { height: 2, ), SelectableText( - moneroCommit, - style: indicationStyle, + signature, + style: + STextStyles.itemSubtitle( + context), ), ], - ); - }), - ], - ), - const SizedBox(height: 35), - Row( - children: [ - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Website", - style: STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark), - ), - const SizedBox( - height: 2, - ), - BlueTextButton( - text: "https://stackwallet.com", - onTap: () { - launchUrl( - Uri.parse( - "https://stackwallet.com"), - mode: LaunchMode - .externalApplication, - ); - }, - ), - ], - ) - ], - ) - ], - ); - }, + ), + const SizedBox( + width: 350, + ), + FutureBuilder( + future: commitFiroFuture, + builder: (context, + AsyncSnapshot<dynamic> + snapshot) { + bool commitExists = false; + bool isHead = false; + CommitStatus stateOfCommit = + CommitStatus.notLoaded; + + if (snapshot.connectionState == + ConnectionState + .done && + snapshot.hasData) { + commitExists = snapshot + .data![0] as bool; + isHead = snapshot.data![1] + as bool; + if (commitExists && + isHead) { + stateOfCommit = + CommitStatus.isHead; + } else if (commitExists) { + stateOfCommit = + CommitStatus + .isOldCommit; + } else { + stateOfCommit = + CommitStatus + .notACommit; + } + } + TextStyle indicationStyle = + STextStyles.itemSubtitle( + context); + switch (stateOfCommit) { + case CommitStatus.isHead: + indicationStyle = STextStyles + .itemSubtitle( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .accentColorGreen); + break; + case CommitStatus + .isOldCommit: + indicationStyle = STextStyles + .itemSubtitle( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .accentColorYellow); + break; + case CommitStatus + .notACommit: + indicationStyle = STextStyles + .itemSubtitle( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .accentColorRed); + break; + default: + break; + } + return Column( + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + Text( + "Firo Build Commit", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textDark), + ), + const SizedBox( + height: 2, + ), + SelectableText( + firoCommit, + style: indicationStyle, + ), + ], + ); + }), + ], + ), + const SizedBox(height: 35), + Row( + children: [ + FutureBuilder( + future: commitEpicFuture, + builder: (context, + AsyncSnapshot<dynamic> + snapshot) { + bool commitExists = false; + bool isHead = false; + CommitStatus stateOfCommit = + CommitStatus.notLoaded; + + if (snapshot.connectionState == + ConnectionState + .done && + snapshot.hasData) { + commitExists = snapshot + .data![0] as bool; + isHead = snapshot.data![1] + as bool; + if (commitExists && + isHead) { + stateOfCommit = + CommitStatus.isHead; + } else if (commitExists) { + stateOfCommit = + CommitStatus + .isOldCommit; + } else { + stateOfCommit = + CommitStatus + .notACommit; + } + } + TextStyle indicationStyle = + STextStyles.itemSubtitle( + context); + switch (stateOfCommit) { + case CommitStatus.isHead: + indicationStyle = STextStyles + .itemSubtitle( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .accentColorGreen); + break; + case CommitStatus + .isOldCommit: + indicationStyle = STextStyles + .itemSubtitle( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .accentColorYellow); + break; + case CommitStatus + .notACommit: + indicationStyle = STextStyles + .itemSubtitle( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .accentColorRed); + break; + default: + break; + } + return Column( + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + Text( + "Epic Cash Build Commit", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textDark), + ), + const SizedBox( + height: 2, + ), + SelectableText( + epicCashCommit, + style: indicationStyle, + ), + ], + ); + }), + const SizedBox( + width: 105, + ), + FutureBuilder( + future: commitMoneroFuture, + builder: (context, + AsyncSnapshot<dynamic> + snapshot) { + bool commitExists = false; + bool isHead = false; + CommitStatus stateOfCommit = + CommitStatus.notLoaded; + + if (snapshot.connectionState == + ConnectionState + .done && + snapshot.hasData) { + commitExists = snapshot + .data![0] as bool; + isHead = snapshot.data![1] + as bool; + if (commitExists && + isHead) { + stateOfCommit = + CommitStatus.isHead; + } else if (commitExists) { + stateOfCommit = + CommitStatus + .isOldCommit; + } else { + stateOfCommit = + CommitStatus + .notACommit; + } + } + TextStyle indicationStyle = + STextStyles.itemSubtitle( + context); + switch (stateOfCommit) { + case CommitStatus.isHead: + indicationStyle = STextStyles + .itemSubtitle( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .accentColorGreen); + break; + case CommitStatus + .isOldCommit: + indicationStyle = STextStyles + .itemSubtitle( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .accentColorYellow); + break; + case CommitStatus + .notACommit: + indicationStyle = STextStyles + .itemSubtitle( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .accentColorRed); + break; + default: + break; + } + return Column( + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + Text( + "Monero Build Commit", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textDark), + ), + const SizedBox( + height: 2, + ), + SelectableText( + moneroCommit, + style: indicationStyle, + ), + ], + ); + }), + ], + ), + const SizedBox(height: 35), + Row( + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Website", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textDark), + ), + const SizedBox( + height: 2, + ), + BlueTextButton( + text: + "https://stackwallet.com", + onTap: () { + launchUrl( + Uri.parse( + "https://stackwallet.com"), + mode: LaunchMode + .externalApplication, + ); + }, + ), + ], + ) + ], + ) + ], + ); + }, + ) + ], + ), ) ], ), - ) - ], + ), + ), ), - ), + ], ), ), ], From 543f9631d845838c1cab2e39fc38e9f077e7d4a9 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Tue, 8 Nov 2022 09:34:47 -0700 Subject: [PATCH 166/426] changed desktop textbox fontsize --- .../stack_backup_views/create_backup_view.dart | 4 ++++ .../stack_backup_views/restore_from_file_view.dart | 2 ++ .../backup_and_restore/backup_and_restore_settings.dart | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart index 30fcb7962..48f1a1b7f 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart @@ -272,6 +272,8 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { passwordFocusNode, context, ).copyWith( + labelStyle: + isDesktop ? STextStyles.fieldLabel(context) : null, suffixIcon: UnconstrainedBox( child: Row( children: [ @@ -403,6 +405,8 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { passwordRepeatFocusNode, context, ).copyWith( + labelStyle: + isDesktop ? STextStyles.fieldLabel(context) : null, suffixIcon: UnconstrainedBox( child: Row( children: [ 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 0c101d0b3..c73d596f0 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 @@ -245,6 +245,8 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { passwordFocusNode, context, ).copyWith( + labelStyle: + isDesktop ? STextStyles.fieldLabel(context) : null, suffixIcon: UnconstrainedBox( child: Row( children: [ diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart index 49debf22f..8928a268d 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart @@ -269,7 +269,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: EdgeInsets.all( + padding: const EdgeInsets.all( 10, ), child: restoreBackup From eea5225ba5c05a4c0cae4607cb8522b85a1cdc56 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Tue, 8 Nov 2022 10:07:38 -0700 Subject: [PATCH 167/426] button correction for desktop manual backup --- .../create_backup_view.dart | 356 ++++++++++++------ 1 file changed, 239 insertions(+), 117 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart index 48f1a1b7f..51a1d7218 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart @@ -158,24 +158,24 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { ), ), child, - const SizedBox(height: 20), - Row( - children: [ - PrimaryButton( - desktopMed: true, - width: 200, - label: "Create backup", - onPressed: () {}, - ), - const SizedBox(width: 16), - SecondaryButton( - desktopMed: true, - width: 200, - label: "Cancel", - onPressed: () {}, - ), - ], - ), + // const SizedBox(height: 20), + // Row( + // children: [ + // PrimaryButton( + // desktopMed: true, + // width: 200, + // label: "Create backup", + // onPressed: () {}, + // ), + // const SizedBox(width: 16), + // SecondaryButton( + // desktopMed: true, + // width: 200, + // label: "Cancel", + // onPressed: () {}, + // ), + // ], + // ), ], ); }, @@ -447,112 +447,234 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { height: 16, ), if (!isDesktop) const Spacer(), - TextButton( - style: shouldEnableCreate - ? Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context) - : Theme.of(context) - .extension<StackColors>()! - .getPrimaryDisabledButtonColor(context), - onPressed: !shouldEnableCreate - ? null - : () async { - final String pathToSave = fileLocationController.text; - final String passphrase = passwordController.text; - final String repeatPassphrase = - passwordRepeatController.text; + !isDesktop + ? TextButton( + style: shouldEnableCreate + ? Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context) + : Theme.of(context) + .extension<StackColors>()! + .getPrimaryDisabledButtonColor(context), + onPressed: !shouldEnableCreate + ? null + : () async { + final String pathToSave = + fileLocationController.text; + final String passphrase = passwordController.text; + final String repeatPassphrase = + passwordRepeatController.text; - if (pathToSave.isEmpty) { - unawaited(showFloatingFlushBar( - type: FlushBarType.warning, - message: "Directory not chosen", - context: context, - )); - return; - } - if (!(await Directory(pathToSave).exists())) { - unawaited(showFloatingFlushBar( - type: FlushBarType.warning, - message: "Directory does not exist", - context: context, - )); - return; - } - if (passphrase.isEmpty) { - unawaited(showFloatingFlushBar( - type: FlushBarType.warning, - message: "A passphrase is required", - context: context, - )); - return; - } - if (passphrase != repeatPassphrase) { - unawaited(showFloatingFlushBar( - type: FlushBarType.warning, - message: "Passphrase does not match", - context: context, - )); - return; - } + if (pathToSave.isEmpty) { + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "Directory not chosen", + context: context, + )); + return; + } + if (!(await Directory(pathToSave).exists())) { + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "Directory does not exist", + context: context, + )); + return; + } + if (passphrase.isEmpty) { + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "A passphrase is required", + context: context, + )); + return; + } + if (passphrase != repeatPassphrase) { + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "Passphrase does not match", + context: context, + )); + return; + } - unawaited(showDialog<dynamic>( - context: context, - barrierDismissible: false, - builder: (_) => const StackDialog( - title: "Encrypting backup", - message: "This shouldn't take long", - ), - )); - // make sure the dialog is able to be displayed for at least 1 second - await Future<void>.delayed(const Duration(seconds: 1)); + unawaited(showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => const StackDialog( + title: "Encrypting backup", + message: "This shouldn't take long", + ), + )); + // make sure the dialog is able to be displayed for at least 1 second + await Future<void>.delayed( + const Duration(seconds: 1)); - final DateTime now = DateTime.now(); - final String fileToSave = - "$pathToSave/stackbackup_${now.year}_${now.month}_${now.day}_${now.hour}_${now.minute}_${now.second}.swb"; + final DateTime now = DateTime.now(); + final String fileToSave = + "$pathToSave/stackbackup_${now.year}_${now.month}_${now.day}_${now.hour}_${now.minute}_${now.second}.swb"; - final backup = await SWB.createStackWalletJSON(); + final backup = await SWB.createStackWalletJSON(); - bool result = await SWB.encryptStackWalletWithPassphrase( - fileToSave, - passphrase, - jsonEncode(backup), - ); + bool result = + await SWB.encryptStackWalletWithPassphrase( + fileToSave, + passphrase, + jsonEncode(backup), + ); - if (mounted) { - // pop encryption progress dialog - Navigator.of(context).pop(); + if (mounted) { + // pop encryption progress dialog + Navigator.of(context).pop(); - if (result) { - await showDialog<dynamic>( - context: context, - barrierDismissible: false, - builder: (_) => Platform.isAndroid - ? StackOkDialog( - title: "Backup saved to:", - message: fileToSave, - ) - : const StackOkDialog( - title: "Backup creation succeeded"), - ); - passwordController.text = ""; - passwordRepeatController.text = ""; - setState(() {}); - } else { - await showDialog<dynamic>( - context: context, - barrierDismissible: false, - builder: (_) => const StackOkDialog( - title: "Backup creation failed"), - ); - } - } - }, - child: Text( - "Create backup", - style: STextStyles.button(context), - ), - ), + if (result) { + await showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => Platform.isAndroid + ? StackOkDialog( + title: "Backup saved to:", + message: fileToSave, + ) + : const StackOkDialog( + title: "Backup creation succeeded"), + ); + passwordController.text = ""; + passwordRepeatController.text = ""; + setState(() {}); + } else { + await showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => const StackOkDialog( + title: "Backup creation failed"), + ); + } + } + }, + child: Text( + "Create backup", + style: STextStyles.button(context), + ), + ) + : Row( + children: [ + PrimaryButton( + width: 183, + desktopMed: true, + label: "Create backup", + enabled: shouldEnableCreate, + onPressed: !shouldEnableCreate + ? null + : () async { + final String pathToSave = + fileLocationController.text; + final String passphrase = + passwordController.text; + final String repeatPassphrase = + passwordRepeatController.text; + + if (pathToSave.isEmpty) { + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "Directory not chosen", + context: context, + )); + return; + } + if (!(await Directory(pathToSave).exists())) { + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "Directory does not exist", + context: context, + )); + return; + } + if (passphrase.isEmpty) { + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "A passphrase is required", + context: context, + )); + return; + } + if (passphrase != repeatPassphrase) { + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "Passphrase does not match", + context: context, + )); + return; + } + + unawaited(showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => const StackDialog( + title: "Encrypting backup", + message: "This shouldn't take long", + ), + )); + // make sure the dialog is able to be displayed for at least 1 second + await Future<void>.delayed( + const Duration(seconds: 1)); + + final DateTime now = DateTime.now(); + final String fileToSave = + "$pathToSave/stackbackup_${now.year}_${now.month}_${now.day}_${now.hour}_${now.minute}_${now.second}.swb"; + + final backup = + await SWB.createStackWalletJSON(); + + bool result = + await SWB.encryptStackWalletWithPassphrase( + fileToSave, + passphrase, + jsonEncode(backup), + ); + + if (mounted) { + // pop encryption progress dialog + Navigator.of(context).pop(); + + if (result) { + await showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => Platform.isAndroid + ? StackOkDialog( + title: "Backup saved to:", + message: fileToSave, + ) + : const StackOkDialog( + title: + "Backup creation succeeded"), + ); + passwordController.text = ""; + passwordRepeatController.text = ""; + setState(() {}); + } else { + await showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => const StackOkDialog( + title: "Backup creation failed"), + ); + } + } + }, + ), + const SizedBox( + width: 16, + ), + SecondaryButton( + width: 183, + desktopMed: true, + label: "Cancel", + onPressed: () {}, + ), + ], + ), ], ), ), From 8af1350b95acc35ffb0e83e3ea67a32453b53a2f Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Tue, 8 Nov 2022 10:50:12 -0700 Subject: [PATCH 168/426] button correction for desktop restore backup and other ui fixes --- .../create_backup_view.dart | 37 +- .../restore_from_file_view.dart | 354 ++++++++++++------ 2 files changed, 251 insertions(+), 140 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart index 51a1d7218..eacdda66a 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart @@ -147,7 +147,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: const EdgeInsets.all(10.0), + padding: const EdgeInsets.only(bottom: 10), child: Text( "Choose file location", style: STextStyles.desktopTextExtraExtraSmall(context) @@ -158,24 +158,6 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { ), ), child, - // const SizedBox(height: 20), - // Row( - // children: [ - // PrimaryButton( - // desktopMed: true, - // width: 200, - // label: "Create backup", - // onPressed: () {}, - // ), - // const SizedBox(width: 16), - // SecondaryButton( - // desktopMed: true, - // width: 200, - // label: "Cancel", - // onPressed: () {}, - // ), - // ], - // ), ], ); }, @@ -252,8 +234,21 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { ); }), if (!Platform.isAndroid) - const SizedBox( - height: 8, + SizedBox( + height: !isDesktop ? 8 : 24, + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only(bottom: 10.0), + child: Text( + "Create a passphrase", + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + textAlign: TextAlign.left, + ), ), ClipRRect( borderRadius: BorderRadius.circular( 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 c73d596f0..f7a9883de 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 @@ -131,7 +131,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: const EdgeInsets.all(10.0), + padding: const EdgeInsets.only(bottom: 10.0), child: Text( "Choose file location", style: STextStyles.desktopTextExtraExtraSmall(context) @@ -143,26 +143,6 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { ), ), child, - const SizedBox(height: 20), - Row( - children: [ - PrimaryButton( - desktopMed: true, - width: 200, - label: "Restore", - onPressed: () { - restoreBackupPopup(context); - }, - ), - const SizedBox(width: 16), - SecondaryButton( - desktopMed: true, - width: 200, - label: "Cancel", - onPressed: () {}, - ), - ], - ), ], ); }, @@ -225,9 +205,22 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { ), onChanged: (newValue) {}, ), - const SizedBox( - height: 8, + SizedBox( + height: !isDesktop ? 8 : 24, ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only(bottom: 10.0), + child: Text( + "Enter passphrase", + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + textAlign: TextAlign.left, + ), + ), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -288,113 +281,236 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { height: 16, ), if (!isDesktop) const Spacer(), - TextButton( - style: passwordController.text.isEmpty || - fileLocationController.text.isEmpty - ? Theme.of(context) - .extension<StackColors>()! - .getPrimaryDisabledButtonColor(context) - : Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: passwordController.text.isEmpty || - fileLocationController.text.isEmpty - ? null - : () async { - final String fileToRestore = - fileLocationController.text; - final String passphrase = passwordController.text; + !isDesktop + ? TextButton( + style: passwordController.text.isEmpty || + fileLocationController.text.isEmpty + ? Theme.of(context) + .extension<StackColors>()! + .getPrimaryDisabledButtonColor(context) + : Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + onPressed: passwordController.text.isEmpty || + fileLocationController.text.isEmpty + ? null + : () async { + final String fileToRestore = + fileLocationController.text; + final String passphrase = passwordController.text; - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration(milliseconds: 75)); - } + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75)); + } - if (!(await File(fileToRestore).exists())) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Backup file does not exist", - context: context, - ); - return; - } + if (!(await File(fileToRestore).exists())) { + await showFloatingFlushBar( + type: FlushBarType.warning, + message: "Backup file does not exist", + context: context, + ); + return; + } - bool shouldPop = false; - showDialog<dynamic>( - barrierDismissible: false, - context: context, - builder: (_) => WillPopScope( - onWillPop: () async { - return shouldPop; - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Material( - color: Colors.transparent, - child: Center( - child: Text( - "Decrypting Stack backup file", - style: STextStyles.pageTitleH2(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textWhite, + bool shouldPop = false; + await showDialog<dynamic>( + barrierDismissible: false, + context: context, + builder: (_) => WillPopScope( + onWillPop: () async { + return shouldPop; + }, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Material( + color: Colors.transparent, + child: Center( + child: Text( + "Decrypting Stack backup file", + style: + STextStyles.pageTitleH2(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textWhite, + ), + ), + ), ), + const SizedBox( + height: 64, + ), + const Center( + child: LoadingIndicator( + width: 100, + ), + ), + ], + ), + ), + ); + + final String? jsonString = await compute( + SWB.decryptStackWalletWithPassphrase, + Tuple2(fileToRestore, passphrase), + debugLabel: "stack wallet decryption compute", + ); + + if (mounted) { + // pop LoadingIndicator + shouldPop = true; + Navigator.of(context).pop(); + + passwordController.text = ""; + + if (jsonString == null) { + await showFloatingFlushBar( + type: FlushBarType.warning, + message: "Failed to decrypt backup file", + context: context, + ); + return; + } + + await Navigator.of(context).push( + RouteGenerator.getRoute( + builder: (_) => StackRestoreProgressView( + jsonString: jsonString, ), ), - ), - const SizedBox( - height: 64, - ), - const Center( - child: LoadingIndicator( - width: 100, - ), - ), - ], - ), - ), - ); + ); + } + }, + child: Text( + "Restore", + style: STextStyles.button(context), + ), + ) + : Row( + children: [ + PrimaryButton( + width: 183, + desktopMed: true, + label: "Restore", + enabled: !(passwordController.text.isEmpty || + fileLocationController.text.isEmpty), + onPressed: passwordController.text.isEmpty || + fileLocationController.text.isEmpty + ? null + : () async { + final String fileToRestore = + fileLocationController.text; + final String passphrase = + passwordController.text; - final String? jsonString = await compute( - SWB.decryptStackWalletWithPassphrase, - Tuple2(fileToRestore, passphrase), - debugLabel: "stack wallet decryption compute", - ); + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75)); + } - if (mounted) { - // pop LoadingIndicator - shouldPop = true; - Navigator.of(context).pop(); + if (!(await File(fileToRestore).exists())) { + await showFloatingFlushBar( + type: FlushBarType.warning, + message: "Backup file does not exist", + context: context, + ); + return; + } - passwordController.text = ""; + bool shouldPop = false; + await showDialog<dynamic>( + barrierDismissible: false, + context: context, + builder: (_) => WillPopScope( + onWillPop: () async { + return shouldPop; + }, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Material( + color: Colors.transparent, + child: Center( + child: Text( + "Decrypting Stack backup file", + style: STextStyles.pageTitleH2( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textWhite, + ), + ), + ), + ), + const SizedBox( + height: 64, + ), + const Center( + child: LoadingIndicator( + width: 100, + ), + ), + ], + ), + ), + ); - if (jsonString == null) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Failed to decrypt backup file", - context: context, - ); - return; - } + final String? jsonString = await compute( + SWB.decryptStackWalletWithPassphrase, + Tuple2(fileToRestore, passphrase), + debugLabel: + "stack wallet decryption compute", + ); - Navigator.of(context).push( - RouteGenerator.getRoute( - builder: (_) => StackRestoreProgressView( - jsonString: jsonString, - ), - ), - ); - } - }, - child: Text( - "Restore", - style: STextStyles.button(context), - ), - ), + if (mounted) { + // pop LoadingIndicator + shouldPop = true; + Navigator.of(context).pop(); + + passwordController.text = ""; + + if (jsonString == null) { + await showFloatingFlushBar( + type: FlushBarType.warning, + message: + "Failed to decrypt backup file", + context: context, + ); + return; + } + + await Navigator.of(context).push( + RouteGenerator.getRoute( + builder: (_) => + StackRestoreProgressView( + jsonString: jsonString, + ), + ), + ); + } + }, + ), + const SizedBox( + width: 16, + ), + SecondaryButton( + width: 183, + desktopMed: true, + label: "Cancel", + onPressed: () {}, + ), + ], + ), ], ), )); From 95716bd0f6be7f5b7c6dcbb08b03efa6a5dfb545 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Tue, 8 Nov 2022 12:15:31 -0700 Subject: [PATCH 169/426] added textfield functionality to desktop create auto backup --- .../create_auto_backup.dart | 489 +++++++++++------- 1 file changed, 306 insertions(+), 183 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart index f3e502bcb..57a8d7a64 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart @@ -1,14 +1,23 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/stack_file_system.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/log_level_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/utilities/util.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.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/progress_bar.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:zxcvbn/zxcvbn.dart'; class CreateAutoBackup extends StatefulWidget { const CreateAutoBackup({Key? key}) : super(key: key); @@ -22,13 +31,24 @@ class _CreateAutoBackup extends State<CreateAutoBackup> { late final TextEditingController passphraseController; late final TextEditingController passphraseRepeatController; - late final FocusNode chooseFileLocation; + late final StackFileSystem stackFileSystem; late final FocusNode passphraseFocusNode; late final FocusNode passphraseRepeatFocusNode; + final zxcvbn = Zxcvbn(); bool shouldShowPasswordHint = true; bool hidePassword = true; + String passwordFeedback = + "Add another word or two. Uncommon words are better. Use a few words, avoid common phrases. No need for symbols, digits, or uppercase letters."; + double passwordStrength = 0.0; + + bool get shouldEnableCreate { + return fileLocationController.text.isNotEmpty && + passphraseController.text.isNotEmpty && + passphraseRepeatController.text.isNotEmpty; + } + bool get fieldsMatch => passphraseController.text == passphraseRepeatController.text; @@ -42,14 +62,26 @@ class _CreateAutoBackup extends State<CreateAutoBackup> { @override void initState() { + stackFileSystem = StackFileSystem(); + fileLocationController = TextEditingController(); passphraseController = TextEditingController(); passphraseRepeatController = TextEditingController(); - chooseFileLocation = FocusNode(); passphraseFocusNode = FocusNode(); passphraseRepeatFocusNode = FocusNode(); + if (Platform.isAndroid) { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + final dir = await stackFileSystem.prepareStorage(); + if (mounted) { + setState(() { + fileLocationController.text = dir.path; + }); + } + }); + } + super.initState(); } @@ -59,7 +91,6 @@ class _CreateAutoBackup extends State<CreateAutoBackup> { passphraseController.dispose(); passphraseRepeatController.dispose(); - chooseFileLocation.dispose(); passphraseFocusNode.dispose(); passphraseRepeatFocusNode.dispose(); @@ -71,9 +102,9 @@ class _CreateAutoBackup extends State<CreateAutoBackup> { debugPrint("BUILD: $runtimeType "); String? selectedItem = "Every 10 minutes"; - + final isDesktop = Util.isDesktop; return DesktopDialog( - maxHeight: 650, + maxHeight: 680, maxWidth: 600, child: Column( children: [ @@ -127,198 +158,289 @@ class _CreateAutoBackup extends State<CreateAutoBackup> { height: 10, ), Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - ), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("backupChooseFileLocation"), - focusNode: chooseFileLocation, - controller: fileLocationController, - style: STextStyles.desktopTextMedium(context).copyWith( - height: 2, - ), - textAlign: TextAlign.left, - enableSuggestions: false, - autocorrect: false, - decoration: standardInputDecoration( - "Save to...", - chooseFileLocation, - context, - ).copyWith( - labelStyle: - STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: - Theme.of(context).extension<StackColors>()!.textDark3, - ), - suffixIcon: Container( - decoration: BoxDecoration( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (!Platform.isAndroid) + Consumer(builder: (context, ref, __) { + return Container( color: Colors.transparent, - borderRadius: BorderRadius.circular(1000), - ), - height: 32, - width: 32, - child: Center( - child: SvgPicture.asset( - Assets.svg.folder, - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, - width: 20, - height: 17.5, - ), - ), - ), - ), - ), - ), - ), - const SizedBox( - height: 24, - ), - Container( - alignment: Alignment.centerLeft, - padding: const EdgeInsets.only(left: 32), - child: Text( - "Create a passphrase", - style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context).extension<StackColors>()!.textDark3, - ), - textAlign: TextAlign.left, - ), - ), - const SizedBox( - height: 10, - ), - Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - ), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("createBackupPassphrase"), - focusNode: passphraseFocusNode, - controller: passphraseController, - style: STextStyles.desktopTextMedium(context).copyWith( - height: 2, - ), - obscureText: hidePassword, - enableSuggestions: false, - autocorrect: false, - decoration: standardInputDecoration( - "Create passphrase", - passphraseFocusNode, - context, - ).copyWith( - labelStyle: - STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: - Theme.of(context).extension<StackColors>()!.textDark3, - ), - suffixIcon: UnconstrainedBox( - child: GestureDetector( - key: const Key( - "createDesktopAutoBackupShowPassphraseButton1"), - onTap: () async { - setState(() { - hidePassword = !hidePassword; - }); - }, - child: Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(1000), - ), - height: 32, - width: 32, - child: Center( - child: SvgPicture.asset( - hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, - width: 20, - height: 17.5, + child: TextField( + autocorrect: false, + enableSuggestions: false, + onTap: Platform.isAndroid + ? null + : () async { + try { + await stackFileSystem.prepareStorage(); + + if (mounted) { + await stackFileSystem.pickDir(context); + } + + if (mounted) { + setState(() { + fileLocationController.text = + stackFileSystem.dirPath ?? ""; + }); + } + } catch (e, s) { + Logging.instance + .log("$e\n$s", level: LogLevel.Error); + } + }, + controller: fileLocationController, + style: STextStyles.field(context), + decoration: InputDecoration( + hintText: "Save to...", + hintStyle: STextStyles.fieldLabel(context), + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox( + width: 16, + ), + SvgPicture.asset( + Assets.svg.folder, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 16, + height: 16, + ), + const SizedBox( + width: 12, + ), + ], + ), ), ), + key: const Key( + "createBackupSaveToFileLocationTextFieldKey"), + readOnly: true, + toolbarOptions: const ToolbarOptions( + copy: true, + cut: false, + paste: false, + selectAll: false, + ), + onChanged: (newValue) { + // ref.read(addressEntryDataProvider(widget.id)).address = newValue; + }, ), + ); + }), + if (!Platform.isAndroid) + const SizedBox( + height: 24, + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only(bottom: 10.0), + child: Text( + "Create a passphrase", + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + textAlign: TextAlign.left, ), ), - ), - ), - ), - ), - const SizedBox( - height: 16, - ), - Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - ), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("createBackupPassphrase"), - focusNode: passphraseRepeatFocusNode, - controller: passphraseRepeatController, - style: STextStyles.desktopTextMedium(context).copyWith( - height: 2, - ), - obscureText: hidePassword, - enableSuggestions: false, - autocorrect: false, - decoration: standardInputDecoration( - "Confirm passphrase", - passphraseRepeatFocusNode, - context, - ).copyWith( - labelStyle: - STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: - Theme.of(context).extension<StackColors>()!.textDark3, + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - suffixIcon: UnconstrainedBox( - child: GestureDetector( - key: const Key( - "createDesktopAutoBackupShowPassphraseButton2"), - onTap: () async { + child: TextField( + key: const Key("createBackupPasswordFieldKey1"), + focusNode: passphraseFocusNode, + controller: passphraseController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Create passphrase", + passphraseFocusNode, + context, + ).copyWith( + labelStyle: + isDesktop ? STextStyles.fieldLabel(context) : null, + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox( + width: 16, + ), + GestureDetector( + key: const Key( + "createBackupPasswordFieldShowPasswordButtonKey"), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 16, + height: 16, + ), + ), + const SizedBox( + width: 12, + ), + ], + ), + ), + ), + onChanged: (newValue) { + if (newValue.isEmpty) { setState(() { - hidePassword = !hidePassword; + passwordFeedback = ""; }); - }, - child: Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(1000), - ), - height: 32, - width: 32, - child: Center( - child: SvgPicture.asset( - hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, - width: 20, - height: 17.5, - ), + return; + } + final result = zxcvbn.evaluate(newValue); + String suggestionsAndTips = ""; + for (var sug in result.feedback.suggestions!.toSet()) { + suggestionsAndTips += "$sug\n"; + } + suggestionsAndTips += result.feedback.warning!; + String feedback = + // "Password Strength: ${((result.score! / 4.0) * 100).toInt()}%\n" + suggestionsAndTips; + + passwordStrength = result.score! / 4; + + // hack fix to format back string returned from zxcvbn + if (feedback.contains("phrasesNo need")) { + feedback = feedback.replaceFirst( + "phrasesNo need", "phrases\nNo need"); + } + + if (feedback.endsWith("\n")) { + feedback = feedback.substring(0, feedback.length - 2); + } + + setState(() { + passwordFeedback = feedback; + }); + }, + ), + ), + if (passphraseFocusNode.hasFocus || + passphraseRepeatFocusNode.hasFocus || + passphraseController.text.isNotEmpty) + Padding( + padding: EdgeInsets.only( + left: 12, + right: 12, + top: passwordFeedback.isNotEmpty ? 4 : 0, + ), + child: passwordFeedback.isNotEmpty + ? Text( + passwordFeedback, + style: STextStyles.infoSmall(context), + ) + : null, + ), + if (passphraseFocusNode.hasFocus || + passphraseRepeatFocusNode.hasFocus || + passphraseController.text.isNotEmpty) + Padding( + padding: const EdgeInsets.only( + left: 12, + right: 12, + top: 10, + ), + child: ProgressBar( + key: const Key("createStackBackUpProgressBar"), + width: 510, + height: 5, + fillColor: passwordStrength < 0.51 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorRed + : passwordStrength < 1 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorYellow + : Theme.of(context) + .extension<StackColors>()! + .accentColorGreen, + backgroundColor: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + percent: + passwordStrength < 0.25 ? 0.03 : passwordStrength, + ), + ), + const SizedBox( + height: 10, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("createBackupPasswordFieldKey2"), + focusNode: passphraseRepeatFocusNode, + controller: passphraseRepeatController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Confirm passphrase", + passphraseRepeatFocusNode, + context, + ).copyWith( + labelStyle: STextStyles.fieldLabel(context), + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox( + width: 16, + ), + GestureDetector( + key: const Key( + "createBackupPasswordFieldShowPasswordButtonKey"), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 16, + height: 16, + ), + ), + const SizedBox( + width: 12, + ), + ], ), ), ), + onChanged: (newValue) { + setState(() {}); + // TODO: ? check if passwords match? + }, ), ), - ), + ], ), ), const SizedBox( @@ -376,6 +498,7 @@ class _CreateAutoBackup extends State<CreateAutoBackup> { }, ), ), + const Spacer(), Padding( padding: const EdgeInsets.all(32), child: Row( From 48e8501e27c8f2e8494c2746710c18be276dbce2 Mon Sep 17 00:00:00 2001 From: sneurlax <sneurlax@gmail.com> Date: Tue, 8 Nov 2022 13:35:27 -0600 Subject: [PATCH 170/426] cherrypick e41f8088b02c8aeaac8caaebac27dcbe7cc1d893 --- .../restore_options_view.dart | 36 +++++++++++++++---- .../restore_wallet_view.dart | 5 +++ .../coins/wownero/wownero_wallet.dart | 11 ++++-- lib/utilities/constants.dart | 6 +--- 4 files changed, 45 insertions(+), 13 deletions(-) diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart index 76e74fa14..1ce5d713a 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart @@ -252,7 +252,11 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> { SizedBox( height: isDesktop ? 40 : 24, ), - if (coin == Coin.monero || coin == Coin.epicCash) + if (coin == Coin.monero || + coin == Coin.epicCash || + (coin == Coin.wownero && + ref.watch(mnemonicWordCountStateProvider.state).state == + 25)) Text( "Choose start date", style: isDesktop @@ -264,11 +268,19 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> { : STextStyles.smallMed12(context), textAlign: TextAlign.left, ), - if (coin == Coin.monero || coin == Coin.epicCash) + if (coin == Coin.monero || + coin == Coin.epicCash || + (coin == Coin.wownero && + ref.watch(mnemonicWordCountStateProvider.state).state == + 25)) SizedBox( height: isDesktop ? 16 : 8, ), - if (coin == Coin.monero || coin == Coin.epicCash) + if (coin == Coin.monero || + coin == Coin.epicCash || + (coin == Coin.wownero && + ref.watch(mnemonicWordCountStateProvider.state).state == + 25)) // if (!isDesktop) RestoreFromDatePicker( @@ -278,11 +290,19 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> { // if (isDesktop) // // TODO desktop date picker - if (coin == Coin.monero || coin == Coin.epicCash) + if (coin == Coin.monero || + coin == Coin.epicCash || + (coin == Coin.wownero && + ref.watch(mnemonicWordCountStateProvider.state).state == + 25)) const SizedBox( height: 8, ), - if (coin == Coin.monero || coin == Coin.epicCash) + if (coin == Coin.monero || + coin == Coin.epicCash || + (coin == Coin.wownero && + ref.watch(mnemonicWordCountStateProvider.state).state == + 25)) RoundedWhiteContainer( child: Center( child: Text( @@ -299,7 +319,11 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> { ), ), ), - if (coin == Coin.monero || coin == Coin.epicCash) + if (coin == Coin.monero || + coin == Coin.epicCash || + (coin == Coin.wownero && + ref.watch(mnemonicWordCountStateProvider.state).state == + 25)) SizedBox( height: isDesktop ? 24 : 16, ), diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart index def0724b5..a6b7e7e77 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart @@ -149,6 +149,7 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> { super.dispose(); } + // TODO: check for wownero wordlist? bool _isValidMnemonicWord(String word) { // TODO: get the actual language if (widget.coin == Coin.monero) { @@ -181,6 +182,10 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> { if (widget.coin == Coin.monero) { height = monero.getHeigthByDate(date: widget.restoreFromDate); } + // todo: wait until this implemented + // else if (widget.coin == Coin.wownero) { + // height = wownero.getHeightByDate(date: widget.restoreFromDate); + // } // TODO: make more robust estimate of date maybe using https://explorer.epic.tech/api-index if (widget.coin == Coin.epicCash) { diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart index a0dc7bfe0..5c419afd1 100644 --- a/lib/services/coins/wownero/wownero_wallet.dart +++ b/lib/services/coins/wownero/wownero_wallet.dart @@ -948,6 +948,11 @@ class WowneroWallet extends CoinServiceAPI { required int maxNumberOfIndexesToCheck, required int height, }) async { + final int seedLength = mnemonic.trim().split(" ").length; + if (!(seedLength == 14 || seedLength == 25)) { + throw Exception("Invalid wownero mnemonic length found: $seedLength"); + } + await _prefs.init(); longMutex = true; final start = DateTime.now(); @@ -975,8 +980,10 @@ class WowneroWallet extends CoinServiceAPI { await _secureStore.write( key: '${_walletId}_mnemonic', value: mnemonic.trim()); - height = 0; - //height = getSeedHeightSync(mnemonic.trim()); // TODO use an alternative to wow_seed's get_seed_height + // extract seed height from 14 word seed + if (seedLength == 14) { + height = getSeedHeightSync(mnemonic.trim()); + } await DB.instance .put<dynamic>(boxName: walletId, key: "restoreHeight", value: height); diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index 4fb3fb54b..e27fbaa3d 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -35,10 +35,6 @@ abstract class Constants { static const int pinLength = 4; - // enable testnet - // TODO: currently unused - static const bool allowTestnets = true; - // Enable Logger.print statements static const bool disableLogger = false; @@ -66,7 +62,7 @@ abstract class Constants { values.addAll([25]); break; case Coin.wownero: - values.addAll([14]); + values.addAll([14, 25]); break; } return values; From d23f6f2823a2d53b578f930c11fb4b41e0b25a27 Mon Sep 17 00:00:00 2001 From: sneurlax <sneurlax@gmail.com> Date: Tue, 8 Nov 2022 13:48:29 -0600 Subject: [PATCH 171/426] return to use of final for bufferedCreateHeight using inline if and use wowlet's height estimation function for 14 word seeds --- crypto_plugins/flutter_libmonero | 2 +- lib/services/coins/wownero/wownero_wallet.dart | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index afdee4b88..2d5f5e563 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit afdee4b880202f39a2375afc320f0642e98a1827 +Subproject commit 2d5f5e5636bdc4b211b2236492268167b5b969d0 diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart index 5c419afd1..55f43cdd1 100644 --- a/lib/services/coins/wownero/wownero_wallet.dart +++ b/lib/services/coins/wownero/wownero_wallet.dart @@ -714,12 +714,8 @@ class WowneroWallet extends CoinServiceAPI { final wallet = await _walletCreationService?.create(credentials); // subtract a couple days to ensure we have a buffer for SWB - if (seedWordsLength == 14) { - final bufferedCreateHeight = getSeedHeightSync(wallet?.seed.trim() as String); - } else { - final bufferedCreateHeight = 0; - // TODO use an alternative to wow_seed's get_seed_height - } + final bufferedCreateHeight = (seedWordsLength == 14) ? getSeedHeightSync(wallet?.seed.trim() as String) : 0; + // TODO use an alternative to wow_seed's get_seed_height instead of 0 above await DB.instance.put<dynamic>( boxName: walletId, key: "restoreHeight", value: bufferedCreateHeight); From 015f33326948aff663299206ed47ba3157c31a42 Mon Sep 17 00:00:00 2001 From: sneurlax <sneurlax@gmail.com> Date: Tue, 8 Nov 2022 14:33:16 -0600 Subject: [PATCH 172/426] do not rely upon nullable variable --- crypto_plugins/flutter_libmonero | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index 2d5f5e563..0b355aee5 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit 2d5f5e5636bdc4b211b2236492268167b5b969d0 +Subproject commit 0b355aee55608f497ca54aba151d0b3e9e2c4579 From cede571350db905f4f70ba532f758c8869a9e15c Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 8 Nov 2022 13:50:53 -0600 Subject: [PATCH 173/426] desktop login/password screen --- .../desktop_login_view.dart | 166 ++++++++++++++++-- lib/utilities/text_styles.dart | 19 ++ .../custom_buttons/blue_text_button.dart | 11 +- 3 files changed, 179 insertions(+), 17 deletions(-) diff --git a/lib/pages_desktop_specific/desktop_login_view.dart b/lib/pages_desktop_specific/desktop_login_view.dart index c986bffde..1c70a5d98 100644 --- a/lib/pages_desktop_specific/desktop_login_view.dart +++ b/lib/pages_desktop_specific/desktop_login_view.dart @@ -1,7 +1,14 @@ import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; class DesktopLoginView extends StatefulWidget { const DesktopLoginView({ @@ -18,28 +25,155 @@ class DesktopLoginView extends StatefulWidget { } class _DesktopLoginViewState extends State<DesktopLoginView> { + late final TextEditingController passwordController; + + late final FocusNode passwordFocusNode; + + bool hidePassword = true; + bool _continueEnabled = false; + + @override + void initState() { + passwordController = TextEditingController(); + passwordFocusNode = FocusNode(); + + super.initState(); + } + + @override + void dispose() { + passwordController.dispose(); + passwordFocusNode.dispose(); + + super.dispose(); + } + @override Widget build(BuildContext context) { - return Material( - child: Column( + return DesktopScaffold( + body: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text( - "Login", - style: STextStyles.desktopH3(context), - ), - PrimaryButton( - label: "Login", - onPressed: () { - // todo auth + SizedBox( + width: 480, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset( + Assets.svg.stackIcon(context), + width: 100, + ), + const SizedBox( + height: 42, + ), + Text( + "Stack Wallet", + style: STextStyles.desktopH1(context), + ), + const SizedBox( + height: 24, + ), + SizedBox( + width: 350, + child: Text( + "Open source multicoin wallet for everyone", + textAlign: TextAlign.center, + style: STextStyles.desktopSubtitleH1(context), + ), + ), + const SizedBox( + height: 24, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("desktopLoginPasswordFieldKey"), + focusNode: passwordFocusNode, + controller: passwordController, + style: STextStyles.desktopTextMedium(context).copyWith( + height: 2, + ), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Enter password", + passwordFocusNode, + context, + ).copyWith( + suffixIcon: UnconstrainedBox( + child: SizedBox( + height: 70, + child: Row( + children: [ + const SizedBox( + width: 24, + ), + GestureDetector( + key: const Key( + "restoreFromFilePasswordFieldShowPasswordButtonKey"), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 24, + height: 24, + ), + ), + const SizedBox( + width: 12, + ), + ], + ), + ), + ), + ), + onChanged: (newValue) { + setState(() { + _continueEnabled = passwordController.text.isNotEmpty; + }); + }, + ), + ), + const SizedBox( + height: 24, + ), + PrimaryButton( + label: "Continue", + enabled: _continueEnabled, + onPressed: () { + // todo auth - Navigator.of(context).pushNamedAndRemoveUntil( - DesktopHomeView.routeName, - (route) => false, - ); - }, - ) + Navigator.of(context).pushNamedAndRemoveUntil( + DesktopHomeView.routeName, + (route) => false, + ); + }, + ), + const SizedBox( + height: 60, + ), + BlueTextButton( + text: "Forgot password?", + textSize: 20, + onTap: () { + // todo: new screen + }, + ), + ], + ), + ), ], ), ); diff --git a/lib/utilities/text_styles.dart b/lib/utilities/text_styles.dart index 299ba5bec..63aa19afb 100644 --- a/lib/utilities/text_styles.dart +++ b/lib/utilities/text_styles.dart @@ -508,6 +508,25 @@ class STextStyles { // Desktop + static TextStyle desktopH1(BuildContext context) { + switch (_theme(context).themeType) { + case ThemeType.light: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 40, + height: 40 / 40, + ); + case ThemeType.dark: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 40, + height: 40 / 40, + ); + } + } + static TextStyle desktopH2(BuildContext context) { switch (_theme(context).themeType) { case ThemeType.light: diff --git a/lib/widgets/custom_buttons/blue_text_button.dart b/lib/widgets/custom_buttons/blue_text_button.dart index aa7f75b1f..a87d1e6b2 100644 --- a/lib/widgets/custom_buttons/blue_text_button.dart +++ b/lib/widgets/custom_buttons/blue_text_button.dart @@ -10,11 +10,13 @@ class BlueTextButton extends ConsumerStatefulWidget { required this.text, this.onTap, this.enabled = true, + this.textSize, }) : super(key: key); final String text; final VoidCallback? onTap; final bool enabled; + final double? textSize; @override ConsumerState<BlueTextButton> createState() => _BlueTextButtonState(); @@ -67,7 +69,14 @@ class _BlueTextButtonState extends ConsumerState<BlueTextButton> textAlign: TextAlign.center, text: TextSpan( text: widget.text, - style: STextStyles.link2(context).copyWith(color: color), + style: widget.textSize == null + ? STextStyles.link2(context).copyWith( + color: color, + ) + : STextStyles.link2(context).copyWith( + color: color, + fontSize: widget.textSize, + ), recognizer: widget.enabled ? (TapGestureRecognizer() ..onTap = () { From 97b4407957639d50a1cc9a52c710b6ef9a500baa Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 8 Nov 2022 14:04:00 -0600 Subject: [PATCH 174/426] desktop forgot password ui --- .../desktop_login_view.dart | 5 +- .../forgot_password_desktop_view.dart | 101 ++++++++++++++++++ lib/route_generator.dart | 7 ++ 3 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 lib/pages_desktop_specific/forgot_password_desktop_view.dart diff --git a/lib/pages_desktop_specific/desktop_login_view.dart b/lib/pages_desktop_specific/desktop_login_view.dart index 1c70a5d98..fe05d719f 100644 --- a/lib/pages_desktop_specific/desktop_login_view.dart +++ b/lib/pages_desktop_specific/desktop_login_view.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages_desktop_specific/forgot_password_desktop_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -168,7 +169,9 @@ class _DesktopLoginViewState extends State<DesktopLoginView> { text: "Forgot password?", textSize: 20, onTap: () { - // todo: new screen + Navigator.of(context).pushNamed( + ForgotPasswordDesktopView.routeName, + ); }, ), ], diff --git a/lib/pages_desktop_specific/forgot_password_desktop_view.dart b/lib/pages_desktop_specific/forgot_password_desktop_view.dart new file mode 100644 index 000000000..d501cbd38 --- /dev/null +++ b/lib/pages_desktop_specific/forgot_password_desktop_view.dart @@ -0,0 +1,101 @@ +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/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; + +class ForgotPasswordDesktopView extends StatefulWidget { + const ForgotPasswordDesktopView({ + Key? key, + }) : super(key: key); + + static const String routeName = "/forgotPasswordDesktop"; + + @override + State<ForgotPasswordDesktopView> createState() => + _ForgotPasswordDesktopViewState(); +} + +class _ForgotPasswordDesktopViewState extends State<ForgotPasswordDesktopView> { + @override + Widget build(BuildContext context) { + return DesktopScaffold( + appBar: DesktopAppBar( + leading: AppBarBackButton( + onPressed: () async { + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + isCompactHeight: false, + ), + body: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: 480, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset( + Assets.svg.stackIcon(context), + width: 100, + ), + const SizedBox( + height: 42, + ), + Text( + "Stack Wallet", + style: STextStyles.desktopH1(context), + ), + const SizedBox( + height: 24, + ), + SizedBox( + width: 400, + child: Text( + "Stack Wallet does not store your password. Create new wallet or use a Stack backup file to restore your wallet.", + textAlign: TextAlign.center, + style: STextStyles.desktopTextSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ), + ), + const SizedBox( + height: 48, + ), + PrimaryButton( + label: "Create new wallet", + onPressed: () { + // // todo delete everything and start fresh? + }, + ), + const SizedBox( + height: 24, + ), + SecondaryButton( + label: "Restore from backup", + onPressed: () { + // todo SWB restore + }, + ), + const SizedBox( + height: kDesktopAppBarHeight, + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 40f11dc57..30963781b 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -85,6 +85,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/create_password/create_password_view.dart'; +import 'package:stackwallet/pages_desktop_specific/forgot_password_desktop_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_settings_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_view.dart'; @@ -998,6 +999,12 @@ class RouteGenerator { builder: (_) => const CreatePasswordView(), settings: RouteSettings(name: settings.name)); + case ForgotPasswordDesktopView.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const ForgotPasswordDesktopView(), + settings: RouteSettings(name: settings.name)); + case DesktopHomeView.routeName: return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, From c66e382fc380c45afb15690fbb69467ba1bde0d8 Mon Sep 17 00:00:00 2001 From: sneurlax <sneurlax@gmail.com> Date: Tue, 8 Nov 2022 17:30:09 -0600 Subject: [PATCH 175/426] get appropriate WowneroWordList based on seed length --- crypto_plugins/flutter_libmonero | 2 +- .../restore_wallet_view/restore_wallet_view.dart | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index 0b355aee5..e440e9a3a 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit 0b355aee55608f497ca54aba151d0b3e9e2c4579 +Subproject commit e440e9a3a125ee2030551ad7dea9114dd6a06aa0 diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart index a6b7e7e77..7596d7ac8 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart @@ -8,6 +8,7 @@ import 'package:bip39/src/wordlists/english.dart' as bip39wordlist; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_libmonero/monero/monero.dart'; +import 'package:flutter_libmonero/wownero/wownero.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; @@ -149,13 +150,17 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> { super.dispose(); } - // TODO: check for wownero wordlist? bool _isValidMnemonicWord(String word) { // TODO: get the actual language if (widget.coin == Coin.monero) { var moneroWordList = monero.getMoneroWordList("English"); return moneroWordList.contains(word); } + if (widget.coin == Coin.wownero) { + var wowneroWordList = wownero.getWowneroWordList("English", + seedWordsLength: widget.seedWordsLength); + return wowneroWordList.contains(word); + } return _wordListHashSet.contains(word); } From a94e66da9eec4ea809cc8b4a09e717c4fc852e62 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 8 Nov 2022 19:07:18 -0600 Subject: [PATCH 176/426] temp disable wow 25 word option in ui --- lib/utilities/constants.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index e27fbaa3d..e170dad2a 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -62,7 +62,9 @@ abstract class Constants { values.addAll([25]); break; case Coin.wownero: - values.addAll([14, 25]); + values.addAll([14]); + // todo: uncomment when wownero 25 word seeds implemented + // values.addAll([14, 25]); break; } return values; From a54d9a561e50593b6fe13a204776d631f7545f77 Mon Sep 17 00:00:00 2001 From: sneurlax <sneurlax@gmail.com> Date: Tue, 8 Nov 2022 23:10:27 -0600 Subject: [PATCH 177/426] track changes in flutter_libmonero --- crypto_plugins/flutter_libmonero | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index e440e9a3a..9267fd0f0 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit e440e9a3a125ee2030551ad7dea9114dd6a06aa0 +Subproject commit 9267fd0f0442a8d54b899473d77fc92ddc6d2391 From 357b93d6e897a2ece44eacdee8c32eac578177df Mon Sep 17 00:00:00 2001 From: sneurlax <sneurlax@gmail.com> Date: Wed, 9 Nov 2022 00:16:21 -0600 Subject: [PATCH 178/426] use wownero.getHeightByDate and save bufferedHeight upon Monero wallet creation --- crypto_plugins/flutter_libmonero | 2 +- .../restore_wallet_view.dart | 6 ++--- lib/services/coins/monero/monero_wallet.dart | 2 +- .../coins/wownero/wownero_wallet.dart | 23 +++++++++++++------ 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index 9267fd0f0..e705ba2d5 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit 9267fd0f0442a8d54b899473d77fc92ddc6d2391 +Subproject commit e705ba2d5126685adae9367b62921b676d7126ed diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart index 7596d7ac8..913302a98 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart @@ -186,11 +186,9 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> { if (widget.coin == Coin.monero) { height = monero.getHeigthByDate(date: widget.restoreFromDate); + } else if (widget.coin == Coin.wownero) { + height = wownero.getHeightByDate(date: widget.restoreFromDate); } - // todo: wait until this implemented - // else if (widget.coin == Coin.wownero) { - // height = wownero.getHeightByDate(date: widget.restoreFromDate); - // } // TODO: make more robust estimate of date maybe using https://explorer.epic.tech/api-index if (widget.coin == Coin.epicCash) { diff --git a/lib/services/coins/monero/monero_wallet.dart b/lib/services/coins/monero/monero_wallet.dart index b0ebac4e6..8e7873014 100644 --- a/lib/services/coins/monero/monero_wallet.dart +++ b/lib/services/coins/monero/monero_wallet.dart @@ -699,7 +699,7 @@ class MoneroWallet extends CoinServiceAPI { name: name, type: WalletType.monero, isRecovery: false, - restoreHeight: credentials.height ?? 0, + restoreHeight: bufferedCreateHeight, date: DateTime.now(), path: path, dirPath: dirPath, diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart index 55f43cdd1..0134cb1fe 100644 --- a/lib/services/coins/wownero/wownero_wallet.dart +++ b/lib/services/coins/wownero/wownero_wallet.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:cw_core/get_height_by_date.dart'; import 'package:cw_core/monero_transaction_priority.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/pending_transaction.dart'; @@ -685,10 +686,7 @@ class WowneroWallet extends CoinServiceAPI { await pathForWalletDir(name: name, type: WalletType.wownero); final path = await pathForWallet(name: name, type: WalletType.wownero); credentials = wownero.createWowneroNewWalletCredentials( - name: name, - language: "English", - seedWordsLength: seedWordsLength - ); + name: name, language: "English", seedWordsLength: seedWordsLength); walletInfo = WalletInfo.external( id: WalletBase.idFor(name, WalletType.wownero), @@ -713,9 +711,12 @@ class WowneroWallet extends CoinServiceAPI { // To restore from a seed final wallet = await _walletCreationService?.create(credentials); - // subtract a couple days to ensure we have a buffer for SWB - final bufferedCreateHeight = (seedWordsLength == 14) ? getSeedHeightSync(wallet?.seed.trim() as String) : 0; - // TODO use an alternative to wow_seed's get_seed_height instead of 0 above + final bufferedCreateHeight = (seedWordsLength == 14) + ? getSeedHeightSync(wallet?.seed.trim() as String) + : wownero.getHeightByDate( + date: DateTime.now().subtract(const Duration( + days: + 2))); // subtract a couple days to ensure we have a buffer for SWB await DB.instance.put<dynamic>( boxName: walletId, key: "restoreHeight", value: bufferedCreateHeight); @@ -979,6 +980,14 @@ class WowneroWallet extends CoinServiceAPI { // extract seed height from 14 word seed if (seedLength == 14) { height = getSeedHeightSync(mnemonic.trim()); + } else { + // 25 word seed. TODO validate + if (height == 0) { + height = wownero.getHeightByDate( + date: DateTime.now().subtract(const Duration( + days: + 2))); // subtract a couple days to ensure we have a buffer for SWB\ + } } await DB.instance From 510233255f7a75b8f881754d5f60b2e0379272ff Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 9 Nov 2022 09:46:59 -0600 Subject: [PATCH 179/426] desktop swb restore fix --- .../restore_from_file_view.dart | 76 ++++++++++--------- 1 file changed, 42 insertions(+), 34 deletions(-) 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 f7a9883de..014099abd 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 @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; @@ -424,43 +425,47 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { } bool shouldPop = false; - await showDialog<dynamic>( - barrierDismissible: false, - context: context, - builder: (_) => WillPopScope( - onWillPop: () async { - return shouldPop; - }, - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Material( - color: Colors.transparent, - child: Center( - child: Text( - "Decrypting Stack backup file", - style: STextStyles.pageTitleH2( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textWhite, + unawaited( + showDialog<dynamic>( + barrierDismissible: false, + context: context, + builder: (_) => WillPopScope( + onWillPop: () async { + return shouldPop; + }, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Material( + color: Colors.transparent, + child: Center( + child: Text( + "Decrypting Stack backup file", + style: + STextStyles.pageTitleH2( + context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textWhite, + ), ), ), ), - ), - const SizedBox( - height: 64, - ), - const Center( - child: LoadingIndicator( - width: 100, + const SizedBox( + height: 64, ), - ), - ], + const Center( + child: LoadingIndicator( + width: 100, + ), + ), + ], + ), ), ), ); @@ -475,7 +480,10 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { if (mounted) { // pop LoadingIndicator shouldPop = true; - Navigator.of(context).pop(); + Navigator.of( + context, + rootNavigator: true, + ).pop(); passwordController.text = ""; From 041e23a5a5a18682daf352bab79242fb654a6403 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Wed, 9 Nov 2022 08:57:41 -0700 Subject: [PATCH 180/426] resolved route_generator conflict --- .../desktop_address_book.dart | 139 ++++++++++++++++++ .../home/desktop_home_view.dart | 8 +- lib/route_generator.dart | 7 + 3 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart diff --git a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart new file mode 100644 index 000000000..3622fcf1e --- /dev/null +++ b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart @@ -0,0 +1,139 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; + +class DesktopAddressBook extends ConsumerStatefulWidget { + const DesktopAddressBook({Key? key}) : super(key: key); + + static const String routeName = "/desktopAddressBook"; + + @override + ConsumerState<DesktopAddressBook> createState() => _DesktopAddressBook(); +} + +class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { + late final TextEditingController _searchController; + + late final FocusNode _searchFocusNode; + + String filter = ""; + + @override + void initState() { + _searchController = TextEditingController(); + _searchFocusNode = FocusNode(); + + super.initState(); + } + + @override + void dispose() { + _searchController.dispose(); + _searchFocusNode.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + final hasWallets = ref.watch(walletsChangeNotifierProvider).hasWallets; + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + DesktopAppBar( + isCompactHeight: true, + leading: Row( + children: [ + const SizedBox( + width: 24, + ), + Text( + "Address Book", + style: STextStyles.desktopH3(context), + ) + ], + ), + ), + const SizedBox(height: 53), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Row( + children: [ + SizedBox( + height: 60, + width: 489, + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: false, + enableSuggestions: false, + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: (newString) { + setState(() => filter = newString); + }, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search...", + _searchFocusNode, + context, + ).copyWith( + labelStyle: STextStyles.fieldLabel(context) + .copyWith(fontSize: 16), + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), + ), + suffixIcon: _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + filter = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + ), + ], + ), + ), + // Expanded( + // child: hasWallets ? const MyWallets() : const EmptyWallets(), + // ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/desktop_home_view.dart b/lib/pages_desktop_specific/home/desktop_home_view.dart index 14d2dae03..fab78e1f4 100644 --- a/lib/pages_desktop_specific/home/desktop_home_view.dart +++ b/lib/pages_desktop_specific/home/desktop_home_view.dart @@ -8,6 +8,8 @@ import 'package:stackwallet/pages_desktop_specific/home/support_and_about_view/d import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'address_book_view/desktop_address_book.dart'; + class DesktopHomeView extends ConsumerStatefulWidget { const DesktopHomeView({Key? key}) : super(key: key); @@ -31,8 +33,10 @@ class _DesktopHomeViewState extends ConsumerState<DesktopHomeView> { Container( color: Colors.red, ), - Container( - color: Colors.orange, + const Navigator( + key: Key("desktopAddressBookHomeKey"), + onGenerateRoute: RouteGenerator.generateRoute, + initialRoute: DesktopAddressBook.routeName, ), const Navigator( key: Key("desktopSettingHomeKey"), diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 30963781b..f3e37e383 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -86,6 +86,7 @@ import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; import 'package:stackwallet/pages/wallets_view/wallets_view.dart'; import 'package:stackwallet/pages_desktop_specific/create_password/create_password_view.dart'; import 'package:stackwallet/pages_desktop_specific/forgot_password_desktop_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/address_book_view/desktop_address_book.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_settings_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_view.dart'; @@ -1105,6 +1106,12 @@ class RouteGenerator { builder: (_) => const DesktopAboutView(), settings: RouteSettings(name: settings.name)); + case DesktopAddressBook.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const DesktopAddressBook(), + settings: RouteSettings(name: settings.name)); + case WalletKeysDesktopPopup.routeName: if (args is List<String>) { return FadePageRoute( From 095f9c4ed9a7d692047131e1bdc043043822971e Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 9 Nov 2022 12:54:10 -0600 Subject: [PATCH 181/426] mobile swb restore unawaited --- .../restore_from_file_view.dart | 67 ++++++++++--------- 1 file changed, 35 insertions(+), 32 deletions(-) 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 014099abd..a237d9ea9 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 @@ -316,42 +316,45 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { } bool shouldPop = false; - await showDialog<dynamic>( - barrierDismissible: false, - context: context, - builder: (_) => WillPopScope( - onWillPop: () async { - return shouldPop; - }, - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Material( - color: Colors.transparent, - child: Center( - child: Text( - "Decrypting Stack backup file", - style: - STextStyles.pageTitleH2(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textWhite, + unawaited( + showDialog<dynamic>( + barrierDismissible: false, + context: context, + builder: (_) => WillPopScope( + onWillPop: () async { + return shouldPop; + }, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Material( + color: Colors.transparent, + child: Center( + child: Text( + "Decrypting Stack backup file", + style: STextStyles.pageTitleH2( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textWhite, + ), ), ), ), - ), - const SizedBox( - height: 64, - ), - const Center( - child: LoadingIndicator( - width: 100, + const SizedBox( + height: 64, ), - ), - ], + const Center( + child: LoadingIndicator( + width: 100, + ), + ), + ], + ), ), ), ); From d15f022c4d6d86c5341972fb6c4824b24a01a503 Mon Sep 17 00:00:00 2001 From: sneurlax <sneurlax@gmail.com> Date: Wed, 9 Nov 2022 14:09:12 -0600 Subject: [PATCH 182/426] update wownero's first blocks per month --- crypto_plugins/flutter_libmonero | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index e705ba2d5..b9bc2dcc5 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit e705ba2d5126685adae9367b62921b676d7126ed +Subproject commit b9bc2dcc56e13f235a6c5b0fc02c0e543eb87758 From 61f945aa98e8bb47858155556c37c1fd12585299 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Wed, 9 Nov 2022 13:13:42 -0700 Subject: [PATCH 183/426] dialog uses BackupFrequencyTypes and has offset for dropDownButton --- .../create_auto_backup.dart | 136 ++++++++++++------ 1 file changed, 93 insertions(+), 43 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart index 57a8d7a64..07a1f1b78 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart @@ -1,11 +1,14 @@ import 'dart:io'; +import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/stack_file_system.dart'; +import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; import 'package:stackwallet/utilities/enums/log_level_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -19,14 +22,14 @@ import 'package:stackwallet/widgets/progress_bar.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:zxcvbn/zxcvbn.dart'; -class CreateAutoBackup extends StatefulWidget { +class CreateAutoBackup extends ConsumerStatefulWidget { const CreateAutoBackup({Key? key}) : super(key: key); @override - State<StatefulWidget> createState() => _CreateAutoBackup(); + ConsumerState<CreateAutoBackup> createState() => _CreateAutoBackup(); } -class _CreateAutoBackup extends State<CreateAutoBackup> { +class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> { late final TextEditingController fileLocationController; late final TextEditingController passphraseController; late final TextEditingController passphraseRepeatController; @@ -52,12 +55,13 @@ class _CreateAutoBackup extends State<CreateAutoBackup> { bool get fieldsMatch => passphraseController.text == passphraseRepeatController.text; - String _currentDropDownValue = "Every 10 minutes"; + BackupFrequencyType _currentDropDownValue = + BackupFrequencyType.everyTenMinutes; - final List<String> _dropDownItems = [ - "Every 10 minutes", - "Every 20 minutes", - "Every 30 minutes", + final List<BackupFrequencyType> _dropDownItems = [ + BackupFrequencyType.everyTenMinutes, + BackupFrequencyType.everyAppStart, + BackupFrequencyType.afterClosingAWallet, ]; @override @@ -101,6 +105,9 @@ class _CreateAutoBackup extends State<CreateAutoBackup> { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType "); + bool isEnabledAutoBackup = ref.watch(prefsChangeNotifierProvider + .select((value) => value.isAutoBackupEnabled)); + String? selectedItem = "Every 10 minutes"; final isDesktop = Util.isDesktop; return DesktopDialog( @@ -225,9 +232,7 @@ class _CreateAutoBackup extends State<CreateAutoBackup> { paste: false, selectAll: false, ), - onChanged: (newValue) { - // ref.read(addressEntryDataProvider(widget.id)).address = newValue; - }, + onChanged: (newValue) {}, ), ); }), @@ -361,7 +366,7 @@ class _CreateAutoBackup extends State<CreateAutoBackup> { ), child: ProgressBar( key: const Key("createStackBackUpProgressBar"), - width: 510, + width: 512, height: 5, fillColor: passwordStrength < 0.51 ? Theme.of(context) @@ -465,38 +470,83 @@ class _CreateAutoBackup extends State<CreateAutoBackup> { left: 32, right: 32, ), - child: DropdownButtonFormField( - isExpanded: true, - elevation: 0, - style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context).extension<StackColors>()!.textDark, - ), - icon: SvgPicture.asset( - Assets.svg.chevronDown, - width: 10, - height: 5, - color: Theme.of(context).extension<StackColors>()!.textDark3, - ), - dropdownColor: - Theme.of(context).extension<StackColors>()!.textFieldActiveBG, - // focusColor: , - value: _currentDropDownValue, - items: _dropDownItems - .map( - (e) => DropdownMenuItem( - value: e, - child: Text(e), + child: isDesktop + ? DropdownButtonHideUnderline( + child: DropdownButton2( + offset: Offset(0, -10), + isExpanded: true, + dropdownElevation: 0, + value: _currentDropDownValue, + items: [ + ..._dropDownItems.map( + (e) { + String message = ""; + switch (e) { + case BackupFrequencyType.everyTenMinutes: + message = "Every 10 minutes"; + break; + case BackupFrequencyType.everyAppStart: + message = "Every app startup"; + break; + case BackupFrequencyType.afterClosingAWallet: + message = + "After closing a cryptocurrency wallet"; + break; + } + + return DropdownMenuItem( + value: e, + child: Text(message), + ); + }, + ), + ], + onChanged: (value) { + if (value is BackupFrequencyType) { + if (ref + .read(prefsChangeNotifierProvider) + .backupFrequencyType != + value) { + ref + .read(prefsChangeNotifierProvider) + .backupFrequencyType = value; + } + setState(() { + _currentDropDownValue = value; + }); + } + }, + icon: SvgPicture.asset( + Assets.svg.chevronDown, + width: 10, + height: 5, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + ), + buttonPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + buttonDecoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + dropdownDecoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), ), ) - .toList(), - onChanged: (value) { - if (value is String) { - setState(() { - _currentDropDownValue = value; - }); - } - }, - ), + : null, ), const Spacer(), Padding( @@ -518,7 +568,7 @@ class _CreateAutoBackup extends State<CreateAutoBackup> { Expanded( child: PrimaryButton( label: "Enable Auto Backup", - enabled: false, + enabled: shouldEnableCreate, onPressed: () {}, ), ) From cc779be46031b2491ad817af592d7d9266b1598b Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Wed, 9 Nov 2022 15:32:39 -0700 Subject: [PATCH 184/426] enable and disable auto back up --- .../backup_and_restore_settings.dart | 235 +++++++++++++++++- .../create_auto_backup.dart | 180 +++++++++++++- lib/widgets/stack_dialog.dart | 18 +- 3 files changed, 415 insertions(+), 18 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart index 8928a268d..a444f4b51 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart @@ -2,15 +2,27 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:intl/intl.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart'; +import 'package:stackwallet/providers/global/locale_provider.dart'; +import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; +import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:url_launcher/url_launcher.dart'; +import '../../../../providers/global/auto_swb_service_provider.dart'; +import '../../../../widgets/custom_buttons/blue_text_button.dart'; + class BackupRestoreSettings extends ConsumerStatefulWidget { const BackupRestoreSettings({Key? key}) : super(key: key); @@ -24,6 +36,49 @@ class BackupRestoreSettings extends ConsumerStatefulWidget { class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { late bool createBackup = false; late bool restoreBackup = false; + // late bool isEnabledAutoBackup; + + final toggleController = DSBController(); + + late final TextEditingController fileLocationController; + late final TextEditingController passwordController; + late final TextEditingController frequencyController; + + late final FocusNode fileLocationFocusNode; + late final FocusNode passwordFocusNode; + + String prettySinceLastBackupString(DateTime? time) { + if (time == null) { + return "-"; + } + final difference = DateTime.now().difference(time); + int value; + String postfix; + if (difference < const Duration(seconds: 60)) { + value = difference.inSeconds; + postfix = "seconds"; + } else if (difference < const Duration(minutes: 60)) { + value = difference.inMinutes; + postfix = "minutes"; + } else if (difference < const Duration(hours: 24)) { + value = difference.inHours; + postfix = "hours"; + } else if (difference.inDays < 8) { + value = difference.inDays; + postfix = "days"; + } else { + // if greater than a week return the actual date + return DateFormat.yMMMMd( + ref.read(localeServiceChangeNotifierProvider).locale) + .format(time); + } + + if (value == 1) { + postfix = postfix.substring(0, postfix.length - 1); + } + + return "$value $postfix ago"; + } Future<void> enableAutoBackup(BuildContext context) async { await showDialog<dynamic>( @@ -36,10 +91,105 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { ); } + Future<void> attemptDisable() async { + final result = await showDialog<bool?>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return StackDialog( + title: "Disable Auto Backup", + message: + "You are turning off Auto Backup. You can turn it back on at any time. Your previous Auto Backup file will not be deleted. Remember to backup your wallets manually so you don't lose important information.", + leftButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Back", + style: STextStyles.button(context).copyWith( + color: + Theme.of(context).extension<StackColors>()!.accentColorDark, + ), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + rightButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + child: Text( + "Disable", + style: STextStyles.button(context), + ), + onPressed: () { + Navigator.of(context).pop(); + setState(() { + ref.watch(prefsChangeNotifierProvider).isAutoBackupEnabled = + false; + }); + }, + ), + ); + }, + ); + if (mounted) { + if (result is bool && result) { + ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled = false; + Navigator.of(context).pop(); + } else { + toggleController.activate?.call(); + } + } + } + + @override + void initState() { + fileLocationController = TextEditingController(); + passwordController = TextEditingController(); + frequencyController = TextEditingController(); + + passwordController.text = "---------------"; + fileLocationController.text = + ref.read(prefsChangeNotifierProvider).autoBackupLocation ?? " "; + frequencyController.text = Format.prettyFrequencyType( + ref.read(prefsChangeNotifierProvider).backupFrequencyType); + + fileLocationFocusNode = FocusNode(); + passwordFocusNode = FocusNode(); + + // _toggle = ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled; + super.initState(); + } + + @override + void dispose() { + fileLocationController.dispose(); + passwordController.dispose(); + frequencyController.dispose(); + + fileLocationFocusNode.dispose(); + passwordFocusNode.dispose(); + + super.dispose(); + } + @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); + bool isEnabledAutoBackup = ref.watch(prefsChangeNotifierProvider + .select((value) => value.isAutoBackupEnabled)); + + ref.listen( + prefsChangeNotifierProvider + .select((value) => value.backupFrequencyType), + (previous, BackupFrequencyType next) { + frequencyController.text = Format.prettyFrequencyType(next); + }); + return LayoutBuilder(builder: (context, constraints) { return SingleChildScrollView( scrollDirection: Axis.vertical, @@ -120,17 +270,80 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: EdgeInsets.all( - 10, - ), - child: PrimaryButton( - desktopMed: true, - width: 200, - label: "Enable auto backup", - onPressed: () { - enableAutoBackup(context); - }, - ), + padding: const EdgeInsets.all(10), + child: !isEnabledAutoBackup + ? PrimaryButton( + desktopMed: true, + width: 200, + label: "Enable auto backup", + onPressed: () { + enableAutoBackup(context); + }, + ) + : Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Container( + width: 403, + color: Theme.of(context) + .extension<StackColors>()! + .background, + child: Padding( + padding: + const EdgeInsets.all(8.0), + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Text( + "Backed up ${prettySinceLastBackupString(ref.watch(prefsChangeNotifierProvider.select((value) => value.lastAutoBackup)))}", + style: STextStyles + .itemSubtitle( + context), + ), + BlueTextButton( + text: "Back up now", + onTap: () { + ref + .read( + autoSWBServiceProvider) + .doBackup(); + }, + ), + ], + ), + ], + ), + ), + ), + const SizedBox( + height: 20, + ), + Row( + children: [ + PrimaryButton( + desktopMed: true, + width: 190, + label: "Disable auto backup", + onPressed: () { + attemptDisable(); + }, + ), + const SizedBox(width: 16), + SecondaryButton( + desktopMed: true, + width: 190, + label: "Edit auto backup", + onPressed: () {}, + ), + ], + ) + ], + ), ), ], ), diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart index 07a1f1b78..e804071cc 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart @@ -1,15 +1,24 @@ +import 'dart:convert'; import 'dart:io'; import 'package:dropdown_button2/dropdown_button2.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stack_wallet_backup/stack_wallet_backup.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/stack_file_system.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; +import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/enums/log_level_enum.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -19,11 +28,19 @@ 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/progress_bar.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:zxcvbn/zxcvbn.dart'; class CreateAutoBackup extends ConsumerStatefulWidget { - const CreateAutoBackup({Key? key}) : super(key: key); + const CreateAutoBackup({ + Key? key, + this.secureStore = const SecureStorageWrapper( + FlutterSecureStorage(), + ), + }) : super(key: key); + + final FlutterSecureStorageInterface secureStore; @override ConsumerState<CreateAutoBackup> createState() => _CreateAutoBackup(); @@ -34,6 +51,8 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> { late final TextEditingController passphraseController; late final TextEditingController passphraseRepeatController; + late final FlutterSecureStorageInterface secureStore; + late final StackFileSystem stackFileSystem; late final FocusNode passphraseFocusNode; late final FocusNode passphraseRepeatFocusNode; @@ -66,6 +85,7 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> { @override void initState() { + secureStore = widget.secureStore; stackFileSystem = StackFileSystem(); fileLocationController = TextEditingController(); @@ -569,7 +589,163 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> { child: PrimaryButton( label: "Enable Auto Backup", enabled: shouldEnableCreate, - onPressed: () {}, + onPressed: !shouldEnableCreate + ? null + : () async { + final String pathToSave = + fileLocationController.text; + final String passphrase = passphraseController.text; + final String repeatPassphrase = + passphraseRepeatController.text; + + if (pathToSave.isEmpty) { + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Directory not chosen", + context: context, + ); + return; + } + if (!(await Directory(pathToSave).exists())) { + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Directory does not exist", + context: context, + ); + return; + } + if (passphrase.isEmpty) { + showFloatingFlushBar( + type: FlushBarType.warning, + message: "A passphrase is required", + context: context, + ); + return; + } + if (passphrase != repeatPassphrase) { + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Passphrase does not match", + context: context, + ); + return; + } + + showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => const StackDialog( + title: "Encrypting initial backup", + message: "This shouldn't take long", + ), + ); + + // make sure the dialog is able to be displayed for at least some time + final fut = Future<void>.delayed( + const Duration(milliseconds: 300)); + + String adkString; + int adkVersion; + try { + final adk = + await compute(generateAdk, passphrase); + adkString = Format.uint8listToString(adk.item2); + adkVersion = adk.item1; + } on Exception catch (e, s) { + String err = getErrorMessageFromSWBException(e); + Logging.instance + .log("$err\n$s", level: LogLevel.Error); + // pop encryption progress dialog + Navigator.of(context).pop(); + showFloatingFlushBar( + type: FlushBarType.warning, + message: err, + context: context, + ); + return; + } catch (e, s) { + Logging.instance + .log("$e\n$s", level: LogLevel.Error); + // pop encryption progress dialog + Navigator.of(context).pop(); + showFloatingFlushBar( + type: FlushBarType.warning, + message: "$e", + context: context, + ); + return; + } + + await secureStore.write( + key: "auto_adk_string", value: adkString); + await secureStore.write( + key: "auto_adk_version_string", + value: adkVersion.toString()); + + final DateTime now = DateTime.now(); + final String fileToSave = + createAutoBackupFilename(pathToSave, now); + + final backup = await SWB.createStackWalletJSON(); + + bool result = await SWB.encryptStackWalletWithADK( + fileToSave, + adkString, + jsonEncode(backup), + adkVersion: adkVersion, + ); + + // this future should already be complete unless there was an error encrypting + await Future.wait([fut]); + + if (mounted) { + // pop encryption progress dialog + int count = 0; + Navigator.of(context) + .popUntil((_) => count++ >= 2); + + if (result) { + ref + .read(prefsChangeNotifierProvider) + .autoBackupLocation = pathToSave; + ref + .read(prefsChangeNotifierProvider) + .lastAutoBackup = now; + + ref + .read(prefsChangeNotifierProvider) + .isAutoBackupEnabled = true; + + await showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => Platform.isAndroid + ? StackOkDialog( + title: + "Stack Auto Backup enabled and saved to:", + message: fileToSave, + ) + : const StackOkDialog( + title: "Stack Auto Backup enabled!"), + ); + if (mounted) { + passphraseController.text = ""; + passphraseRepeatController.text = ""; + + int count = 0; + Navigator.of(context) + .popUntil((_) => count++ >= 2); + } + } else { + await showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => const StackOkDialog( + title: "Failed to enable Auto Backup"), + ); + } + } + }, ), ) ], diff --git a/lib/widgets/stack_dialog.dart b/lib/widgets/stack_dialog.dart index be1d51596..ea2638264 100644 --- a/lib/widgets/stack_dialog.dart +++ b/lib/widgets/stack_dialog.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; class StackDialogBase extends StatelessWidget { const StackDialogBase({ @@ -17,7 +18,8 @@ class StackDialogBase extends StatelessWidget { return Padding( padding: const EdgeInsets.all(16), child: Column( - mainAxisAlignment: MainAxisAlignment.end, + mainAxisAlignment: + !Util.isDesktop ? MainAxisAlignment.end : MainAxisAlignment.center, children: [ Material( borderRadius: BorderRadius.circular( @@ -179,10 +181,16 @@ class StackOkDialog extends StatelessWidget { ), Expanded( child: TextButton( - onPressed: () { - Navigator.of(context).pop(); - onOkPressed?.call("OK"); - }, + onPressed: !Util.isDesktop + ? () { + Navigator.of(context).pop(); + onOkPressed?.call("OK"); + } + : () { + int count = 0; + Navigator.of(context).popUntil((_) => count++ >= 2); + // onOkPressed?.call("OK"); + }, style: Theme.of(context) .extension<StackColors>()! .getPrimaryEnabledButtonColor(context), From 6d0452debb939df3e95f34082eacb4dc7a5712a7 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Wed, 9 Nov 2022 16:23:53 -0700 Subject: [PATCH 185/426] small import fix --- lib/pages_desktop_specific/home/desktop_home_view.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/pages_desktop_specific/home/desktop_home_view.dart b/lib/pages_desktop_specific/home/desktop_home_view.dart index fab78e1f4..cb8aba255 100644 --- a/lib/pages_desktop_specific/home/desktop_home_view.dart +++ b/lib/pages_desktop_specific/home/desktop_home_view.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages_desktop_specific/home/address_book_view/desktop_address_book.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_menu.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_settings_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_view.dart'; @@ -8,8 +9,6 @@ import 'package:stackwallet/pages_desktop_specific/home/support_and_about_view/d import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; -import 'address_book_view/desktop_address_book.dart'; - class DesktopHomeView extends ConsumerStatefulWidget { const DesktopHomeView({Key? key}) : super(key: key); From af47c67231eeb6cb1d04f2740a50847af55f9610 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 9 Nov 2022 12:58:38 -0600 Subject: [PATCH 186/426] BranchedParent class --- lib/widgets/conditional_parent.dart | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/lib/widgets/conditional_parent.dart b/lib/widgets/conditional_parent.dart index 757c8f992..e8c60884a 100644 --- a/lib/widgets/conditional_parent.dart +++ b/lib/widgets/conditional_parent.dart @@ -21,3 +21,27 @@ class ConditionalParent extends StatelessWidget { } } } + +class BranchedParent extends StatelessWidget { + const BranchedParent({ + Key? key, + required this.condition, + required this.conditionBranchBuilder, + required this.otherBranchBuilder, + required this.children, + }) : super(key: key); + + final bool condition; + final Widget Function(List<Widget>) conditionBranchBuilder; + final Widget Function(List<Widget>) otherBranchBuilder; + final List<Widget> children; + + @override + Widget build(BuildContext context) { + if (condition) { + return conditionBranchBuilder(children); + } else { + return otherBranchBuilder(children); + } + } +} From 2aa8dd2becce6c7dd39845273f6fc008db4f5ead Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 9 Nov 2022 13:12:41 -0600 Subject: [PATCH 187/426] WIP: trade details for desktop --- .../exchange_view/trade_details_view.dart | 1315 ++++++++++------- .../sub_widgets/transactions_list.dart | 87 +- 2 files changed, 872 insertions(+), 530 deletions(-) diff --git a/lib/pages/exchange_view/trade_details_view.dart b/lib/pages/exchange_view/trade_details_view.dart index 76c845027..602d588da 100644 --- a/lib/pages/exchange_view/trade_details_view.dart +++ b/lib/pages/exchange_view/trade_details_view.dart @@ -15,17 +15,23 @@ import 'package:stackwallet/pages/wallet_view/transaction_views/edit_note_view.d import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; import 'package:stackwallet/providers/global/trades_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart'; import 'package:stackwallet/services/exchange/exchange.dart'; import 'package:stackwallet/services/exchange/simpleswap/simpleswap_exchange.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; +import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -157,34 +163,113 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> { final sendAmount = Decimal.tryParse(trade.payInAmount) ?? Decimal.parse("-1"); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( + final isDesktop = Util.isDesktop; + + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Scaffold( backgroundColor: Theme.of(context).extension<StackColors>()!.background, - leading: AppBarBackButton( - onPressed: () async { - Navigator.of(context).pop(); - }, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Trade details", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Trade details", - style: STextStyles.navBarTitle(context), + body: Padding( + padding: const EdgeInsets.all(12), + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), + ), ), ), - body: Padding( - padding: const EdgeInsets.all(12), - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( + child: Padding( + padding: isDesktop + ? const EdgeInsets.only(left: 32) + : const EdgeInsets.all(0), + child: BranchedParent( + condition: isDesktop, + conditionBranchBuilder: (children) => Padding( + padding: const EdgeInsets.only( + right: 20, + ), + child: Padding( + padding: const EdgeInsets.only( + right: 12, + ), + child: RoundedWhiteContainer( + borderColor: isDesktop + ? Theme.of(context).extension<StackColors>()!.background + : null, + padding: const EdgeInsets.all(0), + child: ListView( + primary: false, + shrinkWrap: true, + children: children, + ), + ), + ), + ), + otherBranchBuilder: (children) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max, + children: children, + ), + children: [ + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(0) + : const EdgeInsets.all(12), + child: Container( + decoration: isDesktop + ? BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .background, + borderRadius: BorderRadius.vertical( + top: Radius.circular( + Constants.size.circularBorderRadius, + ), + ), + ) + : null, + child: Padding( + padding: isDesktop + ? const EdgeInsets.all(12) + : const EdgeInsets.all(0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + if (isDesktop) + Row( + children: [ + SvgPicture.asset( + _fetchIconAssetForStatus(trade.status), + width: 32, + height: 32, + ), + const SizedBox( + width: 16, + ), + SelectableText( + "Exchange", + style: STextStyles.desktopTextMedium(context), + ), + ], + ), Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: isDesktop + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, children: [ SelectableText( "${trade.payInCurrency.toUpperCase()} → ${trade.payOutCurrency.toUpperCase()}", @@ -194,7 +279,7 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> { height: 4, ), SelectableText( - "${Format.localizedStringAsFixed(value: sendAmount, locale: ref.watch( + "-${Format.localizedStringAsFixed(value: sendAmount, locale: ref.watch( localeServiceChangeNotifierProvider .select((value) => value.locale), ), decimalPlaces: trade.payInCurrency.toLowerCase() == "xmr" ? 12 : 8)} ${trade.payInCurrency.toUpperCase()}", @@ -202,136 +287,178 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> { ), ], ), - Container( - width: 32, - height: 32, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(32), - ), - child: Center( - child: SvgPicture.asset( - _fetchIconAssetForStatus(trade.status), - width: 32, - height: 32, + if (!isDesktop) + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(32), + ), + child: Center( + child: SvgPicture.asset( + _fetchIconAssetForStatus(trade.status), + width: 32, + height: 32, + ), ), ), - ), ], ), ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Status", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 4, - ), - SelectableText( - trade.status, - style: STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .colorForStatus(trade.status), - ), - ), - // ), - // ), - ], - ), - ), - if (!sentFromStack && !hasTx) - const SizedBox( + ), + ), + isDesktop + ? const _Divider() + : const SizedBox( height: 12, ), - if (!sentFromStack && !hasTx) - RoundedContainer( - color: Theme.of(context) - .extension<StackColors>()! - .warningBackground, - child: RichText( - text: TextSpan( + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Status", + style: STextStyles.itemSubtitle(context), + ), + const SizedBox( + height: 4, + ), + SelectableText( + trade.status, + style: STextStyles.itemSubtitle(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .colorForStatus(trade.status), + ), + ), + // ), + // ), + ], + ), + ), + if (!sentFromStack && !hasTx) + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + if (!sentFromStack && !hasTx) + RoundedContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: Theme.of(context) + .extension<StackColors>()! + .warningBackground, + child: RichText( + text: TextSpan( + text: + "You must send at least ${sendAmount.toStringAsFixed( + trade.payInCurrency.toLowerCase() == "xmr" ? 12 : 8, + )} ${trade.payInCurrency.toUpperCase()}. ", + style: STextStyles.label700(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .warningForeground, + ), + children: [ + TextSpan( text: - "You must send at least ${sendAmount.toStringAsFixed( + "If you send less than ${sendAmount.toStringAsFixed( trade.payInCurrency.toLowerCase() == "xmr" ? 12 : 8, - )} ${trade.payInCurrency.toUpperCase()}. ", - style: STextStyles.label700(context).copyWith( + )} ${trade.payInCurrency.toUpperCase()}, your transaction may not be converted and it may not be refunded.", + style: STextStyles.label(context).copyWith( color: Theme.of(context) .extension<StackColors>()! .warningForeground, ), - children: [ - TextSpan( - text: - "If you send less than ${sendAmount.toStringAsFixed( - trade.payInCurrency.toLowerCase() == "xmr" - ? 12 - : 8, - )} ${trade.payInCurrency.toUpperCase()}, your transaction may not be converted and it may not be refunded.", - style: STextStyles.label(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .warningForeground, + ), + ]), + ), + ), + if (sentFromStack) + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + if (sentFromStack) + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Sent from", + style: STextStyles.itemSubtitle(context), + ), + const SizedBox( + height: 4, + ), + SelectableText( + widget.walletName!, + style: STextStyles.itemSubtitle12(context), + ), + const SizedBox( + height: 10, + ), + BlueTextButton( + text: "View transaction", + onTap: () { + final Coin coin = + coinFromTickerCaseInsensitive(trade.payInCurrency); + + if (isDesktop) { + Navigator.of(context).push( + FadePageRoute<void>( + DesktopDialog( + maxHeight: + MediaQuery.of(context).size.height - 64, + maxWidth: 580, + child: TransactionDetailsView( + coin: coin, + transaction: transactionIfSentFromStack!, + walletId: walletId!, + ), + ), + const RouteSettings( + name: TransactionDetailsView.routeName, ), ), - ]), + ); + } else { + Navigator.of(context).pushNamed( + TransactionDetailsView.routeName, + arguments: Tuple3( + transactionIfSentFromStack!, coin, walletId!), + ); + } + }, ), - ), - if (sentFromStack) - const SizedBox( - height: 12, - ), - if (sentFromStack) - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Sent from", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 4, - ), - SelectableText( - widget.walletName!, - style: STextStyles.itemSubtitle12(context), - ), - const SizedBox( - height: 10, - ), - GestureDetector( - onTap: () { - final Coin coin = coinFromTickerCaseInsensitive( - trade.payInCurrency); - - Navigator.of(context).pushNamed( - TransactionDetailsView.routeName, - arguments: Tuple3( - transactionIfSentFromStack!, coin, walletId!), - ); - }, - child: Text( - "View transaction", - style: STextStyles.link2(context), - ), - ), - ], + ], + ), + ), + if (sentFromStack) + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, ), - ), - if (sentFromStack) - const SizedBox( - height: 12, - ), - if (sentFromStack) - RoundedWhiteContainer( - child: Column( + if (sentFromStack) + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( @@ -347,252 +474,224 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> { ), ], ), - ), - if (!sentFromStack && !hasTx) - const SizedBox( - height: 12, - ), - if (!sentFromStack && !hasTx) - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + if (isDesktop) + IconCopyButton( + data: trade.payInAddress, + ), + ], + ), + ), + if (!sentFromStack && !hasTx) + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + if (!sentFromStack && !hasTx) + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Send ${trade.payInCurrency.toUpperCase()} to this address", - style: STextStyles.itemSubtitle(context), - ), - GestureDetector( - onTap: () async { - final address = trade.payInAddress; - await Clipboard.setData( - ClipboardData( - text: address, - ), - ); - unawaited(showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - context: context, - )); - }, - child: Row( + Text( + "Send ${trade.payInCurrency.toUpperCase()} to this address", + style: STextStyles.itemSubtitle(context), + ), + isDesktop + ? IconCopyButton( + data: trade.payInAddress, + ) + : GestureDetector( + onTap: () async { + final address = trade.payInAddress; + await Clipboard.setData( + ClipboardData( + text: address, + ), + ); + unawaited(showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, + )); + }, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.copy, + width: 12, + height: 12, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + ), + const SizedBox( + width: 4, + ), + Text( + "Copy", + style: STextStyles.link2(context), + ), + ], + ), + ), + ], + ), + const SizedBox( + height: 4, + ), + SelectableText( + trade.payInAddress, + style: STextStyles.itemSubtitle12(context), + ), + const SizedBox( + height: 10, + ), + GestureDetector( + onTap: () { + showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (_) { + final width = MediaQuery.of(context).size.width / 2; + return StackDialogBase( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - SvgPicture.asset( - Assets.svg.copy, - width: 12, - height: 12, - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, + Center( + child: Text( + "Send ${trade.payInCurrency.toUpperCase()} to this address", + style: STextStyles.pageTitleH2(context), + ), ), const SizedBox( - width: 4, + height: 12, ), - Text( - "Copy", - style: STextStyles.link2(context), + Center( + child: RepaintBoundary( + // key: _qrKey, + child: SizedBox( + width: width + 20, + height: width + 20, + child: QrImage( + data: trade.payInAddress, + size: width, + backgroundColor: Theme.of(context) + .extension<StackColors>()! + .popupBG, + foregroundColor: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + ), + const SizedBox( + height: 12, + ), + Center( + child: SizedBox( + width: width, + child: TextButton( + onPressed: () async { + // await _capturePng(true); + Navigator.of(context).pop(); + }, + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor( + context), + child: Text( + "Cancel", + style: STextStyles.button(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + ), ), ], ), - ), - ], - ), - const SizedBox( - height: 4, - ), - SelectableText( - trade.payInAddress, - style: STextStyles.itemSubtitle12(context), - ), - const SizedBox( - height: 10, - ), - GestureDetector( - onTap: () { - showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (_) { - final width = - MediaQuery.of(context).size.width / 2; - return StackDialogBase( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: [ - Center( - child: Text( - "Send ${trade.payInCurrency.toUpperCase()} to this address", - style: - STextStyles.pageTitleH2(context), - ), - ), - const SizedBox( - height: 12, - ), - Center( - child: RepaintBoundary( - // key: _qrKey, - child: SizedBox( - width: width + 20, - height: width + 20, - child: QrImage( - data: trade.payInAddress, - size: width, - backgroundColor: Theme.of( - context) - .extension<StackColors>()! - .popupBG, - foregroundColor: Theme.of( - context) - .extension<StackColors>()! - .accentColorDark), - ), - ), - ), - const SizedBox( - height: 12, - ), - Center( - child: SizedBox( - width: width, - child: TextButton( - onPressed: () async { - // await _capturePng(true); - Navigator.of(context).pop(); - }, - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor( - context), - child: Text( - "Cancel", - style: STextStyles.button(context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .accentColorDark), - ), - ), - ), - ), - ], - ), - ); - }, ); }, - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.qrcode, - width: 12, - height: 12, - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - ), - const SizedBox( - width: 4, - ), - Text( - "Show QR code", - style: STextStyles.link2(context), - ), - ], - ), - ), - ], - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + ); + }, + child: Row( children: [ - Text( - "Trade note", - style: STextStyles.itemSubtitle(context), + SvgPicture.asset( + Assets.svg.qrcode, + width: 12, + height: 12, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, ), - GestureDetector( - onTap: () { - Navigator.of(context).pushNamed( - EditTradeNoteView.routeName, - arguments: Tuple2( - tradeId, - ref - .read(tradeNoteServiceProvider) - .getNote(tradeId: tradeId), - ), - ); - }, - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.pencil, - width: 10, - height: 10, - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - ), - const SizedBox( - width: 4, - ), - Text( - "Edit", - style: STextStyles.link2(context), - ), - ], - ), + const SizedBox( + width: 4, + ), + Text( + "Show QR code", + style: STextStyles.link2(context), ), ], ), - const SizedBox( - height: 4, - ), - SelectableText( - ref.watch(tradeNoteServiceProvider.select( - (value) => value.getNote(tradeId: tradeId))), - style: STextStyles.itemSubtitle12(context), - ), - ], - ), + ), + ], ), - if (sentFromStack) - const SizedBox( + ), + isDesktop + ? const _Divider() + : const SizedBox( height: 12, ), - if (sentFromStack) - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Transaction note", - style: STextStyles.itemSubtitle(context), - ), - GestureDetector( + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Trade note", + style: STextStyles.itemSubtitle(context), + ), + isDesktop + ? IconPencilButton( + onPressed: () { + showDialog<void>( + context: context, + builder: (context) { + return DesktopDialog( + maxWidth: 580, + maxHeight: 360, + child: EditTradeNoteView( + tradeId: tradeId, + note: _note, + ), + ); + }, + ); + }, + ) + : GestureDetector( onTap: () { Navigator.of(context).pushNamed( - EditNoteView.routeName, - arguments: Tuple3( - transactionIfSentFromStack!.txid, - walletId!, - _note, + EditTradeNoteView.routeName, + arguments: Tuple2( + tradeId, + ref + .read(tradeNoteServiceProvider) + .getNote(tradeId: tradeId), ), ); }, @@ -616,193 +715,371 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> { ], ), ), - ], - ), - const SizedBox( - height: 4, - ), - FutureBuilder( - future: ref.watch( - notesServiceChangeNotifierProvider(walletId!) - .select((value) => value.getNoteFor( - txid: transactionIfSentFromStack!.txid))), - builder: - (builderContext, AsyncSnapshot<String> snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - _note = snapshot.data ?? ""; - } - return SelectableText( - _note, - style: STextStyles.itemSubtitle12(context), - ); - }, + ], + ), + const SizedBox( + height: 4, + ), + SelectableText( + ref.watch(tradeNoteServiceProvider + .select((value) => value.getNote(tradeId: tradeId))), + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + if (sentFromStack) + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + if (sentFromStack) + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transaction note", + style: STextStyles.itemSubtitle(context), ), + isDesktop + ? IconPencilButton( + onPressed: () { + showDialog<void>( + context: context, + builder: (context) { + return DesktopDialog( + maxWidth: 580, + maxHeight: 360, + child: EditNoteView( + txid: + transactionIfSentFromStack!.txid, + walletId: walletId!, + note: _note, + ), + ); + }, + ); + }, + ) + : GestureDetector( + onTap: () { + Navigator.of(context).pushNamed( + EditNoteView.routeName, + arguments: Tuple3( + transactionIfSentFromStack!.txid, + walletId!, + _note, + ), + ); + }, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.pencil, + width: 10, + height: 10, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + ), + const SizedBox( + width: 4, + ), + Text( + "Edit", + style: STextStyles.link2(context), + ), + ], + ), + ), ], ), - ), - const SizedBox( - height: 12, + const SizedBox( + height: 4, + ), + FutureBuilder( + future: ref.watch( + notesServiceChangeNotifierProvider(walletId!).select( + (value) => value.getNoteFor( + txid: transactionIfSentFromStack!.txid))), + builder: + (builderContext, AsyncSnapshot<String> snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData) { + _note = snapshot.data ?? ""; + } + return SelectableText( + _note, + style: STextStyles.itemSubtitle12(context), + ); + }, + ), + ], ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + ), + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Date", style: STextStyles.itemSubtitle(context), ), - // Flexible( - // child: FittedBox( - // fit: BoxFit.scaleDown, - // child: - SelectableText( - Format.extractDateFrom( - trade.timestamp.millisecondsSinceEpoch ~/ 1000), - style: STextStyles.itemSubtitle12(context), - ), - // ), - // ), + if (isDesktop) + const SizedBox( + height: 2, + ), + if (isDesktop) + SelectableText( + Format.extractDateFrom( + trade.timestamp.millisecondsSinceEpoch ~/ 1000), + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), ], ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + if (!isDesktop) + SelectableText( + Format.extractDateFrom( + trade.timestamp.millisecondsSinceEpoch ~/ 1000), + style: STextStyles.itemSubtitle12(context), + ), + if (isDesktop) + IconCopyButton( + data: Format.extractDateFrom( + trade.timestamp.millisecondsSinceEpoch ~/ 1000), + ), + ], + ), + ), + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Exchange", style: STextStyles.itemSubtitle(context), ), - SelectableText( - trade.exchangeName, - style: STextStyles.itemSubtitle12(context), - ), + if (isDesktop) + const SizedBox( + height: 2, + ), + if (isDesktop) + SelectableText( + trade.exchangeName, + style: STextStyles.itemSubtitle12(context), + ), ], ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Row( + if (isDesktop) + IconCopyButton( + data: trade.exchangeName, + ), + if (!isDesktop) + SelectableText( + trade.exchangeName, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Trade ID", style: STextStyles.itemSubtitle(context), ), - const Spacer(), - Row( - children: [ - Text( - trade.tradeId, - style: STextStyles.itemSubtitle12(context), - ), - const SizedBox( - width: 10, - ), - GestureDetector( - onTap: () async { - final data = ClipboardData(text: trade.tradeId); - await clipboard.setData(data); - unawaited(showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - context: context, - )); - }, - child: SvgPicture.asset( - Assets.svg.copy, - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - width: 12, - ), - ) - ], - ) - ], - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Tracking", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 4, - ), - Builder(builder: (context) { - late final String url; - switch (trade.exchangeName) { - case ChangeNowExchange.exchangeName: - url = - "https://changenow.io/exchange/txs/${trade.tradeId}"; - break; - case SimpleSwapExchange.exchangeName: - url = - "https://simpleswap.io/exchange?id=${trade.tradeId}"; - break; - } - return GestureDetector( - onTap: () { - launchUrl( - Uri.parse(url), - mode: LaunchMode.externalApplication, - ); - }, - child: Text( - url, - style: STextStyles.link2(context), - ), - ); - }), - ], - ), - ), - const SizedBox( - height: 12, - ), - if (isStackCoin(trade.payInCurrency) && - (trade.status == "New" || - trade.status == "new" || - trade.status == "waiting" || - trade.status == "Waiting")) - SecondaryButton( - label: "Send from Stack", - onPressed: () { - final amount = sendAmount; - final address = trade.payInAddress; - - final coin = - coinFromTickerCaseInsensitive(trade.payInCurrency); - - Navigator.of(context).pushNamed( - SendFromView.routeName, - arguments: Tuple4( - coin, - amount, - address, - trade, + if (isDesktop) + const SizedBox( + height: 2, ), - ); - }, + if (isDesktop) + Text( + trade.tradeId, + style: STextStyles.itemSubtitle12(context), + ), + ], ), - ], + if (isDesktop) + IconCopyButton( + data: trade.tradeId, + ), + if (!isDesktop) + Row( + children: [ + Text( + trade.tradeId, + style: STextStyles.itemSubtitle12(context), + ), + const SizedBox( + width: 10, + ), + GestureDetector( + onTap: () async { + final data = ClipboardData(text: trade.tradeId); + await clipboard.setData(data); + unawaited(showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, + )); + }, + child: SvgPicture.asset( + Assets.svg.copy, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + width: 12, + ), + ) + ], + ), + ], + ), ), - ), + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Tracking", + style: STextStyles.itemSubtitle(context), + ), + const SizedBox( + height: 4, + ), + Builder(builder: (context) { + late final String url; + switch (trade.exchangeName) { + case ChangeNowExchange.exchangeName: + url = + "https://changenow.io/exchange/txs/${trade.tradeId}"; + break; + case SimpleSwapExchange.exchangeName: + url = + "https://simpleswap.io/exchange?id=${trade.tradeId}"; + break; + } + return GestureDetector( + onTap: () { + launchUrl( + Uri.parse(url), + mode: LaunchMode.externalApplication, + ); + }, + child: Text( + url, + style: STextStyles.link2(context), + ), + ); + }), + ], + ), + ), + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + if (isStackCoin(trade.payInCurrency) && + (trade.status == "New" || + trade.status == "new" || + trade.status == "waiting" || + trade.status == "Waiting")) + SecondaryButton( + label: "Send from Stack", + onPressed: () { + final amount = sendAmount; + final address = trade.payInAddress; + + final coin = + coinFromTickerCaseInsensitive(trade.payInCurrency); + + Navigator.of(context).pushNamed( + SendFromView.routeName, + arguments: Tuple4( + coin, + amount, + address, + trade, + ), + ); + }, + ), + ], ), ), ); } } + +class _Divider extends StatelessWidget { + const _Divider({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ); + } +} diff --git a/lib/pages/wallet_view/sub_widgets/transactions_list.dart b/lib/pages/wallet_view/sub_widgets/transactions_list.dart index d23d3082f..11353c7c6 100644 --- a/lib/pages/wallet_view/sub_widgets/transactions_list.dart +++ b/lib/pages/wallet_view/sub_widgets/transactions_list.dart @@ -7,10 +7,14 @@ import 'package:stackwallet/pages/exchange_view/trade_details_view.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/no_transactions_found.dart'; import 'package:stackwallet/providers/global/trades_service_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/trade_card.dart'; import 'package:stackwallet/widgets/transaction_card.dart'; @@ -94,18 +98,79 @@ class _TransactionsListState extends ConsumerState<TransactionsList> { // this may mess with combined firo transactions key: Key(tx.toString() + trade.uuid), // trade: trade, - onTap: () { - unawaited( - Navigator.of(context).pushNamed( - TradeDetailsView.routeName, - arguments: Tuple4( - trade.tradeId, - tx, - widget.walletId, - ref.read(managerProvider).walletName, + onTap: () async { + if (Util.isDesktop) { + await showDialog<void>( + context: context, + builder: (context) => Navigator( + initialRoute: TradeDetailsView.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + FadePageRoute( + DesktopDialog( + // maxHeight: + // MediaQuery.of(context).size.height - 64, + maxHeight: double.infinity, + maxWidth: 580, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 16, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Trade details", + style: STextStyles.desktopH3(context), + ), + DesktopDialogCloseButton( + onPressedOverride: Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + ], + ), + ), + Flexible( + child: TradeDetailsView( + tradeId: trade.tradeId, + transactionIfSentFromStack: tx, + walletName: + ref.read(managerProvider).walletName, + walletId: widget.walletId, + ), + ), + ], + ), + ), + const RouteSettings( + name: TradeDetailsView.routeName, + ), + ), + ]; + }, ), - ), - ); + ); + } else { + unawaited( + Navigator.of(context).pushNamed( + TradeDetailsView.routeName, + arguments: Tuple4( + trade.tradeId, + tx, + widget.walletId, + ref.read(managerProvider).walletName, + ), + ), + ); + } }, ) ], From 2bdf5f152c606cd83311ef5e5b899e3a9b8cb0d7 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 9 Nov 2022 16:43:26 -0600 Subject: [PATCH 188/426] dynamic secure storage provider --- lib/hive/db.dart | 2 + lib/main.dart | 29 +- ...w_wallet_recovery_phrase_warning_view.dart | 2 + .../restore_wallet_view.dart | 2 + lib/pages/pinpad_views/create_pin_view.dart | 8 +- lib/pages/pinpad_views/lock_screen_view.dart | 8 +- .../add_edit_node_view.dart | 8 +- .../manage_nodes_views/node_details_view.dart | 8 +- .../change_pin_view/change_pin_view.dart | 16 +- .../create_auto_backup_view.dart | 16 +- .../create_backup_view.dart | 424 +++++++++--------- .../edit_auto_backup_view.dart | 16 +- .../helpers/restore_create_backup.dart | 112 +++-- .../stack_restore_progress_view.dart | 2 + .../create_password/create_password_view.dart | 7 - .../global/auto_swb_service_provider.dart | 8 +- .../global/node_service_provider.dart | 5 +- .../global/secure_store_provider.dart | 18 + .../global/wallets_service_provider.dart | 7 +- lib/services/auto_swb_service.dart | 5 +- .../coins/bitcoin/bitcoin_wallet.dart | 11 +- .../coins/bitcoincash/bitcoincash_wallet.dart | 11 +- lib/services/coins/coin_service.dart | 16 + .../coins/dogecoin/dogecoin_wallet.dart | 11 +- .../coins/epiccash/epiccash_wallet.dart | 9 +- lib/services/coins/firo/firo_wallet.dart | 11 +- .../coins/litecoin/litecoin_wallet.dart | 11 +- lib/services/coins/monero/monero_wallet.dart | 8 +- .../coins/namecoin/namecoin_wallet.dart | 11 +- .../coins/wownero/wownero_wallet.dart | 8 +- lib/services/node_service.dart | 5 +- lib/services/wallets.dart | 3 +- lib/services/wallets_service.dart | 6 +- lib/utilities/db_version_migration.dart | 16 +- lib/utilities/desktop_password_service.dart | 33 +- .../flutter_secure_storage_interface.dart | 119 +++-- ...flutter_secure_storage_interface_test.dart | 6 +- .../coins/bitcoin/bitcoin_wallet_test.dart | 10 +- .../bitcoincash/bitcoincash_wallet_test.dart | 10 +- .../coins/dogecoin/dogecoin_wallet_test.dart | 8 +- .../coins/namecoin/namecoin_wallet_test.dart | 210 ++++----- test/services/node_service_test.dart | 3 +- test/services/wallets_service_test.dart | 24 +- test/services/wallets_service_test.mocks.dart | 2 +- 44 files changed, 701 insertions(+), 564 deletions(-) create mode 100644 lib/providers/global/secure_store_provider.dart diff --git a/lib/hive/db.dart b/lib/hive/db.dart index e1232696b..d5402752e 100644 --- a/lib/hive/db.dart +++ b/lib/hive/db.dart @@ -33,6 +33,7 @@ class DB { static const String boxNamePriceCache = "priceAPIPrice24hCache"; static const String boxNameDBInfo = "dbInfo"; static const String boxNameTheme = "theme"; + static const String boxNameDesktopData = "desktopData"; String boxNameTxCache({required Coin coin}) => "${coin.name}_txCache"; String boxNameSetCache({required Coin coin}) => @@ -58,6 +59,7 @@ class DB { late final Box<dynamic> _boxPrefs; late final Box<TradeWalletLookup> _boxTradeLookup; late final Box<dynamic> _boxDBInfo; + late final Box<String> _boxDesktopData; final Map<String, Box<dynamic>> _walletBoxes = {}; diff --git a/lib/main.dart b/lib/main.dart index 77a8b1441..7f7c4a44d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,6 +10,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_libmonero/monero/monero.dart'; import 'package:flutter_libmonero/wownero/wownero.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:isar/isar.dart'; @@ -51,6 +52,7 @@ import 'package:stackwallet/services/wallets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/db_version_migration.dart'; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/theme/color_theme.dart'; @@ -143,15 +145,24 @@ void main() async { await Hive.initFlutter(appDirectory.path); await Hive.openBox<dynamic>(DB.boxNameDBInfo); - int dbVersion = DB.instance.get<dynamic>( - boxName: DB.boxNameDBInfo, key: "hive_data_version") as int? ?? - 0; - if (dbVersion < Constants.currentHiveDbVersion) { - try { - await DbVersionMigrator().migrate(dbVersion); - } catch (e, s) { - Logging.instance.log("Cannot migrate database\n$e $s", - level: LogLevel.Error, printFullLength: true); + + if (!Util.isDesktop) { + int dbVersion = DB.instance.get<dynamic>( + boxName: DB.boxNameDBInfo, key: "hive_data_version") as int? ?? + 0; + if (dbVersion < Constants.currentHiveDbVersion) { + try { + await DbVersionMigrator().migrate( + dbVersion, + secureStore: const SecureStorageWrapper( + store: FlutterSecureStorage(), + isDesktop: false, + ), + ); + } catch (e, s) { + Logging.instance.log("Cannot migrate database\n$e $s", + level: LogLevel.Error, printFullLength: true); + } } } diff --git a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart index 83dc43933..24a2e1a44 100644 --- a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart +++ b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/exit_to_my_stack_button.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/coins/coin_service.dart'; import 'package:stackwallet/services/coins/manager.dart'; @@ -241,6 +242,7 @@ class _NewWalletRecoveryPhraseWarningViewState coin, walletId, walletName, + ref.read(secureStoreProvider), node, txTracker, ref.read(prefsChangeNotifierProvider), diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart index a6b7e7e77..2f325f536 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart @@ -18,6 +18,7 @@ import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/sub_widge import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/exit_to_my_stack_button.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/coins/coin_service.dart'; import 'package:stackwallet/services/coins/manager.dart'; @@ -265,6 +266,7 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> { widget.coin, walletId, widget.walletName, + ref.read(secureStoreProvider), node, txTracker, ref.read(prefsChangeNotifierProvider), diff --git a/lib/pages/pinpad_views/create_pin_view.dart b/lib/pages/pinpad_views/create_pin_view.dart index f8b84cfb4..5ea8bc363 100644 --- a/lib/pages/pinpad_views/create_pin_view.dart +++ b/lib/pages/pinpad_views/create_pin_view.dart @@ -2,10 +2,10 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/biometrics.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -20,15 +20,11 @@ class CreatePinView extends ConsumerStatefulWidget { const CreatePinView({ Key? key, this.popOnSuccess = false, - this.secureStore = const SecureStorageWrapper( - FlutterSecureStorage(), - ), this.biometrics = const Biometrics(), }) : super(key: key); static const String routeName = "/createPin"; - final FlutterSecureStorageInterface secureStore; final Biometrics biometrics; final bool popOnSuccess; @@ -63,7 +59,7 @@ class _CreatePinViewState extends ConsumerState<CreatePinView> { @override initState() { - _secureStore = widget.secureStore; + _secureStore = ref.read(secureStoreProvider); biometrics = widget.biometrics; super.initState(); } diff --git a/lib/pages/pinpad_views/lock_screen_view.dart b/lib/pages/pinpad_views/lock_screen_view.dart index 00d8b1914..137f3d55d 100644 --- a/lib/pages/pinpad_views/lock_screen_view.dart +++ b/lib/pages/pinpad_views/lock_screen_view.dart @@ -2,12 +2,12 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; // import 'package:stackwallet/providers/global/has_authenticated_start_state_provider.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; // import 'package:stackwallet/providers/global/should_show_lockscreen_on_resume_state_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -33,9 +33,6 @@ class LockscreenView extends ConsumerStatefulWidget { this.popOnSuccess = false, this.isInitialAppLogin = false, this.routeOnSuccessArguments, - this.secureStore = const SecureStorageWrapper( - FlutterSecureStorage(), - ), this.biometrics = const Biometrics(), this.onSuccess, }) : super(key: key); @@ -50,7 +47,6 @@ class LockscreenView extends ConsumerStatefulWidget { final String biometricsAuthenticationTitle; final String biometricsLocalizedReason; final String biometricsCancelButtonString; - final FlutterSecureStorageInterface secureStore; final Biometrics biometrics; final VoidCallback? onSuccess; @@ -134,7 +130,7 @@ class _LockscreenViewState extends ConsumerState<LockscreenView> { void initState() { _shakeController = ShakeController(); - _secureStore = widget.secureStore; + _secureStore = ref.read(secureStoreProvider); biometrics = widget.biometrics; _attempts = 0; _timeout = Duration.zero; diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index 382e3f09e..39ab493f0 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -3,11 +3,11 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -40,9 +40,6 @@ class AddEditNodeView extends ConsumerStatefulWidget { required this.coin, required this.nodeId, required this.routeOnSuccessOrDelete, - this.secureStore = const SecureStorageWrapper( - FlutterSecureStorage(), - ), }) : super(key: key); static const String routeName = "/addEditNode"; @@ -51,7 +48,6 @@ class AddEditNodeView extends ConsumerStatefulWidget { final Coin coin; final String routeOnSuccessOrDelete; final String? nodeId; - final FlutterSecureStorageInterface secureStore; @override ConsumerState<AddEditNodeView> createState() => _AddEditNodeViewState(); @@ -533,7 +529,7 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> { children: [ NodeForm( node: node, - secureStore: widget.secureStore, + secureStore: ref.read(secureStoreProvider), readOnly: false, coin: widget.coin, onChanged: (canSave, canTest) { diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart index f9b64c460..6d9641b7d 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart @@ -2,11 +2,11 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -32,14 +32,10 @@ class NodeDetailsView extends ConsumerStatefulWidget { required this.coin, required this.nodeId, required this.popRouteName, - this.secureStore = const SecureStorageWrapper( - FlutterSecureStorage(), - ), }) : super(key: key); static const String routeName = "/nodeDetails"; - final FlutterSecureStorageInterface secureStore; final Coin coin; final String nodeId; final String popRouteName; @@ -58,7 +54,7 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> { @override initState() { - secureStore = widget.secureStore; + secureStore = ref.read(secureStoreProvider); coin = widget.coin; nodeId = widget.nodeId; popRouteName = widget.popRouteName; diff --git a/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart b/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart index 39c95cad7..46c2fd9cf 100644 --- a/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart +++ b/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/security_views/security_view.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; @@ -11,23 +12,18 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_pin_put/custom_pin_put.dart'; -class ChangePinView extends StatefulWidget { +class ChangePinView extends ConsumerStatefulWidget { const ChangePinView({ Key? key, - this.secureStore = const SecureStorageWrapper( - FlutterSecureStorage(), - ), }) : super(key: key); static const String routeName = "/changePin"; - final FlutterSecureStorageInterface secureStore; - @override - State<ChangePinView> createState() => _ChangePinViewState(); + ConsumerState<ChangePinView> createState() => _ChangePinViewState(); } -class _ChangePinViewState extends State<ChangePinView> { +class _ChangePinViewState extends ConsumerState<ChangePinView> { BoxDecoration get _pinPutDecoration { return BoxDecoration( color: Theme.of(context).extension<StackColors>()!.textSubtitle2, @@ -53,7 +49,7 @@ class _ChangePinViewState extends State<ChangePinView> { @override void initState() { - _secureStore = widget.secureStore; + _secureStore = ref.read(secureStoreProvider); super.initState(); } diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart index 3b5dbd0b0..1082acc99 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stack_wallet_backup/stack_wallet_backup.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; @@ -13,6 +12,7 @@ import 'package:stackwallet/pages/settings_views/global_settings_view/stack_back import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/stack_file_system.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/sub_views/backup_frequency_type_select_sheet.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; @@ -21,26 +21,20 @@ import 'package:stackwallet/utilities/format.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/utilities/util.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/progress_bar.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:zxcvbn/zxcvbn.dart'; -import 'package:stackwallet/utilities/util.dart'; - class CreateAutoBackupView extends ConsumerStatefulWidget { const CreateAutoBackupView({ Key? key, - this.secureStore = const SecureStorageWrapper( - FlutterSecureStorage(), - ), }) : super(key: key); static const String routeName = "/createAutoBackup"; - final FlutterSecureStorageInterface secureStore; - @override ConsumerState<CreateAutoBackupView> createState() => _EnableAutoBackupViewState(); @@ -75,7 +69,7 @@ class _EnableAutoBackupViewState extends ConsumerState<CreateAutoBackupView> { @override void initState() { - secureStore = widget.secureStore; + secureStore = ref.read(secureStoreProvider); stackFileSystem = StackFileSystem(); fileLocationController = TextEditingController(); passwordController = TextEditingController(); @@ -585,7 +579,9 @@ class _EnableAutoBackupViewState extends ConsumerState<CreateAutoBackupView> { final String fileToSave = createAutoBackupFilename(pathToSave, now); - final backup = await SWB.createStackWalletJSON(); + final backup = await SWB.createStackWalletJSON( + secureStorage: secureStore, + ); bool result = await SWB.encryptStackWalletWithADK( fileToSave, diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart index eacdda66a..fc4719fe1 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart @@ -8,6 +8,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/stack_file_system.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; @@ -443,222 +444,229 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { ), if (!isDesktop) const Spacer(), !isDesktop - ? TextButton( - style: shouldEnableCreate - ? Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context) - : Theme.of(context) - .extension<StackColors>()! - .getPrimaryDisabledButtonColor(context), - onPressed: !shouldEnableCreate - ? null - : () async { - final String pathToSave = - fileLocationController.text; - final String passphrase = passwordController.text; - final String repeatPassphrase = - passwordRepeatController.text; + ? Consumer(builder: (context, ref, __) { + return TextButton( + style: shouldEnableCreate + ? Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context) + : Theme.of(context) + .extension<StackColors>()! + .getPrimaryDisabledButtonColor(context), + onPressed: !shouldEnableCreate + ? null + : () async { + final String pathToSave = + fileLocationController.text; + final String passphrase = passwordController.text; + final String repeatPassphrase = + passwordRepeatController.text; - if (pathToSave.isEmpty) { - unawaited(showFloatingFlushBar( - type: FlushBarType.warning, - message: "Directory not chosen", - context: context, - )); - return; - } - if (!(await Directory(pathToSave).exists())) { - unawaited(showFloatingFlushBar( - type: FlushBarType.warning, - message: "Directory does not exist", - context: context, - )); - return; - } - if (passphrase.isEmpty) { - unawaited(showFloatingFlushBar( - type: FlushBarType.warning, - message: "A passphrase is required", - context: context, - )); - return; - } - if (passphrase != repeatPassphrase) { - unawaited(showFloatingFlushBar( - type: FlushBarType.warning, - message: "Passphrase does not match", - context: context, - )); - return; - } - - unawaited(showDialog<dynamic>( - context: context, - barrierDismissible: false, - builder: (_) => const StackDialog( - title: "Encrypting backup", - message: "This shouldn't take long", - ), - )); - // make sure the dialog is able to be displayed for at least 1 second - await Future<void>.delayed( - const Duration(seconds: 1)); - - final DateTime now = DateTime.now(); - final String fileToSave = - "$pathToSave/stackbackup_${now.year}_${now.month}_${now.day}_${now.hour}_${now.minute}_${now.second}.swb"; - - final backup = await SWB.createStackWalletJSON(); - - bool result = - await SWB.encryptStackWalletWithPassphrase( - fileToSave, - passphrase, - jsonEncode(backup), - ); - - if (mounted) { - // pop encryption progress dialog - Navigator.of(context).pop(); - - if (result) { - await showDialog<dynamic>( + if (pathToSave.isEmpty) { + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "Directory not chosen", context: context, - barrierDismissible: false, - builder: (_) => Platform.isAndroid - ? StackOkDialog( - title: "Backup saved to:", - message: fileToSave, - ) - : const StackOkDialog( - title: "Backup creation succeeded"), - ); - passwordController.text = ""; - passwordRepeatController.text = ""; - setState(() {}); - } else { - await showDialog<dynamic>( - context: context, - barrierDismissible: false, - builder: (_) => const StackOkDialog( - title: "Backup creation failed"), - ); + )); + return; } - } - }, - child: Text( - "Create backup", - style: STextStyles.button(context), - ), - ) + if (!(await Directory(pathToSave).exists())) { + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "Directory does not exist", + context: context, + )); + return; + } + if (passphrase.isEmpty) { + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "A passphrase is required", + context: context, + )); + return; + } + if (passphrase != repeatPassphrase) { + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "Passphrase does not match", + context: context, + )); + return; + } + + unawaited(showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => const StackDialog( + title: "Encrypting backup", + message: "This shouldn't take long", + ), + )); + // make sure the dialog is able to be displayed for at least 1 second + await Future<void>.delayed( + const Duration(seconds: 1)); + + final DateTime now = DateTime.now(); + final String fileToSave = + "$pathToSave/stackbackup_${now.year}_${now.month}_${now.day}_${now.hour}_${now.minute}_${now.second}.swb"; + + final backup = await SWB.createStackWalletJSON( + secureStorage: ref.read(secureStoreProvider)); + + bool result = + await SWB.encryptStackWalletWithPassphrase( + fileToSave, + passphrase, + jsonEncode(backup), + ); + + if (mounted) { + // pop encryption progress dialog + Navigator.of(context).pop(); + + if (result) { + await showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => Platform.isAndroid + ? StackOkDialog( + title: "Backup saved to:", + message: fileToSave, + ) + : const StackOkDialog( + title: "Backup creation succeeded"), + ); + passwordController.text = ""; + passwordRepeatController.text = ""; + setState(() {}); + } else { + await showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => const StackOkDialog( + title: "Backup creation failed"), + ); + } + } + }, + child: Text( + "Create backup", + style: STextStyles.button(context), + ), + ); + }) : Row( children: [ - PrimaryButton( - width: 183, - desktopMed: true, - label: "Create backup", - enabled: shouldEnableCreate, - onPressed: !shouldEnableCreate - ? null - : () async { - final String pathToSave = - fileLocationController.text; - final String passphrase = - passwordController.text; - final String repeatPassphrase = - passwordRepeatController.text; + Consumer(builder: (context, ref, __) { + return PrimaryButton( + width: 183, + desktopMed: true, + label: "Create backup", + enabled: shouldEnableCreate, + onPressed: !shouldEnableCreate + ? null + : () async { + final String pathToSave = + fileLocationController.text; + final String passphrase = + passwordController.text; + final String repeatPassphrase = + passwordRepeatController.text; - if (pathToSave.isEmpty) { - unawaited(showFloatingFlushBar( - type: FlushBarType.warning, - message: "Directory not chosen", - context: context, - )); - return; - } - if (!(await Directory(pathToSave).exists())) { - unawaited(showFloatingFlushBar( - type: FlushBarType.warning, - message: "Directory does not exist", - context: context, - )); - return; - } - if (passphrase.isEmpty) { - unawaited(showFloatingFlushBar( - type: FlushBarType.warning, - message: "A passphrase is required", - context: context, - )); - return; - } - if (passphrase != repeatPassphrase) { - unawaited(showFloatingFlushBar( - type: FlushBarType.warning, - message: "Passphrase does not match", - context: context, - )); - return; - } - - unawaited(showDialog<dynamic>( - context: context, - barrierDismissible: false, - builder: (_) => const StackDialog( - title: "Encrypting backup", - message: "This shouldn't take long", - ), - )); - // make sure the dialog is able to be displayed for at least 1 second - await Future<void>.delayed( - const Duration(seconds: 1)); - - final DateTime now = DateTime.now(); - final String fileToSave = - "$pathToSave/stackbackup_${now.year}_${now.month}_${now.day}_${now.hour}_${now.minute}_${now.second}.swb"; - - final backup = - await SWB.createStackWalletJSON(); - - bool result = - await SWB.encryptStackWalletWithPassphrase( - fileToSave, - passphrase, - jsonEncode(backup), - ); - - if (mounted) { - // pop encryption progress dialog - Navigator.of(context).pop(); - - if (result) { - await showDialog<dynamic>( + if (pathToSave.isEmpty) { + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "Directory not chosen", context: context, - barrierDismissible: false, - builder: (_) => Platform.isAndroid - ? StackOkDialog( - title: "Backup saved to:", - message: fileToSave, - ) - : const StackOkDialog( - title: - "Backup creation succeeded"), - ); - passwordController.text = ""; - passwordRepeatController.text = ""; - setState(() {}); - } else { - await showDialog<dynamic>( - context: context, - barrierDismissible: false, - builder: (_) => const StackOkDialog( - title: "Backup creation failed"), - ); + )); + return; } - } - }, - ), + if (!(await Directory(pathToSave).exists())) { + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "Directory does not exist", + context: context, + )); + return; + } + if (passphrase.isEmpty) { + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "A passphrase is required", + context: context, + )); + return; + } + if (passphrase != repeatPassphrase) { + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "Passphrase does not match", + context: context, + )); + return; + } + + unawaited(showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => const StackDialog( + title: "Encrypting backup", + message: "This shouldn't take long", + ), + )); + // make sure the dialog is able to be displayed for at least 1 second + await Future<void>.delayed( + const Duration(seconds: 1)); + + final DateTime now = DateTime.now(); + final String fileToSave = + "$pathToSave/stackbackup_${now.year}_${now.month}_${now.day}_${now.hour}_${now.minute}_${now.second}.swb"; + + final backup = + await SWB.createStackWalletJSON( + secureStorage: + ref.read(secureStoreProvider)); + + bool result = await SWB + .encryptStackWalletWithPassphrase( + fileToSave, + passphrase, + jsonEncode(backup), + ); + + if (mounted) { + // pop encryption progress dialog + Navigator.of(context).pop(); + + if (result) { + await showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => Platform.isAndroid + ? StackOkDialog( + title: "Backup saved to:", + message: fileToSave, + ) + : const StackOkDialog( + title: + "Backup creation succeeded"), + ); + passwordController.text = ""; + passwordRepeatController.text = ""; + setState(() {}); + } else { + await showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => const StackOkDialog( + title: "Backup creation failed"), + ); + } + } + }, + ); + }), const SizedBox( width: 16, ), diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart index 105146aa0..d5ba21634 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stack_wallet_backup/stack_wallet_backup.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; @@ -13,6 +12,7 @@ import 'package:stackwallet/pages/settings_views/global_settings_view/stack_back import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/stack_file_system.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/sub_views/backup_frequency_type_select_sheet.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; @@ -21,26 +21,20 @@ import 'package:stackwallet/utilities/format.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/utilities/util.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/progress_bar.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:zxcvbn/zxcvbn.dart'; -import '../../../../utilities/util.dart'; - class EditAutoBackupView extends ConsumerStatefulWidget { const EditAutoBackupView({ Key? key, - this.secureStore = const SecureStorageWrapper( - FlutterSecureStorage(), - ), }) : super(key: key); static const String routeName = "/editAutoBackup"; - final FlutterSecureStorageInterface secureStore; - @override ConsumerState<EditAutoBackupView> createState() => _EditAutoBackupViewState(); } @@ -74,7 +68,7 @@ class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> { @override void initState() { - secureStore = widget.secureStore; + secureStore = ref.read(secureStoreProvider); stackFileSystem = StackFileSystem(); fileLocationController = TextEditingController(); passwordController = TextEditingController(); @@ -586,7 +580,9 @@ class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> { final String fileToSave = createAutoBackupFilename(pathToSave, now); - final backup = await SWB.createStackWalletJSON(); + final backup = await SWB.createStackWalletJSON( + secureStorage: ref.read(secureStoreProvider), + ); bool result = await SWB.encryptStackWalletWithADK( fileToSave, diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart index 6d803eb6d..9b755c95a 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart @@ -3,10 +3,7 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:stack_wallet_backup/stack_wallet_backup.dart'; -import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; -import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/models/contact.dart'; import 'package:stackwallet/models/contact_address_entry.dart'; @@ -91,10 +88,13 @@ abstract class SWB { static bool _shouldCancelRestore = false; - static bool _checkShouldCancel(PreRestoreState? revertToState) { + static bool _checkShouldCancel( + PreRestoreState? revertToState, + FlutterSecureStorageInterface secureStorageInterface, + ) { if (_shouldCancelRestore) { if (revertToState != null) { - _revert(revertToState); + _revert(revertToState, secureStorageInterface); } else { _cancelCompleter!.complete(); _shouldCancelRestore = false; @@ -193,15 +193,15 @@ abstract class SWB { /// [secureStorage] parameter exposed for testing purposes static Future<Map<String, dynamic>> createStackWalletJSON({ - FlutterSecureStorageInterface? secureStorage, + required FlutterSecureStorageInterface secureStorage, }) async { Logging.instance .log("Starting createStackWalletJSON...", level: LogLevel.Info); final _wallets = Wallets.sharedInstance; Map<String, dynamic> backupJson = {}; - NodeService nodeService = NodeService(); - final _secureStore = - secureStorage ?? const SecureStorageWrapper(FlutterSecureStorage()); + NodeService nodeService = + NodeService(secureStorageInterface: secureStorage); + final _secureStore = secureStorage; Logging.instance.log("createStackWalletJSON awaiting DB.instance.mutex...", level: LogLevel.Info); @@ -448,6 +448,7 @@ abstract class SWB { Map<String, dynamic> validJSON, StackRestoringUIState? uiState, Map<String, String> oldToNewWalletIdMap, + FlutterSecureStorageInterface secureStorageInterface, ) async { Map<String, dynamic> prefs = validJSON["prefs"] as Map<String, dynamic>; List<dynamic>? addressBookEntries = @@ -486,7 +487,11 @@ abstract class SWB { "SWB restoring nodes", level: LogLevel.Warning, ); - await _restoreNodes(nodes, primaryNodes); + await _restoreNodes( + nodes, + primaryNodes, + secureStorageInterface, + ); uiState?.nodes = StackRestoringStatus.success; uiState?.trades = StackRestoringStatus.restoring; @@ -543,6 +548,7 @@ abstract class SWB { static Future<bool?> restoreStackWalletJSON( String jsonBackup, StackRestoringUIState? uiState, + FlutterSecureStorageInterface secureStorageInterface, ) async { if (!Platform.isLinux) await Wakelock.enable(); @@ -550,7 +556,8 @@ abstract class SWB { "SWB creating temp backup", level: LogLevel.Warning, ); - final preRestoreJSON = await createStackWalletJSON(); + final preRestoreJSON = + await createStackWalletJSON(secureStorage: secureStorageInterface); Logging.instance.log( "SWB temp backup created", level: LogLevel.Warning, @@ -587,19 +594,34 @@ abstract class SWB { // basic cancel check here // no reverting required yet as nothing has been written to store - if (_checkShouldCancel(null)) { + if (_checkShouldCancel( + null, + secureStorageInterface, + )) { return false; } - await _restoreEverythingButWallets(validJSON, uiState, oldToNewWalletIdMap); + await _restoreEverythingButWallets( + validJSON, + uiState, + oldToNewWalletIdMap, + secureStorageInterface, + ); // check if cancel was requested and restore previous state - if (_checkShouldCancel(preRestoreState)) { + if (_checkShouldCancel( + preRestoreState, + secureStorageInterface, + )) { return false; } - final nodeService = NodeService(); - final walletsService = WalletsService(); + final nodeService = NodeService( + secureStorageInterface: secureStorageInterface, + ); + final walletsService = WalletsService( + secureStorageInterface: secureStorageInterface, + ); final _prefs = Prefs.instance; await _prefs.init(); @@ -609,7 +631,10 @@ abstract class SWB { for (var walletbackup in wallets) { // check if cancel was requested and restore previous state - if (_checkShouldCancel(preRestoreState)) { + if (_checkShouldCancel( + preRestoreState, + secureStorageInterface, + )) { return false; } @@ -647,7 +672,10 @@ abstract class SWB { final failovers = nodeService.failoverNodesFor(coin: coin); // check if cancel was requested and restore previous state - if (_checkShouldCancel(preRestoreState)) { + if (_checkShouldCancel( + preRestoreState, + secureStorageInterface, + )) { return false; } @@ -655,6 +683,7 @@ abstract class SWB { coin, walletId, walletName, + secureStorageInterface, node, txTracker, _prefs, @@ -665,7 +694,10 @@ abstract class SWB { managers.add(Tuple2(walletbackup, manager)); // check if cancel was requested and restore previous state - if (_checkShouldCancel(preRestoreState)) { + if (_checkShouldCancel( + preRestoreState, + secureStorageInterface, + )) { return false; } @@ -679,7 +711,10 @@ abstract class SWB { } // check if cancel was requested and restore previous state - if (_checkShouldCancel(preRestoreState)) { + if (_checkShouldCancel( + preRestoreState, + secureStorageInterface, + )) { return false; } @@ -690,7 +725,10 @@ abstract class SWB { // start restoring wallets for (final tuple in managers) { // check if cancel was requested and restore previous state - if (_checkShouldCancel(preRestoreState)) { + if (_checkShouldCancel( + preRestoreState, + secureStorageInterface, + )) { return false; } final bools = await asyncRestore(tuple, uiState, walletsService); @@ -698,13 +736,19 @@ abstract class SWB { } // check if cancel was requested and restore previous state - if (_checkShouldCancel(preRestoreState)) { + if (_checkShouldCancel( + preRestoreState, + secureStorageInterface, + )) { return false; } for (Future<bool> status in restoreStatuses) { // check if cancel was requested and restore previous state - if (_checkShouldCancel(preRestoreState)) { + if (_checkShouldCancel( + preRestoreState, + secureStorageInterface, + )) { return false; } await status; @@ -712,7 +756,10 @@ abstract class SWB { if (!Platform.isLinux) await Wakelock.disable(); // check if cancel was requested and restore previous state - if (_checkShouldCancel(preRestoreState)) { + if (_checkShouldCancel( + preRestoreState, + secureStorageInterface, + )) { return false; } @@ -720,7 +767,10 @@ abstract class SWB { return true; } - static Future<void> _revert(PreRestoreState revertToState) async { + static Future<void> _revert( + PreRestoreState revertToState, + FlutterSecureStorageInterface secureStorageInterface, + ) async { Map<String, dynamic> prefs = revertToState.validJSON["prefs"] as Map<String, dynamic>; List<dynamic>? addressBookEntries = @@ -788,7 +838,9 @@ abstract class SWB { } // nodes - NodeService nodeService = NodeService(); + NodeService nodeService = NodeService( + secureStorageInterface: secureStorageInterface, + ); final currentNodes = nodeService.nodes; if (nodes == null) { // no pre nodes found so we delete all but defaults @@ -914,7 +966,8 @@ abstract class SWB { } // finally remove any added wallets - final walletsService = WalletsService(); + final walletsService = + WalletsService(secureStorageInterface: secureStorageInterface); final namesData = await walletsService.walletNames; for (final entry in namesData.entries) { if (!revertToState.walletIds.contains(entry.value.walletId)) { @@ -989,8 +1042,11 @@ abstract class SWB { static Future<void> _restoreNodes( List<dynamic>? nodes, List<dynamic>? primaryNodes, + FlutterSecureStorageInterface secureStorageInterface, ) async { - NodeService nodeService = NodeService(); + NodeService nodeService = NodeService( + secureStorageInterface: secureStorageInterface, + ); if (nodes != null) { for (var node in nodes) { await nodeService.add( 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 5e5142425..3097ec72a 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 @@ -10,6 +10,7 @@ import 'package:stackwallet/pages/settings_views/global_settings_view/stack_back import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/stack_backup_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/sub_widgets/restoring_item_card.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/sub_widgets/restoring_wallet_card.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/stack_restore/stack_restoring_ui_state_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -107,6 +108,7 @@ class _StackRestoreProgressViewState finished = await SWB.restoreStackWalletJSON( widget.jsonString, uiState, + ref.read(secureStoreProvider), ); } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Warning); diff --git a/lib/pages_desktop_specific/create_password/create_password_view.dart b/lib/pages_desktop_specific/create_password/create_password_view.dart index 0a8429058..1e137500d 100644 --- a/lib/pages_desktop_specific/create_password/create_password_view.dart +++ b/lib/pages_desktop_specific/create_password/create_password_view.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; @@ -10,7 +9,6 @@ import 'package:stackwallet/providers/desktop/storage_crypto_handler_provider.da import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; -import 'package:stackwallet/utilities/flutter_secure_storage_interface.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'; @@ -23,15 +21,10 @@ import 'package:zxcvbn/zxcvbn.dart'; class CreatePasswordView extends ConsumerStatefulWidget { const CreatePasswordView({ Key? key, - this.secureStore = const SecureStorageWrapper( - FlutterSecureStorage(), - ), }) : super(key: key); static const String routeName = "/createPasswordDesktop"; - final FlutterSecureStorageInterface secureStore; - @override ConsumerState<CreatePasswordView> createState() => _CreatePasswordViewState(); } diff --git a/lib/providers/global/auto_swb_service_provider.dart b/lib/providers/global/auto_swb_service_provider.dart index 51a7c4e59..10cf2592f 100644 --- a/lib/providers/global/auto_swb_service_provider.dart +++ b/lib/providers/global/auto_swb_service_provider.dart @@ -1,5 +1,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/services/auto_swb_service.dart'; -final autoSWBServiceProvider = - ChangeNotifierProvider<AutoSWBService>((_) => AutoSWBService()); +final autoSWBServiceProvider = ChangeNotifierProvider<AutoSWBService>( + (ref) => AutoSWBService( + secureStorageInterface: ref.read(secureStoreProvider), + ), +); diff --git a/lib/providers/global/node_service_provider.dart b/lib/providers/global/node_service_provider.dart index 97cea48f9..81ab22426 100644 --- a/lib/providers/global/node_service_provider.dart +++ b/lib/providers/global/node_service_provider.dart @@ -1,15 +1,16 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/services/node_service.dart'; int _count = 0; final nodeServiceChangeNotifierProvider = - ChangeNotifierProvider<NodeService>((_) { + ChangeNotifierProvider<NodeService>((ref) { if (kDebugMode) { _count++; debugPrint( "nodeServiceChangeNotifierProvider instantiation count: $_count"); } - return NodeService(); + return NodeService(secureStorageInterface: ref.read(secureStoreProvider)); }); diff --git a/lib/providers/global/secure_store_provider.dart b/lib/providers/global/secure_store_provider.dart new file mode 100644 index 000000000..299ea0f5c --- /dev/null +++ b/lib/providers/global/secure_store_provider.dart @@ -0,0 +1,18 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:stackwallet/providers/desktop/storage_crypto_handler_provider.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:stackwallet/utilities/util.dart'; + +final secureStoreProvider = Provider<FlutterSecureStorageInterface>((ref) { + if (Util.isDesktop) { + final handler = ref.read(storageCryptoHandlerProvider).handler; + return SecureStorageWrapper( + store: DesktopPWStore(handler), isDesktop: true); + } else { + return const SecureStorageWrapper( + store: FlutterSecureStorage(), + isDesktop: false, + ); + } +}); diff --git a/lib/providers/global/wallets_service_provider.dart b/lib/providers/global/wallets_service_provider.dart index 7e46d076b..363d57613 100644 --- a/lib/providers/global/wallets_service_provider.dart +++ b/lib/providers/global/wallets_service_provider.dart @@ -1,16 +1,19 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/services/wallets_service.dart'; int _count = 0; final walletsServiceChangeNotifierProvider = - ChangeNotifierProvider<WalletsService>((_) { + ChangeNotifierProvider<WalletsService>((ref) { if (kDebugMode) { _count++; debugPrint( "walletsServiceChangeNotifierProvider instantiation count: $_count"); } - return WalletsService(); + return WalletsService( + secureStorageInterface: ref.read(secureStoreProvider), + ); }); diff --git a/lib/services/auto_swb_service.dart b/lib/services/auto_swb_service.dart index e3ce02f15..06ec7a2fb 100644 --- a/lib/services/auto_swb_service.dart +++ b/lib/services/auto_swb_service.dart @@ -3,7 +3,6 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/logger.dart'; @@ -27,9 +26,7 @@ class AutoSWBService extends ChangeNotifier { final FlutterSecureStorageInterface secureStorageInterface; - AutoSWBService( - {this.secureStorageInterface = - const SecureStorageWrapper(FlutterSecureStorage())}); + AutoSWBService({required this.secureStorageInterface}); /// Attempt a backup. Future<void> doBackup() async { diff --git a/lib/services/coins/bitcoin/bitcoin_wallet.dart b/lib/services/coins/bitcoin/bitcoin_wallet.dart index d0920075d..e3c2f13fd 100644 --- a/lib/services/coins/bitcoin/bitcoin_wallet.dart +++ b/lib/services/coins/bitcoin/bitcoin_wallet.dart @@ -12,7 +12,6 @@ import 'package:crypto/crypto.dart'; import 'package:decimal/decimal.dart'; import 'package:devicelocale/devicelocale.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; @@ -1369,7 +1368,7 @@ class BitcoinWallet extends CoinServiceAPI { required CachedElectrumX cachedClient, required TransactionNotificationTracker tracker, PriceAPI? priceAPI, - FlutterSecureStorageInterface? secureStore, + required FlutterSecureStorageInterface secureStore, }) { txTracker = tracker; _walletId = walletId; @@ -1379,13 +1378,12 @@ class BitcoinWallet extends CoinServiceAPI { _cachedElectrumXClient = cachedClient; _priceAPI = priceAPI ?? PriceAPI(Client()); - _secureStore = - secureStore ?? const SecureStorageWrapper(FlutterSecureStorage()); + _secureStore = secureStore; } @override Future<void> updateNode(bool shouldRefresh) async { - final failovers = NodeService() + final failovers = NodeService(secureStorageInterface: _secureStore) .failoverNodesFor(coin: coin) .map((e) => ElectrumXNode( address: e.host, @@ -1423,7 +1421,8 @@ class BitcoinWallet extends CoinServiceAPI { } Future<ElectrumXNode> getCurrentNode() async { - final node = NodeService().getPrimaryNodeFor(coin: coin) ?? + final node = NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: coin) ?? DefaultNodes.getNodeFor(coin); return ElectrumXNode( diff --git a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart index 5b3b54663..063c74315 100644 --- a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart +++ b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart @@ -13,7 +13,6 @@ import 'package:crypto/crypto.dart'; import 'package:decimal/decimal.dart'; import 'package:devicelocale/devicelocale.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; @@ -1274,7 +1273,7 @@ class BitcoinCashWallet extends CoinServiceAPI { required CachedElectrumX cachedClient, required TransactionNotificationTracker tracker, PriceAPI? priceAPI, - FlutterSecureStorageInterface? secureStore, + required FlutterSecureStorageInterface secureStore, }) { txTracker = tracker; _walletId = walletId; @@ -1284,13 +1283,12 @@ class BitcoinCashWallet extends CoinServiceAPI { _cachedElectrumXClient = cachedClient; _priceAPI = priceAPI ?? PriceAPI(Client()); - _secureStore = - secureStore ?? const SecureStorageWrapper(FlutterSecureStorage()); + _secureStore = secureStore; } @override Future<void> updateNode(bool shouldRefresh) async { - final failovers = NodeService() + final failovers = NodeService(secureStorageInterface: _secureStore) .failoverNodesFor(coin: coin) .map((e) => ElectrumXNode( address: e.host, @@ -1328,7 +1326,8 @@ class BitcoinCashWallet extends CoinServiceAPI { } Future<ElectrumXNode> getCurrentNode() async { - final node = NodeService().getPrimaryNodeFor(coin: coin) ?? + final node = NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: coin) ?? DefaultNodes.getNodeFor(coin); return ElectrumXNode( diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart index 655865494..e63ff86e1 100644 --- a/lib/services/coins/coin_service.dart +++ b/lib/services/coins/coin_service.dart @@ -13,6 +13,7 @@ import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart'; import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'litecoin/litecoin_wallet.dart'; @@ -24,6 +25,7 @@ abstract class CoinServiceAPI { Coin coin, String walletId, String walletName, + FlutterSecureStorageInterface secureStorageInterface, NodeModel node, TransactionNotificationTracker tracker, Prefs prefs, @@ -68,6 +70,7 @@ abstract class CoinServiceAPI { walletId: walletId, walletName: walletName, coin: coin, + secureStore: secureStorageInterface, client: client, cachedClient: cachedClient, tracker: tracker, @@ -77,6 +80,7 @@ abstract class CoinServiceAPI { walletId: walletId, walletName: walletName, coin: coin, + secureStore: secureStorageInterface, client: client, cachedClient: cachedClient, tracker: tracker, @@ -87,6 +91,7 @@ abstract class CoinServiceAPI { walletId: walletId, walletName: walletName, coin: coin, + secureStore: secureStorageInterface, client: client, cachedClient: cachedClient, tracker: tracker, @@ -97,6 +102,7 @@ abstract class CoinServiceAPI { walletId: walletId, walletName: walletName, coin: coin, + secureStore: secureStorageInterface, client: client, cachedClient: cachedClient, tracker: tracker, @@ -107,6 +113,7 @@ abstract class CoinServiceAPI { walletId: walletId, walletName: walletName, coin: coin, + secureStore: secureStorageInterface, client: client, cachedClient: cachedClient, tracker: tracker, @@ -117,6 +124,7 @@ abstract class CoinServiceAPI { walletId: walletId, walletName: walletName, coin: coin, + secureStore: secureStorageInterface, client: client, cachedClient: cachedClient, tracker: tracker, @@ -127,6 +135,7 @@ abstract class CoinServiceAPI { walletId: walletId, walletName: walletName, coin: coin, + secureStore: secureStorageInterface, client: client, cachedClient: cachedClient, tracker: tracker, @@ -137,6 +146,7 @@ abstract class CoinServiceAPI { walletId: walletId, walletName: walletName, coin: coin, + secureStore: secureStorageInterface, client: client, cachedClient: cachedClient, tracker: tracker, @@ -147,6 +157,7 @@ abstract class CoinServiceAPI { walletId: walletId, walletName: walletName, coin: coin, + secureStore: secureStorageInterface, client: client, cachedClient: cachedClient, tracker: tracker, @@ -157,6 +168,7 @@ abstract class CoinServiceAPI { walletId: walletId, walletName: walletName, coin: coin, + secureStore: secureStorageInterface, // tracker: tracker, ); @@ -165,6 +177,7 @@ abstract class CoinServiceAPI { walletId: walletId, walletName: walletName, coin: coin, + secureStore: secureStorageInterface, // tracker: tracker, ); @@ -173,6 +186,7 @@ abstract class CoinServiceAPI { walletId: walletId, walletName: walletName, coin: coin, + secureStore: secureStorageInterface, // tracker: tracker, ); @@ -181,6 +195,7 @@ abstract class CoinServiceAPI { walletId: walletId, walletName: walletName, coin: coin, + secureStore: secureStorageInterface, tracker: tracker, cachedClient: cachedClient, client: client, @@ -191,6 +206,7 @@ abstract class CoinServiceAPI { walletId: walletId, walletName: walletName, coin: coin, + secureStore: secureStorageInterface, client: client, cachedClient: cachedClient, tracker: tracker, diff --git a/lib/services/coins/dogecoin/dogecoin_wallet.dart b/lib/services/coins/dogecoin/dogecoin_wallet.dart index 67be291a2..c3100b0f7 100644 --- a/lib/services/coins/dogecoin/dogecoin_wallet.dart +++ b/lib/services/coins/dogecoin/dogecoin_wallet.dart @@ -12,7 +12,6 @@ import 'package:crypto/crypto.dart'; import 'package:decimal/decimal.dart'; import 'package:devicelocale/devicelocale.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; @@ -1137,7 +1136,7 @@ class DogecoinWallet extends CoinServiceAPI { required CachedElectrumX cachedClient, required TransactionNotificationTracker tracker, PriceAPI? priceAPI, - FlutterSecureStorageInterface? secureStore, + required FlutterSecureStorageInterface secureStore, }) { txTracker = tracker; _walletId = walletId; @@ -1147,13 +1146,12 @@ class DogecoinWallet extends CoinServiceAPI { _cachedElectrumXClient = cachedClient; _priceAPI = priceAPI ?? PriceAPI(Client()); - _secureStore = - secureStore ?? const SecureStorageWrapper(FlutterSecureStorage()); + _secureStore = secureStore; } @override Future<void> updateNode(bool shouldRefresh) async { - final failovers = NodeService() + final failovers = NodeService(secureStorageInterface: _secureStore) .failoverNodesFor(coin: coin) .map((e) => ElectrumXNode( address: e.host, @@ -1191,7 +1189,8 @@ class DogecoinWallet extends CoinServiceAPI { } Future<ElectrumXNode> getCurrentNode() async { - final node = NodeService().getPrimaryNodeFor(coin: coin) ?? + final node = NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: coin) ?? DefaultNodes.getNodeFor(coin); return ElectrumXNode( diff --git a/lib/services/coins/epiccash/epiccash_wallet.dart b/lib/services/coins/epiccash/epiccash_wallet.dart index 6c71f39e4..d7b7f35dd 100644 --- a/lib/services/coins/epiccash/epiccash_wallet.dart +++ b/lib/services/coins/epiccash/epiccash_wallet.dart @@ -6,7 +6,6 @@ import 'dart:isolate'; import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_libepiccash/epic_cash.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:hive/hive.dart'; import 'package:http/http.dart'; import 'package:mutex/mutex.dart'; @@ -518,14 +517,13 @@ class EpicCashWallet extends CoinServiceAPI { required String walletName, required Coin coin, PriceAPI? priceAPI, - FlutterSecureStorageInterface? secureStore}) { + required FlutterSecureStorageInterface secureStore}) { _walletId = walletId; _walletName = walletName; _coin = coin; _priceAPI = priceAPI ?? PriceAPI(Client()); - _secureStore = - secureStore ?? const SecureStorageWrapper(FlutterSecureStorage()); + _secureStore = secureStore; Logging.instance.log("$walletName isolate length: ${isolates.length}", level: LogLevel.Info); @@ -537,7 +535,8 @@ class EpicCashWallet extends CoinServiceAPI { @override Future<void> updateNode(bool shouldRefresh) async { - _epicNode = NodeService().getPrimaryNodeFor(coin: coin) ?? + _epicNode = NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: coin) ?? DefaultNodes.getNodeFor(coin); // TODO notify ui/ fire event for node changed? diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index d19c4f1ab..d0f99ef1d 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -11,7 +11,6 @@ import 'package:bitcoindart/bitcoindart.dart'; import 'package:decimal/decimal.dart'; import 'package:devicelocale/devicelocale.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; import 'package:lelantus/lelantus.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; @@ -1321,7 +1320,7 @@ class FiroWallet extends CoinServiceAPI { required CachedElectrumX cachedClient, required TransactionNotificationTracker tracker, PriceAPI? priceAPI, - FlutterSecureStorageInterface? secureStore, + required FlutterSecureStorageInterface secureStore, }) { txTracker = tracker; _walletId = walletId; @@ -1331,8 +1330,7 @@ class FiroWallet extends CoinServiceAPI { _cachedElectrumXClient = cachedClient; _priceAPI = priceAPI ?? PriceAPI(Client()); - _secureStore = - secureStore ?? const SecureStorageWrapper(FlutterSecureStorage()); + _secureStore = secureStore; Logging.instance.log("$walletName isolates length: ${isolates.length}", level: LogLevel.Info); @@ -1870,7 +1868,7 @@ class FiroWallet extends CoinServiceAPI { @override Future<void> updateNode(bool shouldRefresh) async { - final failovers = NodeService() + final failovers = NodeService(secureStorageInterface: _secureStore) .failoverNodesFor(coin: coin) .map( (e) => ElectrumXNode( @@ -3071,7 +3069,8 @@ class FiroWallet extends CoinServiceAPI { } Future<ElectrumXNode> _getCurrentNode() async { - final node = NodeService().getPrimaryNodeFor(coin: coin) ?? + final node = NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: coin) ?? DefaultNodes.getNodeFor(coin); return ElectrumXNode( diff --git a/lib/services/coins/litecoin/litecoin_wallet.dart b/lib/services/coins/litecoin/litecoin_wallet.dart index 4551325f7..30d6ede39 100644 --- a/lib/services/coins/litecoin/litecoin_wallet.dart +++ b/lib/services/coins/litecoin/litecoin_wallet.dart @@ -12,7 +12,6 @@ import 'package:crypto/crypto.dart'; import 'package:decimal/decimal.dart'; import 'package:devicelocale/devicelocale.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; @@ -1371,7 +1370,7 @@ class LitecoinWallet extends CoinServiceAPI { required CachedElectrumX cachedClient, required TransactionNotificationTracker tracker, PriceAPI? priceAPI, - FlutterSecureStorageInterface? secureStore, + required FlutterSecureStorageInterface secureStore, }) { txTracker = tracker; _walletId = walletId; @@ -1381,13 +1380,12 @@ class LitecoinWallet extends CoinServiceAPI { _cachedElectrumXClient = cachedClient; _priceAPI = priceAPI ?? PriceAPI(Client()); - _secureStore = - secureStore ?? const SecureStorageWrapper(FlutterSecureStorage()); + _secureStore = secureStore; } @override Future<void> updateNode(bool shouldRefresh) async { - final failovers = NodeService() + final failovers = NodeService(secureStorageInterface: _secureStore) .failoverNodesFor(coin: coin) .map((e) => ElectrumXNode( address: e.host, @@ -1425,7 +1423,8 @@ class LitecoinWallet extends CoinServiceAPI { } Future<ElectrumXNode> getCurrentNode() async { - final node = NodeService().getPrimaryNodeFor(coin: coin) ?? + final node = NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: coin) ?? DefaultNodes.getNodeFor(coin); return ElectrumXNode( diff --git a/lib/services/coins/monero/monero_wallet.dart b/lib/services/coins/monero/monero_wallet.dart index 662d4077b..569498a15 100644 --- a/lib/services/coins/monero/monero_wallet.dart +++ b/lib/services/coins/monero/monero_wallet.dart @@ -72,7 +72,8 @@ class MoneroWallet extends CoinServiceAPI { late PriceAPI _priceAPI; Future<NodeModel> getCurrentNode() async { - return NodeService().getPrimaryNodeFor(coin: coin) ?? + return NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: coin) ?? DefaultNodes.getNodeFor(coin); } @@ -81,14 +82,13 @@ class MoneroWallet extends CoinServiceAPI { required String walletName, required Coin coin, PriceAPI? priceAPI, - FlutterSecureStorageInterface? secureStore}) { + required FlutterSecureStorageInterface secureStore}) { _walletId = walletId; _walletName = walletName; _coin = coin; _priceAPI = priceAPI ?? PriceAPI(Client()); - _secureStore = - secureStore ?? const SecureStorageWrapper(FlutterSecureStorage()); + _secureStore = secureStore; } bool _shouldAutoSync = false; diff --git a/lib/services/coins/namecoin/namecoin_wallet.dart b/lib/services/coins/namecoin/namecoin_wallet.dart index 8a4b26012..1ebf87233 100644 --- a/lib/services/coins/namecoin/namecoin_wallet.dart +++ b/lib/services/coins/namecoin/namecoin_wallet.dart @@ -12,7 +12,6 @@ import 'package:crypto/crypto.dart'; import 'package:decimal/decimal.dart'; import 'package:devicelocale/devicelocale.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; @@ -1362,7 +1361,7 @@ class NamecoinWallet extends CoinServiceAPI { required CachedElectrumX cachedClient, required TransactionNotificationTracker tracker, PriceAPI? priceAPI, - FlutterSecureStorageInterface? secureStore, + required FlutterSecureStorageInterface secureStore, }) { txTracker = tracker; _walletId = walletId; @@ -1372,13 +1371,12 @@ class NamecoinWallet extends CoinServiceAPI { _cachedElectrumXClient = cachedClient; _priceAPI = priceAPI ?? PriceAPI(Client()); - _secureStore = - secureStore ?? const SecureStorageWrapper(FlutterSecureStorage()); + _secureStore = secureStore; } @override Future<void> updateNode(bool shouldRefresh) async { - final failovers = NodeService() + final failovers = NodeService(secureStorageInterface: _secureStore) .failoverNodesFor(coin: coin) .map((e) => ElectrumXNode( address: e.host, @@ -1416,7 +1414,8 @@ class NamecoinWallet extends CoinServiceAPI { } Future<ElectrumXNode> getCurrentNode() async { - final node = NodeService().getPrimaryNodeFor(coin: coin) ?? + final node = NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: coin) ?? DefaultNodes.getNodeFor(coin); return ElectrumXNode( diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart index 788f2f9d8..7a219158a 100644 --- a/lib/services/coins/wownero/wownero_wallet.dart +++ b/lib/services/coins/wownero/wownero_wallet.dart @@ -73,7 +73,8 @@ class WowneroWallet extends CoinServiceAPI { late PriceAPI _priceAPI; Future<NodeModel> getCurrentNode() async { - return NodeService().getPrimaryNodeFor(coin: coin) ?? + return NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: coin) ?? DefaultNodes.getNodeFor(coin); } @@ -82,14 +83,13 @@ class WowneroWallet extends CoinServiceAPI { required String walletName, required Coin coin, PriceAPI? priceAPI, - FlutterSecureStorageInterface? secureStore}) { + required FlutterSecureStorageInterface secureStore}) { _walletId = walletId; _walletName = walletName; _coin = coin; _priceAPI = priceAPI ?? PriceAPI(Client()); - _secureStore = - secureStore ?? const SecureStorageWrapper(FlutterSecureStorage()); + _secureStore = secureStore; } bool _shouldAutoSync = false; diff --git a/lib/services/node_service.dart b/lib/services/node_service.dart index 5b9fe5063..0dd706781 100644 --- a/lib/services/node_service.dart +++ b/lib/services/node_service.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/models/node_model.dart'; @@ -17,9 +16,7 @@ class NodeService extends ChangeNotifier { /// Exposed [secureStorageInterface] in order to inject mock for tests NodeService({ - this.secureStorageInterface = const SecureStorageWrapper( - FlutterSecureStorage(), - ), + required this.secureStorageInterface, }); Future<void> updateDefaults() async { diff --git a/lib/services/wallets.dart b/lib/services/wallets.dart index db3011d4e..cebba2ce5 100644 --- a/lib/services/wallets.dart +++ b/lib/services/wallets.dart @@ -205,13 +205,14 @@ class Wallets extends ChangeNotifier { final txTracker = TransactionNotificationTracker(walletId: walletId); - final failovers = NodeService().failoverNodesFor(coin: coin); + final failovers = nodeService.failoverNodesFor(coin: coin); // load wallet final wallet = CoinServiceAPI.from( coin, walletId, entry.value.name, + nodeService.secureStorageInterface, node, txTracker, prefs, diff --git a/lib/services/wallets_service.dart b/lib/services/wallets_service.dart index b30f9e9e5..237df8026 100644 --- a/lib/services/wallets_service.dart +++ b/lib/services/wallets_service.dart @@ -3,7 +3,6 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_libmonero/monero/monero.dart'; import 'package:flutter_libmonero/wownero/wownero.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart'; import 'package:stackwallet/services/notifications_service.dart'; @@ -55,10 +54,7 @@ class WalletsService extends ChangeNotifier { _walletNames ??= _fetchWalletNames(); WalletsService({ - FlutterSecureStorageInterface secureStorageInterface = - const SecureStorageWrapper( - FlutterSecureStorage(), - ), + required FlutterSecureStorageInterface secureStorageInterface, }) { _secureStore = secureStorageInterface; } diff --git a/lib/utilities/db_version_migration.dart b/lib/utilities/db_version_migration.dart index d1763e266..afb38a487 100644 --- a/lib/utilities/db_version_migration.dart +++ b/lib/utilities/db_version_migration.dart @@ -1,4 +1,3 @@ -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:hive/hive.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/hive/db.dart'; @@ -17,9 +16,7 @@ import 'package:stackwallet/utilities/prefs.dart'; class DbVersionMigrator { Future<void> migrate( int fromVersion, { - FlutterSecureStorageInterface secureStore = const SecureStorageWrapper( - FlutterSecureStorage(), - ), + required FlutterSecureStorageInterface secureStore, }) async { Logging.instance.log( "Running migrate fromVersion $fromVersion", @@ -29,8 +26,9 @@ class DbVersionMigrator { case 0: await Hive.openBox<dynamic>(DB.boxNameAllWalletsData); await Hive.openBox<dynamic>(DB.boxNamePrefs); - final walletsService = WalletsService(); - final nodeService = NodeService(); + final walletsService = + WalletsService(secureStorageInterface: secureStore); + final nodeService = NodeService(secureStorageInterface: secureStore); final prefs = Prefs.instance; final walletInfoList = await walletsService.walletNames; await prefs.init(); @@ -118,7 +116,7 @@ class DbVersionMigrator { boxName: DB.boxNameDBInfo, key: "hive_data_version", value: 1); // try to continue migrating - return await migrate(1); + return await migrate(1, secureStore: secureStore); case 1: await Hive.openBox<ExchangeTransaction>(DB.boxNameTrades); @@ -142,7 +140,7 @@ class DbVersionMigrator { boxName: DB.boxNameDBInfo, key: "hive_data_version", value: 2); // try to continue migrating - return await migrate(2); + return await migrate(2, secureStore: secureStore); case 2: await Hive.openBox<dynamic>(DB.boxNamePrefs); final prefs = Prefs.instance; @@ -154,7 +152,7 @@ class DbVersionMigrator { // update version await DB.instance.put<dynamic>( boxName: DB.boxNameDBInfo, key: "hive_data_version", value: 3); - return await migrate(3); + return await migrate(3, secureStore: secureStore); default: // finally return diff --git a/lib/utilities/desktop_password_service.dart b/lib/utilities/desktop_password_service.dart index da537b3c2..7a2047c30 100644 --- a/lib/utilities/desktop_password_service.dart +++ b/lib/utilities/desktop_password_service.dart @@ -1,6 +1,6 @@ -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:hive/hive.dart'; import 'package:stack_wallet_backup/secure_storage.dart'; -import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/utilities/logger.dart'; const String _kKeyBlobKey = "swbKeyBlobKeyStringID"; @@ -24,7 +24,6 @@ String _getMessageFromException(Object exception) { class DPS { StorageCryptoHandler? _handler; - final SecureStorageWrapper secureStorageWrapper; StorageCryptoHandler get handler { if (_handler == null) { @@ -34,11 +33,7 @@ class DPS { return _handler!; } - DPS({ - this.secureStorageWrapper = const SecureStorageWrapper( - FlutterSecureStorage(), - ), - }); + DPS(); Future<void> initFromNew(String passphrase) async { if (_handler != null) { @@ -47,10 +42,14 @@ class DPS { try { _handler = await StorageCryptoHandler.fromNewPassphrase(passphrase); - await secureStorageWrapper.write( + + final box = await Hive.openBox<String>(DB.boxNameDesktopData); + await DB.instance.put<String>( + boxName: DB.boxNameDesktopData, key: _kKeyBlobKey, value: await _handler!.getKeyBlob(), ); + await box.close(); } catch (e, s) { Logging.instance.log( "${_getMessageFromException(e)}\n$s", @@ -65,7 +64,13 @@ class DPS { throw Exception( "DPS: attempted to re initialize with existing passphrase"); } - final keyBlob = await secureStorageWrapper.read(key: _kKeyBlobKey); + + final box = await Hive.openBox<String>(DB.boxNameDesktopData); + final keyBlob = DB.instance.get<String>( + boxName: DB.boxNameDesktopData, + key: _kKeyBlobKey, + ); + await box.close(); if (keyBlob == null) { throw Exception( @@ -84,6 +89,12 @@ class DPS { } Future<bool> hasPassword() async { - return (await secureStorageWrapper.read(key: _kKeyBlobKey)) != null; + final box = await Hive.openBox<String>(DB.boxNameDesktopData); + final keyBlob = DB.instance.get<String>( + boxName: DB.boxNameDesktopData, + key: _kKeyBlobKey, + ); + await box.close(); + return keyBlob != null; } } diff --git a/lib/utilities/flutter_secure_storage_interface.dart b/lib/utilities/flutter_secure_storage_interface.dart index f8163ae49..f36af94f7 100644 --- a/lib/utilities/flutter_secure_storage_interface.dart +++ b/lib/utilities/flutter_secure_storage_interface.dart @@ -1,4 +1,6 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:isar/isar.dart'; +import 'package:stack_wallet_backup/secure_storage.dart'; abstract class FlutterSecureStorageInterface { Future<void> write({ @@ -33,10 +35,49 @@ abstract class FlutterSecureStorageInterface { }); } -class SecureStorageWrapper implements FlutterSecureStorageInterface { - final FlutterSecureStorage secureStore; +class DesktopPWStore { + final StorageCryptoHandler handler; + late final Isar isar; - const SecureStorageWrapper(this.secureStore); + DesktopPWStore(this.handler); + + Future<void> init() async {} + + Future<String?> read({ + required String key, + }) async { + // final String encryptedString = + + return ""; + } + + Future<void> write({ + required String key, + required String? value, + }) async { + return; + } + + Future<void> delete({ + required String key, + }) async { + return; + } +} + +/// all *Options params ignored on desktop +class SecureStorageWrapper implements FlutterSecureStorageInterface { + final dynamic _store; + final bool _isDesktop; + + const SecureStorageWrapper({ + required dynamic store, + required bool isDesktop, + }) : assert(isDesktop + ? store is DesktopPWStore + : store is FlutterSecureStorage), + _store = store, + _isDesktop = isDesktop; @override Future<String?> read({ @@ -47,16 +88,20 @@ class SecureStorageWrapper implements FlutterSecureStorageInterface { WebOptions? webOptions, MacOsOptions? mOptions, WindowsOptions? wOptions, - }) { - return secureStore.read( - key: key, - iOptions: iOptions, - aOptions: aOptions, - lOptions: lOptions, - webOptions: webOptions, - mOptions: mOptions, - wOptions: wOptions, - ); + }) async { + if (_isDesktop) { + return await (_store as DesktopPWStore).read(key: key); + } else { + return await (_store as FlutterSecureStorage).read( + key: key, + iOptions: iOptions, + aOptions: aOptions, + lOptions: lOptions, + webOptions: webOptions, + mOptions: mOptions, + wOptions: wOptions, + ); + } } @override @@ -69,17 +114,21 @@ class SecureStorageWrapper implements FlutterSecureStorageInterface { WebOptions? webOptions, MacOsOptions? mOptions, WindowsOptions? wOptions, - }) { - return secureStore.write( - key: key, - value: value, - iOptions: iOptions, - aOptions: aOptions, - lOptions: lOptions, - webOptions: webOptions, - mOptions: mOptions, - wOptions: wOptions, - ); + }) async { + if (_isDesktop) { + return await (_store as DesktopPWStore).write(key: key, value: value); + } else { + return await (_store as FlutterSecureStorage).write( + key: key, + value: value, + iOptions: iOptions, + aOptions: aOptions, + lOptions: lOptions, + webOptions: webOptions, + mOptions: mOptions, + wOptions: wOptions, + ); + } } @override @@ -92,15 +141,19 @@ class SecureStorageWrapper implements FlutterSecureStorageInterface { MacOsOptions? mOptions, WindowsOptions? wOptions, }) async { - await secureStore.delete( - key: key, - iOptions: iOptions, - aOptions: aOptions, - lOptions: lOptions, - webOptions: webOptions, - mOptions: mOptions, - wOptions: wOptions, - ); + if (_isDesktop) { + return (_store as DesktopPWStore).delete(key: key); + } else { + return await (_store as FlutterSecureStorage).delete( + key: key, + iOptions: iOptions, + aOptions: aOptions, + lOptions: lOptions, + webOptions: webOptions, + mOptions: mOptions, + wOptions: wOptions, + ); + } } } diff --git a/test/flutter_secure_storage_interface_test.dart b/test/flutter_secure_storage_interface_test.dart index 90dfdcf13..a421b14c1 100644 --- a/test/flutter_secure_storage_interface_test.dart +++ b/test/flutter_secure_storage_interface_test.dart @@ -13,7 +13,7 @@ void main() { when(secureStore.write(key: "testKey", value: "some value")) .thenAnswer((_) async => null); - final wrapper = SecureStorageWrapper(secureStore); + final wrapper = SecureStorageWrapper(store: secureStore, isDesktop: false); await expectLater( () async => await wrapper.write(key: "testKey", value: "some value"), @@ -27,7 +27,7 @@ void main() { final secureStore = MockFlutterSecureStorage(); when(secureStore.read(key: "testKey")) .thenAnswer((_) async => "some value"); - final wrapper = SecureStorageWrapper(secureStore); + final wrapper = SecureStorageWrapper(store: secureStore, isDesktop: false); final result = await wrapper.read(key: "testKey"); @@ -40,7 +40,7 @@ void main() { test("SecureStorageWrapper delete", () async { final secureStore = MockFlutterSecureStorage(); when(secureStore.delete(key: "testKey")).thenAnswer((_) async {}); - final wrapper = SecureStorageWrapper(secureStore); + final wrapper = SecureStorageWrapper(store: secureStore, isDesktop: false); await expectLater( () async => await wrapper.delete(key: "testKey"), returnsNormally); diff --git a/test/services/coins/bitcoin/bitcoin_wallet_test.dart b/test/services/coins/bitcoin/bitcoin_wallet_test.dart index 8a240a3dd..e33ffafdf 100644 --- a/test/services/coins/bitcoin/bitcoin_wallet_test.dart +++ b/test/services/coins/bitcoin/bitcoin_wallet_test.dart @@ -103,7 +103,7 @@ void main() { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; BitcoinWallet? testnetWallet; @@ -194,7 +194,7 @@ void main() { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; BitcoinWallet? mainnetWallet; @@ -363,7 +363,7 @@ void main() { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; BitcoinWallet? btc; @@ -428,7 +428,7 @@ void main() { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; BitcoinWallet? btc; @@ -640,7 +640,7 @@ void main() { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; BitcoinWallet? btc; diff --git a/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart b/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart index 077163809..1c32802c9 100644 --- a/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart +++ b/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart @@ -64,7 +64,7 @@ void main() { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; BitcoinCashWallet? mainnetWallet; @@ -203,7 +203,7 @@ void main() { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; BitcoinCashWallet? mainnetWallet; @@ -314,7 +314,7 @@ void main() { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; BitcoinCashWallet? bch; @@ -383,7 +383,7 @@ void main() { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; BitcoinCashWallet? bch; @@ -606,7 +606,7 @@ void main() { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; BitcoinCashWallet? bch; diff --git a/test/services/coins/dogecoin/dogecoin_wallet_test.dart b/test/services/coins/dogecoin/dogecoin_wallet_test.dart index 7fcb1cdbd..7c1535ec5 100644 --- a/test/services/coins/dogecoin/dogecoin_wallet_test.dart +++ b/test/services/coins/dogecoin/dogecoin_wallet_test.dart @@ -97,7 +97,7 @@ void main() { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; DogecoinWallet? mainnetWallet; @@ -196,7 +196,7 @@ void main() { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; DogecoinWallet? doge; @@ -266,7 +266,7 @@ void main() { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; DogecoinWallet? doge; @@ -489,7 +489,7 @@ void main() { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; DogecoinWallet? doge; diff --git a/test/services/coins/namecoin/namecoin_wallet_test.dart b/test/services/coins/namecoin/namecoin_wallet_test.dart index f6bc1b065..46afd06bd 100644 --- a/test/services/coins/namecoin/namecoin_wallet_test.dart +++ b/test/services/coins/namecoin/namecoin_wallet_test.dart @@ -103,7 +103,7 @@ void main() { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; NamecoinWallet? mainnetWallet; @@ -132,7 +132,7 @@ void main() { mainnetWallet?.addressType( address: "N673DDbjPcrNgJmrhJ1xQXF9LLizQzvjEs"), DerivePathType.bip44); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(tracker); @@ -144,7 +144,7 @@ void main() { mainnetWallet?.addressType( address: "nc1q6k4x8ye6865z3rc8zkt8gyu52na7njqt6hsk4v"), DerivePathType.bip84); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(tracker); @@ -156,7 +156,7 @@ void main() { () => mainnetWallet?.addressType( address: "tb1qzzlm6mnc8k54mx6akehl8p9ray8r439va5ndyq"), throwsArgumentError); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(tracker); @@ -168,7 +168,7 @@ void main() { () => mainnetWallet?.addressType( address: "mpMk94ETazqonHutyC1v6ajshgtP8oiFKU"), throwsArgumentError); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(tracker); @@ -180,7 +180,7 @@ void main() { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; NamecoinWallet? nmc; @@ -208,7 +208,7 @@ void main() { when(client?.ping()).thenAnswer((_) async => false); final bool? result = await nmc?.testNetworkConnection(); expect(result, false); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verify(client?.ping()).called(1); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); @@ -219,7 +219,7 @@ void main() { when(client?.ping()).thenThrow(Exception); final bool? result = await nmc?.testNetworkConnection(); expect(result, false); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verify(client?.ping()).called(1); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); @@ -230,7 +230,7 @@ void main() { when(client?.ping()).thenAnswer((_) async => true); final bool? result = await nmc?.testNetworkConnection(); expect(result, true); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verify(client?.ping()).called(1); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); @@ -245,7 +245,7 @@ void main() { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; NamecoinWallet? nmc; @@ -271,7 +271,7 @@ void main() { test("get networkType main", () async { expect(Coin.namecoin, Coin.namecoin); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); @@ -289,7 +289,7 @@ void main() { secureStore: secureStore, ); expect(Coin.namecoin, Coin.namecoin); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); @@ -297,7 +297,7 @@ void main() { test("get cryptoCurrency", () async { expect(Coin.namecoin, Coin.namecoin); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); @@ -305,7 +305,7 @@ void main() { test("get coinName", () async { expect(Coin.namecoin, Coin.namecoin); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); @@ -313,7 +313,7 @@ void main() { test("get coinTicker", () async { expect(Coin.namecoin, Coin.namecoin); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); @@ -323,7 +323,7 @@ void main() { expect(Coin.namecoin, Coin.namecoin); nmc?.walletName = "new name"; expect(nmc?.walletName, "new name"); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); @@ -338,7 +338,7 @@ void main() { expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 1699), 712); expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 2000), 712); expect(nmc?.estimateTxFee(vSize: 356, feeRatePerKB: 12345), 4628); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); @@ -372,7 +372,7 @@ void main() { verify(client?.estimateFee(blocks: 1)).called(1); verify(client?.estimateFee(blocks: 5)).called(1); verify(client?.estimateFee(blocks: 20)).called(1); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); @@ -409,7 +409,7 @@ void main() { verify(client?.estimateFee(blocks: 1)).called(1); verify(client?.estimateFee(blocks: 5)).called(1); verify(client?.estimateFee(blocks: 20)).called(1); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); @@ -440,7 +440,7 @@ void main() { // verify(client?.estimateFee(blocks: 1)).called(1); // verify(client?.estimateFee(blocks: 5)).called(1); // verify(client?.estimateFee(blocks: 20)).called(1); - // expect(secureStore?.interactions, 0); + // expect(secureStore.interactions, 0); // verifyNoMoreInteractions(client); // verifyNoMoreInteractions(cachedClient); // verifyNoMoreInteractions(tracker); @@ -457,7 +457,7 @@ void main() { MockElectrumX? client; MockCachedElectrumX? cachedClient; MockPriceAPI? priceAPI; - FakeSecureStorage? secureStore; + late FakeSecureStorage secureStore; MockTransactionNotificationTracker? tracker; NamecoinWallet? nmc; @@ -504,7 +504,7 @@ void main() { // test("initializeWallet no network", () async { // when(client?.ping()).thenAnswer((_) async => false); // expect(await nmc?.initializeWallet(), false); - // expect(secureStore?.interactions, 0); + // expect(secureStore.interactions, 0); // verify(client?.ping()).called(1); // verifyNoMoreInteractions(client); // verifyNoMoreInteractions(cachedClient); @@ -515,7 +515,7 @@ void main() { // when(client?.ping()).thenThrow(Exception("Network connection failed")); // final wallets = await Hive.openBox(testWalletId); // expect(await nmc?.initializeExisting(), false); - // expect(secureStore?.interactions, 0); + // expect(secureStore.interactions, 0); // verify(client?.ping()).called(1); // verifyNoMoreInteractions(client); // verifyNoMoreInteractions(cachedClient); @@ -539,7 +539,7 @@ void main() { expectLater(() => nmc?.initializeExisting(), throwsA(isA<Exception>())) .then((_) { - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); // verify(client?.ping()).called(1); // verify(client?.getServerFeatures()).called(1); verifyNoMoreInteractions(client); @@ -560,13 +560,13 @@ void main() { "hash_function": "sha256", "services": [] }); - await secureStore?.write( + await secureStore.write( key: "${testWalletId}_mnemonic", value: "some mnemonic"); final wallets = await Hive.openBox(testWalletId); expectLater(() => nmc?.initializeExisting(), throwsA(isA<Exception>())) .then((_) { - expect(secureStore?.interactions, 1); + expect(secureStore.interactions, 1); // verify(client?.ping()).called(1); // verify(client?.getServerFeatures()).called(1); verifyNoMoreInteractions(client); @@ -603,7 +603,7 @@ void main() { verify(client?.getServerFeatures()).called(1); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); @@ -623,7 +623,7 @@ void main() { "services": [] }); - await secureStore?.write( + await secureStore.write( key: "${testWalletId}_mnemonic", value: "some mnemonic words"); bool hasThrown = false; @@ -640,7 +640,7 @@ void main() { verify(client?.getServerFeatures()).called(1); - expect(secureStore?.interactions, 2); + expect(secureStore.interactions, 2); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); @@ -691,10 +691,10 @@ void main() { verify(client?.getBatchHistory(args: historyBatchArgs4)).called(1); verify(client?.getBatchHistory(args: historyBatchArgs5)).called(1); - expect(secureStore?.interactions, 20); - expect(secureStore?.writes, 7); - expect(secureStore?.reads, 13); - expect(secureStore?.deletes, 0); + expect(secureStore.interactions, 20); + expect(secureStore.writes, 7); + expect(secureStore.reads, 13); + expect(secureStore.deletes, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); @@ -814,10 +814,10 @@ void main() { true); } - expect(secureStore?.interactions, 14); - expect(secureStore?.writes, 7); - expect(secureStore?.reads, 7); - expect(secureStore?.deletes, 0); + expect(secureStore.interactions, 14); + expect(secureStore.writes, 7); + expect(secureStore.reads, 7); + expect(secureStore.deletes, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); @@ -911,17 +911,17 @@ void main() { final preChangeIndexP2SH = await wallet.get('changeIndexP2SH'); final preChangeIndexP2WPKH = await wallet.get('changeIndexP2WPKH'); final preUtxoData = await wallet.get('latest_utxo_model'); - final preReceiveDerivationsStringP2PKH = await secureStore?.read( + final preReceiveDerivationsStringP2PKH = await secureStore.read( key: "${testWalletId}_receiveDerivationsP2PKH"); - final preChangeDerivationsStringP2PKH = await secureStore?.read( - key: "${testWalletId}_changeDerivationsP2PKH"); - final preReceiveDerivationsStringP2SH = await secureStore?.read( - key: "${testWalletId}_receiveDerivationsP2SH"); + final preChangeDerivationsStringP2PKH = + await secureStore.read(key: "${testWalletId}_changeDerivationsP2PKH"); + final preReceiveDerivationsStringP2SH = + await secureStore.read(key: "${testWalletId}_receiveDerivationsP2SH"); final preChangeDerivationsStringP2SH = - await secureStore?.read(key: "${testWalletId}_changeDerivationsP2SH"); - final preReceiveDerivationsStringP2WPKH = await secureStore?.read( + await secureStore.read(key: "${testWalletId}_changeDerivationsP2SH"); + final preReceiveDerivationsStringP2WPKH = await secureStore.read( key: "${testWalletId}_receiveDerivationsP2WPKH"); - final preChangeDerivationsStringP2WPKH = await secureStore?.read( + final preChangeDerivationsStringP2WPKH = await secureStore.read( key: "${testWalletId}_changeDerivationsP2WPKH"); // destroy the data that the rescan will fix @@ -943,17 +943,17 @@ void main() { await wallet.put('changeIndexP2PKH', 123); await wallet.put('changeIndexP2SH', 123); await wallet.put('changeIndexP2WPKH', 123); - await secureStore?.write( + await secureStore.write( key: "${testWalletId}_receiveDerivationsP2PKH", value: "{}"); - await secureStore?.write( + await secureStore.write( key: "${testWalletId}_changeDerivationsP2PKH", value: "{}"); - await secureStore?.write( + await secureStore.write( key: "${testWalletId}_receiveDerivationsP2SH", value: "{}"); - await secureStore?.write( + await secureStore.write( key: "${testWalletId}_changeDerivationsP2SH", value: "{}"); - await secureStore?.write( + await secureStore.write( key: "${testWalletId}_receiveDerivationsP2WPKH", value: "{}"); - await secureStore?.write( + await secureStore.write( key: "${testWalletId}_changeDerivationsP2WPKH", value: "{}"); bool hasThrown = false; @@ -980,17 +980,17 @@ void main() { final changeIndexP2SH = await wallet.get('changeIndexP2SH'); final changeIndexP2WPKH = await wallet.get('changeIndexP2WPKH'); final utxoData = await wallet.get('latest_utxo_model'); - final receiveDerivationsStringP2PKH = await secureStore?.read( + final receiveDerivationsStringP2PKH = await secureStore.read( key: "${testWalletId}_receiveDerivationsP2PKH"); - final changeDerivationsStringP2PKH = await secureStore?.read( - key: "${testWalletId}_changeDerivationsP2PKH"); - final receiveDerivationsStringP2SH = await secureStore?.read( - key: "${testWalletId}_receiveDerivationsP2SH"); + final changeDerivationsStringP2PKH = + await secureStore.read(key: "${testWalletId}_changeDerivationsP2PKH"); + final receiveDerivationsStringP2SH = + await secureStore.read(key: "${testWalletId}_receiveDerivationsP2SH"); final changeDerivationsStringP2SH = - await secureStore?.read(key: "${testWalletId}_changeDerivationsP2SH"); - final receiveDerivationsStringP2WPKH = await secureStore?.read( + await secureStore.read(key: "${testWalletId}_changeDerivationsP2SH"); + final receiveDerivationsStringP2WPKH = await secureStore.read( key: "${testWalletId}_receiveDerivationsP2WPKH"); - final changeDerivationsStringP2WPKH = await secureStore?.read( + final changeDerivationsStringP2WPKH = await secureStore.read( key: "${testWalletId}_changeDerivationsP2WPKH"); expect(preReceivingAddressesP2PKH, receivingAddressesP2PKH); @@ -1082,9 +1082,9 @@ void main() { // // argCount.forEach((key, value) => print("arg: $key\ncount: $value")); - expect(secureStore?.writes, 25); - expect(secureStore?.reads, 32); - expect(secureStore?.deletes, 6); + expect(secureStore.writes, 25); + expect(secureStore.reads, 32); + expect(secureStore.deletes, 6); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); @@ -1182,17 +1182,17 @@ void main() { final preChangeIndexP2SH = await wallet.get('changeIndexP2SH'); final preChangeIndexP2WPKH = await wallet.get('changeIndexP2WPKH'); final preUtxoData = await wallet.get('latest_utxo_model'); - final preReceiveDerivationsStringP2PKH = await secureStore?.read( + final preReceiveDerivationsStringP2PKH = await secureStore.read( key: "${testWalletId}_receiveDerivationsP2PKH"); - final preChangeDerivationsStringP2PKH = await secureStore?.read( - key: "${testWalletId}_changeDerivationsP2PKH"); - final preReceiveDerivationsStringP2SH = await secureStore?.read( - key: "${testWalletId}_receiveDerivationsP2SH"); + final preChangeDerivationsStringP2PKH = + await secureStore.read(key: "${testWalletId}_changeDerivationsP2PKH"); + final preReceiveDerivationsStringP2SH = + await secureStore.read(key: "${testWalletId}_receiveDerivationsP2SH"); final preChangeDerivationsStringP2SH = - await secureStore?.read(key: "${testWalletId}_changeDerivationsP2SH"); - final preReceiveDerivationsStringP2WPKH = await secureStore?.read( + await secureStore.read(key: "${testWalletId}_changeDerivationsP2SH"); + final preReceiveDerivationsStringP2WPKH = await secureStore.read( key: "${testWalletId}_receiveDerivationsP2WPKH"); - final preChangeDerivationsStringP2WPKH = await secureStore?.read( + final preChangeDerivationsStringP2WPKH = await secureStore.read( key: "${testWalletId}_changeDerivationsP2WPKH"); when(client?.getBatchHistory(args: historyBatchArgs0)) @@ -1222,17 +1222,17 @@ void main() { final changeIndexP2SH = await wallet.get('changeIndexP2SH'); final changeIndexP2WPKH = await wallet.get('changeIndexP2WPKH'); final utxoData = await wallet.get('latest_utxo_model'); - final receiveDerivationsStringP2PKH = await secureStore?.read( + final receiveDerivationsStringP2PKH = await secureStore.read( key: "${testWalletId}_receiveDerivationsP2PKH"); - final changeDerivationsStringP2PKH = await secureStore?.read( - key: "${testWalletId}_changeDerivationsP2PKH"); - final receiveDerivationsStringP2SH = await secureStore?.read( - key: "${testWalletId}_receiveDerivationsP2SH"); + final changeDerivationsStringP2PKH = + await secureStore.read(key: "${testWalletId}_changeDerivationsP2PKH"); + final receiveDerivationsStringP2SH = + await secureStore.read(key: "${testWalletId}_receiveDerivationsP2SH"); final changeDerivationsStringP2SH = - await secureStore?.read(key: "${testWalletId}_changeDerivationsP2SH"); - final receiveDerivationsStringP2WPKH = await secureStore?.read( + await secureStore.read(key: "${testWalletId}_changeDerivationsP2SH"); + final receiveDerivationsStringP2WPKH = await secureStore.read( key: "${testWalletId}_receiveDerivationsP2WPKH"); - final changeDerivationsStringP2WPKH = await secureStore?.read( + final changeDerivationsStringP2WPKH = await secureStore.read( key: "${testWalletId}_changeDerivationsP2WPKH"); expect(preReceivingAddressesP2PKH, receivingAddressesP2PKH); @@ -1296,9 +1296,9 @@ void main() { verify(cachedClient?.clearSharedTransactionCache(coin: Coin.namecoin)) .called(1); - expect(secureStore?.writes, 19); - expect(secureStore?.reads, 32); - expect(secureStore?.deletes, 12); + expect(secureStore.writes, 19); + expect(secureStore.reads, 32); + expect(secureStore.deletes, 12); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); @@ -1366,21 +1366,21 @@ void main() { height: 4000); // modify addresses to properly mock data to build a tx - final rcv44 = await secureStore?.read( + final rcv44 = await secureStore.read( key: testWalletId + "_receiveDerivationsP2PKH"); - await secureStore?.write( + await secureStore.write( key: testWalletId + "_receiveDerivationsP2PKH", value: rcv44?.replaceFirst("1RMSPixoLPuaXuhR2v4HsUMcRjLncKDaw", "16FuTPaeRSPVxxCnwQmdyx2PQWxX6HWzhQ")); - final rcv49 = await secureStore?.read( - key: testWalletId + "_receiveDerivationsP2SH"); - await secureStore?.write( + final rcv49 = + await secureStore.read(key: testWalletId + "_receiveDerivationsP2SH"); + await secureStore.write( key: testWalletId + "_receiveDerivationsP2SH", value: rcv49?.replaceFirst("3AV74rKfibWmvX34F99yEvUcG4LLQ9jZZk", "36NvZTcMsMowbt78wPzJaHHWaNiyR73Y4g")); - final rcv84 = await secureStore?.read( + final rcv84 = await secureStore.read( key: testWalletId + "_receiveDerivationsP2WPKH"); - await secureStore?.write( + await secureStore.write( key: testWalletId + "_receiveDerivationsP2WPKH", value: rcv84?.replaceFirst( "bc1qggtj4ka8jsaj44hhd5mpamx7mp34m2d3w7k0m0", @@ -1436,10 +1436,10 @@ void main() { true); } - expect(secureStore?.interactions, 20); - expect(secureStore?.writes, 10); - expect(secureStore?.reads, 10); - expect(secureStore?.deletes, 0); + expect(secureStore.interactions, 20); + expect(secureStore.writes, 10); + expect(secureStore.reads, 10); + expect(secureStore.deletes, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); @@ -1456,7 +1456,7 @@ void main() { expect(didThrow, true); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); @@ -1472,7 +1472,7 @@ void main() { expect(didThrow, true); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); @@ -1492,7 +1492,7 @@ void main() { rawTx: "a string", requestID: anyNamed("requestID"))) .called(1); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); @@ -1513,7 +1513,7 @@ void main() { rawTx: "a string", requestID: anyNamed("requestID"))) .called(1); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(priceAPI); @@ -1538,7 +1538,7 @@ void main() { rawTx: "a string", requestID: anyNamed("requestID"))) .called(1); - expect(secureStore?.interactions, 0); + expect(secureStore.interactions, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); verifyNoMoreInteractions(tracker); @@ -1658,10 +1658,10 @@ void main() { true); } - expect(secureStore?.interactions, 14); - expect(secureStore?.writes, 7); - expect(secureStore?.reads, 7); - expect(secureStore?.deletes, 0); + expect(secureStore.interactions, 14); + expect(secureStore.writes, 7); + expect(secureStore.reads, 7); + expect(secureStore.deletes, 0); verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); @@ -1726,10 +1726,10 @@ void main() { verify(client?.getBatchHistory(args: map)).called(1); } - expect(secureStore?.interactions, 14); - expect(secureStore?.writes, 7); - expect(secureStore?.reads, 7); - expect(secureStore?.deletes, 0); + expect(secureStore.interactions, 14); + expect(secureStore.writes, 7); + expect(secureStore.reads, 7); + expect(secureStore.deletes, 0); // verifyNoMoreInteractions(client); verifyNoMoreInteractions(cachedClient); diff --git a/test/services/node_service_test.dart b/test/services/node_service_test.dart index 81f8ca6ed..cea30be2d 100644 --- a/test/services/node_service_test.dart +++ b/test/services/node_service_test.dart @@ -141,7 +141,8 @@ void main() { ); setUp(() async { - await NodeService().updateDefaults(); + await NodeService(secureStorageInterface: FakeSecureStorage()) + .updateDefaults(); }); test("setPrimaryNodeFor and getPrimaryNodeFor", () async { diff --git a/test/services/wallets_service_test.dart b/test/services/wallets_service_test.dart index 1cb712759..9cd808b7c 100644 --- a/test/services/wallets_service_test.dart +++ b/test/services/wallets_service_test.dart @@ -32,7 +32,7 @@ void main() { }); test("get walletNames", () async { - final service = WalletsService(); + final service = WalletsService(secureStorageInterface: FakeSecureStorage()); expect((await service.walletNames).toString(), '{wallet_id: WalletInfo: {"name":"My Firo Wallet","id":"wallet_id","coin":"bitcoin"}, wallet_id2: WalletInfo: {"name":"wallet2","id":"wallet_id2","coin":"bitcoin"}}'); }); @@ -40,13 +40,13 @@ void main() { test("get null wallet names", () async { final wallets = await Hive.openBox<dynamic>('wallets'); await wallets.put('names', null); - final service = WalletsService(); + final service = WalletsService(secureStorageInterface: FakeSecureStorage()); expect(await service.walletNames, <String, WalletInfo>{}); expect((await service.walletNames).toString(), '{}'); }); test("rename wallet to same name", () async { - final service = WalletsService(); + final service = WalletsService(secureStorageInterface: FakeSecureStorage()); expect( await service.renameWallet( from: "My Firo Wallet", @@ -58,7 +58,7 @@ void main() { }); test("rename wallet to new name", () async { - final service = WalletsService(); + final service = WalletsService(secureStorageInterface: FakeSecureStorage()); expect( await service.renameWallet( from: "My Firo Wallet", @@ -71,7 +71,7 @@ void main() { }); test("attempt rename wallet to another existing name", () async { - final service = WalletsService(); + final service = WalletsService(secureStorageInterface: FakeSecureStorage()); expect( await service.renameWallet( from: "My Firo Wallet", @@ -83,7 +83,7 @@ void main() { }); test("add new wallet name", () async { - final service = WalletsService(); + final service = WalletsService(secureStorageInterface: FakeSecureStorage()); expect( await service.addNewWallet( name: "wallet3", coin: Coin.bitcoin, shouldNotifyListeners: false), @@ -92,7 +92,7 @@ void main() { }); test("add duplicate wallet name fails", () async { - final service = WalletsService(); + final service = WalletsService(secureStorageInterface: FakeSecureStorage()); expect( await service.addNewWallet( name: "wallet2", coin: Coin.bitcoin, shouldNotifyListeners: false), @@ -103,27 +103,27 @@ void main() { test("check for duplicates when null names", () async { final wallets = await Hive.openBox<dynamic>('wallets'); await wallets.put('names', null); - final service = WalletsService(); + final service = WalletsService(secureStorageInterface: FakeSecureStorage()); expect(await service.checkForDuplicate("anything"), false); }); test("check for duplicates when some names with no matches", () async { - final service = WalletsService(); + final service = WalletsService(secureStorageInterface: FakeSecureStorage()); expect(await service.checkForDuplicate("anything"), false); }); test("check for duplicates when some names with a match", () async { - final service = WalletsService(); + final service = WalletsService(secureStorageInterface: FakeSecureStorage()); expect(await service.checkForDuplicate("wallet2"), true); }); test("get existing wallet id", () async { - final service = WalletsService(); + final service = WalletsService(secureStorageInterface: FakeSecureStorage()); expect(await service.getWalletId("wallet2"), "wallet_id2"); }); test("get non existent wallet id", () async { - final service = WalletsService(); + final service = WalletsService(secureStorageInterface: FakeSecureStorage()); expectLater(await service.getWalletId("wallet 99"), null); }); diff --git a/test/services/wallets_service_test.mocks.dart b/test/services/wallets_service_test.mocks.dart index c553bbf93..0950a2cbf 100644 --- a/test/services/wallets_service_test.mocks.dart +++ b/test/services/wallets_service_test.mocks.dart @@ -42,7 +42,7 @@ class MockSecureStorageWrapper extends _i1.Mock } @override - _i2.FlutterSecureStorage get secureStore => (super.noSuchMethod( + _i2.FlutterSecureStorage get _secureStore => (super.noSuchMethod( Invocation.getter(#secureStore), returnValue: _FakeFlutterSecureStorage_0( this, From 3ee0e97628060446550a4809d974d45eee224d5f Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 9 Nov 2022 17:48:43 -0600 Subject: [PATCH 189/426] DesktopSecureStore implementation using Isar as backend, renamed FlutterSecureStorageInterface --- .../isar/models/encrypted_string_value.dart | 18 + .../isar/models/encrypted_string_value.g.dart | 748 ++++++++++++++++++ lib/models/node_model.dart | 3 +- lib/pages/pinpad_views/create_pin_view.dart | 2 +- lib/pages/pinpad_views/lock_screen_view.dart | 2 +- .../add_edit_node_view.dart | 2 +- .../manage_nodes_views/node_details_view.dart | 2 +- .../change_pin_view/change_pin_view.dart | 2 +- .../create_auto_backup_view.dart | 2 +- .../edit_auto_backup_view.dart | 2 +- .../helpers/restore_create_backup.dart | 12 +- .../create_password/create_password_view.dart | 5 + .../desktop_login_view.dart | 39 +- .../global/secure_store_provider.dart | 4 +- lib/services/auto_swb_service.dart | 2 +- .../coins/bitcoin/bitcoin_wallet.dart | 4 +- .../coins/bitcoincash/bitcoincash_wallet.dart | 4 +- lib/services/coins/coin_service.dart | 2 +- .../coins/dogecoin/dogecoin_wallet.dart | 4 +- .../coins/epiccash/epiccash_wallet.dart | 6 +- lib/services/coins/firo/firo_wallet.dart | 4 +- .../coins/litecoin/litecoin_wallet.dart | 4 +- lib/services/coins/monero/monero_wallet.dart | 4 +- .../coins/namecoin/namecoin_wallet.dart | 4 +- .../coins/wownero/wownero_wallet.dart | 4 +- lib/services/node_service.dart | 2 +- lib/services/wallets_service.dart | 4 +- lib/utilities/db_version_migration.dart | 2 +- lib/utilities/desktop_password_service.dart | 2 +- .../flutter_secure_storage_interface.dart | 59 +- test/cached_electrumx_test.mocks.dart | 8 + test/electrumx_test.mocks.dart | 8 + .../pages/send_view/send_view_test.mocks.dart | 45 +- .../exchange/exchange_view_test.mocks.dart | 8 + .../lockscreen_view_screen_test.mocks.dart | 7 +- .../create_pin_view_screen_test.mocks.dart | 7 +- ...restore_wallet_view_screen_test.mocks.dart | 6 +- ...dd_custom_node_view_screen_test.mocks.dart | 7 +- .../node_details_view_screen_test.mocks.dart | 7 +- ...twork_settings_view_screen_test.mocks.dart | 7 +- ...allet_settings_view_screen_test.mocks.dart | 16 + .../bitcoin/bitcoin_wallet_test.mocks.dart | 16 + .../bitcoincash_wallet_test.mocks.dart | 16 + .../dogecoin/dogecoin_wallet_test.mocks.dart | 16 + .../coins/firo/firo_wallet_test.mocks.dart | 16 + test/services/coins/manager_test.mocks.dart | 18 + .../namecoin/namecoin_wallet_test.mocks.dart | 16 + test/services/wallets_service_test.mocks.dart | 85 +- .../managed_favorite_test.mocks.dart | 35 +- test/widget_tests/node_card_test.mocks.dart | 7 +- .../node_options_sheet_test.mocks.dart | 14 +- .../table_view/table_view_row_test.mocks.dart | 28 + .../transaction_card_test.mocks.dart | 36 + test/widget_tests/wallet_card_test.mocks.dart | 18 + ...et_info_row_balance_future_test.mocks.dart | 35 +- .../wallet_info_row_test.mocks.dart | 35 +- 56 files changed, 1306 insertions(+), 165 deletions(-) create mode 100644 lib/models/isar/models/encrypted_string_value.dart create mode 100644 lib/models/isar/models/encrypted_string_value.g.dart diff --git a/lib/models/isar/models/encrypted_string_value.dart b/lib/models/isar/models/encrypted_string_value.dart new file mode 100644 index 000000000..79e9fcaae --- /dev/null +++ b/lib/models/isar/models/encrypted_string_value.dart @@ -0,0 +1,18 @@ +import 'package:isar/isar.dart'; + +part 'encrypted_string_value.g.dart'; + +@Collection() +class EncryptedStringValue { + Id id = Isar.autoIncrement; + + @Index(unique: true, replace: true) + late String key; + + late String value; + + @override + String toString() { + return "EncryptedStringValue {\n key=$key\n value=$value\n}"; + } +} diff --git a/lib/models/isar/models/encrypted_string_value.g.dart b/lib/models/isar/models/encrypted_string_value.g.dart new file mode 100644 index 000000000..2315c5d85 --- /dev/null +++ b/lib/models/isar/models/encrypted_string_value.g.dart @@ -0,0 +1,748 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'encrypted_string_value.dart'; + +// ************************************************************************** +// IsarCollectionGenerator +// ************************************************************************** + +// coverage:ignore-file +// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, join_return_with_assignment, avoid_js_rounded_ints, prefer_final_locals + +extension GetEncryptedStringValueCollection on Isar { + IsarCollection<EncryptedStringValue> get encryptedStringValues => + this.collection(); +} + +const EncryptedStringValueSchema = CollectionSchema( + name: r'EncryptedStringValue', + id: 4826543019451092626, + properties: { + r'key': PropertySchema( + id: 0, + name: r'key', + type: IsarType.string, + ), + r'value': PropertySchema( + id: 1, + name: r'value', + type: IsarType.string, + ) + }, + estimateSize: _encryptedStringValueEstimateSize, + serializeNative: _encryptedStringValueSerializeNative, + deserializeNative: _encryptedStringValueDeserializeNative, + deserializePropNative: _encryptedStringValueDeserializePropNative, + serializeWeb: _encryptedStringValueSerializeWeb, + deserializeWeb: _encryptedStringValueDeserializeWeb, + deserializePropWeb: _encryptedStringValueDeserializePropWeb, + idName: r'id', + indexes: { + r'key': IndexSchema( + id: -4906094122524121629, + name: r'key', + unique: true, + replace: true, + properties: [ + IndexPropertySchema( + name: r'key', + type: IndexType.hash, + caseSensitive: true, + ) + ], + ) + }, + links: {}, + embeddedSchemas: {}, + getId: _encryptedStringValueGetId, + getLinks: _encryptedStringValueGetLinks, + attach: _encryptedStringValueAttach, + version: 5, +); + +int _encryptedStringValueEstimateSize( + EncryptedStringValue object, + List<int> offsets, + Map<Type, List<int>> allOffsets, +) { + var bytesCount = offsets.last; + bytesCount += 3 + object.key.length * 3; + bytesCount += 3 + object.value.length * 3; + return bytesCount; +} + +int _encryptedStringValueSerializeNative( + EncryptedStringValue object, + IsarBinaryWriter writer, + List<int> offsets, + Map<Type, List<int>> allOffsets, +) { + writer.writeString(offsets[0], object.key); + writer.writeString(offsets[1], object.value); + return writer.usedBytes; +} + +EncryptedStringValue _encryptedStringValueDeserializeNative( + int id, + IsarBinaryReader reader, + List<int> offsets, + Map<Type, List<int>> allOffsets, +) { + final object = EncryptedStringValue(); + object.id = id; + object.key = reader.readString(offsets[0]); + object.value = reader.readString(offsets[1]); + return object; +} + +P _encryptedStringValueDeserializePropNative<P>( + Id id, + IsarBinaryReader reader, + int propertyId, + int offset, + Map<Type, List<int>> allOffsets, +) { + switch (propertyId) { + case 0: + return (reader.readString(offset)) as P; + case 1: + return (reader.readString(offset)) as P; + default: + throw IsarError('Unknown property with id $propertyId'); + } +} + +Object _encryptedStringValueSerializeWeb( + IsarCollection<EncryptedStringValue> collection, + EncryptedStringValue object) { + /*final jsObj = IsarNative.newJsObject();*/ throw UnimplementedError(); +} + +EncryptedStringValue _encryptedStringValueDeserializeWeb( + IsarCollection<EncryptedStringValue> collection, Object jsObj) { + /*final object = EncryptedStringValue();object.id = IsarNative.jsObjectGet(jsObj, r'id') ?? (double.negativeInfinity as int);object.key = IsarNative.jsObjectGet(jsObj, r'key') ?? '';object.value = IsarNative.jsObjectGet(jsObj, r'value') ?? '';*/ + //return object; + throw UnimplementedError(); +} + +P _encryptedStringValueDeserializePropWeb<P>( + Object jsObj, String propertyName) { + switch (propertyName) { + default: + throw IsarError('Illegal propertyName'); + } +} + +int? _encryptedStringValueGetId(EncryptedStringValue object) { + if (object.id == Isar.autoIncrement) { + return null; + } else { + return object.id; + } +} + +List<IsarLinkBase<dynamic>> _encryptedStringValueGetLinks( + EncryptedStringValue object) { + return []; +} + +void _encryptedStringValueAttach( + IsarCollection<dynamic> col, Id id, EncryptedStringValue object) { + object.id = id; +} + +extension EncryptedStringValueByIndex on IsarCollection<EncryptedStringValue> { + Future<EncryptedStringValue?> getByKey(String key) { + return getByIndex(r'key', [key]); + } + + EncryptedStringValue? getByKeySync(String key) { + return getByIndexSync(r'key', [key]); + } + + Future<bool> deleteByKey(String key) { + return deleteByIndex(r'key', [key]); + } + + bool deleteByKeySync(String key) { + return deleteByIndexSync(r'key', [key]); + } + + Future<List<EncryptedStringValue?>> getAllByKey(List<String> keyValues) { + final values = keyValues.map((e) => [e]).toList(); + return getAllByIndex(r'key', values); + } + + List<EncryptedStringValue?> getAllByKeySync(List<String> keyValues) { + final values = keyValues.map((e) => [e]).toList(); + return getAllByIndexSync(r'key', values); + } + + Future<int> deleteAllByKey(List<String> keyValues) { + final values = keyValues.map((e) => [e]).toList(); + return deleteAllByIndex(r'key', values); + } + + int deleteAllByKeySync(List<String> keyValues) { + final values = keyValues.map((e) => [e]).toList(); + return deleteAllByIndexSync(r'key', values); + } + + Future<int> putByKey(EncryptedStringValue object) { + return putByIndex(r'key', object); + } + + int putByKeySync(EncryptedStringValue object, {bool saveLinks = true}) { + return putByIndexSync(r'key', object, saveLinks: saveLinks); + } + + Future<List<int>> putAllByKey(List<EncryptedStringValue> objects) { + return putAllByIndex(r'key', objects); + } + + List<int> putAllByKeySync(List<EncryptedStringValue> objects, + {bool saveLinks = true}) { + return putAllByIndexSync(r'key', objects, saveLinks: saveLinks); + } +} + +extension EncryptedStringValueQueryWhereSort + on QueryBuilder<EncryptedStringValue, EncryptedStringValue, QWhere> { + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterWhere> + anyId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension EncryptedStringValueQueryWhere + on QueryBuilder<EncryptedStringValue, EncryptedStringValue, QWhereClause> { + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterWhereClause> + idEqualTo(int id) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: id, + upper: id, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterWhereClause> + idNotEqualTo(int id) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ) + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ); + } else { + return query + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ) + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ); + } + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterWhereClause> + idGreaterThan(int id, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: include), + ); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterWhereClause> + idLessThan(int id, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: include), + ); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterWhereClause> + idBetween( + int lowerId, + int upperId, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: lowerId, + includeLower: includeLower, + upper: upperId, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterWhereClause> + keyEqualTo(String key) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'key', + value: [key], + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterWhereClause> + keyNotEqualTo(String key) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'key', + lower: [], + upper: [key], + includeUpper: false, + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'key', + lower: [key], + includeLower: false, + upper: [], + )); + } else { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'key', + lower: [key], + includeLower: false, + upper: [], + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'key', + lower: [], + upper: [key], + includeUpper: false, + )); + } + }); + } +} + +extension EncryptedStringValueQueryFilter on QueryBuilder<EncryptedStringValue, + EncryptedStringValue, QFilterCondition> { + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> idEqualTo(int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'id', + value: value, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> idGreaterThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> idLessThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> idBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'id', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> keyEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'key', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> keyGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'key', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> keyLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'key', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> keyBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'key', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> keyStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'key', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> keyEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'key', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> + keyContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'key', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> + keyMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'key', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> valueEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'value', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> valueGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'value', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> valueLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'value', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> valueBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'value', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> valueStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'value', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> valueEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'value', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> + valueContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'value', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, + QAfterFilterCondition> + valueMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'value', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } +} + +extension EncryptedStringValueQueryObject on QueryBuilder<EncryptedStringValue, + EncryptedStringValue, QFilterCondition> {} + +extension EncryptedStringValueQueryLinks on QueryBuilder<EncryptedStringValue, + EncryptedStringValue, QFilterCondition> {} + +extension EncryptedStringValueQuerySortBy + on QueryBuilder<EncryptedStringValue, EncryptedStringValue, QSortBy> { + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterSortBy> + sortByKey() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'key', Sort.asc); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterSortBy> + sortByKeyDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'key', Sort.desc); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterSortBy> + sortByValue() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'value', Sort.asc); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterSortBy> + sortByValueDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'value', Sort.desc); + }); + } +} + +extension EncryptedStringValueQuerySortThenBy + on QueryBuilder<EncryptedStringValue, EncryptedStringValue, QSortThenBy> { + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterSortBy> + thenById() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.asc); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterSortBy> + thenByIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.desc); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterSortBy> + thenByKey() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'key', Sort.asc); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterSortBy> + thenByKeyDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'key', Sort.desc); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterSortBy> + thenByValue() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'value', Sort.asc); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QAfterSortBy> + thenByValueDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'value', Sort.desc); + }); + } +} + +extension EncryptedStringValueQueryWhereDistinct + on QueryBuilder<EncryptedStringValue, EncryptedStringValue, QDistinct> { + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QDistinct> + distinctByKey({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'key', caseSensitive: caseSensitive); + }); + } + + QueryBuilder<EncryptedStringValue, EncryptedStringValue, QDistinct> + distinctByValue({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'value', caseSensitive: caseSensitive); + }); + } +} + +extension EncryptedStringValueQueryProperty on QueryBuilder< + EncryptedStringValue, EncryptedStringValue, QQueryProperty> { + QueryBuilder<EncryptedStringValue, int, QQueryOperations> idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'id'); + }); + } + + QueryBuilder<EncryptedStringValue, String, QQueryOperations> keyProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'key'); + }); + } + + QueryBuilder<EncryptedStringValue, String, QQueryOperations> valueProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'value'); + }); + } +} diff --git a/lib/models/node_model.dart b/lib/models/node_model.dart index 342f8b2ef..2628c5dd9 100644 --- a/lib/models/node_model.dart +++ b/lib/models/node_model.dart @@ -65,8 +65,7 @@ class NodeModel { } /// convenience getter to retrieve login password - Future<String?> getPassword( - FlutterSecureStorageInterface secureStorage) async { + Future<String?> getPassword(SecureStorageInterface secureStorage) async { return await secureStorage.read(key: "${id}_nodePW"); } diff --git a/lib/pages/pinpad_views/create_pin_view.dart b/lib/pages/pinpad_views/create_pin_view.dart index 5ea8bc363..766f10fa5 100644 --- a/lib/pages/pinpad_views/create_pin_view.dart +++ b/lib/pages/pinpad_views/create_pin_view.dart @@ -54,7 +54,7 @@ class _CreatePinViewState extends ConsumerState<CreatePinView> { final TextEditingController _pinPutController2 = TextEditingController(); final FocusNode _pinPutFocusNode2 = FocusNode(); - late FlutterSecureStorageInterface _secureStore; + late SecureStorageInterface _secureStore; late Biometrics biometrics; @override diff --git a/lib/pages/pinpad_views/lock_screen_view.dart b/lib/pages/pinpad_views/lock_screen_view.dart index 137f3d55d..60d317e21 100644 --- a/lib/pages/pinpad_views/lock_screen_view.dart +++ b/lib/pages/pinpad_views/lock_screen_view.dart @@ -158,7 +158,7 @@ class _LockscreenViewState extends ConsumerState<LockscreenView> { final _pinTextController = TextEditingController(); final FocusNode _pinFocusNode = FocusNode(); - late FlutterSecureStorageInterface _secureStore; + late SecureStorageInterface _secureStore; late Biometrics biometrics; Scaffold get _body => Scaffold( diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index 39ab493f0..890953caf 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -634,7 +634,7 @@ class NodeForm extends ConsumerStatefulWidget { }) : super(key: key); final NodeModel? node; - final FlutterSecureStorageInterface secureStore; + final SecureStorageInterface secureStore; final bool readOnly; final Coin coin; final void Function(bool canSave, bool canTestConnection)? onChanged; diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart index 6d9641b7d..a80a64147 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart @@ -45,7 +45,7 @@ class NodeDetailsView extends ConsumerStatefulWidget { } class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> { - late final FlutterSecureStorageInterface secureStore; + late final SecureStorageInterface secureStore; late final Coin coin; late final String nodeId; late final String popRouteName; diff --git a/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart b/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart index 46c2fd9cf..c88da0521 100644 --- a/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart +++ b/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart @@ -45,7 +45,7 @@ class _ChangePinViewState extends ConsumerState<ChangePinView> { final TextEditingController _pinPutController2 = TextEditingController(); final FocusNode _pinPutFocusNode2 = FocusNode(); - late final FlutterSecureStorageInterface _secureStore; + late final SecureStorageInterface _secureStore; @override void initState() { diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart index 1082acc99..334d50e35 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart @@ -41,7 +41,7 @@ class CreateAutoBackupView extends ConsumerStatefulWidget { } class _EnableAutoBackupViewState extends ConsumerState<CreateAutoBackupView> { - late final FlutterSecureStorageInterface secureStore; + late final SecureStorageInterface secureStore; late final TextEditingController fileLocationController; late final TextEditingController passwordController; diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart index d5ba21634..0be718549 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart @@ -40,7 +40,7 @@ class EditAutoBackupView extends ConsumerStatefulWidget { } class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> { - late final FlutterSecureStorageInterface secureStore; + late final SecureStorageInterface secureStore; late final TextEditingController fileLocationController; late final TextEditingController passwordController; diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart index 9b755c95a..3d599f717 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart @@ -90,7 +90,7 @@ abstract class SWB { static bool _checkShouldCancel( PreRestoreState? revertToState, - FlutterSecureStorageInterface secureStorageInterface, + SecureStorageInterface secureStorageInterface, ) { if (_shouldCancelRestore) { if (revertToState != null) { @@ -193,7 +193,7 @@ abstract class SWB { /// [secureStorage] parameter exposed for testing purposes static Future<Map<String, dynamic>> createStackWalletJSON({ - required FlutterSecureStorageInterface secureStorage, + required SecureStorageInterface secureStorage, }) async { Logging.instance .log("Starting createStackWalletJSON...", level: LogLevel.Info); @@ -448,7 +448,7 @@ abstract class SWB { Map<String, dynamic> validJSON, StackRestoringUIState? uiState, Map<String, String> oldToNewWalletIdMap, - FlutterSecureStorageInterface secureStorageInterface, + SecureStorageInterface secureStorageInterface, ) async { Map<String, dynamic> prefs = validJSON["prefs"] as Map<String, dynamic>; List<dynamic>? addressBookEntries = @@ -548,7 +548,7 @@ abstract class SWB { static Future<bool?> restoreStackWalletJSON( String jsonBackup, StackRestoringUIState? uiState, - FlutterSecureStorageInterface secureStorageInterface, + SecureStorageInterface secureStorageInterface, ) async { if (!Platform.isLinux) await Wakelock.enable(); @@ -769,7 +769,7 @@ abstract class SWB { static Future<void> _revert( PreRestoreState revertToState, - FlutterSecureStorageInterface secureStorageInterface, + SecureStorageInterface secureStorageInterface, ) async { Map<String, dynamic> prefs = revertToState.validJSON["prefs"] as Map<String, dynamic>; @@ -1042,7 +1042,7 @@ abstract class SWB { static Future<void> _restoreNodes( List<dynamic>? nodes, List<dynamic>? primaryNodes, - FlutterSecureStorageInterface secureStorageInterface, + SecureStorageInterface secureStorageInterface, ) async { NodeService nodeService = NodeService( secureStorageInterface: secureStorageInterface, diff --git a/lib/pages_desktop_specific/create_password/create_password_view.dart b/lib/pages_desktop_specific/create_password/create_password_view.dart index 1e137500d..d5ce42679 100644 --- a/lib/pages_desktop_specific/create_password/create_password_view.dart +++ b/lib/pages_desktop_specific/create_password/create_password_view.dart @@ -72,6 +72,11 @@ class _CreatePasswordViewState extends ConsumerState<CreatePasswordView> { } try { + if (await ref.read(storageCryptoHandlerProvider).hasPassword()) { + throw Exception( + "Tried creating a new password and attempted to overwrite an existing entry!"); + } + await ref.read(storageCryptoHandlerProvider).initFromNew(passphrase); } catch (e) { unawaited(showFloatingFlushBar( diff --git a/lib/pages_desktop_specific/desktop_login_view.dart b/lib/pages_desktop_specific/desktop_login_view.dart index fe05d719f..0a47c2a96 100644 --- a/lib/pages_desktop_specific/desktop_login_view.dart +++ b/lib/pages_desktop_specific/desktop_login_view.dart @@ -1,9 +1,15 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages_desktop_specific/forgot_password_desktop_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; +import 'package:stackwallet/providers/desktop/storage_crypto_handler_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; @@ -11,7 +17,7 @@ import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; -class DesktopLoginView extends StatefulWidget { +class DesktopLoginView extends ConsumerStatefulWidget { const DesktopLoginView({ Key? key, this.startupWalletId, @@ -22,10 +28,10 @@ class DesktopLoginView extends StatefulWidget { final String? startupWalletId; @override - State<DesktopLoginView> createState() => _DesktopLoginViewState(); + ConsumerState<DesktopLoginView> createState() => _DesktopLoginViewState(); } -class _DesktopLoginViewState extends State<DesktopLoginView> { +class _DesktopLoginViewState extends ConsumerState<DesktopLoginView> { late final TextEditingController passwordController; late final FocusNode passwordFocusNode; @@ -153,13 +159,28 @@ class _DesktopLoginViewState extends State<DesktopLoginView> { PrimaryButton( label: "Continue", enabled: _continueEnabled, - onPressed: () { - // todo auth + onPressed: () async { + try { + await ref + .read(storageCryptoHandlerProvider) + .initFromExisting(passwordController.text); - Navigator.of(context).pushNamedAndRemoveUntil( - DesktopHomeView.routeName, - (route) => false, - ); + // if no errors passphrase is correct + if (mounted) { + unawaited( + Navigator.of(context).pushNamedAndRemoveUntil( + DesktopHomeView.routeName, + (route) => false, + ), + ); + } + } catch (e) { + await showFloatingFlushBar( + type: FlushBarType.warning, + message: e.toString(), + context: context, + ); + } }, ), const SizedBox( diff --git a/lib/providers/global/secure_store_provider.dart b/lib/providers/global/secure_store_provider.dart index 299ea0f5c..32304d0f2 100644 --- a/lib/providers/global/secure_store_provider.dart +++ b/lib/providers/global/secure_store_provider.dart @@ -4,11 +4,11 @@ import 'package:stackwallet/providers/desktop/storage_crypto_handler_provider.da import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/util.dart'; -final secureStoreProvider = Provider<FlutterSecureStorageInterface>((ref) { +final secureStoreProvider = Provider<SecureStorageInterface>((ref) { if (Util.isDesktop) { final handler = ref.read(storageCryptoHandlerProvider).handler; return SecureStorageWrapper( - store: DesktopPWStore(handler), isDesktop: true); + store: DesktopSecureStore(handler), isDesktop: true); } else { return const SecureStorageWrapper( store: FlutterSecureStorage(), diff --git a/lib/services/auto_swb_service.dart b/lib/services/auto_swb_service.dart index 06ec7a2fb..f7efc994e 100644 --- a/lib/services/auto_swb_service.dart +++ b/lib/services/auto_swb_service.dart @@ -24,7 +24,7 @@ class AutoSWBService extends ChangeNotifier { bool _isActiveTimer = false; bool get isActivePeriodicTimer => _isActiveTimer; - final FlutterSecureStorageInterface secureStorageInterface; + final SecureStorageInterface secureStorageInterface; AutoSWBService({required this.secureStorageInterface}); diff --git a/lib/services/coins/bitcoin/bitcoin_wallet.dart b/lib/services/coins/bitcoin/bitcoin_wallet.dart index e3c2f13fd..dfd5ea180 100644 --- a/lib/services/coins/bitcoin/bitcoin_wallet.dart +++ b/lib/services/coins/bitcoin/bitcoin_wallet.dart @@ -1356,7 +1356,7 @@ class BitcoinWallet extends CoinServiceAPI { CachedElectrumX get cachedElectrumXClient => _cachedElectrumXClient; - late FlutterSecureStorageInterface _secureStore; + late SecureStorageInterface _secureStore; late PriceAPI _priceAPI; @@ -1368,7 +1368,7 @@ class BitcoinWallet extends CoinServiceAPI { required CachedElectrumX cachedClient, required TransactionNotificationTracker tracker, PriceAPI? priceAPI, - required FlutterSecureStorageInterface secureStore, + required SecureStorageInterface secureStore, }) { txTracker = tracker; _walletId = walletId; diff --git a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart index 063c74315..429af898e 100644 --- a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart +++ b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart @@ -1261,7 +1261,7 @@ class BitcoinCashWallet extends CoinServiceAPI { CachedElectrumX get cachedElectrumXClient => _cachedElectrumXClient; - late FlutterSecureStorageInterface _secureStore; + late SecureStorageInterface _secureStore; late PriceAPI _priceAPI; @@ -1273,7 +1273,7 @@ class BitcoinCashWallet extends CoinServiceAPI { required CachedElectrumX cachedClient, required TransactionNotificationTracker tracker, PriceAPI? priceAPI, - required FlutterSecureStorageInterface secureStore, + required SecureStorageInterface secureStore, }) { txTracker = tracker; _walletId = walletId; diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart index e63ff86e1..16015ea0c 100644 --- a/lib/services/coins/coin_service.dart +++ b/lib/services/coins/coin_service.dart @@ -25,7 +25,7 @@ abstract class CoinServiceAPI { Coin coin, String walletId, String walletName, - FlutterSecureStorageInterface secureStorageInterface, + SecureStorageInterface secureStorageInterface, NodeModel node, TransactionNotificationTracker tracker, Prefs prefs, diff --git a/lib/services/coins/dogecoin/dogecoin_wallet.dart b/lib/services/coins/dogecoin/dogecoin_wallet.dart index c3100b0f7..41778a9e0 100644 --- a/lib/services/coins/dogecoin/dogecoin_wallet.dart +++ b/lib/services/coins/dogecoin/dogecoin_wallet.dart @@ -1124,7 +1124,7 @@ class DogecoinWallet extends CoinServiceAPI { CachedElectrumX get cachedElectrumXClient => _cachedElectrumXClient; - late FlutterSecureStorageInterface _secureStore; + late SecureStorageInterface _secureStore; late PriceAPI _priceAPI; @@ -1136,7 +1136,7 @@ class DogecoinWallet extends CoinServiceAPI { required CachedElectrumX cachedClient, required TransactionNotificationTracker tracker, PriceAPI? priceAPI, - required FlutterSecureStorageInterface secureStore, + required SecureStorageInterface secureStore, }) { txTracker = tracker; _walletId = walletId; diff --git a/lib/services/coins/epiccash/epiccash_wallet.dart b/lib/services/coins/epiccash/epiccash_wallet.dart index d7b7f35dd..683e26544 100644 --- a/lib/services/coins/epiccash/epiccash_wallet.dart +++ b/lib/services/coins/epiccash/epiccash_wallet.dart @@ -250,7 +250,7 @@ Future<String> _deleteWalletWrapper(String wallet) async { Future<String> deleteEpicWallet({ required String walletId, - required FlutterSecureStorageInterface secureStore, + required SecureStorageInterface secureStore, }) async { String? config = await secureStore.read(key: '${walletId}_config'); if (Platform.isIOS) { @@ -517,7 +517,7 @@ class EpicCashWallet extends CoinServiceAPI { required String walletName, required Coin coin, PriceAPI? priceAPI, - required FlutterSecureStorageInterface secureStore}) { + required SecureStorageInterface secureStore}) { _walletId = walletId; _walletName = walletName; _coin = coin; @@ -658,7 +658,7 @@ class EpicCashWallet extends CoinServiceAPI { @override Coin get coin => _coin; - late FlutterSecureStorageInterface _secureStore; + late SecureStorageInterface _secureStore; late PriceAPI _priceAPI; diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index d0f99ef1d..4bd863f2c 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -1305,7 +1305,7 @@ class FiroWallet extends CoinServiceAPI { late CachedElectrumX _cachedElectrumXClient; CachedElectrumX get cachedElectrumXClient => _cachedElectrumXClient; - late FlutterSecureStorageInterface _secureStore; + late SecureStorageInterface _secureStore; late PriceAPI _priceAPI; @@ -1320,7 +1320,7 @@ class FiroWallet extends CoinServiceAPI { required CachedElectrumX cachedClient, required TransactionNotificationTracker tracker, PriceAPI? priceAPI, - required FlutterSecureStorageInterface secureStore, + required SecureStorageInterface secureStore, }) { txTracker = tracker; _walletId = walletId; diff --git a/lib/services/coins/litecoin/litecoin_wallet.dart b/lib/services/coins/litecoin/litecoin_wallet.dart index 30d6ede39..db7c9d1fa 100644 --- a/lib/services/coins/litecoin/litecoin_wallet.dart +++ b/lib/services/coins/litecoin/litecoin_wallet.dart @@ -1358,7 +1358,7 @@ class LitecoinWallet extends CoinServiceAPI { CachedElectrumX get cachedElectrumXClient => _cachedElectrumXClient; - late FlutterSecureStorageInterface _secureStore; + late SecureStorageInterface _secureStore; late PriceAPI _priceAPI; @@ -1370,7 +1370,7 @@ class LitecoinWallet extends CoinServiceAPI { required CachedElectrumX cachedClient, required TransactionNotificationTracker tracker, PriceAPI? priceAPI, - required FlutterSecureStorageInterface secureStore, + required SecureStorageInterface secureStore, }) { txTracker = tracker; _walletId = walletId; diff --git a/lib/services/coins/monero/monero_wallet.dart b/lib/services/coins/monero/monero_wallet.dart index 569498a15..69e0bd38d 100644 --- a/lib/services/coins/monero/monero_wallet.dart +++ b/lib/services/coins/monero/monero_wallet.dart @@ -67,7 +67,7 @@ class MoneroWallet extends CoinServiceAPI { Timer? moneroAutosaveTimer; late Coin _coin; - late FlutterSecureStorageInterface _secureStore; + late SecureStorageInterface _secureStore; late PriceAPI _priceAPI; @@ -82,7 +82,7 @@ class MoneroWallet extends CoinServiceAPI { required String walletName, required Coin coin, PriceAPI? priceAPI, - required FlutterSecureStorageInterface secureStore}) { + required SecureStorageInterface secureStore}) { _walletId = walletId; _walletName = walletName; _coin = coin; diff --git a/lib/services/coins/namecoin/namecoin_wallet.dart b/lib/services/coins/namecoin/namecoin_wallet.dart index 1ebf87233..c8c84fb27 100644 --- a/lib/services/coins/namecoin/namecoin_wallet.dart +++ b/lib/services/coins/namecoin/namecoin_wallet.dart @@ -1349,7 +1349,7 @@ class NamecoinWallet extends CoinServiceAPI { CachedElectrumX get cachedElectrumXClient => _cachedElectrumXClient; - late FlutterSecureStorageInterface _secureStore; + late SecureStorageInterface _secureStore; late PriceAPI _priceAPI; @@ -1361,7 +1361,7 @@ class NamecoinWallet extends CoinServiceAPI { required CachedElectrumX cachedClient, required TransactionNotificationTracker tracker, PriceAPI? priceAPI, - required FlutterSecureStorageInterface secureStore, + required SecureStorageInterface secureStore, }) { txTracker = tracker; _walletId = walletId; diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart index 7a219158a..7476afbef 100644 --- a/lib/services/coins/wownero/wownero_wallet.dart +++ b/lib/services/coins/wownero/wownero_wallet.dart @@ -68,7 +68,7 @@ class WowneroWallet extends CoinServiceAPI { Timer? wowneroAutosaveTimer; late Coin _coin; - late FlutterSecureStorageInterface _secureStore; + late SecureStorageInterface _secureStore; late PriceAPI _priceAPI; @@ -83,7 +83,7 @@ class WowneroWallet extends CoinServiceAPI { required String walletName, required Coin coin, PriceAPI? priceAPI, - required FlutterSecureStorageInterface secureStore}) { + required SecureStorageInterface secureStore}) { _walletId = walletId; _walletName = walletName; _coin = coin; diff --git a/lib/services/node_service.dart b/lib/services/node_service.dart index 0dd706781..aa8d5a6d9 100644 --- a/lib/services/node_service.dart +++ b/lib/services/node_service.dart @@ -12,7 +12,7 @@ import 'package:stackwallet/utilities/logger.dart'; const kStackCommunityNodesEndpoint = "https://extras.stackwallet.com"; class NodeService extends ChangeNotifier { - final FlutterSecureStorageInterface secureStorageInterface; + final SecureStorageInterface secureStorageInterface; /// Exposed [secureStorageInterface] in order to inject mock for tests NodeService({ diff --git a/lib/services/wallets_service.dart b/lib/services/wallets_service.dart index 237df8026..1371b17b6 100644 --- a/lib/services/wallets_service.dart +++ b/lib/services/wallets_service.dart @@ -47,14 +47,14 @@ class WalletInfo { } class WalletsService extends ChangeNotifier { - late final FlutterSecureStorageInterface _secureStore; + late final SecureStorageInterface _secureStore; Future<Map<String, WalletInfo>>? _walletNames; Future<Map<String, WalletInfo>> get walletNames => _walletNames ??= _fetchWalletNames(); WalletsService({ - required FlutterSecureStorageInterface secureStorageInterface, + required SecureStorageInterface secureStorageInterface, }) { _secureStore = secureStorageInterface; } diff --git a/lib/utilities/db_version_migration.dart b/lib/utilities/db_version_migration.dart index afb38a487..ae5190fc4 100644 --- a/lib/utilities/db_version_migration.dart +++ b/lib/utilities/db_version_migration.dart @@ -16,7 +16,7 @@ import 'package:stackwallet/utilities/prefs.dart'; class DbVersionMigrator { Future<void> migrate( int fromVersion, { - required FlutterSecureStorageInterface secureStore, + required SecureStorageInterface secureStore, }) async { Logging.instance.log( "Running migrate fromVersion $fromVersion", diff --git a/lib/utilities/desktop_password_service.dart b/lib/utilities/desktop_password_service.dart index 7a2047c30..178871e69 100644 --- a/lib/utilities/desktop_password_service.dart +++ b/lib/utilities/desktop_password_service.dart @@ -84,7 +84,7 @@ class DPS { "${_getMessageFromException(e)}\n$s", level: LogLevel.Error, ); - rethrow; + throw Exception(_getMessageFromException(e)); } } diff --git a/lib/utilities/flutter_secure_storage_interface.dart b/lib/utilities/flutter_secure_storage_interface.dart index f36af94f7..852c13f3d 100644 --- a/lib/utilities/flutter_secure_storage_interface.dart +++ b/lib/utilities/flutter_secure_storage_interface.dart @@ -1,8 +1,11 @@ +import 'dart:io'; + import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:isar/isar.dart'; import 'package:stack_wallet_backup/secure_storage.dart'; +import 'package:stackwallet/models/isar/models/encrypted_string_value.dart'; -abstract class FlutterSecureStorageInterface { +abstract class SecureStorageInterface { Future<void> write({ required String key, required String? value, @@ -35,38 +38,66 @@ abstract class FlutterSecureStorageInterface { }); } -class DesktopPWStore { +class DesktopSecureStore { final StorageCryptoHandler handler; late final Isar isar; - DesktopPWStore(this.handler); + DesktopSecureStore(this.handler); - Future<void> init() async {} + Future<void> init() async { + Directory? appDirectory; + if (Platform.isLinux) { + appDirectory = Directory("${Platform.environment['HOME']}/.stackwallet"); + await appDirectory.create(); + } + isar = await Isar.open( + [EncryptedStringValueSchema], + directory: appDirectory!.path, + inspector: false, + ); + } Future<String?> read({ required String key, }) async { - // final String encryptedString = + final value = + await isar.encryptedStringValues.filter().keyEqualTo(key).findFirst(); - return ""; + // value does not exist; + if (value == null) { + return null; + } + + return await handler.decryptValue(key, value.value); } Future<void> write({ required String key, required String? value, }) async { - return; + if (value == null) { + // here we assume that a value is to be deleted + await isar.encryptedStringValues.deleteByKey(key); + } else { + // otherwise created encrypted object value + final object = EncryptedStringValue(); + object.key = key; + object.value = await handler.encryptValue(key, value); + + // store object value + await isar.encryptedStringValues.put(object); + } } Future<void> delete({ required String key, }) async { - return; + await isar.encryptedStringValues.deleteByKey(key); } } /// all *Options params ignored on desktop -class SecureStorageWrapper implements FlutterSecureStorageInterface { +class SecureStorageWrapper implements SecureStorageInterface { final dynamic _store; final bool _isDesktop; @@ -74,7 +105,7 @@ class SecureStorageWrapper implements FlutterSecureStorageInterface { required dynamic store, required bool isDesktop, }) : assert(isDesktop - ? store is DesktopPWStore + ? store is DesktopSecureStore : store is FlutterSecureStorage), _store = store, _isDesktop = isDesktop; @@ -90,7 +121,7 @@ class SecureStorageWrapper implements FlutterSecureStorageInterface { WindowsOptions? wOptions, }) async { if (_isDesktop) { - return await (_store as DesktopPWStore).read(key: key); + return await (_store as DesktopSecureStore).read(key: key); } else { return await (_store as FlutterSecureStorage).read( key: key, @@ -116,7 +147,7 @@ class SecureStorageWrapper implements FlutterSecureStorageInterface { WindowsOptions? wOptions, }) async { if (_isDesktop) { - return await (_store as DesktopPWStore).write(key: key, value: value); + return await (_store as DesktopSecureStore).write(key: key, value: value); } else { return await (_store as FlutterSecureStorage).write( key: key, @@ -142,7 +173,7 @@ class SecureStorageWrapper implements FlutterSecureStorageInterface { WindowsOptions? wOptions, }) async { if (_isDesktop) { - return (_store as DesktopPWStore).delete(key: key); + return (_store as DesktopSecureStore).delete(key: key); } else { return await (_store as FlutterSecureStorage).delete( key: key, @@ -158,7 +189,7 @@ class SecureStorageWrapper implements FlutterSecureStorageInterface { } // Mock class for testing purposes -class FakeSecureStorage implements FlutterSecureStorageInterface { +class FakeSecureStorage implements SecureStorageInterface { final Map<String, String?> _store = {}; int _interactions = 0; int get interactions => _interactions; diff --git a/test/cached_electrumx_test.mocks.dart b/test/cached_electrumx_test.mocks.dart index 1e3e70a0a..a45cdd402 100644 --- a/test/cached_electrumx_test.mocks.dart +++ b/test/cached_electrumx_test.mocks.dart @@ -678,6 +678,14 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs { returnValueForMissingStub: _i4.Future<void>.value(), ) as _i4.Future<void>); @override + _i4.Future<bool> isExternalCallsSet() => (super.noSuchMethod( + Invocation.method( + #isExternalCallsSet, + [], + ), + returnValue: _i4.Future<bool>.value(false), + ) as _i4.Future<bool>); + @override void addListener(_i9.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, diff --git a/test/electrumx_test.mocks.dart b/test/electrumx_test.mocks.dart index fa48ea2fd..9f29ae1e5 100644 --- a/test/electrumx_test.mocks.dart +++ b/test/electrumx_test.mocks.dart @@ -399,6 +399,14 @@ class MockPrefs extends _i1.Mock implements _i4.Prefs { returnValueForMissingStub: _i3.Future<void>.value(), ) as _i3.Future<void>); @override + _i3.Future<bool> isExternalCallsSet() => (super.noSuchMethod( + Invocation.method( + #isExternalCallsSet, + [], + ), + returnValue: _i3.Future<bool>.value(false), + ) as _i3.Future<bool>); + @override void addListener(_i8.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, diff --git a/test/pages/send_view/send_view_test.mocks.dart b/test/pages/send_view/send_view_test.mocks.dart index a07377309..d63dafb04 100644 --- a/test/pages/send_view/send_view_test.mocks.dart +++ b/test/pages/send_view/send_view_test.mocks.dart @@ -1,5 +1,5 @@ // Mocks generated by Mockito 5.3.2 from annotations -// in stackwallet/test/pages/send_view_test.dart. +// in stackwallet/test/pages/send_view/send_view_test.dart. // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes @@ -86,7 +86,7 @@ class _FakeManager_3 extends _i1.SmartFake implements _i6.Manager { } class _FakeFlutterSecureStorageInterface_4 extends _i1.SmartFake - implements _i7.FlutterSecureStorageInterface { + implements _i7.SecureStorageInterface { _FakeFlutterSecureStorageInterface_4( Object parent, Invocation parentInvocation, @@ -621,14 +621,13 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { } @override - _i7.FlutterSecureStorageInterface get secureStorageInterface => - (super.noSuchMethod( + _i7.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), returnValue: _FakeFlutterSecureStorageInterface_4( this, Invocation.getter(#secureStorageInterface), ), - ) as _i7.FlutterSecureStorageInterface); + ) as _i7.SecureStorageInterface); @override List<_i19.NodeModel> get primaryNodes => (super.noSuchMethod( Invocation.getter(#primaryNodes), @@ -890,6 +889,14 @@ class MockBitcoinWallet extends _i1.Mock implements _i20.BitcoinWallet { returnValueForMissingStub: null, ); @override + set cachedTxData(_i9.TransactionData? _cachedTxData) => super.noSuchMethod( + Invocation.setter( + #cachedTxData, + _cachedTxData, + ), + returnValueForMissingStub: null, + ); + @override bool get isActive => (super.noSuchMethod( Invocation.getter(#isActive), returnValue: false, @@ -1272,6 +1279,16 @@ class MockBitcoinWallet extends _i1.Mock implements _i20.BitcoinWallet { returnValueForMissingStub: _i16.Future<void>.value(), ) as _i16.Future<void>); @override + _i16.Future<void> updateSentCachedTxData(Map<String, dynamic>? txData) => + (super.noSuchMethod( + Invocation.method( + #updateSentCachedTxData, + [txData], + ), + returnValue: _i16.Future<void>.value(), + returnValueForMissingStub: _i16.Future<void>.value(), + ) as _i16.Future<void>); + @override bool validateAddress(String? address) => (super.noSuchMethod( Invocation.method( #validateAddress, @@ -1875,6 +1892,14 @@ class MockPrefs extends _i1.Mock implements _i17.Prefs { returnValueForMissingStub: _i16.Future<void>.value(), ) as _i16.Future<void>); @override + _i16.Future<bool> isExternalCallsSet() => (super.noSuchMethod( + Invocation.method( + #isExternalCallsSet, + [], + ), + returnValue: _i16.Future<bool>.value(false), + ) as _i16.Future<bool>); + @override void addListener(_i18.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, @@ -2623,4 +2648,14 @@ class MockCoinServiceAPI extends _i1.Mock implements _i13.CoinServiceAPI { ), returnValue: _i16.Future<bool>.value(false), ) as _i16.Future<bool>); + @override + _i16.Future<void> updateSentCachedTxData(Map<String, dynamic>? txData) => + (super.noSuchMethod( + Invocation.method( + #updateSentCachedTxData, + [txData], + ), + returnValue: _i16.Future<void>.value(), + returnValueForMissingStub: _i16.Future<void>.value(), + ) as _i16.Future<void>); } diff --git a/test/screen_tests/exchange/exchange_view_test.mocks.dart b/test/screen_tests/exchange/exchange_view_test.mocks.dart index 1d93023d5..0da12dbd0 100644 --- a/test/screen_tests/exchange/exchange_view_test.mocks.dart +++ b/test/screen_tests/exchange/exchange_view_test.mocks.dart @@ -350,6 +350,14 @@ class MockPrefs extends _i1.Mock implements _i3.Prefs { returnValueForMissingStub: _i7.Future<void>.value(), ) as _i7.Future<void>); @override + _i7.Future<bool> isExternalCallsSet() => (super.noSuchMethod( + Invocation.method( + #isExternalCallsSet, + [], + ), + returnValue: _i7.Future<bool>.value(false), + ) as _i7.Future<bool>); + @override void addListener(_i8.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, diff --git a/test/screen_tests/lockscreen_view_screen_test.mocks.dart b/test/screen_tests/lockscreen_view_screen_test.mocks.dart index 1cd697e05..a33c4ef28 100644 --- a/test/screen_tests/lockscreen_view_screen_test.mocks.dart +++ b/test/screen_tests/lockscreen_view_screen_test.mocks.dart @@ -30,7 +30,7 @@ import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' // ignore_for_file: subtype_of_sealed_class class _FakeFlutterSecureStorageInterface_0 extends _i1.SmartFake - implements _i2.FlutterSecureStorageInterface { + implements _i2.SecureStorageInterface { _FakeFlutterSecureStorageInterface_0( Object parent, Invocation parentInvocation, @@ -309,14 +309,13 @@ class MockWalletsService extends _i1.Mock implements _i6.WalletsService { /// See the documentation for Mockito's code generation for more information. class MockNodeService extends _i1.Mock implements _i10.NodeService { @override - _i2.FlutterSecureStorageInterface get secureStorageInterface => - (super.noSuchMethod( + _i2.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), returnValue: _FakeFlutterSecureStorageInterface_0( this, Invocation.getter(#secureStorageInterface), ), - ) as _i2.FlutterSecureStorageInterface); + ) as _i2.SecureStorageInterface); @override List<_i11.NodeModel> get primaryNodes => (super.noSuchMethod( Invocation.getter(#primaryNodes), diff --git a/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart b/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart index 62d3c597d..3aed1dcb8 100644 --- a/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart @@ -30,7 +30,7 @@ import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' // ignore_for_file: subtype_of_sealed_class class _FakeFlutterSecureStorageInterface_0 extends _i1.SmartFake - implements _i2.FlutterSecureStorageInterface { + implements _i2.SecureStorageInterface { _FakeFlutterSecureStorageInterface_0( Object parent, Invocation parentInvocation, @@ -309,14 +309,13 @@ class MockWalletsService extends _i1.Mock implements _i6.WalletsService { /// See the documentation for Mockito's code generation for more information. class MockNodeService extends _i1.Mock implements _i10.NodeService { @override - _i2.FlutterSecureStorageInterface get secureStorageInterface => - (super.noSuchMethod( + _i2.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), returnValue: _FakeFlutterSecureStorageInterface_0( this, Invocation.getter(#secureStorageInterface), ), - ) as _i2.FlutterSecureStorageInterface); + ) as _i2.SecureStorageInterface); @override List<_i11.NodeModel> get primaryNodes => (super.noSuchMethod( Invocation.getter(#primaryNodes), diff --git a/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart b/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart index cf891e829..cd4986e13 100644 --- a/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart @@ -84,7 +84,7 @@ class _FakeTransactionData_4 extends _i1.SmartFake } class _FakeFlutterSecureStorageInterface_5 extends _i1.SmartFake - implements _i6.FlutterSecureStorageInterface { + implements _i6.SecureStorageInterface { _FakeFlutterSecureStorageInterface_5( Object parent, Invocation parentInvocation, @@ -744,14 +744,14 @@ class MockManager extends _i1.Mock implements _i12.Manager { /// See the documentation for Mockito's code generation for more information. class MockNodeService extends _i1.Mock implements _i13.NodeService { @override - _i6.FlutterSecureStorageInterface get secureStorageInterface => + _i6.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), returnValue: _FakeFlutterSecureStorageInterface_5( this, Invocation.getter(#secureStorageInterface), ), - ) as _i6.FlutterSecureStorageInterface); + ) as _i6.SecureStorageInterface); @override List<_i14.NodeModel> get primaryNodes => (super.noSuchMethod( Invocation.getter(#primaryNodes), diff --git a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart index d80b97852..3ac5afcc7 100644 --- a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart @@ -29,7 +29,7 @@ import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' // ignore_for_file: subtype_of_sealed_class class _FakeFlutterSecureStorageInterface_0 extends _i1.SmartFake - implements _i2.FlutterSecureStorageInterface { + implements _i2.SecureStorageInterface { _FakeFlutterSecureStorageInterface_0( Object parent, Invocation parentInvocation, @@ -86,14 +86,13 @@ class _FakeTransactionData_4 extends _i1.SmartFake /// See the documentation for Mockito's code generation for more information. class MockNodeService extends _i1.Mock implements _i6.NodeService { @override - _i2.FlutterSecureStorageInterface get secureStorageInterface => - (super.noSuchMethod( + _i2.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), returnValue: _FakeFlutterSecureStorageInterface_0( this, Invocation.getter(#secureStorageInterface), ), - ) as _i2.FlutterSecureStorageInterface); + ) as _i2.SecureStorageInterface); @override List<_i7.NodeModel> get primaryNodes => (super.noSuchMethod( Invocation.getter(#primaryNodes), diff --git a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart index 89820ee97..0f6447a9e 100644 --- a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart @@ -29,7 +29,7 @@ import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' // ignore_for_file: subtype_of_sealed_class class _FakeFlutterSecureStorageInterface_0 extends _i1.SmartFake - implements _i2.FlutterSecureStorageInterface { + implements _i2.SecureStorageInterface { _FakeFlutterSecureStorageInterface_0( Object parent, Invocation parentInvocation, @@ -86,14 +86,13 @@ class _FakeTransactionData_4 extends _i1.SmartFake /// See the documentation for Mockito's code generation for more information. class MockNodeService extends _i1.Mock implements _i6.NodeService { @override - _i2.FlutterSecureStorageInterface get secureStorageInterface => - (super.noSuchMethod( + _i2.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), returnValue: _FakeFlutterSecureStorageInterface_0( this, Invocation.getter(#secureStorageInterface), ), - ) as _i2.FlutterSecureStorageInterface); + ) as _i2.SecureStorageInterface); @override List<_i7.NodeModel> get primaryNodes => (super.noSuchMethod( Invocation.getter(#primaryNodes), diff --git a/test/screen_tests/settings_view/settings_subviews/network_settings_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/network_settings_view_screen_test.mocks.dart index 7e8ff731e..707da7345 100644 --- a/test/screen_tests/settings_view/settings_subviews/network_settings_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/network_settings_view_screen_test.mocks.dart @@ -25,7 +25,7 @@ import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' // ignore_for_file: subtype_of_sealed_class class _FakeFlutterSecureStorageInterface_0 extends _i1.SmartFake - implements _i2.FlutterSecureStorageInterface { + implements _i2.SecureStorageInterface { _FakeFlutterSecureStorageInterface_0( Object parent, Invocation parentInvocation, @@ -40,14 +40,13 @@ class _FakeFlutterSecureStorageInterface_0 extends _i1.SmartFake /// See the documentation for Mockito's code generation for more information. class MockNodeService extends _i1.Mock implements _i3.NodeService { @override - _i2.FlutterSecureStorageInterface get secureStorageInterface => - (super.noSuchMethod( + _i2.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), returnValue: _FakeFlutterSecureStorageInterface_0( this, Invocation.getter(#secureStorageInterface), ), - ) as _i2.FlutterSecureStorageInterface); + ) as _i2.SecureStorageInterface); @override List<_i4.NodeModel> get primaryNodes => (super.noSuchMethod( Invocation.getter(#primaryNodes), diff --git a/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart index 27ae85597..7eb0853dd 100644 --- a/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart @@ -139,6 +139,22 @@ class MockCachedElectrumX extends _i1.Mock implements _i6.CachedElectrumX { _i8.Future<Map<String, dynamic>>.value(<String, dynamic>{}), ) as _i8.Future<Map<String, dynamic>>); @override + String base64ToHex(String? source) => (super.noSuchMethod( + Invocation.method( + #base64ToHex, + [source], + ), + returnValue: '', + ) as String); + @override + String base64ToReverseHex(String? source) => (super.noSuchMethod( + Invocation.method( + #base64ToReverseHex, + [source], + ), + returnValue: '', + ) as String); + @override _i8.Future<Map<String, dynamic>> getTransaction({ required String? txHash, required _i9.Coin? coin, diff --git a/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart b/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart index 99e2e6231..32a6c7195 100644 --- a/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart +++ b/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart @@ -465,6 +465,22 @@ class MockCachedElectrumX extends _i1.Mock implements _i7.CachedElectrumX { _i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}), ) as _i6.Future<Map<String, dynamic>>); @override + String base64ToHex(String? source) => (super.noSuchMethod( + Invocation.method( + #base64ToHex, + [source], + ), + returnValue: '', + ) as String); + @override + String base64ToReverseHex(String? source) => (super.noSuchMethod( + Invocation.method( + #base64ToReverseHex, + [source], + ), + returnValue: '', + ) as String); + @override _i6.Future<Map<String, dynamic>> getTransaction({ required String? txHash, required _i8.Coin? coin, diff --git a/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart b/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart index a5a2018e7..bfc5f793b 100644 --- a/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart +++ b/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart @@ -465,6 +465,22 @@ class MockCachedElectrumX extends _i1.Mock implements _i7.CachedElectrumX { _i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}), ) as _i6.Future<Map<String, dynamic>>); @override + String base64ToHex(String? source) => (super.noSuchMethod( + Invocation.method( + #base64ToHex, + [source], + ), + returnValue: '', + ) as String); + @override + String base64ToReverseHex(String? source) => (super.noSuchMethod( + Invocation.method( + #base64ToReverseHex, + [source], + ), + returnValue: '', + ) as String); + @override _i6.Future<Map<String, dynamic>> getTransaction({ required String? txHash, required _i8.Coin? coin, diff --git a/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart b/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart index 944e5faea..f7220922e 100644 --- a/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart +++ b/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart @@ -465,6 +465,22 @@ class MockCachedElectrumX extends _i1.Mock implements _i7.CachedElectrumX { _i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}), ) as _i6.Future<Map<String, dynamic>>); @override + String base64ToHex(String? source) => (super.noSuchMethod( + Invocation.method( + #base64ToHex, + [source], + ), + returnValue: '', + ) as String); + @override + String base64ToReverseHex(String? source) => (super.noSuchMethod( + Invocation.method( + #base64ToReverseHex, + [source], + ), + returnValue: '', + ) as String); + @override _i6.Future<Map<String, dynamic>> getTransaction({ required String? txHash, required _i8.Coin? coin, diff --git a/test/services/coins/firo/firo_wallet_test.mocks.dart b/test/services/coins/firo/firo_wallet_test.mocks.dart index 4fe6af52a..2d32cef48 100644 --- a/test/services/coins/firo/firo_wallet_test.mocks.dart +++ b/test/services/coins/firo/firo_wallet_test.mocks.dart @@ -465,6 +465,22 @@ class MockCachedElectrumX extends _i1.Mock implements _i7.CachedElectrumX { _i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}), ) as _i6.Future<Map<String, dynamic>>); @override + String base64ToHex(String? source) => (super.noSuchMethod( + Invocation.method( + #base64ToHex, + [source], + ), + returnValue: '', + ) as String); + @override + String base64ToReverseHex(String? source) => (super.noSuchMethod( + Invocation.method( + #base64ToReverseHex, + [source], + ), + returnValue: '', + ) as String); + @override _i6.Future<Map<String, dynamic>> getTransaction({ required String? txHash, required _i8.Coin? coin, diff --git a/test/services/coins/manager_test.mocks.dart b/test/services/coins/manager_test.mocks.dart index 0c20b752d..9a958702f 100644 --- a/test/services/coins/manager_test.mocks.dart +++ b/test/services/coins/manager_test.mocks.dart @@ -117,6 +117,14 @@ class MockFiroWallet extends _i1.Mock implements _i7.FiroWallet { returnValueForMissingStub: null, ); @override + set cachedTxData(_i4.TransactionData? _cachedTxData) => super.noSuchMethod( + Invocation.setter( + #cachedTxData, + _cachedTxData, + ), + returnValueForMissingStub: null, + ); + @override _i2.TransactionNotificationTracker get txTracker => (super.noSuchMethod( Invocation.getter(#txTracker), returnValue: _FakeTransactionNotificationTracker_0( @@ -375,6 +383,16 @@ class MockFiroWallet extends _i1.Mock implements _i7.FiroWallet { returnValue: false, ) as bool); @override + _i8.Future<void> updateSentCachedTxData(Map<String, dynamic>? txData) => + (super.noSuchMethod( + Invocation.method( + #updateSentCachedTxData, + [txData], + ), + returnValue: _i8.Future<void>.value(), + returnValueForMissingStub: _i8.Future<void>.value(), + ) as _i8.Future<void>); + @override _i8.Future<bool> testNetworkConnection() => (super.noSuchMethod( Invocation.method( #testNetworkConnection, diff --git a/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart b/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart index a81c27fe0..91c3e5bfa 100644 --- a/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart +++ b/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart @@ -465,6 +465,22 @@ class MockCachedElectrumX extends _i1.Mock implements _i7.CachedElectrumX { _i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}), ) as _i6.Future<Map<String, dynamic>>); @override + String base64ToHex(String? source) => (super.noSuchMethod( + Invocation.method( + #base64ToHex, + [source], + ), + returnValue: '', + ) as String); + @override + String base64ToReverseHex(String? source) => (super.noSuchMethod( + Invocation.method( + #base64ToReverseHex, + [source], + ), + returnValue: '', + ) as String); + @override _i6.Future<Map<String, dynamic>> getTransaction({ required String? txHash, required _i8.Coin? coin, diff --git a/test/services/wallets_service_test.mocks.dart b/test/services/wallets_service_test.mocks.dart index 0950a2cbf..19d525196 100644 --- a/test/services/wallets_service_test.mocks.dart +++ b/test/services/wallets_service_test.mocks.dart @@ -3,12 +3,12 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i4; +import 'dart:async' as _i3; -import 'package:flutter_secure_storage/flutter_secure_storage.dart' as _i2; +import 'package:flutter_secure_storage/flutter_secure_storage.dart' as _i4; import 'package:mockito/mockito.dart' as _i1; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' - as _i3; + as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -21,43 +21,24 @@ import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeFlutterSecureStorage_0 extends _i1.SmartFake - implements _i2.FlutterSecureStorage { - _FakeFlutterSecureStorage_0( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - /// A class which mocks [SecureStorageWrapper]. /// /// See the documentation for Mockito's code generation for more information. class MockSecureStorageWrapper extends _i1.Mock - implements _i3.SecureStorageWrapper { + implements _i2.SecureStorageWrapper { MockSecureStorageWrapper() { _i1.throwOnMissingStub(this); } @override - _i2.FlutterSecureStorage get _secureStore => (super.noSuchMethod( - Invocation.getter(#secureStore), - returnValue: _FakeFlutterSecureStorage_0( - this, - Invocation.getter(#secureStore), - ), - ) as _i2.FlutterSecureStorage); - @override - _i4.Future<String?> read({ + _i3.Future<String?> read({ required String? key, - _i2.IOSOptions? iOptions, - _i2.AndroidOptions? aOptions, - _i2.LinuxOptions? lOptions, - _i2.WebOptions? webOptions, - _i2.MacOsOptions? mOptions, - _i2.WindowsOptions? wOptions, + _i4.IOSOptions? iOptions, + _i4.AndroidOptions? aOptions, + _i4.LinuxOptions? lOptions, + _i4.WebOptions? webOptions, + _i4.MacOsOptions? mOptions, + _i4.WindowsOptions? wOptions, }) => (super.noSuchMethod( Invocation.method( @@ -73,18 +54,18 @@ class MockSecureStorageWrapper extends _i1.Mock #wOptions: wOptions, }, ), - returnValue: _i4.Future<String?>.value(), - ) as _i4.Future<String?>); + returnValue: _i3.Future<String?>.value(), + ) as _i3.Future<String?>); @override - _i4.Future<void> write({ + _i3.Future<void> write({ required String? key, required String? value, - _i2.IOSOptions? iOptions, - _i2.AndroidOptions? aOptions, - _i2.LinuxOptions? lOptions, - _i2.WebOptions? webOptions, - _i2.MacOsOptions? mOptions, - _i2.WindowsOptions? wOptions, + _i4.IOSOptions? iOptions, + _i4.AndroidOptions? aOptions, + _i4.LinuxOptions? lOptions, + _i4.WebOptions? webOptions, + _i4.MacOsOptions? mOptions, + _i4.WindowsOptions? wOptions, }) => (super.noSuchMethod( Invocation.method( @@ -101,18 +82,18 @@ class MockSecureStorageWrapper extends _i1.Mock #wOptions: wOptions, }, ), - returnValue: _i4.Future<void>.value(), - returnValueForMissingStub: _i4.Future<void>.value(), - ) as _i4.Future<void>); + returnValue: _i3.Future<void>.value(), + returnValueForMissingStub: _i3.Future<void>.value(), + ) as _i3.Future<void>); @override - _i4.Future<void> delete({ + _i3.Future<void> delete({ required String? key, - _i2.IOSOptions? iOptions, - _i2.AndroidOptions? aOptions, - _i2.LinuxOptions? lOptions, - _i2.WebOptions? webOptions, - _i2.MacOsOptions? mOptions, - _i2.WindowsOptions? wOptions, + _i4.IOSOptions? iOptions, + _i4.AndroidOptions? aOptions, + _i4.LinuxOptions? lOptions, + _i4.WebOptions? webOptions, + _i4.MacOsOptions? mOptions, + _i4.WindowsOptions? wOptions, }) => (super.noSuchMethod( Invocation.method( @@ -128,7 +109,7 @@ class MockSecureStorageWrapper extends _i1.Mock #wOptions: wOptions, }, ), - returnValue: _i4.Future<void>.value(), - returnValueForMissingStub: _i4.Future<void>.value(), - ) as _i4.Future<void>); + returnValue: _i3.Future<void>.value(), + returnValueForMissingStub: _i3.Future<void>.value(), + ) as _i3.Future<void>); } diff --git a/test/widget_tests/managed_favorite_test.mocks.dart b/test/widget_tests/managed_favorite_test.mocks.dart index 7d1d864df..50f906c3c 100644 --- a/test/widget_tests/managed_favorite_test.mocks.dart +++ b/test/widget_tests/managed_favorite_test.mocks.dart @@ -166,7 +166,7 @@ class _FakeElectrumXNode_11 extends _i1.SmartFake } class _FakeFlutterSecureStorageInterface_12 extends _i1.SmartFake - implements _i12.FlutterSecureStorageInterface { + implements _i12.SecureStorageInterface { _FakeFlutterSecureStorageInterface_12( Object parent, Invocation parentInvocation, @@ -681,6 +681,14 @@ class MockBitcoinWallet extends _i1.Mock implements _i19.BitcoinWallet { returnValueForMissingStub: null, ); @override + set cachedTxData(_i8.TransactionData? _cachedTxData) => super.noSuchMethod( + Invocation.setter( + #cachedTxData, + _cachedTxData, + ), + returnValueForMissingStub: null, + ); + @override bool get isActive => (super.noSuchMethod( Invocation.getter(#isActive), returnValue: false, @@ -1063,6 +1071,16 @@ class MockBitcoinWallet extends _i1.Mock implements _i19.BitcoinWallet { returnValueForMissingStub: _i16.Future<void>.value(), ) as _i16.Future<void>); @override + _i16.Future<void> updateSentCachedTxData(Map<String, dynamic>? txData) => + (super.noSuchMethod( + Invocation.method( + #updateSentCachedTxData, + [txData], + ), + returnValue: _i16.Future<void>.value(), + returnValueForMissingStub: _i16.Future<void>.value(), + ) as _i16.Future<void>); + @override bool validateAddress(String? address) => (super.noSuchMethod( Invocation.method( #validateAddress, @@ -1380,14 +1398,13 @@ class MockLocaleService extends _i1.Mock implements _i20.LocaleService { /// See the documentation for Mockito's code generation for more information. class MockNodeService extends _i1.Mock implements _i3.NodeService { @override - _i12.FlutterSecureStorageInterface get secureStorageInterface => - (super.noSuchMethod( + _i12.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), returnValue: _FakeFlutterSecureStorageInterface_12( this, Invocation.getter(#secureStorageInterface), ), - ) as _i12.FlutterSecureStorageInterface); + ) as _i12.SecureStorageInterface); @override List<_i21.NodeModel> get primaryNodes => (super.noSuchMethod( Invocation.getter(#primaryNodes), @@ -2291,4 +2308,14 @@ class MockCoinServiceAPI extends _i1.Mock implements _i13.CoinServiceAPI { ), returnValue: _i16.Future<bool>.value(false), ) as _i16.Future<bool>); + @override + _i16.Future<void> updateSentCachedTxData(Map<String, dynamic>? txData) => + (super.noSuchMethod( + Invocation.method( + #updateSentCachedTxData, + [txData], + ), + returnValue: _i16.Future<void>.value(), + returnValueForMissingStub: _i16.Future<void>.value(), + ) as _i16.Future<void>); } diff --git a/test/widget_tests/node_card_test.mocks.dart b/test/widget_tests/node_card_test.mocks.dart index 673182ceb..2bb32b58d 100644 --- a/test/widget_tests/node_card_test.mocks.dart +++ b/test/widget_tests/node_card_test.mocks.dart @@ -25,7 +25,7 @@ import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' // ignore_for_file: subtype_of_sealed_class class _FakeFlutterSecureStorageInterface_0 extends _i1.SmartFake - implements _i2.FlutterSecureStorageInterface { + implements _i2.SecureStorageInterface { _FakeFlutterSecureStorageInterface_0( Object parent, Invocation parentInvocation, @@ -44,14 +44,13 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { } @override - _i2.FlutterSecureStorageInterface get secureStorageInterface => - (super.noSuchMethod( + _i2.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), returnValue: _FakeFlutterSecureStorageInterface_0( this, Invocation.getter(#secureStorageInterface), ), - ) as _i2.FlutterSecureStorageInterface); + ) as _i2.SecureStorageInterface); @override List<_i4.NodeModel> get primaryNodes => (super.noSuchMethod( Invocation.getter(#primaryNodes), diff --git a/test/widget_tests/node_options_sheet_test.mocks.dart b/test/widget_tests/node_options_sheet_test.mocks.dart index 173edf1bf..c9e4e2bb8 100644 --- a/test/widget_tests/node_options_sheet_test.mocks.dart +++ b/test/widget_tests/node_options_sheet_test.mocks.dart @@ -77,7 +77,7 @@ class _FakeManager_3 extends _i1.SmartFake implements _i6.Manager { } class _FakeFlutterSecureStorageInterface_4 extends _i1.SmartFake - implements _i7.FlutterSecureStorageInterface { + implements _i7.SecureStorageInterface { _FakeFlutterSecureStorageInterface_4( Object parent, Invocation parentInvocation, @@ -573,6 +573,14 @@ class MockPrefs extends _i1.Mock implements _i11.Prefs { returnValueForMissingStub: _i10.Future<void>.value(), ) as _i10.Future<void>); @override + _i10.Future<bool> isExternalCallsSet() => (super.noSuchMethod( + Invocation.method( + #isExternalCallsSet, + [], + ), + returnValue: _i10.Future<bool>.value(false), + ) as _i10.Future<bool>); + @override void addListener(_i12.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, @@ -615,14 +623,14 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { } @override - _i7.FlutterSecureStorageInterface get secureStorageInterface => + _i7.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), returnValue: _FakeFlutterSecureStorageInterface_4( this, Invocation.getter(#secureStorageInterface), ), - ) as _i7.FlutterSecureStorageInterface); + ) as _i7.SecureStorageInterface); @override List<_i16.NodeModel> get primaryNodes => (super.noSuchMethod( Invocation.getter(#primaryNodes), diff --git a/test/widget_tests/table_view/table_view_row_test.mocks.dart b/test/widget_tests/table_view/table_view_row_test.mocks.dart index b4a0e9cc7..5a47436ff 100644 --- a/test/widget_tests/table_view/table_view_row_test.mocks.dart +++ b/test/widget_tests/table_view/table_view_row_test.mocks.dart @@ -666,6 +666,14 @@ class MockBitcoinWallet extends _i1.Mock implements _i18.BitcoinWallet { returnValueForMissingStub: null, ); @override + set cachedTxData(_i8.TransactionData? _cachedTxData) => super.noSuchMethod( + Invocation.setter( + #cachedTxData, + _cachedTxData, + ), + returnValueForMissingStub: null, + ); + @override bool get isActive => (super.noSuchMethod( Invocation.getter(#isActive), returnValue: false, @@ -1048,6 +1056,16 @@ class MockBitcoinWallet extends _i1.Mock implements _i18.BitcoinWallet { returnValueForMissingStub: _i15.Future<void>.value(), ) as _i15.Future<void>); @override + _i15.Future<void> updateSentCachedTxData(Map<String, dynamic>? txData) => + (super.noSuchMethod( + Invocation.method( + #updateSentCachedTxData, + [txData], + ), + returnValue: _i15.Future<void>.value(), + returnValueForMissingStub: _i15.Future<void>.value(), + ) as _i15.Future<void>); + @override bool validateAddress(String? address) => (super.noSuchMethod( Invocation.method( #validateAddress, @@ -2013,4 +2031,14 @@ class MockCoinServiceAPI extends _i1.Mock implements _i12.CoinServiceAPI { ), returnValue: _i15.Future<bool>.value(false), ) as _i15.Future<bool>); + @override + _i15.Future<void> updateSentCachedTxData(Map<String, dynamic>? txData) => + (super.noSuchMethod( + Invocation.method( + #updateSentCachedTxData, + [txData], + ), + returnValue: _i15.Future<void>.value(), + returnValueForMissingStub: _i15.Future<void>.value(), + ) as _i15.Future<void>); } diff --git a/test/widget_tests/transaction_card_test.mocks.dart b/test/widget_tests/transaction_card_test.mocks.dart index df696801a..f258a402b 100644 --- a/test/widget_tests/transaction_card_test.mocks.dart +++ b/test/widget_tests/transaction_card_test.mocks.dart @@ -1108,6 +1108,16 @@ class MockCoinServiceAPI extends _i1.Mock implements _i7.CoinServiceAPI { ), returnValue: _i16.Future<bool>.value(false), ) as _i16.Future<bool>); + @override + _i16.Future<void> updateSentCachedTxData(Map<String, dynamic>? txData) => + (super.noSuchMethod( + Invocation.method( + #updateSentCachedTxData, + [txData], + ), + returnValue: _i16.Future<void>.value(), + returnValueForMissingStub: _i16.Future<void>.value(), + ) as _i16.Future<void>); } /// A class which mocks [FiroWallet]. @@ -1127,6 +1137,14 @@ class MockFiroWallet extends _i1.Mock implements _i19.FiroWallet { returnValueForMissingStub: null, ); @override + set cachedTxData(_i8.TransactionData? _cachedTxData) => super.noSuchMethod( + Invocation.setter( + #cachedTxData, + _cachedTxData, + ), + returnValueForMissingStub: null, + ); + @override _i10.TransactionNotificationTracker get txTracker => (super.noSuchMethod( Invocation.getter(#txTracker), returnValue: _FakeTransactionNotificationTracker_8( @@ -1386,6 +1404,16 @@ class MockFiroWallet extends _i1.Mock implements _i19.FiroWallet { returnValue: false, ) as bool); @override + _i16.Future<void> updateSentCachedTxData(Map<String, dynamic>? txData) => + (super.noSuchMethod( + Invocation.method( + #updateSentCachedTxData, + [txData], + ), + returnValue: _i16.Future<void>.value(), + returnValueForMissingStub: _i16.Future<void>.value(), + ) as _i16.Future<void>); + @override _i16.Future<bool> testNetworkConnection() => (super.noSuchMethod( Invocation.method( #testNetworkConnection, @@ -2312,6 +2340,14 @@ class MockPrefs extends _i1.Mock implements _i17.Prefs { returnValueForMissingStub: _i16.Future<void>.value(), ) as _i16.Future<void>); @override + _i16.Future<bool> isExternalCallsSet() => (super.noSuchMethod( + Invocation.method( + #isExternalCallsSet, + [], + ), + returnValue: _i16.Future<bool>.value(false), + ) as _i16.Future<bool>); + @override void addListener(_i18.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, diff --git a/test/widget_tests/wallet_card_test.mocks.dart b/test/widget_tests/wallet_card_test.mocks.dart index ec4f90e22..e323911d4 100644 --- a/test/widget_tests/wallet_card_test.mocks.dart +++ b/test/widget_tests/wallet_card_test.mocks.dart @@ -429,6 +429,14 @@ class MockBitcoinWallet extends _i1.Mock implements _i17.BitcoinWallet { returnValueForMissingStub: null, ); @override + set cachedTxData(_i8.TransactionData? _cachedTxData) => super.noSuchMethod( + Invocation.setter( + #cachedTxData, + _cachedTxData, + ), + returnValueForMissingStub: null, + ); + @override bool get isActive => (super.noSuchMethod( Invocation.getter(#isActive), returnValue: false, @@ -811,6 +819,16 @@ class MockBitcoinWallet extends _i1.Mock implements _i17.BitcoinWallet { returnValueForMissingStub: _i14.Future<void>.value(), ) as _i14.Future<void>); @override + _i14.Future<void> updateSentCachedTxData(Map<String, dynamic>? txData) => + (super.noSuchMethod( + Invocation.method( + #updateSentCachedTxData, + [txData], + ), + returnValue: _i14.Future<void>.value(), + returnValueForMissingStub: _i14.Future<void>.value(), + ) as _i14.Future<void>); + @override bool validateAddress(String? address) => (super.noSuchMethod( Invocation.method( #validateAddress, diff --git a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart index e5dc17bc4..a249c997d 100644 --- a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart +++ b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart @@ -165,7 +165,7 @@ class _FakeElectrumXNode_11 extends _i1.SmartFake } class _FakeFlutterSecureStorageInterface_12 extends _i1.SmartFake - implements _i12.FlutterSecureStorageInterface { + implements _i12.SecureStorageInterface { _FakeFlutterSecureStorageInterface_12( Object parent, Invocation parentInvocation, @@ -680,6 +680,14 @@ class MockBitcoinWallet extends _i1.Mock implements _i19.BitcoinWallet { returnValueForMissingStub: null, ); @override + set cachedTxData(_i8.TransactionData? _cachedTxData) => super.noSuchMethod( + Invocation.setter( + #cachedTxData, + _cachedTxData, + ), + returnValueForMissingStub: null, + ); + @override bool get isActive => (super.noSuchMethod( Invocation.getter(#isActive), returnValue: false, @@ -1062,6 +1070,16 @@ class MockBitcoinWallet extends _i1.Mock implements _i19.BitcoinWallet { returnValueForMissingStub: _i16.Future<void>.value(), ) as _i16.Future<void>); @override + _i16.Future<void> updateSentCachedTxData(Map<String, dynamic>? txData) => + (super.noSuchMethod( + Invocation.method( + #updateSentCachedTxData, + [txData], + ), + returnValue: _i16.Future<void>.value(), + returnValueForMissingStub: _i16.Future<void>.value(), + ) as _i16.Future<void>); + @override bool validateAddress(String? address) => (super.noSuchMethod( Invocation.method( #validateAddress, @@ -1317,14 +1335,13 @@ class MockBitcoinWallet extends _i1.Mock implements _i19.BitcoinWallet { /// See the documentation for Mockito's code generation for more information. class MockNodeService extends _i1.Mock implements _i3.NodeService { @override - _i12.FlutterSecureStorageInterface get secureStorageInterface => - (super.noSuchMethod( + _i12.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), returnValue: _FakeFlutterSecureStorageInterface_12( this, Invocation.getter(#secureStorageInterface), ), - ) as _i12.FlutterSecureStorageInterface); + ) as _i12.SecureStorageInterface); @override List<_i20.NodeModel> get primaryNodes => (super.noSuchMethod( Invocation.getter(#primaryNodes), @@ -2228,4 +2245,14 @@ class MockCoinServiceAPI extends _i1.Mock implements _i13.CoinServiceAPI { ), returnValue: _i16.Future<bool>.value(false), ) as _i16.Future<bool>); + @override + _i16.Future<void> updateSentCachedTxData(Map<String, dynamic>? txData) => + (super.noSuchMethod( + Invocation.method( + #updateSentCachedTxData, + [txData], + ), + returnValue: _i16.Future<void>.value(), + returnValueForMissingStub: _i16.Future<void>.value(), + ) as _i16.Future<void>); } diff --git a/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart b/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart index 2b7bedb15..820fbd96d 100644 --- a/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart +++ b/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart @@ -165,7 +165,7 @@ class _FakeElectrumXNode_11 extends _i1.SmartFake } class _FakeFlutterSecureStorageInterface_12 extends _i1.SmartFake - implements _i12.FlutterSecureStorageInterface { + implements _i12.SecureStorageInterface { _FakeFlutterSecureStorageInterface_12( Object parent, Invocation parentInvocation, @@ -680,6 +680,14 @@ class MockBitcoinWallet extends _i1.Mock implements _i19.BitcoinWallet { returnValueForMissingStub: null, ); @override + set cachedTxData(_i8.TransactionData? _cachedTxData) => super.noSuchMethod( + Invocation.setter( + #cachedTxData, + _cachedTxData, + ), + returnValueForMissingStub: null, + ); + @override bool get isActive => (super.noSuchMethod( Invocation.getter(#isActive), returnValue: false, @@ -1062,6 +1070,16 @@ class MockBitcoinWallet extends _i1.Mock implements _i19.BitcoinWallet { returnValueForMissingStub: _i16.Future<void>.value(), ) as _i16.Future<void>); @override + _i16.Future<void> updateSentCachedTxData(Map<String, dynamic>? txData) => + (super.noSuchMethod( + Invocation.method( + #updateSentCachedTxData, + [txData], + ), + returnValue: _i16.Future<void>.value(), + returnValueForMissingStub: _i16.Future<void>.value(), + ) as _i16.Future<void>); + @override bool validateAddress(String? address) => (super.noSuchMethod( Invocation.method( #validateAddress, @@ -1317,14 +1335,13 @@ class MockBitcoinWallet extends _i1.Mock implements _i19.BitcoinWallet { /// See the documentation for Mockito's code generation for more information. class MockNodeService extends _i1.Mock implements _i3.NodeService { @override - _i12.FlutterSecureStorageInterface get secureStorageInterface => - (super.noSuchMethod( + _i12.SecureStorageInterface get secureStorageInterface => (super.noSuchMethod( Invocation.getter(#secureStorageInterface), returnValue: _FakeFlutterSecureStorageInterface_12( this, Invocation.getter(#secureStorageInterface), ), - ) as _i12.FlutterSecureStorageInterface); + ) as _i12.SecureStorageInterface); @override List<_i20.NodeModel> get primaryNodes => (super.noSuchMethod( Invocation.getter(#primaryNodes), @@ -2228,4 +2245,14 @@ class MockCoinServiceAPI extends _i1.Mock implements _i13.CoinServiceAPI { ), returnValue: _i16.Future<bool>.value(false), ) as _i16.Future<bool>); + @override + _i16.Future<void> updateSentCachedTxData(Map<String, dynamic>? txData) => + (super.noSuchMethod( + Invocation.method( + #updateSentCachedTxData, + [txData], + ), + returnValue: _i16.Future<void>.value(), + returnValueForMissingStub: _i16.Future<void>.value(), + ) as _i16.Future<void>); } From 8b7e222d416ba2d38a1236a7a6b60b64efd21ffe Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 9 Nov 2022 17:54:12 -0600 Subject: [PATCH 190/426] WIP: proper home directory location for linux --- lib/main.dart | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/main.dart b/lib/main.dart index 7f7c4a44d..c88ae2164 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -85,10 +85,20 @@ void main() async { if (Platform.isIOS) { appDirectory = (await getLibraryDirectory()); } - if (Platform.isLinux || Logging.isArmLinux) { + + if (Logging.isArmLinux) { appDirectory = Directory("${appDirectory.path}/.stackwallet"); await appDirectory.create(); } + + if (Platform.isLinux) { + appDirectory = Directory("${Platform.environment['HOME']}/.stackwallet"); + + if (!appDirectory.existsSync()) { + await appDirectory.create(); + } + } + // FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); if (!(Logging.isArmLinux || Logging.isTestEnv)) { final isar = await Isar.open( From 2082e875536c1fea506ba633526bc5c2518a5c56 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 9 Nov 2022 17:55:10 -0600 Subject: [PATCH 191/426] WIP: desktop loading order --- lib/main.dart | 93 +++++++++++++++++++++++++++------------------------ 1 file changed, 50 insertions(+), 43 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index c88ae2164..a8b515b02 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -60,6 +60,7 @@ import 'package:stackwallet/utilities/theme/dark_colors.dart'; import 'package:stackwallet/utilities/theme/light_colors.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:window_size/window_size.dart'; final openedFromSWBFileStringStateProvider = @@ -568,50 +569,56 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme> _buildOutlineInputBorder(colorScheme.textFieldDefaultBG), ), ), - home: FutureBuilder( - future: load(), - builder: (BuildContext context, AsyncSnapshot<void> snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - // FlutterNativeSplash.remove(); - if (Util.isDesktop && - (_wallets.hasWallets || _desktopHasPassword)) { - String? startupWalletId; - if (ref.read(prefsChangeNotifierProvider).gotoWalletOnStartup) { - startupWalletId = - ref.read(prefsChangeNotifierProvider).startupWalletId; - } - - return DesktopLoginView(startupWalletId: startupWalletId); - } else if (!Util.isDesktop && - (_wallets.hasWallets || _prefs.hasPin)) { - // return HomeView(); - - String? startupWalletId; - if (ref.read(prefsChangeNotifierProvider).gotoWalletOnStartup) { - startupWalletId = - ref.read(prefsChangeNotifierProvider).startupWalletId; - } - - return LockscreenView( - isInitialAppLogin: true, - routeOnSuccess: HomeView.routeName, - routeOnSuccessArguments: startupWalletId, - biometricsAuthenticationTitle: "Unlock Stack", - biometricsLocalizedReason: - "Unlock your stack wallet using biometrics", - biometricsCancelButtonString: "Cancel", - ); - } else { - return const IntroView(); - } - } else { - // CURRENTLY DISABLED as cannot be animated - // technically not needed as FlutterNativeSplash will overlay - // anything returned here until the future completes but - // FutureBuilder requires you to return something - return const LoadingView(); - } + home: ConditionalParent( + condition: Util.isDesktop, + builder: (child) { + return child; }, + child: FutureBuilder( + future: load(), + builder: (BuildContext context, AsyncSnapshot<void> snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + // FlutterNativeSplash.remove(); + if (Util.isDesktop && + (_wallets.hasWallets || _desktopHasPassword)) { + String? startupWalletId; + if (ref.read(prefsChangeNotifierProvider).gotoWalletOnStartup) { + startupWalletId = + ref.read(prefsChangeNotifierProvider).startupWalletId; + } + + return DesktopLoginView(startupWalletId: startupWalletId); + } else if (!Util.isDesktop && + (_wallets.hasWallets || _prefs.hasPin)) { + // return HomeView(); + + String? startupWalletId; + if (ref.read(prefsChangeNotifierProvider).gotoWalletOnStartup) { + startupWalletId = + ref.read(prefsChangeNotifierProvider).startupWalletId; + } + + return LockscreenView( + isInitialAppLogin: true, + routeOnSuccess: HomeView.routeName, + routeOnSuccessArguments: startupWalletId, + biometricsAuthenticationTitle: "Unlock Stack", + biometricsLocalizedReason: + "Unlock your stack wallet using biometrics", + biometricsCancelButtonString: "Cancel", + ); + } else { + return const IntroView(); + } + } else { + // CURRENTLY DISABLED as cannot be animated + // technically not needed as FlutterNativeSplash will overlay + // anything returned here until the future completes but + // FutureBuilder requires you to return something + return const LoadingView(); + } + }, + ), ), ); } From 2d9cf9146331e6dc3aa819d2d24751fb24931f5c Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Wed, 9 Nov 2022 16:57:06 -0700 Subject: [PATCH 192/426] v1.5.16 build 88 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 86f24330b..19d38ca4f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.5.14+86 +version: 1.5.16+88 environment: sdk: ">=2.17.0 <3.0.0" From 9d7a052ca0b0241834a54a70af953463c53fd22f Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 10 Nov 2022 09:42:05 -0600 Subject: [PATCH 193/426] qr uri fix --- .../generate_receiving_uri_qr_code_view.dart | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart b/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart index 4c3c4c968..0f699642e 100644 --- a/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart +++ b/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart @@ -11,6 +11,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:share_plus/share_plus.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/utilities/address_utils.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -101,26 +102,21 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> { return null; } - String query = ""; + Map<String, String> queryParams = {}; if (amountString.isNotEmpty) { - query += "amount=$amountString"; + queryParams["amount"] = amountString; } if (noteString.isNotEmpty) { - if (query.isNotEmpty) { - query += "&"; - } - query += "message=$noteString"; + queryParams["message"] = noteString; } - final uri = Uri( - scheme: widget.coin.uriScheme, - host: widget.receivingAddress, - query: query.isNotEmpty ? query : null, + final uriString = AddressUtils.buildUriString( + widget.coin, + widget.receivingAddress, + queryParams, ); - final uriString = uri.toString().replaceFirst("://", ":"); - Logging.instance.log("Generated receiving QR code for: $uriString", level: LogLevel.Info); From be952d3e35bfbdb1939e7aded8dc8e983778e3c4 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 10 Nov 2022 09:54:58 -0600 Subject: [PATCH 194/426] manually add bch uri fixes from https://github.com/cypherstack/stack_wallet/pull/214/commits/28da2b890076a1d679dccaa9970c98516d4ae2d9 --- .../generate_receiving_uri_qr_code_view.dart | 29 +++++++++++++++---- lib/utilities/enums/coin_enum.dart | 2 +- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart b/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart index 0f699642e..ae615bd96 100644 --- a/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart +++ b/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart @@ -111,9 +111,17 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> { queryParams["message"] = noteString; } + String receivingAddress = widget.receivingAddress; + if ((widget.coin == Coin.bitcoincash || + widget.coin == Coin.bitcoincashTestnet) && + receivingAddress.contains(":")) { + // remove cash addr prefix + receivingAddress = receivingAddress.split(":").sublist(1).join(); + } + final uriString = AddressUtils.buildUriString( widget.coin, - widget.receivingAddress, + receivingAddress, queryParams, ); @@ -225,10 +233,21 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> { @override void initState() { isDesktop = Util.isDesktop; - _uriString = Uri( - scheme: widget.coin.uriScheme, - host: widget.receivingAddress, - ).toString().replaceFirst("://", ":"); + + String receivingAddress = widget.receivingAddress; + if ((widget.coin == Coin.bitcoincash || + widget.coin == Coin.bitcoincashTestnet) && + receivingAddress.contains(":")) { + // remove cash addr prefix + receivingAddress = receivingAddress.split(":").sublist(1).join(); + } + + _uriString = AddressUtils.buildUriString( + widget.coin, + receivingAddress, + {}, + ); + amountController = TextEditingController(); noteController = TextEditingController(); super.initState(); diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index 95294c8aa..48212bde8 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -132,7 +132,7 @@ extension CoinExt on Coin { case Coin.litecoinTestNet: return "litecoin"; case Coin.bitcoincashTestnet: - return "bitcoincash"; + return "bchtest"; case Coin.firoTestNet: return "firo"; case Coin.dogecoinTestNet: From 74a04750765d26a592ed62b7e3bc408907da8122 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Thu, 10 Nov 2022 09:09:03 -0700 Subject: [PATCH 195/426] WIP: desktop restore dialog displays --- .../restore_from_file_view.dart | 77 ++- .../stack_restore_progress_view.dart | 556 +++++++++--------- .../restore_backup_dialog.dart | 171 ------ 3 files changed, 355 insertions(+), 449 deletions(-) delete mode 100644 lib/pages_desktop_specific/home/settings_menu/backup_and_restore/restore_backup_dialog.dart 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 a237d9ea9..dbf46c729 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 @@ -20,6 +20,8 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.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/loading_indicator.dart'; @@ -500,14 +502,73 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { return; } - await Navigator.of(context).push( - RouteGenerator.getRoute( - builder: (_) => - StackRestoreProgressView( - jsonString: jsonString, - ), - ), - ); + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return DesktopDialog( + maxHeight: 750, + maxWidth: 600, + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: + constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + mainAxisAlignment: + MainAxisAlignment + .start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Padding( + padding: + const EdgeInsets + .all(32), + child: Text( + "Restoring Stack Wallet", + style: STextStyles + .desktopH3( + context), + textAlign: + TextAlign + .center, + ), + ), + const DesktopDialogCloseButton(), + ], + ), + const SizedBox( + height: 30, + ), + Padding( + padding: EdgeInsets + .symmetric( + horizontal: + 32), + child: + StackRestoreProgressView( + jsonString: + jsonString, + ), + ), + ], + ), + ), + ), + ); + }, + ), + ); + }); } }, ), 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 5e5142425..7dec4e740 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 @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/dialogs/cancel_stack_restore_dialog.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart'; @@ -17,6 +16,8 @@ import 'package:stackwallet/utilities/enums/stack_restoring_status.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/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/icon_widgets/addressbook_icon.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; @@ -39,6 +40,8 @@ class StackRestoreProgressView extends ConsumerStatefulWidget { class _StackRestoreProgressViewState extends ConsumerState<StackRestoreProgressView> { + bool isDesktop = Util.isDesktop; + Future<void> _cancel() async { bool shouldPop = false; unawaited(showDialog<void>( @@ -79,10 +82,15 @@ class _StackRestoreProgressViewState await SWB.cancelRestore(); shouldPop = true; + + int count = 0; + if (mounted) { - Navigator.of(context).popUntil(ModalRoute.withName(widget.fromFile - ? RestoreFromEncryptedStringView.routeName - : StackBackupView.routeName)); + !isDesktop + ? Navigator.of(context).popUntil(ModalRoute.withName(widget.fromFile + ? RestoreFromEncryptedStringView.routeName + : StackBackupView.routeName)) + : Navigator.of(context).popUntil((_) => count++ >= 2); } } @@ -179,281 +187,289 @@ class _StackRestoreProgressViewState @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: _onWillPop, - child: Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 75)); - } - if (_success) { - _addWalletsToHomeView(); - if (mounted) { - Navigator.of(context).pop(); - } - } else { - if (await _requestCancel()) { - await _cancel(); - } - } - }, - ), - title: Text( - "Restoring Stack wallet", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.only( - left: 4, - top: 4, - right: 4, - bottom: 0, + bool isDesktop = Util.isDesktop; + + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return WillPopScope( + onWillPop: _onWillPop, + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75)); + } + if (_success) { + _addWalletsToHomeView(); + if (mounted) { + Navigator.of(context).pop(); + } + } else { + if (await _requestCancel()) { + await _cancel(); + } + } + }, ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Settings", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 12, - ), - Consumer( - builder: (_, ref, __) { - final state = ref.watch(stackRestoringUIStateProvider - .select((value) => value.preferences)); - return RestoringItemCard( - left: SizedBox( - width: 32, - height: 32, - child: RoundedContainer( - padding: const EdgeInsets.all(0), + title: Text( + "Restoring Stack wallet", + style: STextStyles.navBarTitle(context), + ), + ), + body: Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: child, + ), + ), + ); + }, + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.only( + left: 4, + top: 4, + right: 4, + bottom: 0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Settings", + style: STextStyles.itemSubtitle(context), + ), + const SizedBox( + height: 12, + ), + Consumer( + builder: (_, ref, __) { + final state = ref.watch(stackRestoringUIStateProvider + .select((value) => value.preferences)); + return RestoringItemCard( + left: SizedBox( + width: 32, + height: 32, + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + child: Center( + child: SvgPicture.asset( + Assets.svg.gear, + width: 16, + height: 16, color: Theme.of(context) .extension<StackColors>()! - .buttonBackSecondary, - child: Center( - child: SvgPicture.asset( - Assets.svg.gear, - width: 16, - height: 16, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - ), - ), - ), - ), - right: SizedBox( - width: 20, - height: 20, - child: _getIconForState(state), - ), - title: "Preferences", - subTitle: state == StackRestoringStatus.failed - ? Text( - "Something went wrong", - style: STextStyles.errorSmall(context), - ) - : null, - ); - }, - ), - const SizedBox( - height: 12, - ), - Consumer( - builder: (_, ref, __) { - final state = ref.watch(stackRestoringUIStateProvider - .select((value) => value.addressBook)); - return RestoringItemCard( - left: SizedBox( - width: 32, - height: 32, - child: RoundedContainer( - padding: const EdgeInsets.all(0), - color: Theme.of(context) - .extension<StackColors>()! - .buttonBackSecondary, - child: Center( - child: AddressBookIcon( - width: 16, - height: 16, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - ), - ), - ), - ), - right: SizedBox( - width: 20, - height: 20, - child: _getIconForState(state), - ), - title: "Address book", - subTitle: state == StackRestoringStatus.failed - ? Text( - "Something went wrong", - style: STextStyles.errorSmall(context), - ) - : null, - ); - }, - ), - const SizedBox( - height: 12, - ), - Consumer( - builder: (_, ref, __) { - final state = ref.watch(stackRestoringUIStateProvider - .select((value) => value.nodes)); - return RestoringItemCard( - left: SizedBox( - width: 32, - height: 32, - child: RoundedContainer( - padding: const EdgeInsets.all(0), - color: Theme.of(context) - .extension<StackColors>()! - .buttonBackSecondary, - child: Center( - child: SvgPicture.asset( - Assets.svg.node, - width: 16, - height: 16, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - ), - ), - ), - ), - right: SizedBox( - width: 20, - height: 20, - child: _getIconForState(state), - ), - title: "Nodes", - subTitle: state == StackRestoringStatus.failed - ? Text( - "Something went wrong", - style: STextStyles.errorSmall(context), - ) - : null, - ); - }, - ), - const SizedBox( - height: 12, - ), - Consumer( - builder: (_, ref, __) { - final state = ref.watch(stackRestoringUIStateProvider - .select((value) => value.trades)); - return RestoringItemCard( - left: SizedBox( - width: 32, - height: 32, - child: RoundedContainer( - padding: const EdgeInsets.all(0), - color: Theme.of(context) - .extension<StackColors>()! - .buttonBackSecondary, - child: Center( - child: SvgPicture.asset( - Assets.svg.arrowRotate2, - width: 16, - height: 16, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - ), - ), - ), - ), - right: SizedBox( - width: 20, - height: 20, - child: _getIconForState(state), - ), - title: "Exchange history", - subTitle: state == StackRestoringStatus.failed - ? Text( - "Something went wrong", - style: STextStyles.errorSmall(context), - ) - : null, - ); - }, - ), - const SizedBox( - height: 16, - ), - Text( - "Wallets", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 8, - ), - ...ref - .watch(stackRestoringUIStateProvider - .select((value) => value.walletStateProviders)) - .values - .map( - (provider) => Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: RestoringWalletCard( - provider: provider, + .accentColorDark, ), ), ), - const SizedBox( - height: 80, + ), + right: SizedBox( + width: 20, + height: 20, + child: _getIconForState(state), + ), + title: "Preferences", + subTitle: state == StackRestoringStatus.failed + ? Text( + "Something went wrong", + style: STextStyles.errorSmall(context), + ) + : null, + ); + }, + ), + const SizedBox( + height: 12, + ), + Consumer( + builder: (_, ref, __) { + final state = ref.watch(stackRestoringUIStateProvider + .select((value) => value.addressBook)); + return RestoringItemCard( + left: SizedBox( + width: 32, + height: 32, + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + child: Center( + child: AddressBookIcon( + width: 16, + height: 16, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + ), + ), + ), + right: SizedBox( + width: 20, + height: 20, + child: _getIconForState(state), + ), + title: "Address book", + subTitle: state == StackRestoringStatus.failed + ? Text( + "Something went wrong", + style: STextStyles.errorSmall(context), + ) + : null, + ); + }, + ), + const SizedBox( + height: 12, + ), + Consumer( + builder: (_, ref, __) { + final state = ref.watch(stackRestoringUIStateProvider + .select((value) => value.nodes)); + return RestoringItemCard( + left: SizedBox( + width: 32, + height: 32, + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + child: Center( + child: SvgPicture.asset( + Assets.svg.node, + width: 16, + height: 16, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + ), + ), + ), + right: SizedBox( + width: 20, + height: 20, + child: _getIconForState(state), + ), + title: "Nodes", + subTitle: state == StackRestoringStatus.failed + ? Text( + "Something went wrong", + style: STextStyles.errorSmall(context), + ) + : null, + ); + }, + ), + const SizedBox( + height: 12, + ), + Consumer( + builder: (_, ref, __) { + final state = ref.watch(stackRestoringUIStateProvider + .select((value) => value.trades)); + return RestoringItemCard( + left: SizedBox( + width: 32, + height: 32, + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + child: Center( + child: SvgPicture.asset( + Assets.svg.arrowRotate2, + width: 16, + height: 16, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + ), + ), + ), + right: SizedBox( + width: 20, + height: 20, + child: _getIconForState(state), + ), + title: "Exchange history", + subTitle: state == StackRestoringStatus.failed + ? Text( + "Something went wrong", + style: STextStyles.errorSmall(context), + ) + : null, + ); + }, + ), + const SizedBox( + height: 16, + ), + Text( + "Wallets", + style: STextStyles.itemSubtitle(context), + ), + const SizedBox( + height: 8, + ), + ...ref + .watch(stackRestoringUIStateProvider + .select((value) => value.walletStateProviders)) + .values + .map( + (provider) => Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: RestoringWalletCard( + provider: provider, + ), + ), ), - ], + const SizedBox( + height: 30, ), - ), - ), - ), - floatingActionButton: SizedBox( - width: MediaQuery.of(context).size.width - 32, - child: TextButton( - onPressed: () async { - if (_success) { - _addWalletsToHomeView(); - Navigator.of(context) - .popUntil(ModalRoute.withName(HomeView.routeName)); - } else { - if (await _requestCancel()) { - await _cancel(); - } - } - }, - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - child: Text( - _success ? "OK" : "Cancel restore process", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .buttonTextPrimary, + SizedBox( + width: MediaQuery.of(context).size.width - 32, + child: TextButton( + onPressed: () async { + if (_success) { + Navigator.of(context).pop(); + } else { + if (await _requestCancel()) { + await _cancel(); + } + } + }, + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + child: Text( + _success ? "OK" : "Cancel restore process", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextPrimary, + ), + ), + ), ), - ), + ], ), ), ), diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/restore_backup_dialog.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/restore_backup_dialog.dart deleted file mode 100644 index 7f944847d..000000000 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/restore_backup_dialog.dart +++ /dev/null @@ -1,171 +0,0 @@ -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/desktop/desktop_dialog.dart'; -import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; -import 'package:stackwallet/widgets/desktop/secondary_button.dart'; -import 'package:stackwallet/widgets/rounded_white_container.dart'; - -class RestoreBackupDialog extends StatelessWidget { - const RestoreBackupDialog({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return DesktopDialog( - maxHeight: 750, - maxWidth: 600, - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.all(32), - child: Text( - "Restoring Stack Wallet", - style: STextStyles.desktopH3(context), - textAlign: TextAlign.center, - ), - ), - const DesktopDialogCloseButton(), - ], - ), - const SizedBox( - height: 30, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: Row( - children: [ - Text( - "Settings", - style: STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, - ), - textAlign: TextAlign.left, - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, vertical: 12), - child: RoundedWhiteContainer( - borderColor: Theme.of(context) - .extension<StackColors>()! - .background, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - SvgPicture.asset( - Assets.svg.framedAddressBook, - width: 40, - height: 40, - ), - const SizedBox(width: 12), - Text( - "Address Book", - style: - STextStyles.desktopTextSmall(context), - ), - ], - ), - - ///TODO: CHECKMARK ANIMATION - ], - ), - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, vertical: 12), - child: RoundedWhiteContainer( - borderColor: Theme.of(context) - .extension<StackColors>()! - .background, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - SvgPicture.asset( - Assets.svg.framedGear, - width: 40, - height: 40, - ), - const SizedBox(width: 12), - Text( - "Preferences", - style: - STextStyles.desktopTextSmall(context), - ), - ], - ), - - ///TODO: CHECKMARK ANIMATION - ], - ), - ), - ), - const SizedBox( - height: 30, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: Row( - children: [ - Text( - "Wallets", - style: STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, - ), - textAlign: TextAlign.left, - ), - ], - ), - ), - const Spacer(), - Padding( - padding: const EdgeInsets.all(32), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - SecondaryButton( - desktopMed: true, - width: 200, - label: "Cancel restore process", - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ], - ), - ), - ], - ), - ), - ), - ); - }, - )); - } -} From 07c99309ff80e5428ba6888613d2bde20aef6a07 Mon Sep 17 00:00:00 2001 From: sneurlax <sneurlax@gmail.com> Date: Thu, 10 Nov 2022 10:21:07 -0600 Subject: [PATCH 196/426] use native address validation --- crypto_plugins/flutter_libmonero | 2 +- lib/services/coins/monero/monero_wallet.dart | 4 +--- lib/services/coins/wownero/wownero_wallet.dart | 4 +--- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index b9bc2dcc5..4dd55584a 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit b9bc2dcc56e13f235a6c5b0fc02c0e543eb87758 +Subproject commit 4dd55584a023bf6fd863c24ca8e1ffcedcc25162 diff --git a/lib/services/coins/monero/monero_wallet.dart b/lib/services/coins/monero/monero_wallet.dart index 4d29c405c..a96ecac83 100644 --- a/lib/services/coins/monero/monero_wallet.dart +++ b/lib/services/coins/monero/monero_wallet.dart @@ -1353,10 +1353,8 @@ class MoneroWallet extends CoinServiceAPI { Future<List<UtxoObject>> get unspentOutputs => throw UnimplementedError(); @override - // TODO: implement validateAddress bool validateAddress(String address) { - bool valid = RegExp("[a-zA-Z0-9]{95}").hasMatch(address) || - RegExp("[a-zA-Z0-9]{106}").hasMatch(address); + bool valid = walletBase!.validateAddress(address); return valid; } diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart index e2c52c40a..4c02ec037 100644 --- a/lib/services/coins/wownero/wownero_wallet.dart +++ b/lib/services/coins/wownero/wownero_wallet.dart @@ -1378,10 +1378,8 @@ class WowneroWallet extends CoinServiceAPI { Future<List<UtxoObject>> get unspentOutputs => throw UnimplementedError(); @override - // TODO: implement validateAddress bool validateAddress(String address) { - bool valid = RegExp("[a-zA-Z0-9]{95}").hasMatch(address) || - RegExp("[a-zA-Z0-9]{106}").hasMatch(address); + bool valid = walletBase!.validateAddress(address); return valid; } From 6e48fc4ea6ce45b82ed5204ccba5ffd8773e9777 Mon Sep 17 00:00:00 2001 From: sneurlax <sneurlax@gmail.com> Date: Thu, 10 Nov 2022 10:46:09 -0600 Subject: [PATCH 197/426] use hive_test for monero and wownero tests --- .../coins/monero/monero_wallet_test.dart | 26 +++++++++++++------ .../coins/wownero/wownero_wallet_test.dart | 26 +++++++++++++------ 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/test/services/coins/monero/monero_wallet_test.dart b/test/services/coins/monero/monero_wallet_test.dart index d54959ab2..2c755b484 100644 --- a/test/services/coins/monero/monero_wallet_test.dart +++ b/test/services/coins/monero/monero_wallet_test.dart @@ -81,19 +81,29 @@ void main() async { if (Platform.isIOS) { appDir = (await getLibraryDirectory()); } - await Hive.close(); - Hive.init(appDir.path); - Hive.registerAdapter(NodeAdapter()); - Hive.registerAdapter(WalletInfoAdapter()); - Hive.registerAdapter(WalletTypeAdapter()); - Hive.registerAdapter(UnspentCoinsInfoAdapter()); monero.onStartup(); - _walletInfoSource = await Hive.openBox<WalletInfo>(WalletInfo.boxName); - walletService = monero.createMoneroWalletService(_walletInfoSource); + + bool hiveAdaptersRegistered = false; group("Mainnet tests", () { setUp(() async { + await setUpTestHive(); + if (!hiveAdaptersRegistered) { + hiveAdaptersRegistered = true; + + Hive.registerAdapter(NodeAdapter()); + Hive.registerAdapter(WalletInfoAdapter()); + Hive.registerAdapter(WalletTypeAdapter()); + Hive.registerAdapter(UnspentCoinsInfoAdapter()); + + final wallets = await Hive.openBox('wallets'); + await wallets.put('currentWalletName', name); + + _walletInfoSource = await Hive.openBox<WalletInfo>(WalletInfo.boxName); + walletService = monero.createMoneroWalletService(_walletInfoSource); + } + try { // if (name?.isEmpty ?? true) { // name = await generateName(); diff --git a/test/services/coins/wownero/wownero_wallet_test.dart b/test/services/coins/wownero/wownero_wallet_test.dart index 660bc1438..87229a63a 100644 --- a/test/services/coins/wownero/wownero_wallet_test.dart +++ b/test/services/coins/wownero/wownero_wallet_test.dart @@ -72,19 +72,29 @@ void main() async { if (Platform.isIOS) { appDir = (await getLibraryDirectory()); } - await Hive.close(); - Hive.init(appDir.path); - Hive.registerAdapter(NodeAdapter()); - Hive.registerAdapter(WalletInfoAdapter()); - Hive.registerAdapter(WalletTypeAdapter()); - Hive.registerAdapter(UnspentCoinsInfoAdapter()); wownero.onStartup(); - _walletInfoSource = await Hive.openBox<WalletInfo>(WalletInfo.boxName); - walletService = wownero.createWowneroWalletService(_walletInfoSource); + + bool hiveAdaptersRegistered = false; group("Wownero 14 word seed generation", () { setUp(() async { + await setUpTestHive(); + if (!hiveAdaptersRegistered) { + hiveAdaptersRegistered = true; + + Hive.registerAdapter(NodeAdapter()); + Hive.registerAdapter(WalletInfoAdapter()); + Hive.registerAdapter(WalletTypeAdapter()); + Hive.registerAdapter(UnspentCoinsInfoAdapter()); + + final wallets = await Hive.openBox('wallets'); + await wallets.put('currentWalletName', name); + + _walletInfoSource = await Hive.openBox<WalletInfo>(WalletInfo.boxName); + walletService = wownero.createWowneroWalletService(_walletInfoSource); + } + bool hasThrown = false; try { name = 'namee${Random().nextInt(10000000)}'; From 3fb80fdc29146b1ad43e760070edf1414b69c1e0 Mon Sep 17 00:00:00 2001 From: julian-CStack <97684800+julian-CStack@users.noreply.github.com> Date: Thu, 10 Nov 2022 11:16:22 -0600 Subject: [PATCH 198/426] Update test.yaml attempt apt update --- .github/workflows/test.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index d72d33ac4..83de43779 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -23,6 +23,7 @@ jobs: run: | cargo install cargo-ndk rustup target add x86_64-unknown-linux-gnu + sudo apt update sudo apt install -y unzip automake build-essential file pkg-config git python libtool libtinfo5 cmake openjdk-8-jre-headless libgit2-dev clang libncurses5-dev libncursesw5-dev zlib1g-dev llvm sudo apt install -y debhelper libclang-dev cargo rustc opencl-headers libssl-dev ocl-icd-opencl-dev sudo apt install -y libc6-dev-i386 From 50f4f2ff64b28e7a9a18a2fb3a0fc6eeb1289a9f Mon Sep 17 00:00:00 2001 From: julian-CStack <97684800+julian-CStack@users.noreply.github.com> Date: Thu, 10 Nov 2022 11:23:49 -0600 Subject: [PATCH 199/426] Update test.yaml apt update --- .github/workflows/test.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index d72d33ac4..83de43779 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -23,6 +23,7 @@ jobs: run: | cargo install cargo-ndk rustup target add x86_64-unknown-linux-gnu + sudo apt update sudo apt install -y unzip automake build-essential file pkg-config git python libtool libtinfo5 cmake openjdk-8-jre-headless libgit2-dev clang libncurses5-dev libncursesw5-dev zlib1g-dev llvm sudo apt install -y debhelper libclang-dev cargo rustc opencl-headers libssl-dev ocl-icd-opencl-dev sudo apt install -y libc6-dev-i386 From f2bef21853639653b1be0ded1fc4355c55b8cb68 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 10 Nov 2022 11:40:33 -0600 Subject: [PATCH 200/426] temp disable erroring code --- .../restore_from_file_view.dart | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) 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 dbf46c729..ee1fcf666 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 @@ -9,7 +9,7 @@ import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/stack_file_system.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/sub_views/stack_restore_progress_view.dart'; -import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/restore_backup_dialog.dart'; +// import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/restore_backup_dialog.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -49,14 +49,14 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { bool hidePassword = true; Future<void> restoreBackupPopup(BuildContext context) async { - await showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return const RestoreBackupDialog(); - }, - ); + // await showDialog<dynamic>( + // context: context, + // useSafeArea: false, + // barrierDismissible: true, + // builder: (context) { + // return const RestoreBackupDialog(); + // }, + // ); } @override From 15dc2512dbd431ac894169731a11220f97416dad Mon Sep 17 00:00:00 2001 From: sneurlax <sneurlax@gmail.com> Date: Thu, 10 Nov 2022 12:19:51 -0600 Subject: [PATCH 201/426] update iOs headers --- crypto_plugins/flutter_libmonero | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index 4dd55584a..8a8c88cda 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit 4dd55584a023bf6fd863c24ca8e1ffcedcc25162 +Subproject commit 8a8c88cdade6fe18529deea410f862b125167a3b From a50520b37ffdf1630aca2a9bd4f5a12f50db0cf7 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 10 Nov 2022 12:40:16 -0600 Subject: [PATCH 202/426] WIP: desktop password login and auth flow --- lib/hive/db.dart | 52 +++--- lib/main.dart | 153 ++++++++++-------- .../desktop_login_view.dart | 52 +++--- .../create_auto_backup.dart | 15 +- lib/utilities/desktop_password_service.dart | 2 - 5 files changed, 148 insertions(+), 126 deletions(-) diff --git a/lib/hive/db.dart b/lib/hive/db.dart index d5402752e..48e6ba154 100644 --- a/lib/hive/db.dart +++ b/lib/hive/db.dart @@ -43,23 +43,23 @@ class DB { static bool _initialized = false; - late final Box<dynamic> _boxAddressBook; - late final Box<String> _boxDebugInfo; - late final Box<NodeModel> _boxNodeModels; - late final Box<NodeModel> _boxPrimaryNodes; - late final Box<dynamic> _boxAllWalletsData; - late final Box<NotificationModel> _boxNotifications; - late final Box<NotificationModel> _boxWatchedTransactions; - late final Box<NotificationModel> _boxWatchedTrades; - late final Box<ExchangeTransaction> _boxTrades; - late final Box<Trade> _boxTradesV2; - late final Box<String> _boxTradeNotes; - late final Box<String> _boxFavoriteWallets; - late final Box<xmr.WalletInfo> _walletInfoSource; - late final Box<dynamic> _boxPrefs; - late final Box<TradeWalletLookup> _boxTradeLookup; - late final Box<dynamic> _boxDBInfo; - late final Box<String> _boxDesktopData; + Box<dynamic>? _boxAddressBook; + Box<String>? _boxDebugInfo; + Box<NodeModel>? _boxNodeModels; + Box<NodeModel>? _boxPrimaryNodes; + Box<dynamic>? _boxAllWalletsData; + Box<NotificationModel>? _boxNotifications; + Box<NotificationModel>? _boxWatchedTransactions; + Box<NotificationModel>? _boxWatchedTrades; + Box<ExchangeTransaction>? _boxTrades; + Box<Trade>? _boxTradesV2; + Box<String>? _boxTradeNotes; + Box<String>? _boxFavoriteWallets; + Box<xmr.WalletInfo>? _walletInfoSource; + Box<dynamic>? _boxPrefs; + Box<TradeWalletLookup>? _boxTradeLookup; + Box<dynamic>? _boxDBInfo; + Box<String>? _boxDesktopData; final Map<String, Box<dynamic>> _walletBoxes = {}; @@ -68,7 +68,7 @@ class DB { final Map<Coin, Box<dynamic>> _usedSerialsCacheBoxes = {}; // exposed for monero - Box<xmr.WalletInfo> get moneroWalletInfoBox => _walletInfoSource; + Box<xmr.WalletInfo> get moneroWalletInfoBox => _walletInfoSource!; // mutex for stack backup final mutex = Mutex(); @@ -124,6 +124,12 @@ class DB { _boxAllWalletsData = await Hive.openBox<dynamic>(boxNameAllWalletsData); } + if (Hive.isBoxOpen(boxNameDesktopData)) { + _boxDesktopData = Hive.box<String>(boxNameDesktopData); + } else { + _boxDesktopData = await Hive.openBox<String>(boxNameDesktopData); + } + _boxNotifications = await Hive.openBox<NotificationModel>(boxNameNotifications); _boxWatchedTransactions = @@ -147,11 +153,11 @@ class DB { _initialized = true; try { - if (_boxPrefs.get("familiarity") == null) { - await _boxPrefs.put("familiarity", 0); + if (_boxPrefs!.get("familiarity") == null) { + await _boxPrefs!.put("familiarity", 0); } - int count = _boxPrefs.get("familiarity") as int; - await _boxPrefs.put("familiarity", count + 1); + int count = _boxPrefs!.get("familiarity") as int; + await _boxPrefs!.put("familiarity", count + 1); Constants.exchangeForExperiencedUsers(count + 1); } catch (e, s) { print("$e $s"); @@ -160,7 +166,7 @@ class DB { } Future<void> _loadWalletBoxes() async { - final names = _boxAllWalletsData.get("names") as Map? ?? {}; + final names = _boxAllWalletsData!.get("names") as Map? ?? {}; names.removeWhere((name, dyn) { final jsonObject = Map<String, dynamic>.from(dyn as Map); try { diff --git a/lib/main.dart b/lib/main.dart index a8b515b02..a04170bd9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -48,19 +48,16 @@ import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/services/notifications_api.dart'; import 'package:stackwallet/services/notifications_service.dart'; import 'package:stackwallet/services/trade_service.dart'; -import 'package:stackwallet/services/wallets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/db_version_migration.dart'; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/theme/color_theme.dart'; import 'package:stackwallet/utilities/theme/dark_colors.dart'; import 'package:stackwallet/utilities/theme/light_colors.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; -import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:window_size/window_size.dart'; final openedFromSWBFileStringStateProvider = @@ -221,8 +218,8 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme> static const platform = MethodChannel("STACK_WALLET_RESTORE"); final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); - late final Wallets _wallets; - late final Prefs _prefs; + // late final Wallets _wallets; + // late final Prefs _prefs; late final NotificationsService _notificationsService; late final NodeService _nodeService; late final TradesService _tradesService; @@ -232,6 +229,16 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme> bool didLoad = false; bool _desktopHasPassword = false; + Future<void> loadShared() async { + await DB.instance.init(); + await ref.read(prefsChangeNotifierProvider).init(); + + if (Util.isDesktop) { + _desktopHasPassword = + await ref.read(storageCryptoHandlerProvider).hasPassword(); + } + } + Future<void> load() async { try { if (didLoad) { @@ -239,19 +246,15 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme> } didLoad = true; - await DB.instance.init(); - await _prefs.init(); - - if (Util.isDesktop) { - _desktopHasPassword = - await ref.read(storageCryptoHandlerProvider).hasPassword(); + if (!Util.isDesktop) { + await loadShared(); } _notificationsService = ref.read(notificationsProvider); _nodeService = ref.read(nodeServiceChangeNotifierProvider); _tradesService = ref.read(tradesServiceProvider); - NotificationApi.prefs = _prefs; + NotificationApi.prefs = ref.read(prefsChangeNotifierProvider); NotificationApi.notificationsService = _notificationsService; unawaited(ref.read(baseCurrenciesProvider).update()); @@ -260,23 +263,25 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme> await _notificationsService.init( nodeService: _nodeService, tradesService: _tradesService, - prefs: _prefs, + prefs: ref.read(prefsChangeNotifierProvider), ); ref.read(priceAnd24hChangeNotifierProvider).start(true); - await _wallets.load(_prefs); + await ref + .read(walletsChangeNotifierProvider) + .load(ref.read(prefsChangeNotifierProvider)); loadingCompleter.complete(); // TODO: this should probably run unawaited. Keep commented out for now as proper community nodes ui hasn't been implemented yet // unawaited(_nodeService.updateCommunityNodes()); // run without awaiting if (Constants.enableExchange && - _prefs.externalCalls && - await _prefs.isExternalCallsSet()) { + ref.read(prefsChangeNotifierProvider).externalCalls && + await ref.read(prefsChangeNotifierProvider).isExternalCallsSet()) { unawaited(ExchangeDataLoadingService().loadAll(ref)); } - if (_prefs.isAutoBackupEnabled) { - switch (_prefs.backupFrequencyType) { + if (ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled) { + switch (ref.read(prefsChangeNotifierProvider).backupFrequencyType) { case BackupFrequencyType.everyTenMinutes: ref.read(autoSWBServiceProvider).startPeriodicBackupTimer( duration: const Duration(minutes: 10)); @@ -316,9 +321,6 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme> .read(localeServiceChangeNotifierProvider.notifier) .loadLocale(notify: false); - _prefs = ref.read(prefsChangeNotifierProvider); - _wallets = ref.read(walletsChangeNotifierProvider); - WidgetsBinding.instance.addPostFrameCallback((_) async { ref.read(colorThemeProvider.state).state = StackColors.fromStackColorTheme( @@ -423,7 +425,7 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme> } Future<void> goToRestoreSWB(String encrypted) async { - if (!_prefs.hasPin) { + if (!ref.read(prefsChangeNotifierProvider).hasPin) { await Navigator.of(navigatorKey.currentContext!) .pushNamed(CreatePinView.routeName, arguments: true) .then((value) { @@ -569,57 +571,70 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme> _buildOutlineInputBorder(colorScheme.textFieldDefaultBG), ), ), - home: ConditionalParent( - condition: Util.isDesktop, - builder: (child) { - return child; - }, - child: FutureBuilder( - future: load(), - builder: (BuildContext context, AsyncSnapshot<void> snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - // FlutterNativeSplash.remove(); - if (Util.isDesktop && - (_wallets.hasWallets || _desktopHasPassword)) { - String? startupWalletId; - if (ref.read(prefsChangeNotifierProvider).gotoWalletOnStartup) { - startupWalletId = - ref.read(prefsChangeNotifierProvider).startupWalletId; + home: Util.isDesktop + ? FutureBuilder( + future: loadShared(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (_desktopHasPassword) { + String? startupWalletId; + if (ref + .read(prefsChangeNotifierProvider) + .gotoWalletOnStartup) { + startupWalletId = + ref.read(prefsChangeNotifierProvider).startupWalletId; + } + + return DesktopLoginView( + startupWalletId: startupWalletId, + load: load, + ); + } else { + return const IntroView(); + } + } else { + return const LoadingView(); } + }, + ) + : FutureBuilder( + future: load(), + builder: (BuildContext context, AsyncSnapshot<void> snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + // FlutterNativeSplash.remove(); + if (ref.read(walletsChangeNotifierProvider).hasWallets || + ref.read(prefsChangeNotifierProvider).hasPin) { + // return HomeView(); - return DesktopLoginView(startupWalletId: startupWalletId); - } else if (!Util.isDesktop && - (_wallets.hasWallets || _prefs.hasPin)) { - // return HomeView(); + String? startupWalletId; + if (ref + .read(prefsChangeNotifierProvider) + .gotoWalletOnStartup) { + startupWalletId = + ref.read(prefsChangeNotifierProvider).startupWalletId; + } - String? startupWalletId; - if (ref.read(prefsChangeNotifierProvider).gotoWalletOnStartup) { - startupWalletId = - ref.read(prefsChangeNotifierProvider).startupWalletId; + return LockscreenView( + isInitialAppLogin: true, + routeOnSuccess: HomeView.routeName, + routeOnSuccessArguments: startupWalletId, + biometricsAuthenticationTitle: "Unlock Stack", + biometricsLocalizedReason: + "Unlock your stack wallet using biometrics", + biometricsCancelButtonString: "Cancel", + ); + } else { + return const IntroView(); + } + } else { + // CURRENTLY DISABLED as cannot be animated + // technically not needed as FlutterNativeSplash will overlay + // anything returned here until the future completes but + // FutureBuilder requires you to return something + return const LoadingView(); } - - return LockscreenView( - isInitialAppLogin: true, - routeOnSuccess: HomeView.routeName, - routeOnSuccessArguments: startupWalletId, - biometricsAuthenticationTitle: "Unlock Stack", - biometricsLocalizedReason: - "Unlock your stack wallet using biometrics", - biometricsCancelButtonString: "Cancel", - ); - } else { - return const IntroView(); - } - } else { - // CURRENTLY DISABLED as cannot be animated - // technically not needed as FlutterNativeSplash will overlay - // anything returned here until the future completes but - // FutureBuilder requires you to return something - return const LoadingView(); - } - }, - ), - ), + }, + ), ); } } diff --git a/lib/pages_desktop_specific/desktop_login_view.dart b/lib/pages_desktop_specific/desktop_login_view.dart index 0a47c2a96..d90e742fb 100644 --- a/lib/pages_desktop_specific/desktop_login_view.dart +++ b/lib/pages_desktop_specific/desktop_login_view.dart @@ -21,11 +21,13 @@ class DesktopLoginView extends ConsumerStatefulWidget { const DesktopLoginView({ Key? key, this.startupWalletId, + this.load, }) : super(key: key); static const String routeName = "/desktopLogin"; final String? startupWalletId; + final Future<void> Function()? load; @override ConsumerState<DesktopLoginView> createState() => _DesktopLoginViewState(); @@ -39,6 +41,32 @@ class _DesktopLoginViewState extends ConsumerState<DesktopLoginView> { bool hidePassword = true; bool _continueEnabled = false; + Future<void> login() async { + try { + await ref + .read(storageCryptoHandlerProvider) + .initFromExisting(passwordController.text); + + await widget.load?.call(); + + // if no errors passphrase is correct + if (mounted) { + unawaited( + Navigator.of(context).pushNamedAndRemoveUntil( + DesktopHomeView.routeName, + (route) => false, + ), + ); + } + } catch (e) { + await showFloatingFlushBar( + type: FlushBarType.warning, + message: e.toString(), + context: context, + ); + } + } + @override void initState() { passwordController = TextEditingController(); @@ -159,29 +187,7 @@ class _DesktopLoginViewState extends ConsumerState<DesktopLoginView> { PrimaryButton( label: "Continue", enabled: _continueEnabled, - onPressed: () async { - try { - await ref - .read(storageCryptoHandlerProvider) - .initFromExisting(passwordController.text); - - // if no errors passphrase is correct - if (mounted) { - unawaited( - Navigator.of(context).pushNamedAndRemoveUntil( - DesktopHomeView.routeName, - (route) => false, - ), - ); - } - } catch (e) { - await showFloatingFlushBar( - type: FlushBarType.warning, - message: e.toString(), - context: context, - ); - } - }, + onPressed: login, ), const SizedBox( height: 60, diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart index e804071cc..2583e16c5 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart @@ -5,13 +5,13 @@ import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stack_wallet_backup/stack_wallet_backup.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/stack_file_system.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; @@ -35,13 +35,8 @@ import 'package:zxcvbn/zxcvbn.dart'; class CreateAutoBackup extends ConsumerStatefulWidget { const CreateAutoBackup({ Key? key, - this.secureStore = const SecureStorageWrapper( - FlutterSecureStorage(), - ), }) : super(key: key); - final FlutterSecureStorageInterface secureStore; - @override ConsumerState<CreateAutoBackup> createState() => _CreateAutoBackup(); } @@ -51,7 +46,7 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> { late final TextEditingController passphraseController; late final TextEditingController passphraseRepeatController; - late final FlutterSecureStorageInterface secureStore; + late final SecureStorageInterface secureStore; late final StackFileSystem stackFileSystem; late final FocusNode passphraseFocusNode; @@ -85,7 +80,7 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> { @override void initState() { - secureStore = widget.secureStore; + secureStore = ref.read(secureStoreProvider); stackFileSystem = StackFileSystem(); fileLocationController = TextEditingController(); @@ -686,7 +681,9 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> { final String fileToSave = createAutoBackupFilename(pathToSave, now); - final backup = await SWB.createStackWalletJSON(); + final backup = await SWB.createStackWalletJSON( + secureStorage: secureStore, + ); bool result = await SWB.encryptStackWalletWithADK( fileToSave, diff --git a/lib/utilities/desktop_password_service.dart b/lib/utilities/desktop_password_service.dart index 178871e69..24299b855 100644 --- a/lib/utilities/desktop_password_service.dart +++ b/lib/utilities/desktop_password_service.dart @@ -89,12 +89,10 @@ class DPS { } Future<bool> hasPassword() async { - final box = await Hive.openBox<String>(DB.boxNameDesktopData); final keyBlob = DB.instance.get<String>( boxName: DB.boxNameDesktopData, key: _kKeyBlobKey, ); - await box.close(); return keyBlob != null; } } From b635f1663b6a5699e0a24a73693eca1bc4cf3b34 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 10 Nov 2022 15:07:44 -0600 Subject: [PATCH 203/426] comment out broken code in swb desktop restore + clean up unused imports and linter errors in wow/monero tests --- .../restore_from_file_view.dart | 18 +++++----- .../coins/monero/monero_wallet_test.dart | 33 ++++--------------- .../coins/wownero/wownero_wallet_test.dart | 29 ++++------------ 3 files changed, 21 insertions(+), 59 deletions(-) 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 dbf46c729..ee1fcf666 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 @@ -9,7 +9,7 @@ import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/stack_file_system.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/sub_views/stack_restore_progress_view.dart'; -import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/restore_backup_dialog.dart'; +// import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/restore_backup_dialog.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -49,14 +49,14 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { bool hidePassword = true; Future<void> restoreBackupPopup(BuildContext context) async { - await showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return const RestoreBackupDialog(); - }, - ); + // await showDialog<dynamic>( + // context: context, + // useSafeArea: false, + // barrierDismissible: true, + // builder: (context) { + // return const RestoreBackupDialog(); + // }, + // ); } @override diff --git a/test/services/coins/monero/monero_wallet_test.dart b/test/services/coins/monero/monero_wallet_test.dart index 2c755b484..7bbc88ed9 100644 --- a/test/services/coins/monero/monero_wallet_test.dart +++ b/test/services/coins/monero/monero_wallet_test.dart @@ -1,43 +1,27 @@ -import 'dart:async'; import 'dart:core'; import 'dart:core' as core; import 'dart:io'; import 'dart:math'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:hive/hive.dart'; -import 'package:hive_test/hive_test.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; - -import 'package:cw_core/monero_amount_format.dart'; import 'package:cw_core/node.dart'; -import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cw_monero/api/wallet.dart'; -import 'package:cw_monero/api/wallet_manager.dart' as monero_wallet_manager; -import 'package:cw_monero/pending_monero_transaction.dart'; import 'package:cw_monero/monero_wallet.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_libmonero/core/key_service.dart'; import 'package:flutter_libmonero/core/wallet_creation_service.dart'; -import 'package:flutter_libmonero/view_model/send/output.dart'; import 'package:flutter_libmonero/monero/monero.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:hive/hive.dart'; +import 'package:hive_test/hive_test.dart'; +import 'package:mockito/annotations.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; - import 'package:stackwallet/services/wallets.dart'; - -import 'dart:developer' as developer; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; // TODO trim down to the minimum imports above @@ -76,12 +60,6 @@ void main() async { dirPath: ''); late WalletCredentials credentials; - WidgetsFlutterBinding.ensureInitialized(); - Directory appDir = (await getApplicationDocumentsDirectory()); - if (Platform.isIOS) { - appDir = (await getLibraryDirectory()); - } - monero.onStartup(); bool hiveAdaptersRegistered = false; @@ -101,7 +79,8 @@ void main() async { await wallets.put('currentWalletName', name); _walletInfoSource = await Hive.openBox<WalletInfo>(WalletInfo.boxName); - walletService = monero.createMoneroWalletService(_walletInfoSource); + walletService = monero + .createMoneroWalletService(_walletInfoSource as Box<WalletInfo>); } try { diff --git a/test/services/coins/wownero/wownero_wallet_test.dart b/test/services/coins/wownero/wownero_wallet_test.dart index 87229a63a..c58654b4b 100644 --- a/test/services/coins/wownero/wownero_wallet_test.dart +++ b/test/services/coins/wownero/wownero_wallet_test.dart @@ -1,38 +1,26 @@ -import 'dart:async'; import 'dart:core'; import 'dart:core' as core; import 'dart:io'; import 'dart:math'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:hive/hive.dart'; -import 'package:hive_test/hive_test.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; - -import 'package:cw_core/monero_amount_format.dart'; import 'package:cw_core/node.dart'; -import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cw_wownero/api/wallet.dart'; -import 'package:cw_wownero/pending_wownero_transaction.dart'; import 'package:cw_wownero/wownero_wallet.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_libmonero/core/key_service.dart'; import 'package:flutter_libmonero/core/wallet_creation_service.dart'; -import 'package:flutter_libmonero/view_model/send/output.dart'; import 'package:flutter_libmonero/wownero/wownero.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:hive/hive.dart'; +import 'package:hive_test/hive_test.dart'; +import 'package:mockito/annotations.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'wownero_wallet_test_data.dart'; @@ -67,12 +55,6 @@ void main() async { dirPath: ''); late WalletCredentials credentials; - WidgetsFlutterBinding.ensureInitialized(); - Directory appDir = (await getApplicationDocumentsDirectory()); - if (Platform.isIOS) { - appDir = (await getLibraryDirectory()); - } - wownero.onStartup(); bool hiveAdaptersRegistered = false; @@ -92,7 +74,8 @@ void main() async { await wallets.put('currentWalletName', name); _walletInfoSource = await Hive.openBox<WalletInfo>(WalletInfo.boxName); - walletService = wownero.createWowneroWalletService(_walletInfoSource); + walletService = wownero + .createWowneroWalletService(_walletInfoSource as Box<WalletInfo>); } bool hasThrown = false; From 3299f4ecd910c43e291bda713859db71d530615f Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 10 Nov 2022 15:07:44 -0600 Subject: [PATCH 204/426] comment out broken code in swb desktop restore + clean up unused imports and linter errors in wow/monero tests --- .../coins/monero/monero_wallet_test.dart | 57 ++++++++----------- .../coins/wownero/wownero_wallet_test.dart | 53 ++++++++--------- 2 files changed, 46 insertions(+), 64 deletions(-) diff --git a/test/services/coins/monero/monero_wallet_test.dart b/test/services/coins/monero/monero_wallet_test.dart index d54959ab2..7bbc88ed9 100644 --- a/test/services/coins/monero/monero_wallet_test.dart +++ b/test/services/coins/monero/monero_wallet_test.dart @@ -1,43 +1,27 @@ -import 'dart:async'; import 'dart:core'; import 'dart:core' as core; import 'dart:io'; import 'dart:math'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:hive/hive.dart'; -import 'package:hive_test/hive_test.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; - -import 'package:cw_core/monero_amount_format.dart'; import 'package:cw_core/node.dart'; -import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cw_monero/api/wallet.dart'; -import 'package:cw_monero/api/wallet_manager.dart' as monero_wallet_manager; -import 'package:cw_monero/pending_monero_transaction.dart'; import 'package:cw_monero/monero_wallet.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_libmonero/core/key_service.dart'; import 'package:flutter_libmonero/core/wallet_creation_service.dart'; -import 'package:flutter_libmonero/view_model/send/output.dart'; import 'package:flutter_libmonero/monero/monero.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:hive/hive.dart'; +import 'package:hive_test/hive_test.dart'; +import 'package:mockito/annotations.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; - import 'package:stackwallet/services/wallets.dart'; - -import 'dart:developer' as developer; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; // TODO trim down to the minimum imports above @@ -76,24 +60,29 @@ void main() async { dirPath: ''); late WalletCredentials credentials; - WidgetsFlutterBinding.ensureInitialized(); - Directory appDir = (await getApplicationDocumentsDirectory()); - if (Platform.isIOS) { - appDir = (await getLibraryDirectory()); - } - await Hive.close(); - Hive.init(appDir.path); - Hive.registerAdapter(NodeAdapter()); - Hive.registerAdapter(WalletInfoAdapter()); - Hive.registerAdapter(WalletTypeAdapter()); - Hive.registerAdapter(UnspentCoinsInfoAdapter()); - monero.onStartup(); - _walletInfoSource = await Hive.openBox<WalletInfo>(WalletInfo.boxName); - walletService = monero.createMoneroWalletService(_walletInfoSource); + + bool hiveAdaptersRegistered = false; group("Mainnet tests", () { setUp(() async { + await setUpTestHive(); + if (!hiveAdaptersRegistered) { + hiveAdaptersRegistered = true; + + Hive.registerAdapter(NodeAdapter()); + Hive.registerAdapter(WalletInfoAdapter()); + Hive.registerAdapter(WalletTypeAdapter()); + Hive.registerAdapter(UnspentCoinsInfoAdapter()); + + final wallets = await Hive.openBox('wallets'); + await wallets.put('currentWalletName', name); + + _walletInfoSource = await Hive.openBox<WalletInfo>(WalletInfo.boxName); + walletService = monero + .createMoneroWalletService(_walletInfoSource as Box<WalletInfo>); + } + try { // if (name?.isEmpty ?? true) { // name = await generateName(); diff --git a/test/services/coins/wownero/wownero_wallet_test.dart b/test/services/coins/wownero/wownero_wallet_test.dart index 660bc1438..c58654b4b 100644 --- a/test/services/coins/wownero/wownero_wallet_test.dart +++ b/test/services/coins/wownero/wownero_wallet_test.dart @@ -1,38 +1,26 @@ -import 'dart:async'; import 'dart:core'; import 'dart:core' as core; import 'dart:io'; import 'dart:math'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:hive/hive.dart'; -import 'package:hive_test/hive_test.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; - -import 'package:cw_core/monero_amount_format.dart'; import 'package:cw_core/node.dart'; -import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cw_wownero/api/wallet.dart'; -import 'package:cw_wownero/pending_wownero_transaction.dart'; import 'package:cw_wownero/wownero_wallet.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_libmonero/core/key_service.dart'; import 'package:flutter_libmonero/core/wallet_creation_service.dart'; -import 'package:flutter_libmonero/view_model/send/output.dart'; import 'package:flutter_libmonero/wownero/wownero.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:hive/hive.dart'; +import 'package:hive_test/hive_test.dart'; +import 'package:mockito/annotations.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'wownero_wallet_test_data.dart'; @@ -67,24 +55,29 @@ void main() async { dirPath: ''); late WalletCredentials credentials; - WidgetsFlutterBinding.ensureInitialized(); - Directory appDir = (await getApplicationDocumentsDirectory()); - if (Platform.isIOS) { - appDir = (await getLibraryDirectory()); - } - await Hive.close(); - Hive.init(appDir.path); - Hive.registerAdapter(NodeAdapter()); - Hive.registerAdapter(WalletInfoAdapter()); - Hive.registerAdapter(WalletTypeAdapter()); - Hive.registerAdapter(UnspentCoinsInfoAdapter()); - wownero.onStartup(); - _walletInfoSource = await Hive.openBox<WalletInfo>(WalletInfo.boxName); - walletService = wownero.createWowneroWalletService(_walletInfoSource); + + bool hiveAdaptersRegistered = false; group("Wownero 14 word seed generation", () { setUp(() async { + await setUpTestHive(); + if (!hiveAdaptersRegistered) { + hiveAdaptersRegistered = true; + + Hive.registerAdapter(NodeAdapter()); + Hive.registerAdapter(WalletInfoAdapter()); + Hive.registerAdapter(WalletTypeAdapter()); + Hive.registerAdapter(UnspentCoinsInfoAdapter()); + + final wallets = await Hive.openBox('wallets'); + await wallets.put('currentWalletName', name); + + _walletInfoSource = await Hive.openBox<WalletInfo>(WalletInfo.boxName); + walletService = wownero + .createWowneroWalletService(_walletInfoSource as Box<WalletInfo>); + } + bool hasThrown = false; try { name = 'namee${Random().nextInt(10000000)}'; From 7105deeb24b49800d98527cf403e0cca224c5eec Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 10 Nov 2022 15:40:23 -0600 Subject: [PATCH 205/426] initialize isar instance correctly for desktop secure wrapper --- .../desktop_login_view.dart | 4 ++++ .../flutter_secure_storage_interface.dart | 21 ++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/lib/pages_desktop_specific/desktop_login_view.dart b/lib/pages_desktop_specific/desktop_login_view.dart index d90e742fb..93a281bf8 100644 --- a/lib/pages_desktop_specific/desktop_login_view.dart +++ b/lib/pages_desktop_specific/desktop_login_view.dart @@ -7,9 +7,11 @@ import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages_desktop_specific/forgot_password_desktop_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; import 'package:stackwallet/providers/desktop/storage_crypto_handler_provider.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; @@ -47,6 +49,8 @@ class _DesktopLoginViewState extends ConsumerState<DesktopLoginView> { .read(storageCryptoHandlerProvider) .initFromExisting(passwordController.text); + await (ref.read(secureStoreProvider).store as DesktopSecureStore).init(); + await widget.load?.call(); // if no errors passphrase is correct diff --git a/lib/utilities/flutter_secure_storage_interface.dart b/lib/utilities/flutter_secure_storage_interface.dart index 852c13f3d..9e8aef95c 100644 --- a/lib/utilities/flutter_secure_storage_interface.dart +++ b/lib/utilities/flutter_secure_storage_interface.dart @@ -6,6 +6,8 @@ import 'package:stack_wallet_backup/secure_storage.dart'; import 'package:stackwallet/models/isar/models/encrypted_string_value.dart'; abstract class SecureStorageInterface { + dynamic get store; + Future<void> write({ required String key, required String? value, @@ -54,6 +56,7 @@ class DesktopSecureStore { [EncryptedStringValueSchema], directory: appDirectory!.path, inspector: false, + name: "desktopStore", ); } @@ -77,7 +80,9 @@ class DesktopSecureStore { }) async { if (value == null) { // here we assume that a value is to be deleted - await isar.encryptedStringValues.deleteByKey(key); + await isar.writeTxn(() async { + await isar.encryptedStringValues.deleteByKey(key); + }); } else { // otherwise created encrypted object value final object = EncryptedStringValue(); @@ -85,14 +90,18 @@ class DesktopSecureStore { object.value = await handler.encryptValue(key, value); // store object value - await isar.encryptedStringValues.put(object); + await isar.writeTxn(() async { + await isar.encryptedStringValues.put(object); + }); } } Future<void> delete({ required String key, }) async { - await isar.encryptedStringValues.deleteByKey(key); + await isar.writeTxn(() async { + await isar.encryptedStringValues.deleteByKey(key); + }); } } @@ -101,6 +110,9 @@ class SecureStorageWrapper implements SecureStorageInterface { final dynamic _store; final bool _isDesktop; + @override + dynamic get store => _store; + const SecureStorageWrapper({ required dynamic store, required bool isDesktop, @@ -245,4 +257,7 @@ class FakeSecureStorage implements SecureStorageInterface { _deletes++; _store.remove(key); } + + @override + dynamic get store => throw UnimplementedError(); } From 24bdc100fb479843132f8f6e577f69cf62341323 Mon Sep 17 00:00:00 2001 From: sneurlax <sneurlax@gmail.com> Date: Thu, 10 Nov 2022 15:49:19 -0600 Subject: [PATCH 206/426] add address validation tests --- crypto_plugins/flutter_libmonero | 2 +- .../coins/monero/monero_wallet_test.dart | 22 ++++++++++++++++++- .../coins/wownero/wownero_wallet_test.dart | 12 ++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index 8a8c88cda..2da774385 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit 8a8c88cdade6fe18529deea410f862b125167a3b +Subproject commit 2da77438527732dfaa5398aa391eab5253dabe19 diff --git a/test/services/coins/monero/monero_wallet_test.dart b/test/services/coins/monero/monero_wallet_test.dart index d54959ab2..4c4daa9ac 100644 --- a/test/services/coins/monero/monero_wallet_test.dart +++ b/test/services/coins/monero/monero_wallet_test.dart @@ -92,7 +92,7 @@ void main() async { _walletInfoSource = await Hive.openBox<WalletInfo>(WalletInfo.boxName); walletService = monero.createMoneroWalletService(_walletInfoSource); - group("Mainnet tests", () { + group("Mnemonic recovery", () { setUp(() async { try { // if (name?.isEmpty ?? true) { @@ -133,6 +133,26 @@ void main() async { } }); + test("Test address validation", () async { // TODO I'd like to refactor/separate this out so I can test addresses alone, without having to first create a wallet. + final wallet = await _walletCreationService.restoreFromSeed(credentials); + walletBase = wallet as MoneroWalletBase; + + expect( + await walletBase!.validateAddress(''), false); + expect( + await walletBase!.validateAddress('4AeRgkWZsMJhAWKMeCZ3h4ZSPnAcW5VBtRFyLd6gBEf6GgJU2FHXDA6i1DnQTd6h8R3VU5AkbGcWSNhtSwNNPgaD48gp4nn'), true); + expect( + await walletBase!.validateAddress('4asdfkWZsMJhAWKMeCZ3h4ZSPnAcW5VBtRFyLd6gBEf6GgJU2FHXDA6i1DnQTd6h8R3VU5AkbGcWSNhtSwNNPgaD48gpjkl'), false); + expect( + await walletBase!.validateAddress('8AeRgkWZsMJhAWKMeCZ3h4ZSPnAcW5VBtRFyLd6gBEf6GgJU2FHXDA6i1DnQTd6h8R3VU5AkbGcWSNhtSwNNPgaD48gp4nn'), false); + expect( + await walletBase!.validateAddress('84kYPuZ1eaVKGQhf26QPNWbSLQG16BywXdLYYShVrPNMLAUAWce5vcpRc78FxwRphrG6Cda7faCKdUMr8fUCH3peHPenvHy'), true); + expect( + await walletBase!.validateAddress('8asdfuZ1eaVKGQhf26QPNWbSLQG16BywXdLYYShVrPNMLAUAWce5vcpRc78FxwRphrG6Cda7faCKdUMr8fUCH3peHPenjkl'), false); + expect( + await walletBase!.validateAddress('44kYPuZ1eaVKGQhf26QPNWbSLQG16BywXdLYYShVrPNMLAUAWce5vcpRc78FxwRphrG6Cda7faCKdUMr8fUCH3peHPenvHy'), false); + }); + test("Test mainnet address generation from seed", () async { final wallet = await // _walletCreationService.create(credentials); diff --git a/test/services/coins/wownero/wownero_wallet_test.dart b/test/services/coins/wownero/wownero_wallet_test.dart index 660bc1438..8daa91755 100644 --- a/test/services/coins/wownero/wownero_wallet_test.dart +++ b/test/services/coins/wownero/wownero_wallet_test.dart @@ -122,6 +122,18 @@ void main() async { expect(hasThrown, false); }); + test("Test address validation", () async { // TODO I'd like to refactor/separate this out so I can test addresses alone, without having to first create a wallet. + final wallet = await _walletCreationService.restoreFromSeed(credentials); + walletBase = wallet as WowneroWalletBase; + + expect( + await walletBase!.validateAddress(''), false); + expect( + await walletBase!.validateAddress('Wo3jmHvTMLwE6h29fpgcb8PbJSpaKuqM7XTXVfiiu8bLCZsJvrQCbQSJR48Vo3BWNQKsMsXZ4VixndXTH25QtorC27NCjmsEi'), true); + expect( + await walletBase!.validateAddress('WasdfHvTMLwE6h29fpgcb8PbJSpaKuqM7XTXVfiiu8bLCZsJvrQCbQSJR48Vo3BWNQKsMsXZ4VixndXTH25QtorC27NCjmjkl'), false); + }); + test("Wownero 14 word seed address generation", () async { final wallet = await _walletCreationService.create(credentials); // TODO validate mnemonic From 5f106efa4bbd541561f2edd1e85275ff000f66f9 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Thu, 10 Nov 2022 15:11:17 -0700 Subject: [PATCH 207/426] desktop edit auto backup and enabled/disabled button --- assets/svg/enabled-button.svg | 4 ++ .../backup_and_restore_settings.dart | 66 ++++++++++++++----- .../create_auto_backup.dart | 4 +- lib/utilities/assets.dart | 1 + pubspec.yaml | 1 + 5 files changed, 59 insertions(+), 17 deletions(-) create mode 100644 assets/svg/enabled-button.svg diff --git a/assets/svg/enabled-button.svg b/assets/svg/enabled-button.svg new file mode 100644 index 000000000..a26359e81 --- /dev/null +++ b/assets/svg/enabled-button.svg @@ -0,0 +1,4 @@ +<svg width="87" height="38" viewBox="0 0 87 38" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect y="0.5" width="87" height="37" rx="8" fill="#B9E9D4"/> +<path d="M17.6167 24.5V14.3182H24.0002V15.6406H19.1529V18.7429H23.6671V20.0604H19.1529V23.1776H24.0598V24.5H17.6167ZM27.4194 19.9659V24.5H25.9329V16.8636H27.3597V18.1065H27.4542C27.6299 17.7022 27.9049 17.3774 28.2795 17.1321C28.6573 16.8868 29.1329 16.7642 29.7063 16.7642C30.2267 16.7642 30.6824 16.8736 31.0735 17.0923C31.4646 17.3078 31.7679 17.6293 31.9833 18.0568C32.1987 18.4844 32.3065 19.013 32.3065 19.6428V24.5H30.82V19.8217C30.82 19.2682 30.6758 18.8357 30.3874 18.5241C30.0991 18.2093 29.703 18.0518 29.1992 18.0518C28.8545 18.0518 28.5479 18.1264 28.2795 18.2756C28.0143 18.4247 27.8039 18.6435 27.6481 18.9318C27.4956 19.2169 27.4194 19.5616 27.4194 19.9659ZM36.5224 24.669C36.0385 24.669 35.601 24.5795 35.2099 24.4006C34.8188 24.2183 34.5089 23.9548 34.2802 23.6101C34.0548 23.2654 33.9421 22.8428 33.9421 22.3423C33.9421 21.9115 34.025 21.5568 34.1907 21.2784C34.3564 21 34.5801 20.7796 34.8619 20.6172C35.1436 20.4548 35.4585 20.3321 35.8065 20.2493C36.1545 20.1664 36.5091 20.1035 36.8704 20.0604C37.3278 20.0073 37.699 19.9643 37.984 19.9311C38.2691 19.8946 38.4762 19.8366 38.6055 19.7571C38.7347 19.6776 38.7994 19.5483 38.7994 19.3693V19.3345C38.7994 18.9003 38.6767 18.5639 38.4315 18.3253C38.1895 18.0866 37.8282 17.9673 37.3477 17.9673C36.8472 17.9673 36.4528 18.0784 36.1644 18.3004C35.8794 18.5192 35.6822 18.7628 35.5728 19.0312L34.1758 18.7131C34.3415 18.2491 34.5835 17.8745 34.9016 17.5895C35.2231 17.3011 35.5927 17.0923 36.0103 16.9631C36.4279 16.8305 36.8671 16.7642 37.3278 16.7642C37.6327 16.7642 37.9558 16.8007 38.2972 16.8736C38.6419 16.9432 38.9634 17.0724 39.2617 17.2614C39.5633 17.4503 39.8103 17.7204 40.0025 18.0717C40.1947 18.4197 40.2908 18.8722 40.2908 19.429V24.5H38.8391V23.456H38.7795C38.6834 23.6482 38.5392 23.8371 38.3469 24.0227C38.1547 24.2083 37.9078 24.3625 37.6062 24.4851C37.3046 24.6077 36.9433 24.669 36.5224 24.669ZM36.8455 23.4759C37.2565 23.4759 37.6078 23.3946 37.8995 23.2322C38.1945 23.0698 38.4182 22.8577 38.5707 22.5959C38.7264 22.3307 38.8043 22.0473 38.8043 21.7457V20.7614C38.7513 20.8144 38.6486 20.8641 38.4961 20.9105C38.3469 20.9536 38.1763 20.9917 37.984 21.0249C37.7918 21.0547 37.6045 21.0829 37.4222 21.1094C37.2399 21.1326 37.0875 21.1525 36.9648 21.169C36.6765 21.2055 36.413 21.2668 36.1744 21.353C35.939 21.4392 35.7501 21.5634 35.6076 21.7259C35.4684 21.8849 35.3988 22.0971 35.3988 22.3622C35.3988 22.7301 35.5347 23.0085 35.8065 23.1974C36.0782 23.383 36.4246 23.4759 36.8455 23.4759ZM42.3901 24.5V14.3182H43.8766V18.1016H43.9661C44.0523 17.9425 44.1766 17.7585 44.339 17.5497C44.5014 17.3409 44.7267 17.1586 45.0151 17.0028C45.3034 16.8438 45.6846 16.7642 46.1586 16.7642C46.775 16.7642 47.3252 16.92 47.8091 17.2315C48.293 17.5431 48.6725 17.9922 48.9476 18.5788C49.226 19.1655 49.3652 19.8714 49.3652 20.6967C49.3652 21.522 49.2277 22.2296 48.9526 22.8196C48.6775 23.4062 48.2997 23.8587 47.8191 24.1768C47.3385 24.4917 46.79 24.6491 46.1735 24.6491C45.7095 24.6491 45.33 24.5713 45.035 24.4155C44.7433 24.2597 44.5146 24.0774 44.3489 23.8686C44.1832 23.6598 44.0556 23.4742 43.9661 23.3118H43.8418V24.5H42.3901ZM43.8468 20.6818C43.8468 21.2187 43.9247 21.6894 44.0804 22.0938C44.2362 22.4981 44.4616 22.8146 44.7566 23.0433C45.0516 23.2687 45.4128 23.3814 45.8404 23.3814C46.2845 23.3814 46.6557 23.2637 46.954 23.0284C47.2523 22.7898 47.4777 22.4666 47.6301 22.0589C47.7859 21.6513 47.8638 21.1922 47.8638 20.6818C47.8638 20.178 47.7876 19.7256 47.6351 19.3246C47.486 18.9235 47.2606 18.607 46.959 18.375C46.6607 18.143 46.2878 18.027 45.8404 18.027C45.4095 18.027 45.0449 18.138 44.7466 18.3601C44.4516 18.5821 44.2279 18.892 44.0755 19.2898C43.923 19.6875 43.8468 20.1515 43.8468 20.6818ZM52.521 14.3182V24.5H51.0344V14.3182H52.521ZM57.812 24.6541C57.0596 24.6541 56.4116 24.4934 55.8681 24.1719C55.3278 23.8471 54.9102 23.3913 54.6152 22.8047C54.3236 22.2147 54.1777 21.5237 54.1777 20.7315C54.1777 19.9493 54.3236 19.2599 54.6152 18.6634C54.9102 18.0668 55.3212 17.6011 55.8482 17.2663C56.3785 16.9316 56.9983 16.7642 57.7076 16.7642C58.1384 16.7642 58.556 16.8355 58.9604 16.978C59.3648 17.1205 59.7277 17.3442 60.0492 17.6491C60.3707 17.9541 60.6242 18.3501 60.8098 18.8374C60.9954 19.3213 61.0882 19.9096 61.0882 20.6023V21.1293H55.0179V20.0156H59.6316C59.6316 19.6245 59.552 19.2782 59.3929 18.9766C59.2338 18.6716 59.0101 18.4313 58.7218 18.2557C58.4367 18.08 58.102 17.9922 57.7175 17.9922C57.2999 17.9922 56.9353 18.0949 56.6238 18.3004C56.3155 18.5026 56.0769 18.7678 55.9078 19.0959C55.7421 19.4207 55.6593 19.7737 55.6593 20.1548V21.0249C55.6593 21.5353 55.7488 21.9695 55.9277 22.3274C56.11 22.6854 56.3636 22.9588 56.6884 23.1477C57.0132 23.3333 57.3927 23.4261 57.8269 23.4261C58.1086 23.4261 58.3655 23.3864 58.5975 23.3068C58.8295 23.224 59.03 23.1013 59.199 22.9389C59.3681 22.7765 59.4973 22.576 59.5868 22.3374L60.9938 22.5909C60.8811 23.0052 60.6789 23.3681 60.3873 23.6797C60.0989 23.9879 59.736 24.2282 59.2985 24.4006C58.8643 24.5696 58.3688 24.6541 57.812 24.6541ZM65.5962 24.6491C64.9798 24.6491 64.4296 24.4917 63.9457 24.1768C63.4651 23.8587 63.0872 23.4062 62.8121 22.8196C62.5404 22.2296 62.4045 21.522 62.4045 20.6967C62.4045 19.8714 62.542 19.1655 62.8171 18.5788C63.0955 17.9922 63.4767 17.5431 63.9606 17.2315C64.4445 16.92 64.993 16.7642 65.6062 16.7642C66.0801 16.7642 66.4613 16.8438 66.7496 17.0028C67.0413 17.1586 67.2667 17.3409 67.4258 17.5497C67.5882 17.7585 67.7141 17.9425 67.8036 18.1016H67.8931V14.3182H69.3796V24.5H67.9279V23.3118H67.8036C67.7141 23.4742 67.5849 23.6598 67.4158 23.8686C67.2501 24.0774 67.0214 24.2597 66.7298 24.4155C66.4381 24.5713 66.0603 24.6491 65.5962 24.6491ZM65.9244 23.3814C66.3519 23.3814 66.7132 23.2687 67.0082 23.0433C67.3065 22.8146 67.5318 22.4981 67.6843 22.0938C67.8401 21.6894 67.918 21.2187 67.918 20.6818C67.918 20.1515 67.8417 19.6875 67.6893 19.2898C67.5368 18.892 67.3131 18.5821 67.0181 18.3601C66.7231 18.138 66.3585 18.027 65.9244 18.027C65.4769 18.027 65.104 18.143 64.8058 18.375C64.5075 18.607 64.2821 18.9235 64.1296 19.3246C63.9805 19.7256 63.9059 20.178 63.9059 20.6818C63.9059 21.1922 63.9821 21.6513 64.1346 22.0589C64.2871 22.4666 64.5124 22.7898 64.8107 23.0284C65.1123 23.2637 65.4835 23.3814 65.9244 23.3814Z" fill="#232323"/> +</svg> diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart index a444f4b51..3ea6cea6c 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart @@ -5,6 +5,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:intl/intl.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart'; import 'package:stackwallet/providers/global/locale_provider.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; @@ -36,7 +37,6 @@ class BackupRestoreSettings extends ConsumerStatefulWidget { class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { late bool createBackup = false; late bool restoreBackup = false; - // late bool isEnabledAutoBackup; final toggleController = DSBController(); @@ -91,6 +91,17 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { ); } + Future<void> createAutoBackup() async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return CreateAutoBackup(); + }, + ); + } + Future<void> attemptDisable() async { final result = await showDialog<bool?>( context: context, @@ -208,10 +219,25 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SvgPicture.asset( - Assets.svg.backupAuto, - width: 48, - height: 48, + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SvgPicture.asset( + Assets.svg.backupAuto, + width: 48, + height: 48, + ), + isEnabledAutoBackup + ? SvgPicture.asset( + Assets.svg.enableButton, + ) + : SvgPicture.asset( + Assets.svg.disableButton, + ), + ], + ), ), Center( child: Row( @@ -338,7 +364,9 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { desktopMed: true, width: 190, label: "Edit auto backup", - onPressed: () {}, + onPressed: () { + createAutoBackup(); + }, ), ], ) @@ -362,11 +390,14 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SvgPicture.asset( - Assets.svg.backupAdd, - width: 48, - height: 48, - alignment: Alignment.topLeft, + Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + Assets.svg.backupAdd, + width: 48, + height: 48, + alignment: Alignment.topLeft, + ), ), Center( child: Row( @@ -441,11 +472,14 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SvgPicture.asset( - Assets.svg.backupRestore, - width: 48, - height: 48, - alignment: Alignment.topLeft, + Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + Assets.svg.backupRestore, + width: 48, + height: 48, + alignment: Alignment.topLeft, + ), ), Center( child: Row( diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart index e804071cc..acd0e689c 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart @@ -578,7 +578,9 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> { label: "Cancel", onPressed: () { int count = 0; - Navigator.of(context).popUntil((_) => count++ >= 2); + !isEnabledAutoBackup + ? Navigator.of(context).popUntil((_) => count++ >= 2) + : Navigator.of(context).pop(); }, ), ), diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index b0c6b3bf9..f853a00d8 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -69,6 +69,7 @@ class _SVG { String get circleLanguage => "assets/svg/language-circle.svg"; String get circleDollarSign => "assets/svg/dollar-sign-circle.svg"; String get circleLock => "assets/svg/lock-circle.svg"; + String get enableButton => "assets/svg/enabled-button.svg"; String get disableButton => "assets/svg/Button.svg"; String get polygon => "assets/svg/Polygon.svg"; String get personaIncognito => "assets/svg/persona-incognito-1.svg"; diff --git a/pubspec.yaml b/pubspec.yaml index 19d38ca4f..8b03cd57e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -298,6 +298,7 @@ flutter: - assets/svg/persona-easy-1.svg - assets/svg/persona-incognito-1.svg - assets/svg/Button.svg + - assets/svg/enabled-button.svg - assets/svg/lock-circle.svg - assets/svg/dollar-sign-circle.svg - assets/svg/language-circle.svg From a6172f90a9e5024aad844be63e8952bdf98ae07d Mon Sep 17 00:00:00 2001 From: sneurlax <sneurlax@gmail.com> Date: Thu, 10 Nov 2022 16:21:58 -0600 Subject: [PATCH 208/426] cherrypick Julian's test changes --- .../coins/monero/monero_wallet_test.dart | 57 ++++++++----------- .../coins/wownero/wownero_wallet_test.dart | 53 ++++++++--------- 2 files changed, 46 insertions(+), 64 deletions(-) diff --git a/test/services/coins/monero/monero_wallet_test.dart b/test/services/coins/monero/monero_wallet_test.dart index 4c4daa9ac..34024640f 100644 --- a/test/services/coins/monero/monero_wallet_test.dart +++ b/test/services/coins/monero/monero_wallet_test.dart @@ -1,43 +1,27 @@ -import 'dart:async'; import 'dart:core'; import 'dart:core' as core; import 'dart:io'; import 'dart:math'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:hive/hive.dart'; -import 'package:hive_test/hive_test.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; - -import 'package:cw_core/monero_amount_format.dart'; import 'package:cw_core/node.dart'; -import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cw_monero/api/wallet.dart'; -import 'package:cw_monero/api/wallet_manager.dart' as monero_wallet_manager; -import 'package:cw_monero/pending_monero_transaction.dart'; import 'package:cw_monero/monero_wallet.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_libmonero/core/key_service.dart'; import 'package:flutter_libmonero/core/wallet_creation_service.dart'; -import 'package:flutter_libmonero/view_model/send/output.dart'; import 'package:flutter_libmonero/monero/monero.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:hive/hive.dart'; +import 'package:hive_test/hive_test.dart'; +import 'package:mockito/annotations.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; - import 'package:stackwallet/services/wallets.dart'; - -import 'dart:developer' as developer; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; // TODO trim down to the minimum imports above @@ -76,24 +60,29 @@ void main() async { dirPath: ''); late WalletCredentials credentials; - WidgetsFlutterBinding.ensureInitialized(); - Directory appDir = (await getApplicationDocumentsDirectory()); - if (Platform.isIOS) { - appDir = (await getLibraryDirectory()); - } - await Hive.close(); - Hive.init(appDir.path); - Hive.registerAdapter(NodeAdapter()); - Hive.registerAdapter(WalletInfoAdapter()); - Hive.registerAdapter(WalletTypeAdapter()); - Hive.registerAdapter(UnspentCoinsInfoAdapter()); - monero.onStartup(); - _walletInfoSource = await Hive.openBox<WalletInfo>(WalletInfo.boxName); - walletService = monero.createMoneroWalletService(_walletInfoSource); + + bool hiveAdaptersRegistered = false; group("Mnemonic recovery", () { setUp(() async { + await setUpTestHive(); + if (!hiveAdaptersRegistered) { + hiveAdaptersRegistered = true; + + Hive.registerAdapter(NodeAdapter()); + Hive.registerAdapter(WalletInfoAdapter()); + Hive.registerAdapter(WalletTypeAdapter()); + Hive.registerAdapter(UnspentCoinsInfoAdapter()); + + final wallets = await Hive.openBox('wallets'); + await wallets.put('currentWalletName', name); + + _walletInfoSource = await Hive.openBox<WalletInfo>(WalletInfo.boxName); + walletService = monero + .createMoneroWalletService(_walletInfoSource as Box<WalletInfo>); + } + try { // if (name?.isEmpty ?? true) { // name = await generateName(); diff --git a/test/services/coins/wownero/wownero_wallet_test.dart b/test/services/coins/wownero/wownero_wallet_test.dart index 8daa91755..8ad730e13 100644 --- a/test/services/coins/wownero/wownero_wallet_test.dart +++ b/test/services/coins/wownero/wownero_wallet_test.dart @@ -1,38 +1,26 @@ -import 'dart:async'; import 'dart:core'; import 'dart:core' as core; import 'dart:io'; import 'dart:math'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:hive/hive.dart'; -import 'package:hive_test/hive_test.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; - -import 'package:cw_core/monero_amount_format.dart'; import 'package:cw_core/node.dart'; -import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cw_wownero/api/wallet.dart'; -import 'package:cw_wownero/pending_wownero_transaction.dart'; import 'package:cw_wownero/wownero_wallet.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_libmonero/core/key_service.dart'; import 'package:flutter_libmonero/core/wallet_creation_service.dart'; -import 'package:flutter_libmonero/view_model/send/output.dart'; import 'package:flutter_libmonero/wownero/wownero.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:hive/hive.dart'; +import 'package:hive_test/hive_test.dart'; +import 'package:mockito/annotations.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'wownero_wallet_test_data.dart'; @@ -67,24 +55,29 @@ void main() async { dirPath: ''); late WalletCredentials credentials; - WidgetsFlutterBinding.ensureInitialized(); - Directory appDir = (await getApplicationDocumentsDirectory()); - if (Platform.isIOS) { - appDir = (await getLibraryDirectory()); - } - await Hive.close(); - Hive.init(appDir.path); - Hive.registerAdapter(NodeAdapter()); - Hive.registerAdapter(WalletInfoAdapter()); - Hive.registerAdapter(WalletTypeAdapter()); - Hive.registerAdapter(UnspentCoinsInfoAdapter()); - wownero.onStartup(); - _walletInfoSource = await Hive.openBox<WalletInfo>(WalletInfo.boxName); - walletService = wownero.createWowneroWalletService(_walletInfoSource); + + bool hiveAdaptersRegistered = false; group("Wownero 14 word seed generation", () { setUp(() async { + await setUpTestHive(); + if (!hiveAdaptersRegistered) { + hiveAdaptersRegistered = true; + + Hive.registerAdapter(NodeAdapter()); + Hive.registerAdapter(WalletInfoAdapter()); + Hive.registerAdapter(WalletTypeAdapter()); + Hive.registerAdapter(UnspentCoinsInfoAdapter()); + + final wallets = await Hive.openBox('wallets'); + await wallets.put('currentWalletName', name); + + _walletInfoSource = await Hive.openBox<WalletInfo>(WalletInfo.boxName); + walletService = wownero + .createWowneroWalletService(_walletInfoSource as Box<WalletInfo>); + } + bool hasThrown = false; try { name = 'namee${Random().nextInt(10000000)}'; From f61b3857e6e40a45683f2233a34eb6f2087a2463 Mon Sep 17 00:00:00 2001 From: sneurlax <sneurlax@gmail.com> Date: Thu, 10 Nov 2022 17:03:01 -0600 Subject: [PATCH 209/426] add more address validation to tests --- .../coins/monero/monero_wallet_test.dart | 69 ++++++++----------- .../coins/wownero/wownero_wallet_test.dart | 35 ++++------ 2 files changed, 43 insertions(+), 61 deletions(-) diff --git a/test/services/coins/monero/monero_wallet_test.dart b/test/services/coins/monero/monero_wallet_test.dart index 6b134f302..79bf98a41 100644 --- a/test/services/coins/monero/monero_wallet_test.dart +++ b/test/services/coins/monero/monero_wallet_test.dart @@ -60,19 +60,11 @@ void main() async { dirPath: ''); late WalletCredentials credentials; - WidgetsFlutterBinding.ensureInitialized(); - Directory appDir = (await getApplicationDocumentsDirectory()); - if (Platform.isIOS) { - appDir = (await getLibraryDirectory()); - } - monero.onStartup(); bool hiveAdaptersRegistered = false; - bool hiveAdaptersRegistered = false; - - group("Mnemonic recovery", () { + group("Mainnet tests", () { setUp(() async { await setUpTestHive(); if (!hiveAdaptersRegistered) { @@ -87,7 +79,8 @@ void main() async { await wallets.put('currentWalletName', name); _walletInfoSource = await Hive.openBox<WalletInfo>(WalletInfo.boxName); - walletService = monero.createMoneroWalletService(_walletInfoSource); + walletService = monero + .createMoneroWalletService(_walletInfoSource as Box<WalletInfo>); } try { @@ -97,12 +90,12 @@ void main() async { final dirPath = await pathForWalletDir(name: name, type: type); path = await pathForWallet(name: name, type: type); credentials = - // // creating a new wallet - // monero.createMoneroNewWalletCredentials( - // name: name, language: "English"); - // restoring a previous wallet - monero.createMoneroRestoreWalletFromSeedCredentials( - name: name, height: 2580000, mnemonic: testMnemonic); + // // creating a new wallet + // monero.createMoneroNewWalletCredentials( + // name: name, language: "English"); + // restoring a previous wallet + monero.createMoneroRestoreWalletFromSeedCredentials( + name: name, height: 2580000, mnemonic: testMnemonic); walletInfo = WalletInfo.external( id: WalletBase.idFor(name, type), @@ -129,30 +122,10 @@ void main() async { } }); - test("Test address validation", () async { // TODO I'd like to refactor/separate this out so I can test addresses alone, without having to first create a wallet. - final wallet = await _walletCreationService.restoreFromSeed(credentials); - walletBase = wallet as MoneroWalletBase; - - expect( - await walletBase!.validateAddress(''), false); - expect( - await walletBase!.validateAddress('4AeRgkWZsMJhAWKMeCZ3h4ZSPnAcW5VBtRFyLd6gBEf6GgJU2FHXDA6i1DnQTd6h8R3VU5AkbGcWSNhtSwNNPgaD48gp4nn'), true); - expect( - await walletBase!.validateAddress('4asdfkWZsMJhAWKMeCZ3h4ZSPnAcW5VBtRFyLd6gBEf6GgJU2FHXDA6i1DnQTd6h8R3VU5AkbGcWSNhtSwNNPgaD48gpjkl'), false); - expect( - await walletBase!.validateAddress('8AeRgkWZsMJhAWKMeCZ3h4ZSPnAcW5VBtRFyLd6gBEf6GgJU2FHXDA6i1DnQTd6h8R3VU5AkbGcWSNhtSwNNPgaD48gp4nn'), false); - expect( - await walletBase!.validateAddress('84kYPuZ1eaVKGQhf26QPNWbSLQG16BywXdLYYShVrPNMLAUAWce5vcpRc78FxwRphrG6Cda7faCKdUMr8fUCH3peHPenvHy'), true); - expect( - await walletBase!.validateAddress('8asdfuZ1eaVKGQhf26QPNWbSLQG16BywXdLYYShVrPNMLAUAWce5vcpRc78FxwRphrG6Cda7faCKdUMr8fUCH3peHPenjkl'), false); - expect( - await walletBase!.validateAddress('44kYPuZ1eaVKGQhf26QPNWbSLQG16BywXdLYYShVrPNMLAUAWce5vcpRc78FxwRphrG6Cda7faCKdUMr8fUCH3peHPenvHy'), false); - }); - test("Test mainnet address generation from seed", () async { final wallet = await - // _walletCreationService.create(credentials); - _walletCreationService.restoreFromSeed(credentials); + // _walletCreationService.create(credentials); + _walletCreationService.restoreFromSeed(credentials); walletInfo.address = wallet.walletAddresses.address; //print(walletInfo.address); @@ -161,6 +134,9 @@ void main() async { walletBase = wallet as MoneroWalletBase; //print("${walletBase?.seed}"); + expect( + await walletBase!.validateAddress(walletInfo.address ?? ''), true); + // print(walletBase); // loggerPrint(walletBase.toString()); // loggerPrint("name: ${walletBase!.name} seed: ${walletBase!.seed} id: " @@ -180,6 +156,21 @@ void main() async { await walletBase!.getTransactionAddress(1, 1), mainnetTestData[1][1]); expect( await walletBase!.getTransactionAddress(1, 2), mainnetTestData[1][2]); + + expect( + await walletBase!.validateAddress(''), false); + expect( + await walletBase!.validateAddress('4AeRgkWZsMJhAWKMeCZ3h4ZSPnAcW5VBtRFyLd6gBEf6GgJU2FHXDA6i1DnQTd6h8R3VU5AkbGcWSNhtSwNNPgaD48gp4nn'), true); + expect( + await walletBase!.validateAddress('4asdfkWZsMJhAWKMeCZ3h4ZSPnAcW5VBtRFyLd6gBEf6GgJU2FHXDA6i1DnQTd6h8R3VU5AkbGcWSNhtSwNNPgaD48gpjkl'), false); + expect( + await walletBase!.validateAddress('8AeRgkWZsMJhAWKMeCZ3h4ZSPnAcW5VBtRFyLd6gBEf6GgJU2FHXDA6i1DnQTd6h8R3VU5AkbGcWSNhtSwNNPgaD48gp4nn'), false); + expect( + await walletBase!.validateAddress('84kYPuZ1eaVKGQhf26QPNWbSLQG16BywXdLYYShVrPNMLAUAWce5vcpRc78FxwRphrG6Cda7faCKdUMr8fUCH3peHPenvHy'), true); + expect( + await walletBase!.validateAddress('8asdfuZ1eaVKGQhf26QPNWbSLQG16BywXdLYYShVrPNMLAUAWce5vcpRc78FxwRphrG6Cda7faCKdUMr8fUCH3peHPenjkl'), false); + expect( + await walletBase!.validateAddress('44kYPuZ1eaVKGQhf26QPNWbSLQG16BywXdLYYShVrPNMLAUAWce5vcpRc78FxwRphrG6Cda7faCKdUMr8fUCH3peHPenvHy'), false); }); }); /* @@ -238,6 +229,6 @@ Future<String> pathForWalletDir( } Future<String> pathForWallet( - {required String name, required WalletType type}) async => + {required String name, required WalletType type}) async => await pathForWalletDir(name: name, type: type) .then((path) => path + '/$name'); diff --git a/test/services/coins/wownero/wownero_wallet_test.dart b/test/services/coins/wownero/wownero_wallet_test.dart index 71c11677a..78a9c56c1 100644 --- a/test/services/coins/wownero/wownero_wallet_test.dart +++ b/test/services/coins/wownero/wownero_wallet_test.dart @@ -55,12 +55,6 @@ void main() async { dirPath: ''); late WalletCredentials credentials; - WidgetsFlutterBinding.ensureInitialized(); - Directory appDir = (await getApplicationDocumentsDirectory()); - if (Platform.isIOS) { - appDir = (await getLibraryDirectory()); - } - wownero.onStartup(); bool hiveAdaptersRegistered = false; @@ -80,7 +74,8 @@ void main() async { await wallets.put('currentWalletName', name); _walletInfoSource = await Hive.openBox<WalletInfo>(WalletInfo.boxName); - walletService = wownero.createWowneroWalletService(_walletInfoSource); + walletService = wownero + .createWowneroWalletService(_walletInfoSource as Box<WalletInfo>); } bool hasThrown = false; @@ -120,18 +115,6 @@ void main() async { expect(hasThrown, false); }); - test("Test address validation", () async { // TODO I'd like to refactor/separate this out so I can test addresses alone, without having to first create a wallet. - final wallet = await _walletCreationService.restoreFromSeed(credentials); - walletBase = wallet as WowneroWalletBase; - - expect( - await walletBase!.validateAddress(''), false); - expect( - await walletBase!.validateAddress('Wo3jmHvTMLwE6h29fpgcb8PbJSpaKuqM7XTXVfiiu8bLCZsJvrQCbQSJR48Vo3BWNQKsMsXZ4VixndXTH25QtorC27NCjmsEi'), true); - expect( - await walletBase!.validateAddress('WasdfHvTMLwE6h29fpgcb8PbJSpaKuqM7XTXVfiiu8bLCZsJvrQCbQSJR48Vo3BWNQKsMsXZ4VixndXTH25QtorC27NCjmjkl'), false); - }); - test("Wownero 14 word seed address generation", () async { final wallet = await _walletCreationService.create(credentials); // TODO validate mnemonic @@ -143,13 +126,21 @@ void main() async { walletBase?.close(); walletBase = wallet as WowneroWalletBase; - // TODO validate - //expect(walletInfo.address, mainnetTestData14[0][0]); + expect( + await walletBase!.validateAddress(wallet.walletAddresses.address ?? ''), true); } catch (_) { hasThrown = true; } expect(hasThrown, false); + // Address validation + expect( + await walletBase!.validateAddress(''), false); + expect( + await walletBase!.validateAddress('Wo3jmHvTMLwE6h29fpgcb8PbJSpaKuqM7XTXVfiiu8bLCZsJvrQCbQSJR48Vo3BWNQKsMsXZ4VixndXTH25QtorC27NCjmsEi'), true); + expect( + await walletBase!.validateAddress('WasdfHvTMLwE6h29fpgcb8PbJSpaKuqM7XTXVfiiu8bLCZsJvrQCbQSJR48Vo3BWNQKsMsXZ4VixndXTH25QtorC27NCjmjkl'), false); + walletBase?.close(); walletBase = wallet as WowneroWalletBase; }); @@ -376,6 +367,6 @@ Future<String> pathForWalletDir( } Future<String> pathForWallet( - {required String name, required WalletType type}) async => + {required String name, required WalletType type}) async => await pathForWalletDir(name: name, type: type) .then((path) => path + '/$name'); From dac1337a712525727361c3c2bfdbf7698877a2a0 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Thu, 10 Nov 2022 17:49:49 -0700 Subject: [PATCH 210/426] WIP: added border to cards; working on cancel restore --- .../restore_from_file_view.dart | 30 +- .../stack_restore_progress_view.dart | 539 +++++++++++++----- .../sub_widgets/restoring_wallet_card.dart | 398 +++++++++---- 3 files changed, 668 insertions(+), 299 deletions(-) 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 ee1fcf666..e2d16db54 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 @@ -9,7 +9,6 @@ import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/stack_file_system.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/sub_views/stack_restore_progress_view.dart'; -// import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/restore_backup_dialog.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -21,13 +20,14 @@ import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.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/loading_indicator.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:tuple/tuple.dart'; +import '../../../../widgets/desktop/desktop_dialog_close_button.dart'; + class RestoreFromFileView extends ConsumerStatefulWidget { const RestoreFromFileView({Key? key}) : super(key: key); @@ -48,17 +48,6 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { bool hidePassword = true; - Future<void> restoreBackupPopup(BuildContext context) async { - // await showDialog<dynamic>( - // context: context, - // useSafeArea: false, - // barrierDismissible: true, - // builder: (context) { - // return const RestoreBackupDialog(); - // }, - // ); - } - @override void initState() { stackFileSystem = StackFileSystem(); @@ -237,7 +226,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { enableSuggestions: false, autocorrect: false, decoration: standardInputDecoration( - "Enter password", + "Enter passphrase", passwordFocusNode, context, ).copyWith( @@ -534,7 +523,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { const EdgeInsets .all(32), child: Text( - "Restoring Stack Wallet", + "Restore Stack Wallet", style: STextStyles .desktopH3( context), @@ -546,12 +535,10 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { const DesktopDialogCloseButton(), ], ), - const SizedBox( - height: 30, - ), Padding( - padding: EdgeInsets - .symmetric( + padding: + const EdgeInsets + .symmetric( horizontal: 32), child: @@ -560,6 +547,9 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { jsonString, ), ), + const SizedBox( + height: 32, + ), ], ), ), 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 7dec4e740..e7c65346f 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 @@ -19,10 +19,13 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/icon_widgets/addressbook_icon.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; +import '../../../../../widgets/desktop/primary_button.dart'; + class StackRestoreProgressView extends ConsumerStatefulWidget { const StackRestoreProgressView({ Key? key, @@ -100,6 +103,30 @@ class _StackRestoreProgressViewState context: context, builder: (_) => const CancelStackRestoreDialog(), ); + // : await Row( + // children: [ + // SecondaryButton( + // width: 248, + // desktopMed: true, + // enabled: true, + // label: "Keep restoring", + // onPressed: () { + // false; + // }, + // ), + // const SizedBox(width: 16), + // PrimaryButton( + // width: 248, + // desktopMed: true, + // enabled: true, + // label: "Cancel anyway", + // onPressed: () { + // true; + // }, + // ) + // ], + // ); + if (result is bool && result) { return true; } @@ -128,6 +155,7 @@ class _StackRestoreProgressViewState } bool _success = false; + bool pendingCancel = false; Future<bool> _onWillPop() async { if (_success) { @@ -239,7 +267,7 @@ class _StackRestoreProgressViewState left: 4, top: 4, right: 4, - bottom: 0, + bottom: 4, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -255,40 +283,84 @@ class _StackRestoreProgressViewState builder: (_, ref, __) { final state = ref.watch(stackRestoringUIStateProvider .select((value) => value.preferences)); - return RestoringItemCard( - left: SizedBox( - width: 32, - height: 32, - child: RoundedContainer( - padding: const EdgeInsets.all(0), - color: Theme.of(context) - .extension<StackColors>()! - .buttonBackSecondary, - child: Center( - child: SvgPicture.asset( - Assets.svg.gear, - width: 16, - height: 16, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, + return !isDesktop + ? RestoringItemCard( + left: SizedBox( + width: 32, + height: 32, + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + child: Center( + child: SvgPicture.asset( + Assets.svg.gear, + width: 16, + height: 16, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + ), + ), ), - ), - ), - ), - right: SizedBox( - width: 20, - height: 20, - child: _getIconForState(state), - ), - title: "Preferences", - subTitle: state == StackRestoringStatus.failed - ? Text( - "Something went wrong", - style: STextStyles.errorSmall(context), - ) - : null, - ); + right: SizedBox( + width: 20, + height: 20, + child: _getIconForState(state), + ), + title: "Preferences", + subTitle: state == StackRestoringStatus.failed + ? Text( + "Something went wrong", + style: STextStyles.errorSmall(context), + ) + : null, + ) + : RoundedContainer( + padding: EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .popupBG, + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: RestoringItemCard( + left: SizedBox( + width: 32, + height: 32, + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + child: Center( + child: SvgPicture.asset( + Assets.svg.gear, + width: 16, + height: 16, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + ), + ), + ), + right: SizedBox( + width: 20, + height: 20, + child: _getIconForState(state), + ), + title: "Preferences", + subTitle: state == StackRestoringStatus.failed + ? Text( + "Something went wrong", + style: STextStyles.errorSmall(context), + ) + : null, + ), + ); }, ), const SizedBox( @@ -298,39 +370,82 @@ class _StackRestoreProgressViewState builder: (_, ref, __) { final state = ref.watch(stackRestoringUIStateProvider .select((value) => value.addressBook)); - return RestoringItemCard( - left: SizedBox( - width: 32, - height: 32, - child: RoundedContainer( - padding: const EdgeInsets.all(0), - color: Theme.of(context) - .extension<StackColors>()! - .buttonBackSecondary, - child: Center( - child: AddressBookIcon( - width: 16, - height: 16, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, + return !isDesktop + ? RestoringItemCard( + left: SizedBox( + width: 32, + height: 32, + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + child: Center( + child: AddressBookIcon( + width: 16, + height: 16, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + ), + ), ), - ), - ), - ), - right: SizedBox( - width: 20, - height: 20, - child: _getIconForState(state), - ), - title: "Address book", - subTitle: state == StackRestoringStatus.failed - ? Text( - "Something went wrong", - style: STextStyles.errorSmall(context), - ) - : null, - ); + right: SizedBox( + width: 20, + height: 20, + child: _getIconForState(state), + ), + title: "Address book", + subTitle: state == StackRestoringStatus.failed + ? Text( + "Something went wrong", + style: STextStyles.errorSmall(context), + ) + : null, + ) + : RoundedContainer( + padding: EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .popupBG, + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: RestoringItemCard( + left: SizedBox( + width: 32, + height: 32, + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + child: Center( + child: AddressBookIcon( + width: 16, + height: 16, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + ), + ), + ), + right: SizedBox( + width: 20, + height: 20, + child: _getIconForState(state), + ), + title: "Address book", + subTitle: state == StackRestoringStatus.failed + ? Text( + "Something went wrong", + style: STextStyles.errorSmall(context), + ) + : null, + ), + ); }, ), const SizedBox( @@ -340,40 +455,83 @@ class _StackRestoreProgressViewState builder: (_, ref, __) { final state = ref.watch(stackRestoringUIStateProvider .select((value) => value.nodes)); - return RestoringItemCard( - left: SizedBox( - width: 32, - height: 32, - child: RoundedContainer( - padding: const EdgeInsets.all(0), - color: Theme.of(context) - .extension<StackColors>()! - .buttonBackSecondary, - child: Center( - child: SvgPicture.asset( - Assets.svg.node, - width: 16, - height: 16, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, + return !isDesktop + ? RestoringItemCard( + left: SizedBox( + width: 32, + height: 32, + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + child: Center( + child: SvgPicture.asset( + Assets.svg.node, + width: 16, + height: 16, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + ), + ), ), - ), - ), - ), - right: SizedBox( - width: 20, - height: 20, - child: _getIconForState(state), - ), - title: "Nodes", - subTitle: state == StackRestoringStatus.failed - ? Text( - "Something went wrong", - style: STextStyles.errorSmall(context), - ) - : null, - ); + right: SizedBox( + width: 20, + height: 20, + child: _getIconForState(state), + ), + title: "Nodes", + subTitle: state == StackRestoringStatus.failed + ? Text( + "Something went wrong", + style: STextStyles.errorSmall(context), + ) + : null, + ) + : RoundedContainer( + padding: EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .popupBG, + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: RestoringItemCard( + left: SizedBox( + width: 32, + height: 32, + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + child: Center( + child: SvgPicture.asset( + Assets.svg.node, + width: 16, + height: 16, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + ), + ), + ), + right: SizedBox( + width: 20, + height: 20, + child: _getIconForState(state), + ), + title: "Nodes", + subTitle: state == StackRestoringStatus.failed + ? Text( + "Something went wrong", + style: STextStyles.errorSmall(context), + ) + : null, + )); }, ), const SizedBox( @@ -383,40 +541,86 @@ class _StackRestoreProgressViewState builder: (_, ref, __) { final state = ref.watch(stackRestoringUIStateProvider .select((value) => value.trades)); - return RestoringItemCard( - left: SizedBox( - width: 32, - height: 32, - child: RoundedContainer( - padding: const EdgeInsets.all(0), - color: Theme.of(context) - .extension<StackColors>()! - .buttonBackSecondary, - child: Center( - child: SvgPicture.asset( - Assets.svg.arrowRotate2, - width: 16, - height: 16, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, + return !isDesktop + ? Container( + child: RestoringItemCard( + left: SizedBox( + width: 32, + height: 32, + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + child: Center( + child: SvgPicture.asset( + Assets.svg.arrowRotate2, + width: 16, + height: 16, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + ), + ), + ), + right: SizedBox( + width: 20, + height: 20, + child: _getIconForState(state), + ), + title: "Exchange history", + subTitle: state == StackRestoringStatus.failed + ? Text( + "Something went wrong", + style: STextStyles.errorSmall(context), + ) + : null, ), - ), - ), - ), - right: SizedBox( - width: 20, - height: 20, - child: _getIconForState(state), - ), - title: "Exchange history", - subTitle: state == StackRestoringStatus.failed - ? Text( - "Something went wrong", - style: STextStyles.errorSmall(context), - ) - : null, - ); + ) + : RoundedContainer( + padding: EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .popupBG, + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: RestoringItemCard( + left: SizedBox( + width: 32, + height: 32, + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + child: Center( + child: SvgPicture.asset( + Assets.svg.arrowRotate2, + width: 16, + height: 16, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + ), + ), + ), + right: SizedBox( + width: 20, + height: 20, + child: _getIconForState(state), + ), + title: "Exchange history", + subTitle: state == StackRestoringStatus.failed + ? Text( + "Something went wrong", + style: STextStyles.errorSmall(context), + ) + : null, + ), + ); }, ), const SizedBox( @@ -446,28 +650,55 @@ class _StackRestoreProgressViewState ), SizedBox( width: MediaQuery.of(context).size.width - 32, - child: TextButton( - onPressed: () async { - if (_success) { - Navigator.of(context).pop(); - } else { - if (await _requestCancel()) { - await _cancel(); - } - } - }, - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - child: Text( - _success ? "OK" : "Cancel restore process", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .buttonTextPrimary, - ), - ), - ), + child: !isDesktop + ? TextButton( + onPressed: () async { + if (_success) { + Navigator.of(context).pop(); + } else { + if (await _requestCancel()) { + await _cancel(); + } + } + }, + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + child: Text( + _success ? "OK" : "Cancel restore process", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextPrimary, + ), + ), + ) + : Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + _success + ? PrimaryButton( + width: 248, + desktopMed: true, + enabled: true, + label: "Done", + onPressed: () async { + Navigator.of(context).pop(); + }, + ) + : SecondaryButton( + width: 248, + desktopMed: true, + enabled: true, + label: "Cancel restore process", + onPressed: () async { + if (await _requestCancel()) { + await _cancel(); + } + }, + ), + ], + ), ), ], ), diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_widgets/restoring_wallet_card.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_widgets/restoring_wallet_card.dart index 55f0588d2..2239eee71 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_widgets/restoring_wallet_card.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_widgets/restoring_wallet_card.dart @@ -11,6 +11,7 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/stack_restoring_status.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; @@ -68,140 +69,287 @@ class _RestoringWalletCardState extends ConsumerState<RestoringWalletCard> { final coin = ref.watch(provider.select((value) => value.coin)); final restoringStatus = ref.watch(provider.select((value) => value.restoringState)); - return RestoringItemCard( - left: SizedBox( - width: 32, - height: 32, - child: RoundedContainer( - padding: const EdgeInsets.all(0), - color: Theme.of(context).extension<StackColors>()!.colorForCoin(coin), - child: Center( - child: SvgPicture.asset( - Assets.svg.iconFor( - coin: coin, - ), - height: 20, - width: 20, - ), - ), - ), - ), - onRightTapped: restoringStatus == StackRestoringStatus.failed - ? () async { - final manager = ref.read(provider).manager!; - - ref.read(stackRestoringUIStateProvider).update( - walletId: manager.walletId, - restoringStatus: StackRestoringStatus.restoring); - - try { - final mnemonicList = await manager.mnemonic; - int maxUnusedAddressGap = 20; - if (coin == Coin.firo) { - maxUnusedAddressGap = 50; - } - const maxNumberOfIndexesToCheck = 1000; - - if (mnemonicList.isEmpty) { - await manager.recoverFromMnemonic( - mnemonic: ref.read(provider).mnemonic!, - maxUnusedAddressGap: maxUnusedAddressGap, - maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck, - height: ref.read(provider).height ?? 0, - ); - } else { - await manager.fullRescan( - maxUnusedAddressGap, - maxNumberOfIndexesToCheck, - ); - } - - if (mounted) { - final address = await manager.currentReceivingAddress; - - ref.read(stackRestoringUIStateProvider).update( - walletId: manager.walletId, - restoringStatus: StackRestoringStatus.success, - address: address, - ); - } - } catch (_) { - if (mounted) { - ref.read(stackRestoringUIStateProvider).update( - walletId: manager.walletId, - restoringStatus: StackRestoringStatus.failed, - ); - } - } - } - : null, - right: SizedBox( - width: 20, - height: 20, - child: _getIconForState( - ref.watch(provider.select((value) => value.restoringState)), - ), - ), - title: - "${ref.watch(provider.select((value) => value.walletName))} (${coin.ticker})", - subTitle: restoringStatus == StackRestoringStatus.failed - ? Text( - "Unable to restore. Tap icon to retry.", - style: STextStyles.errorSmall(context), - ) - : ref.watch(provider.select((value) => value.address)) != null - ? Text( - ref.watch(provider.select((value) => value.address))!, - style: STextStyles.infoSmall(context), - ) - : null, - button: restoringStatus == StackRestoringStatus.failed - ? Container( - height: 20, - decoration: BoxDecoration( + return !Util.isDesktop + ? RestoringItemCard( + left: SizedBox( + width: 32, + height: 32, + child: RoundedContainer( + padding: const EdgeInsets.all(0), color: Theme.of(context) .extension<StackColors>()! - .buttonBackSecondary, - borderRadius: BorderRadius.circular( - 1000, - ), - ), - child: RawMaterialButton( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - splashColor: - Theme.of(context).extension<StackColors>()!.highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - 1000, + .colorForCoin(coin), + child: Center( + child: SvgPicture.asset( + Assets.svg.iconFor( + coin: coin, + ), + height: 20, + width: 20, ), ), - onPressed: () async { - final mnemonic = ref.read(provider).mnemonic; + ), + ), + onRightTapped: restoringStatus == StackRestoringStatus.failed + ? () async { + final manager = ref.read(provider).manager!; - if (mnemonic != null) { - Navigator.of(context).push( - RouteGenerator.getRoute( - builder: (_) => RecoverPhraseView( - walletName: ref.read(provider).walletName, - mnemonic: mnemonic.split(" "), + ref.read(stackRestoringUIStateProvider).update( + walletId: manager.walletId, + restoringStatus: StackRestoringStatus.restoring); + + try { + final mnemonicList = await manager.mnemonic; + int maxUnusedAddressGap = 20; + if (coin == Coin.firo) { + maxUnusedAddressGap = 50; + } + const maxNumberOfIndexesToCheck = 1000; + + if (mnemonicList.isEmpty) { + await manager.recoverFromMnemonic( + mnemonic: ref.read(provider).mnemonic!, + maxUnusedAddressGap: maxUnusedAddressGap, + maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck, + height: ref.read(provider).height ?? 0, + ); + } else { + await manager.fullRescan( + maxUnusedAddressGap, + maxNumberOfIndexesToCheck, + ); + } + + if (mounted) { + final address = await manager.currentReceivingAddress; + + ref.read(stackRestoringUIStateProvider).update( + walletId: manager.walletId, + restoringStatus: StackRestoringStatus.success, + address: address, + ); + } + } catch (_) { + if (mounted) { + ref.read(stackRestoringUIStateProvider).update( + walletId: manager.walletId, + restoringStatus: StackRestoringStatus.failed, + ); + } + } + } + : null, + right: SizedBox( + width: 20, + height: 20, + child: _getIconForState( + ref.watch(provider.select((value) => value.restoringState)), + ), + ), + title: + "${ref.watch(provider.select((value) => value.walletName))} (${coin.ticker})", + subTitle: restoringStatus == StackRestoringStatus.failed + ? Text( + "Unable to restore. Tap icon to retry.", + style: STextStyles.errorSmall(context), + ) + : ref.watch(provider.select((value) => value.address)) != null + ? Text( + ref.watch(provider.select((value) => value.address))!, + style: STextStyles.infoSmall(context), + ) + : null, + button: restoringStatus == StackRestoringStatus.failed + ? Container( + height: 20, + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + borderRadius: BorderRadius.circular( + 1000, + ), + ), + child: RawMaterialButton( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + splashColor: + Theme.of(context).extension<StackColors>()!.highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 1000, ), ), - ); - } - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Text( - "Show recovery phrase", - style: STextStyles.infoSmall(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), + onPressed: () async { + final mnemonic = ref.read(provider).mnemonic; + + if (mnemonic != null) { + Navigator.of(context).push( + RouteGenerator.getRoute( + builder: (_) => RecoverPhraseView( + walletName: ref.read(provider).walletName, + mnemonic: mnemonic.split(" "), + ), + ), + ); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + "Show recovery phrase", + style: STextStyles.infoSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + ), + ) + : null, + ) + : RoundedContainer( + padding: EdgeInsets.all(0), + color: Theme.of(context).extension<StackColors>()!.popupBG, + borderColor: Theme.of(context).extension<StackColors>()!.background, + child: RestoringItemCard( + left: SizedBox( + width: 32, + height: 32, + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .colorForCoin(coin), + child: Center( + child: SvgPicture.asset( + Assets.svg.iconFor( + coin: coin, + ), + height: 20, + width: 20, + ), ), ), ), - ) - : null, - ); + onRightTapped: restoringStatus == StackRestoringStatus.failed + ? () async { + final manager = ref.read(provider).manager!; + + ref.read(stackRestoringUIStateProvider).update( + walletId: manager.walletId, + restoringStatus: StackRestoringStatus.restoring); + + try { + final mnemonicList = await manager.mnemonic; + int maxUnusedAddressGap = 20; + if (coin == Coin.firo) { + maxUnusedAddressGap = 50; + } + const maxNumberOfIndexesToCheck = 1000; + + if (mnemonicList.isEmpty) { + await manager.recoverFromMnemonic( + mnemonic: ref.read(provider).mnemonic!, + maxUnusedAddressGap: maxUnusedAddressGap, + maxNumberOfIndexesToCheck: + maxNumberOfIndexesToCheck, + height: ref.read(provider).height ?? 0, + ); + } else { + await manager.fullRescan( + maxUnusedAddressGap, + maxNumberOfIndexesToCheck, + ); + } + + if (mounted) { + final address = await manager.currentReceivingAddress; + + ref.read(stackRestoringUIStateProvider).update( + walletId: manager.walletId, + restoringStatus: StackRestoringStatus.success, + address: address, + ); + } + } catch (_) { + if (mounted) { + ref.read(stackRestoringUIStateProvider).update( + walletId: manager.walletId, + restoringStatus: StackRestoringStatus.failed, + ); + } + } + } + : null, + right: SizedBox( + width: 20, + height: 20, + child: _getIconForState( + ref.watch(provider.select((value) => value.restoringState)), + ), + ), + title: + "${ref.watch(provider.select((value) => value.walletName))} (${coin.ticker})", + subTitle: restoringStatus == StackRestoringStatus.failed + ? Text( + "Unable to restore. Tap icon to retry.", + style: STextStyles.errorSmall(context), + ) + : ref.watch(provider.select((value) => value.address)) != null + ? Text( + ref.watch(provider.select((value) => value.address))!, + style: STextStyles.infoSmall(context), + ) + : null, + button: restoringStatus == StackRestoringStatus.failed + ? Container( + height: 20, + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + borderRadius: BorderRadius.circular( + 1000, + ), + ), + child: RawMaterialButton( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + splashColor: Theme.of(context) + .extension<StackColors>()! + .highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 1000, + ), + ), + onPressed: () async { + final mnemonic = ref.read(provider).mnemonic; + + if (mnemonic != null) { + Navigator.of(context).push( + RouteGenerator.getRoute( + builder: (_) => RecoverPhraseView( + walletName: ref.read(provider).walletName, + mnemonic: mnemonic.split(" "), + ), + ), + ); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + "Show recovery phrase", + style: STextStyles.infoSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + ), + ) + : null, + ), + ); } } From de896d30bcfa87afb74a12bd6d2afe2c9aea141d Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Thu, 10 Nov 2022 22:14:08 -0700 Subject: [PATCH 211/426] desktop dialog for disable auto backup --- .../backup_and_restore_settings.dart | 135 +++++++++++++----- 1 file changed, 99 insertions(+), 36 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart index 3ea6cea6c..0df7c8975 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart @@ -14,7 +14,9 @@ import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.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/rounded_white_container.dart'; @@ -108,42 +110,103 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { useSafeArea: false, barrierDismissible: true, builder: (context) { - return StackDialog( - title: "Disable Auto Backup", - message: - "You are turning off Auto Backup. You can turn it back on at any time. Your previous Auto Backup file will not be deleted. Remember to backup your wallets manually so you don't lose important information.", - leftButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Back", - style: STextStyles.button(context).copyWith( - color: - Theme.of(context).extension<StackColors>()!.accentColorDark, - ), - ), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - rightButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - child: Text( - "Disable", - style: STextStyles.button(context), - ), - onPressed: () { - Navigator.of(context).pop(); - setState(() { - ref.watch(prefsChangeNotifierProvider).isAutoBackupEnabled = - false; - }); - }, - ), - ); + return !Util.isDesktop + ? StackDialog( + title: "Disable Auto Backup", + message: + "You are turning off Auto Backup. You can turn it back on at any time. Your previous Auto Backup file will not be deleted. Remember to backup your wallets manually so you don't lose important information.", + leftButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Back", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + rightButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + child: Text( + "Disable", + style: STextStyles.button(context), + ), + onPressed: () { + Navigator.of(context).pop(); + setState(() { + ref + .watch(prefsChangeNotifierProvider) + .isAutoBackupEnabled = false; + }); + }, + ), + ) + : DesktopDialog( + maxHeight: 270, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 20, horizontal: 32), + child: Column( + children: [ + Text( + "Disable Auto Backup", + style: STextStyles.desktopH3(context), + ), + const SizedBox(height: 24), + SizedBox( + width: 600, + child: Text( + "You are turning off Auto Backup. You can turn it back on at any time. " + "Your previous Auto Backup file will not be deleted. Remember to backup your wallets " + "manually so you don't lose important information.", + style: STextStyles.desktopTextSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + ), + ), + ), + const Spacer(), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SecondaryButton( + width: 248, + desktopMed: true, + enabled: true, + label: "Back", + onPressed: () { + Navigator.of(context).pop(); + }, + ), + const SizedBox(width: 20), + PrimaryButton( + width: 248, + desktopMed: true, + enabled: true, + label: "Disable", + onPressed: () { + Navigator.of(context).pop(); + setState(() { + ref + .watch(prefsChangeNotifierProvider) + .isAutoBackupEnabled = false; + }); + }, + ) + ], + ), + ], + ), + ), + ); }, ); if (mounted) { From 7a0ef9685188231eb8814c9e983e989b22cfc1f4 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Thu, 10 Nov 2022 22:14:47 -0700 Subject: [PATCH 212/426] desktop dialog for cancel stack restore --- .../dialogs/cancel_stack_restore_dialog.dart | 124 +++++++++++++----- 1 file changed, 93 insertions(+), 31 deletions(-) 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 16e51a35d..a9f4e134d 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 @@ -1,6 +1,11 @@ import 'package:flutter/material.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.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/rounded_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; class CancelStackRestoreDialog extends StatelessWidget { @@ -14,38 +19,95 @@ class CancelStackRestoreDialog extends StatelessWidget { onWillPop: () async { return false; }, - child: StackDialog( - title: "Cancel restore process", - message: - "Cancelling will revert any changes that may have been applied", - leftButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Back", - style: STextStyles.itemSubtitle12(context), - ), - onPressed: () { - Navigator.of(context).pop(false); - }, - ), - rightButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - child: Text( - "Yes, cancel", - style: STextStyles.itemSubtitle12(context).copyWith( - color: - Theme.of(context).extension<StackColors>()!.buttonTextPrimary, + child: !Util.isDesktop + ? StackDialog( + title: "Cancel restore process", + message: + "Cancelling will revert any changes that may have been applied", + leftButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Back", + style: STextStyles.itemSubtitle12(context), + ), + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + rightButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + child: Text( + "Yes, cancel", + style: STextStyles.itemSubtitle12(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextPrimary, + ), + ), + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ) + : DesktopDialog( + maxHeight: 250, + maxWidth: 600, + child: Padding( + padding: const EdgeInsets.only( + top: 20, left: 32, right: 32, bottom: 20), + child: Column( + children: [ + Text( + "Cancel Restore Process", + style: STextStyles.desktopH3(context), + ), + const SizedBox(height: 24), + SizedBox( + width: 500, + child: RoundedContainer( + color: Theme.of(context) + .extension<StackColors>()! + .snackBarBackError, + child: Text( + "If you cancel, the restore will not complete, and " + "the wallets will not appear in your Stack.", + style: STextStyles.desktopTextMedium(context), + ), + ), + ), + const Spacer(), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SecondaryButton( + width: 248, + desktopMed: true, + enabled: true, + label: "Keep restoring", + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + const SizedBox(width: 20), + PrimaryButton( + width: 248, + desktopMed: true, + enabled: true, + label: "Cancel anyway", + onPressed: () { + Navigator.of(context).pop(true); + }, + ) + ], + ), + ], + ), + ), ), - ), - onPressed: () { - Navigator.of(context).pop(true); - }, - ), - ), ); } } From 676b26ce3793d4abc697052ac781b15c5ddb8dfb Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 11 Nov 2022 09:30:13 -0600 Subject: [PATCH 213/426] stop logging annoying monero sync non error --- lib/services/coins/monero/monero_wallet.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/services/coins/monero/monero_wallet.dart b/lib/services/coins/monero/monero_wallet.dart index 91bb3c4db..cf0335197 100644 --- a/lib/services/coins/monero/monero_wallet.dart +++ b/lib/services/coins/monero/monero_wallet.dart @@ -196,7 +196,7 @@ class MoneroWallet extends CoinServiceAPI { try { syncingHeight = (walletBase!.syncStatus as SyncingSyncStatus).height; } catch (e, s) { - Logging.instance.log("$e $s", level: LogLevel.Warning); + // Logging.instance.log("$e $s", level: LogLevel.Warning); } final cachedHeight = DB.instance.get<dynamic>(boxName: walletId, key: "storedSyncingHeight") From ca8f63c07a28ae77cb02a15c4427fbd066e8f2f2 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 11 Nov 2022 09:31:01 -0600 Subject: [PATCH 214/426] ensure loadShared is only called once --- lib/main.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/main.dart b/lib/main.dart index a04170bd9..df9d683f5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -227,9 +227,15 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme> late final Completer<void> loadingCompleter; bool didLoad = false; + bool didLoadShared = false; bool _desktopHasPassword = false; Future<void> loadShared() async { + if (didLoadShared) { + return; + } + didLoadShared = true; + await DB.instance.init(); await ref.read(prefsChangeNotifierProvider).init(); From b6613b2fd7eaeda927374ec3be4d34853dc375d4 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 11 Nov 2022 09:32:55 -0600 Subject: [PATCH 215/426] stop logging monero sync non-error --- lib/services/coins/monero/monero_wallet.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/services/coins/monero/monero_wallet.dart b/lib/services/coins/monero/monero_wallet.dart index cf0335197..d6c238df0 100644 --- a/lib/services/coins/monero/monero_wallet.dart +++ b/lib/services/coins/monero/monero_wallet.dart @@ -154,7 +154,7 @@ class MoneroWallet extends CoinServiceAPI { try { _height = (walletBase!.syncStatus as SyncingSyncStatus).height; } catch (e, s) { - Logging.instance.log("$e $s", level: LogLevel.Warning); + // Logging.instance.log("$e $s", level: LogLevel.Warning); } int blocksRemaining = -1; @@ -163,7 +163,7 @@ class MoneroWallet extends CoinServiceAPI { blocksRemaining = (walletBase!.syncStatus as SyncingSyncStatus).blocksLeft; } catch (e, s) { - Logging.instance.log("$e $s", level: LogLevel.Warning); + // Logging.instance.log("$e $s", level: LogLevel.Warning); } int currentHeight = _height + blocksRemaining; if (_height == -1 || blocksRemaining == -1) { @@ -419,7 +419,7 @@ class MoneroWallet extends CoinServiceAPI { try { progress = (walletBase!.syncStatus!).progress(); } catch (e, s) { - Logging.instance.log("$e $s", level: LogLevel.Warning); + // Logging.instance.log("$e $s", level: LogLevel.Warning); } await _fetchTransactionData(); From ba853837ce3f7b3e1877d8da6a53f4bbbd01749b Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 11 Nov 2022 10:45:50 -0600 Subject: [PATCH 216/426] verify passphrase functionality added to password service --- lib/utilities/desktop_password_service.dart | 27 +++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/lib/utilities/desktop_password_service.dart b/lib/utilities/desktop_password_service.dart index 24299b855..f20525873 100644 --- a/lib/utilities/desktop_password_service.dart +++ b/lib/utilities/desktop_password_service.dart @@ -88,6 +88,33 @@ class DPS { } } + Future<bool> verifyPassphrase(String passphrase) async { + final box = await Hive.openBox<String>(DB.boxNameDesktopData); + final keyBlob = DB.instance.get<String>( + boxName: DB.boxNameDesktopData, + key: _kKeyBlobKey, + ); + await box.close(); + + if (keyBlob == null) { + // no passphrase key blob found so any passphrase is technically bad + return false; + } + + try { + await StorageCryptoHandler.fromExisting(passphrase, keyBlob); + // existing passphrase matches key blob + return true; + } catch (e, s) { + Logging.instance.log( + "${_getMessageFromException(e)}\n$s", + level: LogLevel.Warning, + ); + // password is wrong or some other error + return false; + } + } + Future<bool> hasPassword() async { final keyBlob = DB.instance.get<String>( boxName: DB.boxNameDesktopData, From 7646cbe8cb28e6902c65fe181e94ea849033b872 Mon Sep 17 00:00:00 2001 From: Dan Miller <dan@cypherstack.com> Date: Fri, 11 Nov 2022 08:54:38 -0800 Subject: [PATCH 217/426] Handle some bash errors in build script. --- scripts/linux/build_secure_storage_deps.sh | 34 +++++++++++++++------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/scripts/linux/build_secure_storage_deps.sh b/scripts/linux/build_secure_storage_deps.sh index 5ed032b1a..378f7a604 100755 --- a/scripts/linux/build_secure_storage_deps.sh +++ b/scripts/linux/build_secure_storage_deps.sh @@ -1,25 +1,37 @@ #!/bin/bash LINUX_DIRECTORY=$(pwd) -mkdir build +mkdir -p build # Build JsonCPP -cd build -git clone https://github.com/open-source-parsers/jsoncpp.git -cd jsoncpp +cd build || exit +if ! [ -x "$(command -v git)" ]; then + echo 'Error: git is not installed.' >&2 + exit 1 +fi +git -C jsoncpp pull || git clone https://github.com/open-source-parsers/jsoncpp.git jsoncpp +cd jsoncpp || exit git checkout 1.7.4 -mkdir build -cd build +mkdir -p build +cd build || exit cmake -DCMAKE_BUILD_TYPE=release -DBUILD_STATIC_LIBS=ON -DBUILD_SHARED_LIBS=ON -DARCHIVE_INSTALL_DIR=. -G "Unix Makefiles" .. -make -j$(nproc) +make -j"$(nproc)" -cd $LINUX_DIRECTORY +cd "$LINUX_DIRECTORY" || exit # Build libSecret # sudo apt install meson libgirepository1.0-dev valac xsltproc gi-docgen docbook-xsl # sudo apt install python3-pip #pip3 install --user meson --upgrade # pip3 install --user gi-docgen -cd build -git clone https://gitlab.gnome.org/GNOME/libsecret.git -cd libsecret +cd build || exit +git -C libsecret pull || git clone https://gitlab.gnome.org/GNOME/libsecret.git libsecret +cd libsecret || exit +if ! [ -x "$(command -v meson)" ]; then + echo 'Error: meson is not installed.' >&2 + exit 1 +fi meson _build +if ! [ -x "$(command -v ninja)" ]; then + echo 'Error: ninja is not installed.' >&2 + exit 1 +fi ninja -C _build From cd1d6a5bc7af6f9103b9ddd2627a152ade2e45c1 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Fri, 11 Nov 2022 10:35:12 -0700 Subject: [PATCH 218/426] v1.5.17 build 89 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 8b03cd57e..b7947a58d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.5.16+88 +version: 1.5.17+89 environment: sdk: ">=2.17.0 <3.0.0" From 9b09f65f4d44f3b0d8c1829eb5640c8e4eec33d4 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 11 Nov 2022 12:12:01 -0600 Subject: [PATCH 219/426] remove flutter secure storage explicit instantiations from wow/xmr --- lib/main.dart | 1 + lib/services/coins/monero/monero_wallet.dart | 20 ++++++++----------- .../coins/wownero/wownero_wallet.dart | 20 +++++++------------ 3 files changed, 16 insertions(+), 25 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index df9d683f5..70f80e8ed 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -154,6 +154,7 @@ void main() async { await Hive.openBox<dynamic>(DB.boxNameDBInfo); + // todo: db migrate stuff for desktop needs to be handled eventually if (!Util.isDesktop) { int dbVersion = DB.instance.get<dynamic>( boxName: DB.boxNameDBInfo, key: "hive_data_version") as int? ?? diff --git a/lib/services/coins/monero/monero_wallet.dart b/lib/services/coins/monero/monero_wallet.dart index d6c238df0..9e9ffc783 100644 --- a/lib/services/coins/monero/monero_wallet.dart +++ b/lib/services/coins/monero/monero_wallet.dart @@ -23,7 +23,6 @@ import 'package:flutter_libmonero/core/key_service.dart'; import 'package:flutter_libmonero/core/wallet_creation_service.dart'; import 'package:flutter_libmonero/monero/monero.dart'; import 'package:flutter_libmonero/view_model/send/output.dart' as monero_output; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; import 'package:mutex/mutex.dart'; import 'package:path_provider/path_provider.dart'; @@ -670,11 +669,10 @@ class MoneroWallet extends CoinServiceAPI { "Attempted to overwrite mnemonic on generate new wallet!"); } - storage = const FlutterSecureStorage(); walletService = monero.createMoneroWalletService(DB.instance.moneroWalletInfoBox); prefs = await SharedPreferences.getInstance(); - keysStorage = KeyService(storage!); + keysStorage = KeyService(_secureStore); WalletInfo walletInfo; WalletCredentials credentials; try { @@ -708,7 +706,7 @@ class MoneroWallet extends CoinServiceAPI { credentials.walletInfo = walletInfo; _walletCreationService = WalletCreationService( - secureStorage: storage, + secureStorage: _secureStore, sharedPreferences: prefs, walletService: walletService, keyService: keysStorage, @@ -787,11 +785,11 @@ class MoneroWallet extends CoinServiceAPI { // Logging.instance.log("Caught in initializeWallet(): $e\n$s"); // return false; // } - storage = const FlutterSecureStorage(); + walletService = monero.createMoneroWalletService(DB.instance.moneroWalletInfoBox); prefs = await SharedPreferences.getInstance(); - keysStorage = KeyService(storage!); + keysStorage = KeyService(_secureStore); await _generateNewWallet(); // var password; @@ -833,11 +831,10 @@ class MoneroWallet extends CoinServiceAPI { "Attempted to initialize an existing wallet using an unknown wallet ID!"); } - storage = const FlutterSecureStorage(); walletService = monero.createMoneroWalletService(DB.instance.moneroWalletInfoBox); prefs = await SharedPreferences.getInstance(); - keysStorage = KeyService(storage!); + keysStorage = KeyService(_secureStore); await _prefs.init(); final data = @@ -889,7 +886,7 @@ class MoneroWallet extends CoinServiceAPI { bool longMutex = false; // TODO: are these needed? - FlutterSecureStorage? storage; + WalletService? walletService; SharedPreferences? prefs; KeyService? keysStorage; @@ -970,11 +967,10 @@ class MoneroWallet extends CoinServiceAPI { await DB.instance .put<dynamic>(boxName: walletId, key: "restoreHeight", value: height); - storage = const FlutterSecureStorage(); walletService = monero.createMoneroWalletService(DB.instance.moneroWalletInfoBox); prefs = await SharedPreferences.getInstance(); - keysStorage = KeyService(storage!); + keysStorage = KeyService(_secureStore); WalletInfo walletInfo; WalletCredentials credentials; String name = _walletId; @@ -1001,7 +997,7 @@ class MoneroWallet extends CoinServiceAPI { credentials.walletInfo = walletInfo; _walletCreationService = WalletCreationService( - secureStorage: storage, + secureStorage: _secureStore, sharedPreferences: prefs, walletService: walletService, keyService: keysStorage, diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart index c872c7d7e..39d3038ee 100644 --- a/lib/services/coins/wownero/wownero_wallet.dart +++ b/lib/services/coins/wownero/wownero_wallet.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:io'; -import 'package:cw_core/get_height_by_date.dart'; import 'package:cw_core/monero_transaction_priority.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/pending_transaction.dart'; @@ -25,7 +24,6 @@ import 'package:flutter_libmonero/core/wallet_creation_service.dart'; import 'package:flutter_libmonero/view_model/send/output.dart' as wownero_output; import 'package:flutter_libmonero/wownero/wownero.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; import 'package:mutex/mutex.dart'; import 'package:path_provider/path_provider.dart'; @@ -672,12 +670,11 @@ class WowneroWallet extends CoinServiceAPI { "Attempted to overwrite mnemonic on generate new wallet!"); } - storage = const FlutterSecureStorage(); // TODO: Wallet Service may need to be switched to Wownero walletService = wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); prefs = await SharedPreferences.getInstance(); - keysStorage = KeyService(storage!); + keysStorage = KeyService(_secureStore); WalletInfo walletInfo; WalletCredentials credentials; try { @@ -702,7 +699,7 @@ class WowneroWallet extends CoinServiceAPI { credentials.walletInfo = walletInfo; _walletCreationService = WalletCreationService( - secureStorage: storage, + secureStorage: _secureStore, sharedPreferences: prefs, walletService: walletService, keyService: keysStorage, @@ -793,11 +790,10 @@ class WowneroWallet extends CoinServiceAPI { // Logging.instance.log("Caught in initializeWallet(): $e\n$s"); // return false; // } - storage = const FlutterSecureStorage(); walletService = wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); prefs = await SharedPreferences.getInstance(); - keysStorage = KeyService(storage!); + keysStorage = KeyService(_secureStore); await _generateNewWallet(seedWordsLength: seedWordsLength); // var password; @@ -839,11 +835,10 @@ class WowneroWallet extends CoinServiceAPI { "Attempted to initialize an existing wallet using an unknown wallet ID!"); } - storage = const FlutterSecureStorage(); walletService = wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); prefs = await SharedPreferences.getInstance(); - keysStorage = KeyService(storage!); + keysStorage = KeyService(_secureStore); await _prefs.init(); final data = @@ -895,7 +890,7 @@ class WowneroWallet extends CoinServiceAPI { bool longMutex = false; // TODO: are these needed? - FlutterSecureStorage? storage; + WalletService? walletService; SharedPreferences? prefs; KeyService? keysStorage; @@ -993,11 +988,10 @@ class WowneroWallet extends CoinServiceAPI { await DB.instance .put<dynamic>(boxName: walletId, key: "restoreHeight", value: height); - storage = const FlutterSecureStorage(); walletService = wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); prefs = await SharedPreferences.getInstance(); - keysStorage = KeyService(storage!); + keysStorage = KeyService(_secureStore); WalletInfo walletInfo; WalletCredentials credentials; String name = _walletId; @@ -1024,7 +1018,7 @@ class WowneroWallet extends CoinServiceAPI { credentials.walletInfo = walletInfo; _walletCreationService = WalletCreationService( - secureStorage: storage, + secureStorage: _secureStore, sharedPreferences: prefs, walletService: walletService, keyService: keysStorage, From f7bf624028462d4ec4958e1edb05331f12910b65 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 11 Nov 2022 12:37:44 -0600 Subject: [PATCH 220/426] initialize store on successful password creation --- .../create_password/create_password_view.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/pages_desktop_specific/create_password/create_password_view.dart b/lib/pages_desktop_specific/create_password/create_password_view.dart index d5ce42679..c29fc3de6 100644 --- a/lib/pages_desktop_specific/create_password/create_password_view.dart +++ b/lib/pages_desktop_specific/create_password/create_password_view.dart @@ -6,9 +6,11 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; import 'package:stackwallet/providers/desktop/storage_crypto_handler_provider.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.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'; @@ -78,6 +80,7 @@ class _CreatePasswordViewState extends ConsumerState<CreatePasswordView> { } await ref.read(storageCryptoHandlerProvider).initFromNew(passphrase); + await (ref.read(secureStoreProvider).store as DesktopSecureStore).init(); } catch (e) { unawaited(showFloatingFlushBar( type: FlushBarType.warning, From 6ce899cd27348cb682e721295b00f2bc1f5c5dfa Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Fri, 11 Nov 2022 12:27:39 -0700 Subject: [PATCH 221/426] textfields for desktop change password --- .../home/settings_menu/security_settings.dart | 373 ++++++++++++++++-- 1 file changed, 333 insertions(+), 40 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/security_settings.dart b/lib/pages_desktop_specific/home/settings_menu/security_settings.dart index cdcaed49a..40928a0af 100644 --- a/lib/pages_desktop_specific/home/settings_menu/security_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/security_settings.dart @@ -2,11 +2,14 @@ import 'package:flutter/material.dart'; import 'package:flutter/src/widgets/framework.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart'; import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:zxcvbn/zxcvbn.dart'; class SecuritySettings extends ConsumerStatefulWidget { const SecuritySettings({Key? key}) : super(key: key); @@ -18,21 +21,55 @@ class SecuritySettings extends ConsumerStatefulWidget { } class _SecuritySettings extends ConsumerState<SecuritySettings> { - Future<void> enableAutoBackup() async { - // wait for keyboard to disappear - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration(milliseconds: 100), - ); + late bool changePassword = false; - await showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return EnableBackupDialog(); - }, - ); + late final TextEditingController passwordCurrentController; + late final TextEditingController passwordController; + late final TextEditingController passwordRepeatController; + + late final FocusNode passwordCurrentFocusNode; + late final FocusNode passwordFocusNode; + late final FocusNode passwordRepeatFocusNode; + final zxcvbn = Zxcvbn(); + + bool hidePassword = true; + bool shouldShowPasswordHint = true; + + double passwordStrength = 0.0; + + bool get shouldEnableSave { + return passwordCurrentController.text.isNotEmpty && + passwordController.text.isNotEmpty && + passwordRepeatController.text.isNotEmpty; + } + + String passwordFeedback = + "Add another word or two. Uncommon words are better. Use a few words, avoid common phrases. No need for symbols, digits, or uppercase letters."; + + @override + void initState() { + passwordCurrentController = TextEditingController(); + passwordController = TextEditingController(); + passwordRepeatController = TextEditingController(); + + passwordCurrentFocusNode = FocusNode(); + passwordFocusNode = FocusNode(); + passwordRepeatFocusNode = FocusNode(); + + super.initState(); + } + + @override + void dispose() { + passwordCurrentController.dispose(); + passwordController.dispose(); + passwordRepeatController.dispose(); + + passwordCurrentFocusNode.dispose(); + passwordFocusNode.dispose(); + passwordRepeatFocusNode.dispose(); + + super.dispose(); } @override @@ -78,12 +115,290 @@ class _SecuritySettings extends ConsumerState<SecuritySettings> { ), Column( crossAxisAlignment: CrossAxisAlignment.start, - children: const [ + children: [ Padding( padding: EdgeInsets.all( 10, ), - child: NewPasswordButton(), + child: changePassword + ? SizedBox( + width: 512, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Current password", + style: + STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + textAlign: TextAlign.left, + ), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key( + "desktopSecurityRestoreFromFilePasswordFieldKey"), + focusNode: passwordFocusNode, + controller: passwordController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Enter current password", + passwordFocusNode, + context, + ).copyWith( + labelStyle: + STextStyles.fieldLabel(context), + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox( + width: 16, + ), + GestureDetector( + key: const Key( + "desktopSecurityRestoreFromFilePasswordFieldShowPasswordButtonKey"), + onTap: () async { + setState(() { + hidePassword = + !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 16, + height: 16, + ), + ), + const SizedBox( + width: 12, + ), + ], + ), + ), + ), + onChanged: (newValue) {}, + ), + ), + const SizedBox(height: 16), + Text( + "New password", + style: + STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + textAlign: TextAlign.left, + ), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key( + "desktopSecurityCreateNewPasswordFieldKey1"), + focusNode: passwordCurrentFocusNode, + controller: passwordCurrentController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Enter new password", + passwordCurrentFocusNode, + context, + ).copyWith( + labelStyle: + STextStyles.fieldLabel(context), + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox( + width: 16, + ), + GestureDetector( + key: const Key( + "desktopSecurityCreateNewPasswordButtonKey1"), + onTap: () async { + setState(() { + hidePassword = + !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 16, + height: 16, + ), + ), + const SizedBox( + width: 12, + ), + ], + ), + ), + ), + onChanged: (newValue) { + if (newValue.isEmpty) { + setState(() { + passwordFeedback = ""; + }); + return; + } + final result = + zxcvbn.evaluate(newValue); + String suggestionsAndTips = ""; + for (var sug in result + .feedback.suggestions! + .toSet()) { + suggestionsAndTips += "$sug\n"; + } + suggestionsAndTips += + result.feedback.warning!; + String feedback = + // "Password Strength: ${((result.score! / 4.0) * 100).toInt()}%\n" + suggestionsAndTips; + + passwordStrength = result.score! / 4; + + // hack fix to format back string returned from zxcvbn + if (feedback + .contains("phrasesNo need")) { + feedback = feedback.replaceFirst( + "phrasesNo need", + "phrases\nNo need"); + } + + if (feedback.endsWith("\n")) { + feedback = feedback.substring( + 0, feedback.length - 2); + } + + setState(() { + passwordFeedback = feedback; + }); + }, + ), + ), + const SizedBox(height: 16), + Text( + "Confirm new password", + style: + STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + textAlign: TextAlign.left, + ), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key( + "desktopSecurityCreateNewPasswordFieldKey2"), + focusNode: passwordRepeatFocusNode, + controller: passwordRepeatController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Confirm new password", + passwordRepeatFocusNode, + context, + ).copyWith( + labelStyle: + STextStyles.fieldLabel(context), + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox( + width: 16, + ), + GestureDetector( + key: const Key( + "desktopSecurityCreateNewPasswordButtonKey2"), + onTap: () async { + setState(() { + hidePassword = + !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 16, + height: 16, + ), + ), + const SizedBox( + width: 12, + ), + ], + ), + ), + ), + onChanged: (newValue) { + setState(() {}); + }, + ), + ), + const SizedBox(height: 20), + PrimaryButton( + width: 142, + desktopMed: true, + enabled: shouldEnableSave, + label: "Save changes", + onPressed: () { + setState(() { + changePassword = false; + }); + }, + ) + ], + ), + ) + : PrimaryButton( + width: 192, + desktopMed: true, + enabled: true, + label: "Set up new password", + onPressed: () { + setState(() { + changePassword = true; + }); + }, + ), ), ], ), @@ -110,29 +425,7 @@ class NewPasswordButton extends ConsumerWidget { style: Theme.of(context) .extension<StackColors>()! .getPrimaryEnabledButtonColor(context), - onPressed: () { - // Expandable( - // header: Row( - // mainAxisAlignment: MainAxisAlignment.start, - // children: [ - // NewPasswordButton(), - // ], - // ), - // body: Column( - // mainAxisAlignment: MainAxisAlignment.start, - // children: [ - // Text( - // "Current Password", - // style: STextStyles.desktopTextExtraSmall(context).copyWith( - // color: - // Theme.of(context).extension<StackColors>()!.textDark3, - // ), - // textAlign: TextAlign.left, - // ), - // ], - // ), - // ); - }, + onPressed: () {}, child: Text( "Set up new password", style: STextStyles.button(context), From d084fac0571e7ac464bf322213c2a0f53e3c303c Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Fri, 11 Nov 2022 12:42:04 -0700 Subject: [PATCH 222/426] password progressbar fix --- .../home/settings_menu/security_settings.dart | 64 +++++++++++++++++-- 1 file changed, 58 insertions(+), 6 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/security_settings.dart b/lib/pages_desktop_specific/home/settings_menu/security_settings.dart index 40928a0af..9ee9b5bfc 100644 --- a/lib/pages_desktop_specific/home/settings_menu/security_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/security_settings.dart @@ -7,6 +7,7 @@ import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/progress_bar.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:zxcvbn/zxcvbn.dart'; @@ -145,15 +146,15 @@ class _SecuritySettings extends ConsumerState<SecuritySettings> { child: TextField( key: const Key( "desktopSecurityRestoreFromFilePasswordFieldKey"), - focusNode: passwordFocusNode, - controller: passwordController, + focusNode: passwordCurrentFocusNode, + controller: passwordCurrentController, style: STextStyles.field(context), obscureText: hidePassword, enableSuggestions: false, autocorrect: false, decoration: standardInputDecoration( "Enter current password", - passwordFocusNode, + passwordCurrentFocusNode, context, ).copyWith( labelStyle: @@ -214,15 +215,15 @@ class _SecuritySettings extends ConsumerState<SecuritySettings> { child: TextField( key: const Key( "desktopSecurityCreateNewPasswordFieldKey1"), - focusNode: passwordCurrentFocusNode, - controller: passwordCurrentController, + focusNode: passwordFocusNode, + controller: passwordController, style: STextStyles.field(context), obscureText: hidePassword, enableSuggestions: false, autocorrect: false, decoration: standardInputDecoration( "Enter new password", - passwordCurrentFocusNode, + passwordFocusNode, context, ).copyWith( labelStyle: @@ -302,6 +303,57 @@ class _SecuritySettings extends ConsumerState<SecuritySettings> { }, ), ), + if (passwordFocusNode.hasFocus || + passwordRepeatFocusNode.hasFocus || + passwordController.text.isNotEmpty) + Padding( + padding: EdgeInsets.only( + left: 12, + right: 12, + top: + passwordFeedback.isNotEmpty ? 4 : 0, + ), + child: passwordFeedback.isNotEmpty + ? Text( + passwordFeedback, + style: STextStyles.infoSmall( + context), + ) + : null, + ), + if (passwordFocusNode.hasFocus || + passwordRepeatFocusNode.hasFocus || + passwordController.text.isNotEmpty) + Padding( + padding: const EdgeInsets.only( + left: 12, + right: 12, + top: 10, + ), + child: ProgressBar( + key: const Key( + "desktopSecurityCreateStackBackUpProgressBar"), + width: 450, + height: 5, + fillColor: passwordStrength < 0.51 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorRed + : passwordStrength < 1 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorYellow + : Theme.of(context) + .extension<StackColors>()! + .accentColorGreen, + backgroundColor: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + percent: passwordStrength < 0.25 + ? 0.03 + : passwordStrength, + ), + ), const SizedBox(height: 16), Text( "Confirm new password", From 7798ed39a06912425fc6c97bfbb6a6d021f1ed04 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Fri, 11 Nov 2022 15:58:47 -0700 Subject: [PATCH 223/426] desktop address book with no contacts --- assets/svg/plus-circle.svg | 12 +++ .../desktop_address_book.dart | 84 ++++++++++++++++++- lib/utilities/assets.dart | 1 + pubspec.yaml | 1 + 4 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 assets/svg/plus-circle.svg diff --git a/assets/svg/plus-circle.svg b/assets/svg/plus-circle.svg new file mode 100644 index 000000000..e673b9b0e --- /dev/null +++ b/assets/svg/plus-circle.svg @@ -0,0 +1,12 @@ +<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0_5370_82626)"> +<path d="M9.99935 18.3337C14.6017 18.3337 18.3327 14.6027 18.3327 10.0003C18.3327 5.39795 14.6017 1.66699 9.99935 1.66699C5.39698 1.66699 1.66602 5.39795 1.66602 10.0003C1.66602 14.6027 5.39698 18.3337 9.99935 18.3337Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M10 6.66699V13.3337" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M6.66602 10H13.3327" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +</g> +<defs> +<clipPath id="clip0_5370_82626"> +<rect width="20" height="20" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart index 3622fcf1e..dd38b98a8 100644 --- a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart +++ b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart @@ -1,12 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/models/contact.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; @@ -24,6 +27,11 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { late final FocusNode _searchFocusNode; + List<Contact>? _cache; + List<Contact>? _cacheFav; + + late bool hasContacts = false; + String filter = ""; @override @@ -49,6 +57,7 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { return Column( mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ DesktopAppBar( isCompactHeight: true, @@ -127,12 +136,81 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { ), ), ), + const SizedBox(width: 20), + TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getDesktopMenuButtonColorSelected(context), + onPressed: () {}, + child: SizedBox( + width: 200, + height: 56, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: SvgPicture.asset(Assets.svg.filter), + ), + Text( + "Filter", + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + ], + ), + ), + ), + const SizedBox(width: 20), + TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + onPressed: () {}, + child: SizedBox( + width: 200, + height: 56, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: SvgPicture.asset(Assets.svg.circlePlus), + ), + Text( + "Add new", + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .popupBG, + ), + ), + ], + ), + ), + ), ], ), ), - // Expanded( - // child: hasWallets ? const MyWallets() : const EmptyWallets(), - // ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 26), + child: SizedBox( + width: 489, + child: RoundedWhiteContainer( + child: Center( + child: Text( + "Your contacts will appear here", + style: STextStyles.itemSubtitle(context), + ), + ), + ), + ), + ), ], ); } diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index f853a00d8..e76a17c12 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -59,6 +59,7 @@ class _SVG { String txExchangeFailed(BuildContext context) => "assets/svg/${Theme.of(context).extension<StackColors>()!.themeType.name}/tx-exchange-icon-failed.svg"; + String get circlePlus => "assets/svg/plus-circle.svg"; String get framedGear => "assets/svg/framed-gear.svg"; String get framedAddressBook => "assets/svg/framed-address-book.svg"; String get themeLight => "assets/svg/light/light-mode.svg"; diff --git a/pubspec.yaml b/pubspec.yaml index 8b03cd57e..fd1c8a9b5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -314,6 +314,7 @@ flutter: - assets/svg/exit-desktop.svg - assets/svg/keys.svg - assets/svg/arrow-down.svg + - assets/svg/plus-circle.svg # coin icons - assets/svg/coin_icons/Bitcoin.svg - assets/svg/coin_icons/Litecoin.svg From f08a52cd0748a2a085724b21d4f5c3aa926c27d3 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Sat, 12 Nov 2022 09:16:07 -0600 Subject: [PATCH 224/426] remove direct dependency of unused SharedPreferences --- lib/services/coins/monero/monero_wallet.dart | 8 --- .../coins/wownero/wownero_wallet.dart | 8 --- pubspec.lock | 2 +- pubspec.yaml | 2 +- .../coins/monero/monero_wallet_test.dart | 53 ++++++++++--------- .../coins/wownero/wownero_wallet_test.dart | 24 ++++----- 6 files changed, 42 insertions(+), 55 deletions(-) diff --git a/lib/services/coins/monero/monero_wallet.dart b/lib/services/coins/monero/monero_wallet.dart index fd8f9a79e..caf1185a5 100644 --- a/lib/services/coins/monero/monero_wallet.dart +++ b/lib/services/coins/monero/monero_wallet.dart @@ -26,7 +26,6 @@ import 'package:flutter_libmonero/view_model/send/output.dart' as monero_output; import 'package:http/http.dart'; import 'package:mutex/mutex.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; @@ -671,7 +670,6 @@ class MoneroWallet extends CoinServiceAPI { walletService = monero.createMoneroWalletService(DB.instance.moneroWalletInfoBox); - prefs = await SharedPreferences.getInstance(); keysStorage = KeyService(_secureStore); WalletInfo walletInfo; WalletCredentials credentials; @@ -707,7 +705,6 @@ class MoneroWallet extends CoinServiceAPI { _walletCreationService = WalletCreationService( secureStorage: _secureStore, - sharedPreferences: prefs, walletService: walletService, keyService: keysStorage, ); @@ -788,7 +785,6 @@ class MoneroWallet extends CoinServiceAPI { walletService = monero.createMoneroWalletService(DB.instance.moneroWalletInfoBox); - prefs = await SharedPreferences.getInstance(); keysStorage = KeyService(_secureStore); await _generateNewWallet(); @@ -833,7 +829,6 @@ class MoneroWallet extends CoinServiceAPI { walletService = monero.createMoneroWalletService(DB.instance.moneroWalletInfoBox); - prefs = await SharedPreferences.getInstance(); keysStorage = KeyService(_secureStore); await _prefs.init(); @@ -888,7 +883,6 @@ class MoneroWallet extends CoinServiceAPI { // TODO: are these needed? WalletService? walletService; - SharedPreferences? prefs; KeyService? keysStorage; MoneroWalletBase? walletBase; WalletCreationService? _walletCreationService; @@ -969,7 +963,6 @@ class MoneroWallet extends CoinServiceAPI { walletService = monero.createMoneroWalletService(DB.instance.moneroWalletInfoBox); - prefs = await SharedPreferences.getInstance(); keysStorage = KeyService(_secureStore); WalletInfo walletInfo; WalletCredentials credentials; @@ -998,7 +991,6 @@ class MoneroWallet extends CoinServiceAPI { _walletCreationService = WalletCreationService( secureStorage: _secureStore, - sharedPreferences: prefs, walletService: walletService, keyService: keysStorage, ); diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart index d03733edb..8f36352d0 100644 --- a/lib/services/coins/wownero/wownero_wallet.dart +++ b/lib/services/coins/wownero/wownero_wallet.dart @@ -27,7 +27,6 @@ import 'package:flutter_libmonero/wownero/wownero.dart'; import 'package:http/http.dart'; import 'package:mutex/mutex.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; @@ -673,7 +672,6 @@ class WowneroWallet extends CoinServiceAPI { // TODO: Wallet Service may need to be switched to Wownero walletService = wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); - prefs = await SharedPreferences.getInstance(); keysStorage = KeyService(_secureStore); WalletInfo walletInfo; WalletCredentials credentials; @@ -700,7 +698,6 @@ class WowneroWallet extends CoinServiceAPI { _walletCreationService = WalletCreationService( secureStorage: _secureStore, - sharedPreferences: prefs, walletService: walletService, keyService: keysStorage, ); @@ -792,7 +789,6 @@ class WowneroWallet extends CoinServiceAPI { // } walletService = wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); - prefs = await SharedPreferences.getInstance(); keysStorage = KeyService(_secureStore); await _generateNewWallet(seedWordsLength: seedWordsLength); @@ -837,7 +833,6 @@ class WowneroWallet extends CoinServiceAPI { walletService = wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); - prefs = await SharedPreferences.getInstance(); keysStorage = KeyService(_secureStore); await _prefs.init(); @@ -892,7 +887,6 @@ class WowneroWallet extends CoinServiceAPI { // TODO: are these needed? WalletService? walletService; - SharedPreferences? prefs; KeyService? keysStorage; WowneroWalletBase? walletBase; WalletCreationService? _walletCreationService; @@ -990,7 +984,6 @@ class WowneroWallet extends CoinServiceAPI { walletService = wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); - prefs = await SharedPreferences.getInstance(); keysStorage = KeyService(_secureStore); WalletInfo walletInfo; WalletCredentials credentials; @@ -1019,7 +1012,6 @@ class WowneroWallet extends CoinServiceAPI { _walletCreationService = WalletCreationService( secureStorage: _secureStore, - sharedPreferences: prefs, walletService: walletService, keyService: keysStorage, ); diff --git a/pubspec.lock b/pubspec.lock index 2f7770d3a..0b8f49c2c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1244,7 +1244,7 @@ packages: source: hosted version: "3.0.1" shared_preferences: - dependency: "direct main" + dependency: transitive description: name: shared_preferences url: "https://pub.dartlang.org" diff --git a/pubspec.yaml b/pubspec.yaml index b7947a58d..8a41a4edd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -126,7 +126,7 @@ dependencies: pointycastle: ^3.6.0 package_info_plus: ^1.4.2 lottie: ^1.3.0 - shared_preferences: ^2.0.15 +# shared_preferences: ^2.0.15 file_picker: ^5.0.1 connectivity_plus: 2.3.6+1 # document_file_save_plus: ^1.0.5 diff --git a/test/services/coins/monero/monero_wallet_test.dart b/test/services/coins/monero/monero_wallet_test.dart index 79bf98a41..789d91cb1 100644 --- a/test/services/coins/monero/monero_wallet_test.dart +++ b/test/services/coins/monero/monero_wallet_test.dart @@ -19,7 +19,6 @@ import 'package:hive/hive.dart'; import 'package:hive_test/hive_test.dart'; import 'package:mockito/annotations.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:stackwallet/services/wallets.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; @@ -27,10 +26,8 @@ import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'monero_wallet_test_data.dart'; -//FlutterSecureStorage? storage; FakeSecureStorage? storage; WalletService? walletService; -SharedPreferences? prefs; KeyService? keysStorage; MoneroWalletBase? walletBase; late WalletCreationService _walletCreationService; @@ -46,7 +43,6 @@ WalletType type = WalletType.monero; @GenerateMocks([]) void main() async { storage = FakeSecureStorage(); - prefs = await SharedPreferences.getInstance(); keysStorage = KeyService(storage!); WalletInfo walletInfo = WalletInfo.external( id: '', @@ -90,12 +86,12 @@ void main() async { final dirPath = await pathForWalletDir(name: name, type: type); path = await pathForWallet(name: name, type: type); credentials = - // // creating a new wallet - // monero.createMoneroNewWalletCredentials( - // name: name, language: "English"); - // restoring a previous wallet - monero.createMoneroRestoreWalletFromSeedCredentials( - name: name, height: 2580000, mnemonic: testMnemonic); + // // creating a new wallet + // monero.createMoneroNewWalletCredentials( + // name: name, language: "English"); + // restoring a previous wallet + monero.createMoneroRestoreWalletFromSeedCredentials( + name: name, height: 2580000, mnemonic: testMnemonic); walletInfo = WalletInfo.external( id: WalletBase.idFor(name, type), @@ -111,7 +107,6 @@ void main() async { _walletCreationService = WalletCreationService( secureStorage: storage, - sharedPreferences: prefs, walletService: walletService, keyService: keysStorage, ); @@ -124,8 +119,8 @@ void main() async { test("Test mainnet address generation from seed", () async { final wallet = await - // _walletCreationService.create(credentials); - _walletCreationService.restoreFromSeed(credentials); + // _walletCreationService.create(credentials); + _walletCreationService.restoreFromSeed(credentials); walletInfo.address = wallet.walletAddresses.address; //print(walletInfo.address); @@ -134,8 +129,7 @@ void main() async { walletBase = wallet as MoneroWalletBase; //print("${walletBase?.seed}"); - expect( - await walletBase!.validateAddress(walletInfo.address ?? ''), true); + expect(await walletBase!.validateAddress(walletInfo.address ?? ''), true); // print(walletBase); // loggerPrint(walletBase.toString()); @@ -157,20 +151,31 @@ void main() async { expect( await walletBase!.getTransactionAddress(1, 2), mainnetTestData[1][2]); + expect(await walletBase!.validateAddress(''), false); expect( - await walletBase!.validateAddress(''), false); + await walletBase!.validateAddress( + '4AeRgkWZsMJhAWKMeCZ3h4ZSPnAcW5VBtRFyLd6gBEf6GgJU2FHXDA6i1DnQTd6h8R3VU5AkbGcWSNhtSwNNPgaD48gp4nn'), + true); expect( - await walletBase!.validateAddress('4AeRgkWZsMJhAWKMeCZ3h4ZSPnAcW5VBtRFyLd6gBEf6GgJU2FHXDA6i1DnQTd6h8R3VU5AkbGcWSNhtSwNNPgaD48gp4nn'), true); + await walletBase!.validateAddress( + '4asdfkWZsMJhAWKMeCZ3h4ZSPnAcW5VBtRFyLd6gBEf6GgJU2FHXDA6i1DnQTd6h8R3VU5AkbGcWSNhtSwNNPgaD48gpjkl'), + false); expect( - await walletBase!.validateAddress('4asdfkWZsMJhAWKMeCZ3h4ZSPnAcW5VBtRFyLd6gBEf6GgJU2FHXDA6i1DnQTd6h8R3VU5AkbGcWSNhtSwNNPgaD48gpjkl'), false); + await walletBase!.validateAddress( + '8AeRgkWZsMJhAWKMeCZ3h4ZSPnAcW5VBtRFyLd6gBEf6GgJU2FHXDA6i1DnQTd6h8R3VU5AkbGcWSNhtSwNNPgaD48gp4nn'), + false); expect( - await walletBase!.validateAddress('8AeRgkWZsMJhAWKMeCZ3h4ZSPnAcW5VBtRFyLd6gBEf6GgJU2FHXDA6i1DnQTd6h8R3VU5AkbGcWSNhtSwNNPgaD48gp4nn'), false); + await walletBase!.validateAddress( + '84kYPuZ1eaVKGQhf26QPNWbSLQG16BywXdLYYShVrPNMLAUAWce5vcpRc78FxwRphrG6Cda7faCKdUMr8fUCH3peHPenvHy'), + true); expect( - await walletBase!.validateAddress('84kYPuZ1eaVKGQhf26QPNWbSLQG16BywXdLYYShVrPNMLAUAWce5vcpRc78FxwRphrG6Cda7faCKdUMr8fUCH3peHPenvHy'), true); + await walletBase!.validateAddress( + '8asdfuZ1eaVKGQhf26QPNWbSLQG16BywXdLYYShVrPNMLAUAWce5vcpRc78FxwRphrG6Cda7faCKdUMr8fUCH3peHPenjkl'), + false); expect( - await walletBase!.validateAddress('8asdfuZ1eaVKGQhf26QPNWbSLQG16BywXdLYYShVrPNMLAUAWce5vcpRc78FxwRphrG6Cda7faCKdUMr8fUCH3peHPenjkl'), false); - expect( - await walletBase!.validateAddress('44kYPuZ1eaVKGQhf26QPNWbSLQG16BywXdLYYShVrPNMLAUAWce5vcpRc78FxwRphrG6Cda7faCKdUMr8fUCH3peHPenvHy'), false); + await walletBase!.validateAddress( + '44kYPuZ1eaVKGQhf26QPNWbSLQG16BywXdLYYShVrPNMLAUAWce5vcpRc78FxwRphrG6Cda7faCKdUMr8fUCH3peHPenvHy'), + false); }); }); /* @@ -229,6 +234,6 @@ Future<String> pathForWalletDir( } Future<String> pathForWallet( - {required String name, required WalletType type}) async => + {required String name, required WalletType type}) async => await pathForWalletDir(name: name, type: type) .then((path) => path + '/$name'); diff --git a/test/services/coins/wownero/wownero_wallet_test.dart b/test/services/coins/wownero/wownero_wallet_test.dart index 78a9c56c1..8ffb590fc 100644 --- a/test/services/coins/wownero/wownero_wallet_test.dart +++ b/test/services/coins/wownero/wownero_wallet_test.dart @@ -19,14 +19,12 @@ import 'package:hive/hive.dart'; import 'package:hive_test/hive_test.dart'; import 'package:mockito/annotations.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'wownero_wallet_test_data.dart'; FakeSecureStorage? storage; WalletService? walletService; -SharedPreferences? prefs; KeyService? keysStorage; WowneroWalletBase? walletBase; late WalletCreationService _walletCreationService; @@ -41,7 +39,6 @@ WalletType type = WalletType.wownero; @GenerateMocks([]) void main() async { storage = FakeSecureStorage(); - prefs = await SharedPreferences.getInstance(); keysStorage = KeyService(storage!); WalletInfo walletInfo = WalletInfo.external( id: '', @@ -102,7 +99,6 @@ void main() async { _walletCreationService = WalletCreationService( secureStorage: storage, - sharedPreferences: prefs, walletService: walletService, keyService: keysStorage, ); @@ -127,19 +123,24 @@ void main() async { walletBase = wallet as WowneroWalletBase; expect( - await walletBase!.validateAddress(wallet.walletAddresses.address ?? ''), true); + await walletBase! + .validateAddress(wallet.walletAddresses.address ?? ''), + true); } catch (_) { hasThrown = true; } expect(hasThrown, false); // Address validation + expect(await walletBase!.validateAddress(''), false); expect( - await walletBase!.validateAddress(''), false); + await walletBase!.validateAddress( + 'Wo3jmHvTMLwE6h29fpgcb8PbJSpaKuqM7XTXVfiiu8bLCZsJvrQCbQSJR48Vo3BWNQKsMsXZ4VixndXTH25QtorC27NCjmsEi'), + true); expect( - await walletBase!.validateAddress('Wo3jmHvTMLwE6h29fpgcb8PbJSpaKuqM7XTXVfiiu8bLCZsJvrQCbQSJR48Vo3BWNQKsMsXZ4VixndXTH25QtorC27NCjmsEi'), true); - expect( - await walletBase!.validateAddress('WasdfHvTMLwE6h29fpgcb8PbJSpaKuqM7XTXVfiiu8bLCZsJvrQCbQSJR48Vo3BWNQKsMsXZ4VixndXTH25QtorC27NCjmjkl'), false); + await walletBase!.validateAddress( + 'WasdfHvTMLwE6h29fpgcb8PbJSpaKuqM7XTXVfiiu8bLCZsJvrQCbQSJR48Vo3BWNQKsMsXZ4VixndXTH25QtorC27NCjmjkl'), + false); walletBase?.close(); walletBase = wallet as WowneroWalletBase; @@ -174,7 +175,6 @@ void main() async { _walletCreationService = WalletCreationService( secureStorage: storage, - sharedPreferences: prefs, walletService: walletService, keyService: keysStorage, ); @@ -248,7 +248,6 @@ void main() async { _walletCreationService = WalletCreationService( secureStorage: storage, - sharedPreferences: prefs, walletService: walletService, keyService: keysStorage, ); @@ -312,7 +311,6 @@ void main() async { _walletCreationService = WalletCreationService( secureStorage: storage, - sharedPreferences: prefs, walletService: walletService, keyService: keysStorage, ); @@ -367,6 +365,6 @@ Future<String> pathForWalletDir( } Future<String> pathForWallet( - {required String name, required WalletType type}) async => + {required String name, required WalletType type}) async => await pathForWalletDir(name: name, type: type) .then((path) => path + '/$name'); From 5d8a1b030450064a92b3486f3ab00dc2e95ae710 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Sat, 12 Nov 2022 09:17:16 -0600 Subject: [PATCH 225/426] trim unused import --- test/services/coins/monero/monero_wallet_test.dart | 3 --- test/services/coins/wownero/wownero_wallet_test.dart | 1 - 2 files changed, 4 deletions(-) diff --git a/test/services/coins/monero/monero_wallet_test.dart b/test/services/coins/monero/monero_wallet_test.dart index 789d91cb1..d6d600e36 100644 --- a/test/services/coins/monero/monero_wallet_test.dart +++ b/test/services/coins/monero/monero_wallet_test.dart @@ -1,5 +1,4 @@ import 'dart:core'; -import 'dart:core' as core; import 'dart:io'; import 'dart:math'; @@ -22,8 +21,6 @@ import 'package:path_provider/path_provider.dart'; import 'package:stackwallet/services/wallets.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; -// TODO trim down to the minimum imports above - import 'monero_wallet_test_data.dart'; FakeSecureStorage? storage; diff --git a/test/services/coins/wownero/wownero_wallet_test.dart b/test/services/coins/wownero/wownero_wallet_test.dart index 8ffb590fc..637a40b81 100644 --- a/test/services/coins/wownero/wownero_wallet_test.dart +++ b/test/services/coins/wownero/wownero_wallet_test.dart @@ -1,5 +1,4 @@ import 'dart:core'; -import 'dart:core' as core; import 'dart:io'; import 'dart:math'; From ca6578d367b7574bc0b2fd486e7d1afec32b8940 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Sat, 12 Nov 2022 11:10:46 -0700 Subject: [PATCH 226/426] desktop address book filter dialog --- .../subviews/address_book_filter_view.dart | 356 ++++++++++-------- .../desktop_address_book.dart | 21 +- 2 files changed, 229 insertions(+), 148 deletions(-) diff --git a/lib/pages/address_book_views/subviews/address_book_filter_view.dart b/lib/pages/address_book_views/subviews/address_book_filter_view.dart index 35968621a..df779331e 100644 --- a/lib/pages/address_book_views/subviews/address_book_filter_view.dart +++ b/lib/pages/address_book_views/subviews/address_book_filter_view.dart @@ -5,7 +5,12 @@ import 'package:stackwallet/providers/ui/address_book_providers/address_book_fil 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/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.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_white_container.dart'; class AddressBookFilterView extends ConsumerStatefulWidget { @@ -41,167 +46,224 @@ class _AddressBookFilterViewState extends ConsumerState<AddressBookFilterView> { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - leading: AppBarBackButton( - onPressed: () async { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Filter addresses", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(12), - child: LayoutBuilder(builder: (builderContext, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, + final isDesktop = Util.isDesktop; + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Filter addresses", + style: STextStyles.navBarTitle(context), + ), + ), + body: Padding( + padding: const EdgeInsets.all(12), + child: LayoutBuilder(builder: (builderContext, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + child: Text( + "Only selected cryptocurrency addresses will be displayed.", + style: STextStyles.itemSubtitle(context), + ), + ), + const SizedBox( + height: 12, + ), + Text( + "Select cryptocurrency", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 12, + ), + child, + ], + ), + ), + ), + ), + ); + }), + ), + ); + }, + child: ConditionalParent( + condition: isDesktop, + builder: (child) { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(32), + child: Text( + "Select cryptocurrency", + style: STextStyles.desktopH3(context), + textAlign: TextAlign.center, + ), + ), + const DesktopDialogCloseButton(), + ], ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - child: Text( - "Only selected cryptocurrency addresses will be displayed.", - style: STextStyles.itemSubtitle(context), + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + scrollDirection: Axis.vertical, + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + children: [ + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 32), + child: child, + ), + ], + ), ), ), - const SizedBox( - height: 12, - ), - Text( - "Select cryptocurrency", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: Wrap( - children: [ - ..._coins.map( - (coin) => Row( - children: [ - GestureDetector( - onTap: () { - if (ref - .read(addressBookFilterProvider) - .coins - .contains(coin)) { - ref - .read(addressBookFilterProvider) - .remove(coin, true); - } else { + ); + }, + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 32, vertical: 32), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SecondaryButton( + width: 248, + desktopMed: true, + enabled: true, + label: "Cancel", + onPressed: () { + Navigator.of(context).pop(); + }, + ), + // const SizedBox(width: 16), + PrimaryButton( + width: 248, + desktopMed: true, + enabled: true, + label: "Apply", + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ), + ), + ], + ); + }, + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: Wrap( + children: [ + ..._coins.map( + (coin) => Row( + children: [ + GestureDetector( + onTap: () { + if (ref + .read(addressBookFilterProvider) + .coins + .contains(coin)) { + ref + .read(addressBookFilterProvider) + .remove(coin, true); + } else { + ref.read(addressBookFilterProvider).add(coin, true); + } + setState(() {}); + }, + child: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 20, + width: 20, + child: Checkbox( + value: ref + .watch(addressBookFilterProvider + .select((value) => value.coins)) + .contains(coin), + onChanged: (value) { + if (value is bool) { + if (value) { ref .read(addressBookFilterProvider) .add(coin, true); + } else { + ref + .read(addressBookFilterProvider) + .remove(coin, true); } setState(() {}); - }, - child: Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - SizedBox( - height: 20, - width: 20, - child: Checkbox( - value: ref - .watch( - addressBookFilterProvider - .select((value) => - value.coins)) - .contains(coin), - onChanged: (value) { - if (value is bool) { - if (value) { - ref - .read( - addressBookFilterProvider) - .add(coin, true); - } else { - ref - .read( - addressBookFilterProvider) - .remove(coin, true); - } - setState(() {}); - } - }, - ), - ), - const SizedBox( - width: 12, - ), - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - coin.prettyName, - style: - STextStyles.largeMedium14( - context), - ), - const SizedBox( - height: 2, - ), - Text( - coin.ticker, - style: - STextStyles.itemSubtitle( - context), - ), - ], - ) - ], - ), - ), - ), + } + }, + ), + ), + const SizedBox( + width: 12, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + coin.prettyName, + style: STextStyles.largeMedium14(context), + ), + const SizedBox( + height: 2, + ), + Text( + coin.ticker, + style: STextStyles.itemSubtitle(context), ), ], - ), - ), - ], + ) + ], + ), ), ), - const Spacer(), - // Row( - // children: [ - // TextButton( - // onPressed: () {}, - // child: Text("Cancel"), - // ), - // SizedBox( - // width: 16, - // ), - // TextButton( - // onPressed: () {}, - // child: Text("Cancel"), - // ), - // ], - // ) - ], - ), + ), + ], ), ), - ), - ); - }), + ], + ), + ), ), ); } diff --git a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart index dd38b98a8..367671a3e 100644 --- a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart +++ b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart @@ -2,12 +2,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/models/contact.dart'; +import 'package:stackwallet/pages/address_book_views/subviews/address_book_filter_view.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; @@ -34,6 +36,21 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { String filter = ""; + Future<void> selectCryptocurrency() async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return DesktopDialog( + maxHeight: 609, + maxWidth: 576, + child: AddressBookFilterView(), + ); + }, + ); + } + @override void initState() { _searchController = TextEditingController(); @@ -141,7 +158,9 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { style: Theme.of(context) .extension<StackColors>()! .getDesktopMenuButtonColorSelected(context), - onPressed: () {}, + onPressed: () { + selectCryptocurrency(); + }, child: SizedBox( width: 200, height: 56, From 0164679cceac10602ceda50a8f93f8004fc076b1 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Sat, 12 Nov 2022 16:04:16 -0600 Subject: [PATCH 227/426] File system path clean up --- lib/main.dart | 24 ++----- .../advanced_views/debug_view.dart | 23 +++---- .../create_auto_backup_view.dart | 6 +- .../create_backup_view.dart | 6 +- .../edit_auto_backup_view.dart | 6 +- ..._file_system.dart => swb_file_system.dart} | 5 +- .../restore_from_file_view.dart | 9 ++- .../create_auto_backup.dart | 7 +- .../coins/epiccash/epiccash_wallet.dart | 27 ++++---- lib/services/coins/monero/monero_wallet.dart | 12 +--- .../coins/wownero/wownero_wallet.dart | 11 +--- .../flutter_secure_storage_interface.dart | 10 +-- lib/utilities/stack_file_system.dart | 66 +++++++++++++++++++ 13 files changed, 119 insertions(+), 93 deletions(-) rename lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/{stack_file_system.dart => swb_file_system.dart} (95%) create mode 100644 lib/utilities/stack_file_system.dart diff --git a/lib/main.dart b/lib/main.dart index 70f80e8ed..21abd9df7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -53,6 +53,7 @@ import 'package:stackwallet/utilities/db_version_migration.dart'; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/stack_file_system.dart'; import 'package:stackwallet/utilities/theme/color_theme.dart'; import 'package:stackwallet/utilities/theme/dark_colors.dart'; import 'package:stackwallet/utilities/theme/light_colors.dart'; @@ -79,29 +80,11 @@ void main() async { setWindowMaxSize(Size.infinite); } - Directory appDirectory = (await getApplicationDocumentsDirectory()); - if (Platform.isIOS) { - appDirectory = (await getLibraryDirectory()); - } - - if (Logging.isArmLinux) { - appDirectory = Directory("${appDirectory.path}/.stackwallet"); - await appDirectory.create(); - } - - if (Platform.isLinux) { - appDirectory = Directory("${Platform.environment['HOME']}/.stackwallet"); - - if (!appDirectory.existsSync()) { - await appDirectory.create(); - } - } - // FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); if (!(Logging.isArmLinux || Logging.isTestEnv)) { final isar = await Isar.open( [LogSchema], - directory: appDirectory.path, + directory: (await StackFileSystem.applicationIsarDirectory()).path, inspector: false, ); await Logging.instance.init(isar); @@ -150,7 +133,8 @@ void main() async { Hive.registerAdapter(WalletTypeAdapter()); Hive.registerAdapter(UnspentCoinsInfoAdapter()); - await Hive.initFlutter(appDirectory.path); + await Hive.initFlutter( + (await StackFileSystem.applicationHiveDirectory()).path); await Hive.openBox<dynamic>(DB.boxNameDBInfo); diff --git a/lib/pages/settings_views/global_settings_view/advanced_views/debug_view.dart b/lib/pages/settings_views/global_settings_view/advanced_views/debug_view.dart index a3aa925a0..055773ef6 100644 --- a/lib/pages/settings_views/global_settings_view/advanced_views/debug_view.dart +++ b/lib/pages/settings_views/global_settings_view/advanced_views/debug_view.dart @@ -7,19 +7,25 @@ import 'package:event_bus/event_bus.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_libepiccash/git_versions.dart' as EPIC_VERSIONS; +import 'package:flutter_libmonero/git_versions.dart' as MONERO_VERSIONS; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:lelantus/git_versions.dart' as FIRO_VERSIONS; import 'package:package_info_plus/package_info_plus.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:stackwallet/models/isar/models/log.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart'; import 'package:stackwallet/providers/global/debug_service_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/stack_file_system.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/custom_loading_overlay.dart'; @@ -28,15 +34,6 @@ import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; -import 'package:flutter_libepiccash/git_versions.dart' as EPIC_VERSIONS; -import 'package:flutter_libmonero/git_versions.dart' as MONERO_VERSIONS; -import 'package:lelantus/git_versions.dart' as FIRO_VERSIONS; - -import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/stack_file_system.dart'; - -import 'package:stackwallet/utilities/clipboard_interface.dart'; - -import 'package:stackwallet/utilities/util.dart'; class DebugView extends ConsumerStatefulWidget { const DebugView({Key? key}) : super(key: key); @@ -352,10 +349,10 @@ class _DebugViewState extends ConsumerState<DebugView> { BlueTextButton( text: "Save logs to file", onTap: () async { - final systemfile = StackFileSystem(); + final systemfile = SWBFileSystem(); await systemfile.prepareStorage(); - Directory rootPath = - (await getApplicationDocumentsDirectory()); + Directory rootPath = await StackFileSystem + .applicationRootDirectory(); if (Platform.isAndroid) { rootPath = Directory("/storage/emulated/0/"); diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart index 334d50e35..bf8bd40e7 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart @@ -9,7 +9,7 @@ import 'package:stack_wallet_backup/stack_wallet_backup.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart'; -import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/stack_file_system.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/sub_views/backup_frequency_type_select_sheet.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/providers/global/secure_store_provider.dart'; @@ -49,7 +49,7 @@ class _EnableAutoBackupViewState extends ConsumerState<CreateAutoBackupView> { late final FocusNode passwordFocusNode; late final FocusNode passwordRepeatFocusNode; - late final StackFileSystem stackFileSystem; + late final SWBFileSystem stackFileSystem; final zxcvbn = Zxcvbn(); String passwordFeedback = @@ -70,7 +70,7 @@ class _EnableAutoBackupViewState extends ConsumerState<CreateAutoBackupView> { @override void initState() { secureStore = ref.read(secureStoreProvider); - stackFileSystem = StackFileSystem(); + stackFileSystem = SWBFileSystem(); fileLocationController = TextEditingController(); passwordController = TextEditingController(); passwordRepeatController = TextEditingController(); diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart index fc4719fe1..b7ee6b4be 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart @@ -7,7 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart'; -import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/stack_file_system.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart'; import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -41,7 +41,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { late final FocusNode passwordFocusNode; late final FocusNode passwordRepeatFocusNode; - late final StackFileSystem stackFileSystem; + late final SWBFileSystem stackFileSystem; final zxcvbn = Zxcvbn(); String passwordFeedback = @@ -61,7 +61,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { @override void initState() { - stackFileSystem = StackFileSystem(); + stackFileSystem = SWBFileSystem(); fileLocationController = TextEditingController(); passwordController = TextEditingController(); passwordRepeatController = TextEditingController(); diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart index 0be718549..4d3c6ca99 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart @@ -9,7 +9,7 @@ import 'package:stack_wallet_backup/stack_wallet_backup.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart'; -import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/stack_file_system.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/sub_views/backup_frequency_type_select_sheet.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/providers/global/secure_store_provider.dart'; @@ -48,7 +48,7 @@ class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> { late final FocusNode passwordFocusNode; late final FocusNode passwordRepeatFocusNode; - late final StackFileSystem stackFileSystem; + late final SWBFileSystem stackFileSystem; final zxcvbn = Zxcvbn(); String passwordFeedback = @@ -69,7 +69,7 @@ class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> { @override void initState() { secureStore = ref.read(secureStoreProvider); - stackFileSystem = StackFileSystem(); + stackFileSystem = SWBFileSystem(); fileLocationController = TextEditingController(); passwordController = TextEditingController(); passwordRepeatController = TextEditingController(); diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/stack_file_system.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart similarity index 95% rename from lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/stack_file_system.dart rename to lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart index e57c5493f..82d3fd97d 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/stack_file_system.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart @@ -4,15 +4,16 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:stackwallet/utilities/util.dart'; -class StackFileSystem { +class SWBFileSystem { Directory? rootPath; Directory? startPath; String? filePath; String? dirPath; - final bool isDesktop = !(Platform.isAndroid || Platform.isIOS); + final bool isDesktop = Util.isDesktop; Future<Directory> prepareStorage() async { if (Platform.isAndroid) { 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 e2d16db54..c5ccfa6b3 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 @@ -7,7 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart'; -import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/stack_file_system.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/sub_views/stack_restore_progress_view.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -20,14 +20,13 @@ import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.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/loading_indicator.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:tuple/tuple.dart'; -import '../../../../widgets/desktop/desktop_dialog_close_button.dart'; - class RestoreFromFileView extends ConsumerStatefulWidget { const RestoreFromFileView({Key? key}) : super(key: key); @@ -44,13 +43,13 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { late final FocusNode passwordFocusNode; - late final StackFileSystem stackFileSystem; + late final SWBFileSystem stackFileSystem; bool hidePassword = true; @override void initState() { - stackFileSystem = StackFileSystem(); + stackFileSystem = SWBFileSystem(); fileLocationController = TextEditingController(); passwordController = TextEditingController(); diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart index cccb3b0b7..02d33fb95 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart @@ -9,14 +9,13 @@ import 'package:flutter_svg/svg.dart'; import 'package:stack_wallet_backup/stack_wallet_backup.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart'; -import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/stack_file_system.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; -import 'package:stackwallet/utilities/enums/log_level_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; @@ -48,7 +47,7 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> { late final SecureStorageInterface secureStore; - late final StackFileSystem stackFileSystem; + late final SWBFileSystem stackFileSystem; late final FocusNode passphraseFocusNode; late final FocusNode passphraseRepeatFocusNode; final zxcvbn = Zxcvbn(); @@ -81,7 +80,7 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> { @override void initState() { secureStore = ref.read(secureStoreProvider); - stackFileSystem = StackFileSystem(); + stackFileSystem = SWBFileSystem(); fileLocationController = TextEditingController(); passphraseController = TextEditingController(); diff --git a/lib/services/coins/epiccash/epiccash_wallet.dart b/lib/services/coins/epiccash/epiccash_wallet.dart index 683e26544..1a5b2961f 100644 --- a/lib/services/coins/epiccash/epiccash_wallet.dart +++ b/lib/services/coins/epiccash/epiccash_wallet.dart @@ -9,7 +9,6 @@ import 'package:flutter_libepiccash/epic_cash.dart'; import 'package:hive/hive.dart'; import 'package:http/http.dart'; import 'package:mutex/mutex.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:stack_wallet_backup/generate_password.dart'; import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/models/node_model.dart'; @@ -31,6 +30,7 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; +import 'package:stackwallet/utilities/stack_file_system.dart'; import 'package:stackwallet/utilities/test_epic_box_connection.dart'; import 'package:tuple/tuple.dart'; @@ -253,14 +253,16 @@ Future<String> deleteEpicWallet({ required SecureStorageInterface secureStore, }) async { String? config = await secureStore.read(key: '${walletId}_config'); + // TODO: why double check for iOS? if (Platform.isIOS) { - Directory appDir = (await getApplicationDocumentsDirectory()); - if (Platform.isIOS) { - appDir = (await getLibraryDirectory()); - } - if (Platform.isLinux) { - appDir = Directory("${appDir.path}/.stackwallet"); - } + Directory appDir = await StackFileSystem.applicationRootDirectory(); + // todo why double check for ios? + // if (Platform.isIOS) { + // appDir = (await getLibraryDirectory()); + // } + // if (Platform.isLinux) { + // appDir = Directory("${appDir.path}/.stackwallet"); + // } final path = "${appDir.path}/epiccash"; final String name = walletId; @@ -1237,13 +1239,8 @@ class EpicCashWallet extends CoinServiceAPI { } Future<String> currentWalletDirPath() async { - Directory appDir = (await getApplicationDocumentsDirectory()); - if (Platform.isIOS) { - appDir = (await getLibraryDirectory()); - } - if (Platform.isLinux) { - appDir = Directory("${appDir.path}/.stackwallet"); - } + Directory appDir = await StackFileSystem.applicationRootDirectory(); + final path = "${appDir.path}/epiccash"; final String name = _walletId.trim(); return '$path/$name'; diff --git a/lib/services/coins/monero/monero_wallet.dart b/lib/services/coins/monero/monero_wallet.dart index caf1185a5..c35323d53 100644 --- a/lib/services/coins/monero/monero_wallet.dart +++ b/lib/services/coins/monero/monero_wallet.dart @@ -25,7 +25,6 @@ import 'package:flutter_libmonero/monero/monero.dart'; import 'package:flutter_libmonero/view_model/send/output.dart' as monero_output; import 'package:http/http.dart'; import 'package:mutex/mutex.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; @@ -47,6 +46,7 @@ import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; +import 'package:stackwallet/utilities/stack_file_system.dart'; const int MINIMUM_CONFIRMATIONS = 10; @@ -897,14 +897,8 @@ class MoneroWallet extends CoinServiceAPI { required String name, required WalletType type, }) async { - Directory root = (await getApplicationDocumentsDirectory()); - if (Platform.isIOS) { - root = (await getLibraryDirectory()); - } - // - if (Platform.isLinux) { - root = Directory("${root.path}/.stackwallet"); - } + Directory root = await StackFileSystem.applicationRootDirectory(); + final prefix = walletTypeToString(type).toLowerCase(); final walletsDir = Directory('${root.path}/wallets'); final walletDire = Directory('${walletsDir.path}/$prefix/$name'); diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart index 8f36352d0..e39d13005 100644 --- a/lib/services/coins/wownero/wownero_wallet.dart +++ b/lib/services/coins/wownero/wownero_wallet.dart @@ -26,7 +26,6 @@ import 'package:flutter_libmonero/view_model/send/output.dart' import 'package:flutter_libmonero/wownero/wownero.dart'; import 'package:http/http.dart'; import 'package:mutex/mutex.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; @@ -48,6 +47,7 @@ import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; +import 'package:stackwallet/utilities/stack_file_system.dart'; const int MINIMUM_CONFIRMATIONS = 10; @@ -901,13 +901,8 @@ class WowneroWallet extends CoinServiceAPI { required String name, required WalletType type, }) async { - Directory root = (await getApplicationDocumentsDirectory()); - if (Platform.isIOS) { - root = (await getLibraryDirectory()); - } - if (Platform.isLinux) { - root = Directory("${root.path}/.stackwallet"); - } + Directory root = await StackFileSystem.applicationRootDirectory(); + final prefix = walletTypeToString(type).toLowerCase(); final walletsDir = Directory('${root.path}/wallets'); final walletDire = Directory('${walletsDir.path}/$prefix/$name'); diff --git a/lib/utilities/flutter_secure_storage_interface.dart b/lib/utilities/flutter_secure_storage_interface.dart index 9e8aef95c..2d9b19050 100644 --- a/lib/utilities/flutter_secure_storage_interface.dart +++ b/lib/utilities/flutter_secure_storage_interface.dart @@ -1,9 +1,8 @@ -import 'dart:io'; - import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:isar/isar.dart'; import 'package:stack_wallet_backup/secure_storage.dart'; import 'package:stackwallet/models/isar/models/encrypted_string_value.dart'; +import 'package:stackwallet/utilities/stack_file_system.dart'; abstract class SecureStorageInterface { dynamic get store; @@ -47,14 +46,9 @@ class DesktopSecureStore { DesktopSecureStore(this.handler); Future<void> init() async { - Directory? appDirectory; - if (Platform.isLinux) { - appDirectory = Directory("${Platform.environment['HOME']}/.stackwallet"); - await appDirectory.create(); - } isar = await Isar.open( [EncryptedStringValueSchema], - directory: appDirectory!.path, + directory: (await StackFileSystem.applicationIsarDirectory()).path, inspector: false, name: "desktopStore", ); diff --git a/lib/utilities/stack_file_system.dart b/lib/utilities/stack_file_system.dart new file mode 100644 index 000000000..5177f1973 --- /dev/null +++ b/lib/utilities/stack_file_system.dart @@ -0,0 +1,66 @@ +import 'dart:io'; + +import 'package:path_provider/path_provider.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/util.dart'; + +abstract class StackFileSystem { + static Future<Directory> applicationRootDirectory() async { + Directory appDirectory; + + // todo: can merge and do same as regular linux home dir? + if (Logging.isArmLinux) { + appDirectory = await getApplicationDocumentsDirectory(); + appDirectory = Directory("${appDirectory.path}/.stackwallet"); + } else if (Platform.isLinux) { + appDirectory = Directory("${Platform.environment['HOME']}/.stackwallet"); + } else if (Platform.isWindows) { + // TODO: windows root .stackwallet dir location + throw Exception("Unsupported platform"); + } else if (Platform.isMacOS) { + // currently run in ipad mode?? + throw Exception("Unsupported platform"); + } else if (Platform.isIOS) { + // todo: check if we need different behaviour here + if (Util.isDesktop) { + appDirectory = await getLibraryDirectory(); + } else { + appDirectory = await getLibraryDirectory(); + } + } else if (Platform.isAndroid) { + appDirectory = await getApplicationDocumentsDirectory(); + } else { + throw Exception("Unsupported platform"); + } + if (!appDirectory.existsSync()) { + await appDirectory.create(recursive: true); + } + return appDirectory; + } + + static Future<Directory> applicationIsarDirectory() async { + final root = await applicationRootDirectory(); + if (Util.isDesktop) { + final dir = Directory("${root.path}/isar"); + if (!dir.existsSync()) { + await dir.create(); + } + return dir; + } else { + return root; + } + } + + static Future<Directory> applicationHiveDirectory() async { + final root = await applicationRootDirectory(); + if (Util.isDesktop) { + final dir = Directory("${root.path}/hive"); + if (!dir.existsSync()) { + await dir.create(); + } + return dir; + } else { + return root; + } + } +} From 357fd5e6fe23f2f31e3572efd27ad201df83affa Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Sat, 12 Nov 2022 16:34:34 -0600 Subject: [PATCH 228/426] update libmonero submodule dep --- crypto_plugins/flutter_libmonero | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index 2da774385..e5e3f6ee8 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit 2da77438527732dfaa5398aa391eab5253dabe19 +Subproject commit e5e3f6ee866a04f71534d71d62a7e371205b80d4 From 316a34914fd9849efa42a94ed7acf88a5ec8865e Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Sat, 12 Nov 2022 16:46:08 -0600 Subject: [PATCH 229/426] libmonero fix --- crypto_plugins/flutter_libmonero | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index e5e3f6ee8..de29931da 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit e5e3f6ee866a04f71534d71d62a7e371205b80d4 +Subproject commit de29931dacc9aefaf42a9ca139a8754a42adc40d From 94709623c4270a082277ad1f095e809d51be21e5 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 14 Nov 2022 07:37:24 -0600 Subject: [PATCH 230/426] temp firo balance dropdown --- .../wallet_view/sub_widgets/desktop_send.dart | 157 ++++++++++-------- 1 file changed, 89 insertions(+), 68 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index 710bc8685..6f35a8570 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:decimal/decimal.dart'; +import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -9,7 +10,6 @@ import 'package:stackwallet/models/contact_address_entry.dart'; import 'package:stackwallet/models/send_view_auto_fill_data.dart'; import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart'; import 'package:stackwallet/pages/send_view/sub_widgets/building_transaction_dialog.dart'; -import 'package:stackwallet/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart'; import 'package:stackwallet/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart'; import 'package:stackwallet/providers/providers.dart'; @@ -550,13 +550,13 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { Future<String?> _firoBalanceFuture( ChangeNotifierProvider<Manager> provider, String locale, + bool private, ) async { final wallet = ref.read(provider).wallet as FiroWallet?; if (wallet != null) { Decimal? balance; - if (ref.read(publicPrivateBalanceStateProvider.state).state == - "Private") { + if (private) { balance = await wallet.availablePrivateBalance(); } else { balance = await wallet.availablePublicBalance(); @@ -572,24 +572,21 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { Widget firoBalanceFutureBuilder( BuildContext context, AsyncSnapshot<String?> snapshot, + bool private, ) { if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { - if (ref.read(publicPrivateBalanceStateProvider.state).state == - "Private") { + if (private) { _privateBalanceString = snapshot.data!; } else { _publicBalanceString = snapshot.data!; } } - if (ref.read(publicPrivateBalanceStateProvider.state).state == "Private" && - _privateBalanceString != null) { + if (private && _privateBalanceString != null) { return Text( "$_privateBalanceString ${coin.ticker}", style: STextStyles.itemSubtitle(context), ); - } else if (ref.read(publicPrivateBalanceStateProvider.state).state == - "Public" && - _publicBalanceString != null) { + } else if (!private && _publicBalanceString != null) { return Text( "$_publicBalanceString ${coin.ticker}", style: STextStyles.itemSubtitle(context), @@ -889,71 +886,95 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { height: 10, ), if (coin == Coin.firo) - Stack( - children: [ - TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - readOnly: true, - textInputAction: TextInputAction.none, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - ), - child: RawMaterialButton( - splashColor: - Theme.of(context).extension<StackColors>()!.highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - showModalBottomSheet<dynamic>( - backgroundColor: Colors.transparent, - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - builder: (_) => FiroBalanceSelectionSheet( - walletId: walletId, - ), - ); - }, + DropdownButtonHideUnderline( + child: DropdownButton2( + offset: const Offset(0, -10), + isExpanded: true, + dropdownElevation: 0, + value: ref.watch(publicPrivateBalanceStateProvider.state).state, + items: [ + DropdownMenuItem( + value: "Private", child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - children: [ - Text( - "${ref.watch(publicPrivateBalanceStateProvider.state).state} balance", - style: STextStyles.itemSubtitle12(context), - ), - const SizedBox( - width: 10, - ), - FutureBuilder( - future: _firoBalanceFuture(provider, locale), - builder: firoBalanceFutureBuilder, - ), - ], + Text( + "Private balance", + style: STextStyles.itemSubtitle12(context), ), - SvgPicture.asset( - Assets.svg.chevronDown, - width: 8, - height: 4, - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle2, + const SizedBox( + width: 10, + ), + FutureBuilder( + future: _firoBalanceFuture(provider, locale, true), + builder: (context, AsyncSnapshot<String?> snapshot) => + firoBalanceFutureBuilder( + context, + snapshot, + true, + ), ), ], ), ), - ) - ], + DropdownMenuItem( + value: "Public", + child: Row( + children: [ + Text( + "Public balance", + style: STextStyles.itemSubtitle12(context), + ), + const SizedBox( + width: 10, + ), + FutureBuilder( + future: _firoBalanceFuture(provider, locale, false), + builder: (context, AsyncSnapshot<String?> snapshot) => + firoBalanceFutureBuilder( + context, + snapshot, + false, + ), + ), + ], + ), + ), + ], + onChanged: (value) { + if (value is String) { + setState(() { + ref.watch(publicPrivateBalanceStateProvider.state).state = + value; + }); + } + }, + icon: SvgPicture.asset( + Assets.svg.chevronDown, + width: 12, + height: 6, + color: Theme.of(context).extension<StackColors>()!.textDark3, + ), + buttonPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + buttonDecoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + dropdownDecoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), ), if (coin == Coin.firo) const SizedBox( From 9a9b10b1b3c244a5b0e0486187a85af8be055688 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 14 Nov 2022 07:56:07 -0600 Subject: [PATCH 231/426] WIP: fee selection ui --- .../send_view/confirm_transaction_view.dart | 128 +++++++++++++----- 1 file changed, 95 insertions(+), 33 deletions(-) diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index 26d1231f0..eef0f84e6 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -54,13 +54,6 @@ class _ConfirmTransactionViewState late final String routeOnSuccessName; late final bool isDesktop; - int _fee = 12; - final List<int> _dropDownItems = [ - 12, - 22, - 234, - ]; - Future<void> _attemptSend(BuildContext context) async { unawaited(showDialog<dynamic>( context: context, @@ -568,32 +561,101 @@ class _ConfirmTransactionViewState ), if (isDesktop) Padding( - padding: const EdgeInsets.only( - top: 10, - left: 32, - right: 32, - ), - child: DropdownButtonFormField( - value: _fee, - items: _dropDownItems - .map( - (e) => DropdownMenuItem( - value: e, - child: Text( - e.toString(), - ), - ), - ) - .toList(), - onChanged: (value) { - if (value is int) { - setState(() { - _fee = value; - }); - } - }, - ), - ), + padding: const EdgeInsets.only( + top: 10, + left: 32, + right: 32, + ), + child: RoundedContainer( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 18, + ), + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + child: Builder(builder: (context) { + final coin = ref + .watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletId))) + .coin; + + final fee = Format.satoshisToAmount( + transactionInfo["fee"] as int, + coin: coin, + ); + + return Text( + "${Format.localizedStringAsFixed( + value: fee, + locale: ref.watch(localeServiceChangeNotifierProvider + .select((value) => value.locale)), + decimalPlaces: coin == Coin.monero + ? Constants.decimalPlacesMonero + : coin == Coin.wownero + ? Constants.decimalPlacesWownero + : Constants.decimalPlaces, + )} ${coin.ticker}", + style: STextStyles.itemSubtitle(context), + ); + }), + ) + // DropdownButtonHideUnderline( + // child: DropdownButton2( + // offset: const Offset(0, -10), + // isExpanded: true, + // + // dropdownElevation: 0, + // value: _fee, + // items: [ + // ..._dropDownItems.map( + // (e) { + // String message = _fee.toString(); + // + // return DropdownMenuItem( + // value: e, + // child: Text(message), + // ); + // }, + // ), + // ], + // onChanged: (value) { + // if (value is int) { + // setState(() { + // _fee = value; + // }); + // } + // }, + // icon: SvgPicture.asset( + // Assets.svg.chevronDown, + // width: 12, + // height: 6, + // color: + // Theme.of(context).extension<StackColors>()!.textDark3, + // ), + // buttonPadding: const EdgeInsets.symmetric( + // horizontal: 16, + // vertical: 8, + // ), + // buttonDecoration: BoxDecoration( + // color: Theme.of(context) + // .extension<StackColors>()! + // .textFieldDefaultBG, + // borderRadius: BorderRadius.circular( + // Constants.size.circularBorderRadius, + // ), + // ), + // dropdownDecoration: BoxDecoration( + // color: Theme.of(context) + // .extension<StackColors>()! + // .textFieldDefaultBG, + // borderRadius: BorderRadius.circular( + // Constants.size.circularBorderRadius, + // ), + // ), + // ), + // ), + ), if (!isDesktop) const Spacer(), SizedBox( height: isDesktop ? 23 : 12, From 4238ce338ac8cf9f46ff402f7f5cff3186f59a3a Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 14 Nov 2022 09:05:45 -0600 Subject: [PATCH 232/426] desktop password protected send flow --- .../send_view/confirm_transaction_view.dart | 165 ++++++++++++----- .../sending_transaction_dialog.dart | 69 +++++-- .../sub_widgets/desktop_auth_send.dart | 173 ++++++++++++++++++ .../wallet_view/sub_widgets/desktop_send.dart | 2 + 4 files changed, 348 insertions(+), 61 deletions(-) create mode 100644 lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index eef0f84e6..0f1692c08 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -8,6 +8,7 @@ import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart'; import 'package:stackwallet/pages/send_view/sub_widgets/sending_transaction_dialog.dart'; import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart'; import 'package:stackwallet/route_generator.dart'; @@ -23,6 +24,8 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.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/rounded_container.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -55,14 +58,16 @@ class _ConfirmTransactionViewState late final bool isDesktop; Future<void> _attemptSend(BuildContext context) async { - unawaited(showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: false, - builder: (context) { - return const SendingTransactionDialog(); - }, - )); + unawaited( + showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return const SendingTransactionDialog(); + }, + ), + ); final note = transactionInfo["note"] as String? ?? ""; final manager = @@ -115,25 +120,66 @@ class _ConfirmTransactionViewState useSafeArea: false, barrierDismissible: true, builder: (context) { - return StackDialog( - title: "Broadcast transaction failed", - message: e.toString(), - rightButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Ok", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), + if (isDesktop) { + return DesktopDialog( + maxWidth: 450, + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Broadcast transaction failed", + style: STextStyles.desktopH3(context), + ), + const SizedBox( + height: 24, + ), + Text( + e.toString(), + style: STextStyles.smallMed14(context), + ), + const SizedBox( + height: 56, + ), + Row( + children: [ + const Spacer(), + Expanded( + child: PrimaryButton( + desktopMed: true, + label: "Ok", + onPressed: Navigator.of(context).pop, + ), + ), + ], + ) + ], + ), ), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ); + ); + } else { + return StackDialog( + title: "Broadcast transaction failed", + message: e.toString(), + rightButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Ok", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ); + } }, ); } @@ -736,25 +782,56 @@ class _ConfirmTransactionViewState label: "Send", desktopMed: true, onPressed: () async { - final unlocked = await Navigator.push( - context, - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator.useMaterialPageRoute, - builder: (_) => const LockscreenView( - showBackButton: true, - popOnSuccess: true, - routeOnSuccessArguments: true, - routeOnSuccess: "", - biometricsCancelButtonString: "CANCEL", - biometricsLocalizedReason: - "Authenticate to send transaction", - biometricsAuthenticationTitle: "Confirm Transaction", + final dynamic unlocked; + + if (isDesktop) { + unlocked = await showDialog<bool?>( + context: context, + builder: (context) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: const [ + DesktopDialogCloseButton(), + ], + ), + const Padding( + padding: EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: DesktopAuthSend(), + ), + ], + ), ), - settings: - const RouteSettings(name: "/confirmsendlockscreen"), - ), - ); + ); + } else { + unlocked = await Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator.useMaterialPageRoute, + builder: (_) => const LockscreenView( + showBackButton: true, + popOnSuccess: true, + routeOnSuccessArguments: true, + routeOnSuccess: "", + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: + "Authenticate to send transaction", + biometricsAuthenticationTitle: "Confirm Transaction", + ), + settings: + const RouteSettings(name: "/confirmsendlockscreen"), + ), + ); + } if (unlocked is bool && unlocked && mounted) { unawaited(_attemptSend(context)); 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 1eb106b53..e5c86fe2e 100644 --- a/lib/pages/send_view/sub_widgets/sending_transaction_dialog.dart +++ b/lib/pages/send_view/sub_widgets/sending_transaction_dialog.dart @@ -1,7 +1,10 @@ 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/utilities/util.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; class SendingTransactionDialog extends StatefulWidget { @@ -43,24 +46,56 @@ class _RestoringDialogState extends State<SendingTransactionDialog> @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { - return false; - }, - child: StackDialog( - title: "Sending transaction", - // // TODO get message from design team - // message: "<PLACEHOLDER>", - icon: RotationTransition( - turns: _spinAnimation, - child: SvgPicture.asset( - Assets.svg.arrowRotate, - color: Theme.of(context).extension<StackColors>()!.accentColorDark, - width: 24, - height: 24, + if (Util.isDesktop) { + return DesktopDialog( + child: Padding( + padding: const EdgeInsets.all(40), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Sending transaction", + style: STextStyles.desktopH3(context), + ), + const SizedBox( + height: 40, + ), + RotationTransition( + turns: _spinAnimation, + child: SvgPicture.asset( + Assets.svg.arrowRotate, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + width: 24, + height: 24, + ), + ), + ], ), ), - ), - ); + ); + } else { + return WillPopScope( + onWillPop: () async { + return false; + }, + child: StackDialog( + title: "Sending transaction", + // // TODO get message from design team + // message: "<PLACEHOLDER>", + icon: RotationTransition( + turns: _spinAnimation, + child: SvgPicture.asset( + Assets.svg.arrowRotate, + color: + Theme.of(context).extension<StackColors>()!.accentColorDark, + width: 24, + height: 24, + ), + ), + ), + ); + } } } diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart new file mode 100644 index 000000000..566a82b35 --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart @@ -0,0 +1,173 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/providers/desktop/storage_crypto_handler_provider.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; + +class DesktopAuthSend extends ConsumerStatefulWidget { + const DesktopAuthSend({Key? key}) : super(key: key); + + @override + ConsumerState<DesktopAuthSend> createState() => _DesktopAuthSendState(); +} + +class _DesktopAuthSendState extends ConsumerState<DesktopAuthSend> { + late final TextEditingController passwordController; + late final FocusNode passwordFocusNode; + + bool hidePassword = true; + + bool _confirmEnabled = false; + + Future<bool> verifyPassphrase() async { + return await ref + .read(storageCryptoHandlerProvider) + .verifyPassphrase(passwordController.text); + } + + @override + void initState() { + passwordController = TextEditingController(); + passwordFocusNode = FocusNode(); + + super.initState(); + } + + @override + void dispose() { + passwordController.dispose(); + passwordFocusNode.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset( + Assets.svg.keys, + width: 100, + ), + const SizedBox( + height: 56, + ), + Text( + "Confirm transaction", + style: STextStyles.desktopH3(context), + ), + const SizedBox( + height: 16, + ), + Text( + "Enter your wallet password to send BTC", + style: STextStyles.desktopTextMedium(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark3, + ), + ), + const SizedBox( + height: 24, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("desktopLoginPasswordFieldKey"), + focusNode: passwordFocusNode, + controller: passwordController, + style: STextStyles.desktopTextMedium(context).copyWith( + height: 2, + ), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Enter password", + passwordFocusNode, + context, + ).copyWith( + suffixIcon: UnconstrainedBox( + child: SizedBox( + height: 70, + child: Row( + children: [ + const SizedBox( + width: 24, + ), + GestureDetector( + key: const Key( + "restoreFromFilePasswordFieldShowPasswordButtonKey"), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 24, + height: 24, + ), + ), + const SizedBox( + width: 12, + ), + ], + ), + ), + ), + ), + onChanged: (newValue) { + setState(() { + _confirmEnabled = passwordController.text.isNotEmpty; + }); + }, + ), + ), + const SizedBox( + height: 48, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + desktopMed: true, + onPressed: Navigator.of(context).pop, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + enabled: _confirmEnabled, + label: "Confirm", + desktopMed: true, + onPressed: () async { + // TODO show spinner while verifying passphrase + + final passwordIsValid = await verifyPassphrase(); + + if (mounted) { + Navigator.of(context).pop(passwordIsValid); + } + }, + ), + ), + ], + ) + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index 6f35a8570..071122def 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -11,6 +11,7 @@ import 'package:stackwallet/models/send_view_auto_fill_data.dart'; import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart'; import 'package:stackwallet/pages/send_view/sub_widgets/building_transaction_dialog.dart'; import 'package:stackwallet/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart'; +import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/fee_rate_type_state_provider.dart'; @@ -332,6 +333,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { child: ConfirmTransactionView( transactionInfo: txData, walletId: walletId, + routeOnSuccessName: DesktopHomeView.routeName, ), ), ), From daa7708ad0196e16145a551b9f5574c0972ff8e0 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 14 Nov 2022 09:20:35 -0600 Subject: [PATCH 233/426] temp disable exchange option for desktop --- .../home/desktop_home_view.dart | 7 +-- .../home/desktop_menu.dart | 46 +++++++++---------- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/lib/pages_desktop_specific/home/desktop_home_view.dart b/lib/pages_desktop_specific/home/desktop_home_view.dart index cb8aba255..76645442d 100644 --- a/lib/pages_desktop_specific/home/desktop_home_view.dart +++ b/lib/pages_desktop_specific/home/desktop_home_view.dart @@ -26,9 +26,10 @@ class _DesktopHomeViewState extends ConsumerState<DesktopHomeView> { onGenerateRoute: RouteGenerator.generateRoute, initialRoute: MyStackView.routeName, ), - Container( - color: Colors.green, - ), + // Container( + // // todo: exchange + // color: Colors.green, + // ), Container( color: Colors.red, ), diff --git a/lib/pages_desktop_specific/home/desktop_menu.dart b/lib/pages_desktop_specific/home/desktop_menu.dart index 7409a4156..800a8416e 100644 --- a/lib/pages_desktop_specific/home/desktop_menu.dart +++ b/lib/pages_desktop_specific/home/desktop_menu.dart @@ -103,29 +103,29 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { const SizedBox( height: 2, ), - DesktopMenuItem( - icon: SvgPicture.asset( - Assets.svg.exchangeDesktop, - width: 20, - height: 20, - color: 1 == selectedMenuItem - ? Theme.of(context) - .extension<StackColors>()! - .textDark - : Theme.of(context) - .extension<StackColors>()! - .textDark - .withOpacity(0.8), - ), - label: "Exchange", - value: 1, - group: selectedMenuItem, - onChanged: updateSelectedMenuItem, - iconOnly: _width == minimizedWidth, - ), - const SizedBox( - height: 2, - ), + // DesktopMenuItem( + // icon: SvgPicture.asset( + // Assets.svg.exchangeDesktop, + // width: 20, + // height: 20, + // color: 1 == selectedMenuItem + // ? Theme.of(context) + // .extension<StackColors>()! + // .textDark + // : Theme.of(context) + // .extension<StackColors>()! + // .textDark + // .withOpacity(0.8), + // ), + // label: "Exchange", + // value: 1, + // group: selectedMenuItem, + // onChanged: updateSelectedMenuItem, + // iconOnly: _width == minimizedWidth, + // ), + // const SizedBox( + // height: 2, + // ), DesktopMenuItem( icon: SvgPicture.asset( Assets.svg.bell, From 5b47d5806d97f1206d336312d87e3a0a703974ef Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 14 Nov 2022 09:25:06 -0600 Subject: [PATCH 234/426] disable seemingly pointless code --- .../coins/epiccash/epiccash_wallet.dart | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/lib/services/coins/epiccash/epiccash_wallet.dart b/lib/services/coins/epiccash/epiccash_wallet.dart index 1a5b2961f..fcf728fb8 100644 --- a/lib/services/coins/epiccash/epiccash_wallet.dart +++ b/lib/services/coins/epiccash/epiccash_wallet.dart @@ -252,26 +252,27 @@ Future<String> deleteEpicWallet({ required String walletId, required SecureStorageInterface secureStore, }) async { - String? config = await secureStore.read(key: '${walletId}_config'); - // TODO: why double check for iOS? - if (Platform.isIOS) { - Directory appDir = await StackFileSystem.applicationRootDirectory(); - // todo why double check for ios? - // if (Platform.isIOS) { - // appDir = (await getLibraryDirectory()); - // } - // if (Platform.isLinux) { - // appDir = Directory("${appDir.path}/.stackwallet"); - // } - final path = "${appDir.path}/epiccash"; - final String name = walletId; - - final walletDir = '$path/$name'; - var editConfig = jsonDecode(config as String); - - editConfig["wallet_dir"] = walletDir; - config = jsonEncode(editConfig); - } + // is this even needed for anything? + // String? config = await secureStore.read(key: '${walletId}_config'); + // // TODO: why double check for iOS? + // if (Platform.isIOS) { + // Directory appDir = await StackFileSystem.applicationRootDirectory(); + // // todo why double check for ios? + // // if (Platform.isIOS) { + // // appDir = (await getLibraryDirectory()); + // // } + // // if (Platform.isLinux) { + // // appDir = Directory("${appDir.path}/.stackwallet"); + // // } + // final path = "${appDir.path}/epiccash"; + // final String name = walletId; + // + // final walletDir = '$path/$name'; + // var editConfig = jsonDecode(config as String); + // + // editConfig["wallet_dir"] = walletDir; + // config = jsonEncode(editConfig); + // } final wallet = await secureStore.read(key: '${walletId}_wallet'); From 7cf3a8efba9a2968644c0df6e7b56257416db7c6 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 14 Nov 2022 09:50:58 -0600 Subject: [PATCH 235/426] refactor desktop main menu and add WIP notifications view --- .../home/desktop_home_view.dart | 29 +++++---- .../home/desktop_menu.dart | 63 +++++++++++-------- .../desktop_notifications_view.dart | 59 +++++++++++++++++ lib/route_generator.dart | 7 +++ 4 files changed, 119 insertions(+), 39 deletions(-) create mode 100644 lib/pages_desktop_specific/home/notifications/desktop_notifications_view.dart diff --git a/lib/pages_desktop_specific/home/desktop_home_view.dart b/lib/pages_desktop_specific/home/desktop_home_view.dart index 76645442d..c0b0145f7 100644 --- a/lib/pages_desktop_specific/home/desktop_home_view.dart +++ b/lib/pages_desktop_specific/home/desktop_home_view.dart @@ -4,6 +4,7 @@ import 'package:stackwallet/pages_desktop_specific/home/address_book_view/deskto import 'package:stackwallet/pages_desktop_specific/home/desktop_menu.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_settings_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/notifications/desktop_notifications_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart'; import 'package:stackwallet/route_generator.dart'; @@ -19,9 +20,9 @@ class DesktopHomeView extends ConsumerStatefulWidget { } class _DesktopHomeViewState extends ConsumerState<DesktopHomeView> { - int currentViewIndex = 0; - final List<Widget> contentViews = [ - const Navigator( + DesktopMenuItemId currentViewKey = DesktopMenuItemId.myStack; + final Map<DesktopMenuItemId, Widget> contentViews = { + DesktopMenuItemId.myStack: const Navigator( key: Key("desktopStackHomeKey"), onGenerateRoute: RouteGenerator.generateRoute, initialRoute: MyStackView.routeName, @@ -30,34 +31,36 @@ class _DesktopHomeViewState extends ConsumerState<DesktopHomeView> { // // todo: exchange // color: Colors.green, // ), - Container( - color: Colors.red, + DesktopMenuItemId.notifications: const Navigator( + key: Key("desktopNotificationsHomeKey"), + onGenerateRoute: RouteGenerator.generateRoute, + initialRoute: DesktopNotificationsView.routeName, ), - const Navigator( + DesktopMenuItemId.addressBook: const Navigator( key: Key("desktopAddressBookHomeKey"), onGenerateRoute: RouteGenerator.generateRoute, initialRoute: DesktopAddressBook.routeName, ), - const Navigator( + DesktopMenuItemId.settings: const Navigator( key: Key("desktopSettingHomeKey"), onGenerateRoute: RouteGenerator.generateRoute, initialRoute: DesktopSettingsView.routeName, ), - const Navigator( + DesktopMenuItemId.support: const Navigator( key: Key("desktopSupportHomeKey"), onGenerateRoute: RouteGenerator.generateRoute, initialRoute: DesktopSupportView.routeName, ), - const Navigator( + DesktopMenuItemId.about: const Navigator( key: Key("desktopAboutHomeKey"), onGenerateRoute: RouteGenerator.generateRoute, initialRoute: DesktopAboutView.routeName, ), - ]; + }; - void onMenuSelectionChanged(int newIndex) { + void onMenuSelectionChanged(DesktopMenuItemId newKey) { setState(() { - currentViewIndex = newIndex; + currentViewKey = newKey; }); } @@ -75,7 +78,7 @@ class _DesktopHomeViewState extends ConsumerState<DesktopHomeView> { color: Theme.of(context).extension<StackColors>()!.background, ), Expanded( - child: contentViews[currentViewIndex], + child: contentViews[currentViewKey]!, ), ], ), diff --git a/lib/pages_desktop_specific/home/desktop_menu.dart b/lib/pages_desktop_specific/home/desktop_menu.dart index 800a8416e..cfa1a0ff0 100644 --- a/lib/pages_desktop_specific/home/desktop_menu.dart +++ b/lib/pages_desktop_specific/home/desktop_menu.dart @@ -8,13 +8,23 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +enum DesktopMenuItemId { + myStack, + exchange, + notifications, + addressBook, + settings, + support, + about, +} + class DesktopMenu extends ConsumerStatefulWidget { const DesktopMenu({ Key? key, required this.onSelectionChanged, }) : super(key: key); - final void Function(int)? onSelectionChanged; + final void Function(DesktopMenuItemId)? onSelectionChanged; @override ConsumerState<DesktopMenu> createState() => _DesktopMenuState(); @@ -25,13 +35,13 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { static const minimizedWidth = 72.0; double _width = expandedWidth; - int selectedMenuItem = 0; + DesktopMenuItemId selectedMenuItem = DesktopMenuItemId.myStack; - void updateSelectedMenuItem(int index) { + void updateSelectedMenuItem(DesktopMenuItemId idKey) { setState(() { - selectedMenuItem = index; + selectedMenuItem = idKey; }); - widget.onSelectionChanged?.call(index); + widget.onSelectionChanged?.call(idKey); } void toggleMinimize() { @@ -85,7 +95,7 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { Assets.svg.walletDesktop, width: 20, height: 20, - color: 0 == selectedMenuItem + color: DesktopMenuItemId.myStack == selectedMenuItem ? Theme.of(context) .extension<StackColors>()! .textDark @@ -95,7 +105,7 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { .withOpacity(0.8), ), label: "My Stack", - value: 0, + value: DesktopMenuItemId.myStack, group: selectedMenuItem, onChanged: updateSelectedMenuItem, iconOnly: _width == minimizedWidth, @@ -108,7 +118,7 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { // Assets.svg.exchangeDesktop, // width: 20, // height: 20, - // color: 1 == selectedMenuItem + // color: DesktopMenuItemId.exchange == selectedMenuItem // ? Theme.of(context) // .extension<StackColors>()! // .textDark @@ -118,7 +128,7 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { // .withOpacity(0.8), // ), // label: "Exchange", - // value: 1, + // value: DesktopMenuItemId.exchange, // group: selectedMenuItem, // onChanged: updateSelectedMenuItem, // iconOnly: _width == minimizedWidth, @@ -131,17 +141,18 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { Assets.svg.bell, width: 20, height: 20, - color: 2 == selectedMenuItem - ? Theme.of(context) - .extension<StackColors>()! - .textDark - : Theme.of(context) - .extension<StackColors>()! - .textDark - .withOpacity(0.8), + color: + DesktopMenuItemId.notifications == selectedMenuItem + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textDark + .withOpacity(0.8), ), label: "Notifications", - value: 2, + value: DesktopMenuItemId.notifications, group: selectedMenuItem, onChanged: updateSelectedMenuItem, iconOnly: _width == minimizedWidth, @@ -154,7 +165,7 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { Assets.svg.addressBookDesktop, width: 20, height: 20, - color: 3 == selectedMenuItem + color: DesktopMenuItemId.addressBook == selectedMenuItem ? Theme.of(context) .extension<StackColors>()! .textDark @@ -164,7 +175,7 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { .withOpacity(0.8), ), label: "Address Book", - value: 3, + value: DesktopMenuItemId.addressBook, group: selectedMenuItem, onChanged: updateSelectedMenuItem, iconOnly: _width == minimizedWidth, @@ -177,7 +188,7 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { Assets.svg.gear, width: 20, height: 20, - color: 4 == selectedMenuItem + color: DesktopMenuItemId.settings == selectedMenuItem ? Theme.of(context) .extension<StackColors>()! .textDark @@ -187,7 +198,7 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { .withOpacity(0.8), ), label: "Settings", - value: 4, + value: DesktopMenuItemId.settings, group: selectedMenuItem, onChanged: updateSelectedMenuItem, iconOnly: _width == minimizedWidth, @@ -200,7 +211,7 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { Assets.svg.messageQuestion, width: 20, height: 20, - color: 5 == selectedMenuItem + color: DesktopMenuItemId.support == selectedMenuItem ? Theme.of(context) .extension<StackColors>()! .textDark @@ -210,7 +221,7 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { .withOpacity(0.8), ), label: "Support", - value: 5, + value: DesktopMenuItemId.support, group: selectedMenuItem, onChanged: updateSelectedMenuItem, iconOnly: _width == minimizedWidth, @@ -223,7 +234,7 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { Assets.svg.aboutDesktop, width: 20, height: 20, - color: 6 == selectedMenuItem + color: DesktopMenuItemId.about == selectedMenuItem ? Theme.of(context) .extension<StackColors>()! .textDark @@ -233,7 +244,7 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { .withOpacity(0.8), ), label: "About", - value: 6, + value: DesktopMenuItemId.about, group: selectedMenuItem, onChanged: updateSelectedMenuItem, iconOnly: _width == minimizedWidth, diff --git a/lib/pages_desktop_specific/home/notifications/desktop_notifications_view.dart b/lib/pages_desktop_specific/home/notifications/desktop_notifications_view.dart new file mode 100644 index 000000000..c8e688aa6 --- /dev/null +++ b/lib/pages_desktop_specific/home/notifications/desktop_notifications_view.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/notifications/notification_card.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class DesktopNotificationsView extends ConsumerStatefulWidget { + const DesktopNotificationsView({Key? key}) : super(key: key); + + static const String routeName = "/desktopNotifications"; + + @override + ConsumerState<DesktopNotificationsView> createState() => + _DesktopNotificationsViewState(); +} + +class _DesktopNotificationsViewState + extends ConsumerState<DesktopNotificationsView> { + @override + Widget build(BuildContext context) { + final notifications = + ref.watch(notificationsProvider.select((value) => value.notifications)); + + return DesktopScaffold( + background: Theme.of(context).extension<StackColors>()!.background, + appBar: DesktopAppBar( + isCompactHeight: true, + leading: Padding( + padding: const EdgeInsets.only(left: 24), + child: Text( + "Notifications", + style: STextStyles.desktopH3(context), + ), + ), + ), + body: notifications.isEmpty + ? RoundedWhiteContainer( + child: Center( + child: Text( + "Notifications will appear here", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + ) + : ListView.builder( + itemCount: notifications.length, + itemBuilder: (context, index) { + return NotificationCard( + notification: notifications[index], + ); + }, + ), + ); + } +} diff --git a/lib/route_generator.dart b/lib/route_generator.dart index f3e37e383..d7865d013 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -93,6 +93,7 @@ import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_v import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/qr_code_desktop_popup_content.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart'; +import 'package:stackwallet/pages_desktop_specific/home/notifications/desktop_notifications_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/appearance_settings.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart'; @@ -1012,6 +1013,12 @@ class RouteGenerator { builder: (_) => const DesktopHomeView(), settings: RouteSettings(name: settings.name)); + case DesktopNotificationsView.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const DesktopNotificationsView(), + settings: RouteSettings(name: settings.name)); + case DesktopSettingsView.routeName: return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, From 60bdc6151bbffc8183cf0b0e539ee8e3b48005ad Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 14 Nov 2022 10:40:31 -0600 Subject: [PATCH 236/426] desktop notifications view --- lib/notifications/notification_card.dart | 75 ++++++++++++++--- .../home/desktop_home_view.dart | 44 ++++++++-- .../home/desktop_menu.dart | 83 ++++++++++++------- .../desktop_notifications_view.dart | 20 ++++- .../desktop/current_desktop_menu_item.dart | 5 ++ 5 files changed, 179 insertions(+), 48 deletions(-) create mode 100644 lib/providers/desktop/current_desktop_menu_item.dart diff --git a/lib/notifications/notification_card.dart b/lib/notifications/notification_card.dart index 67be236f0..2a181499c 100644 --- a/lib/notifications/notification_card.dart +++ b/lib/notifications/notification_card.dart @@ -4,6 +4,8 @@ import 'package:stackwallet/models/notification_model.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -20,22 +22,33 @@ class NotificationCard extends StatelessWidget { return Format.extractDateFrom(date.millisecondsSinceEpoch ~/ 1000); } + static const double mobileIconSize = 24; + static const double desktopIconSize = 30; + @override Widget build(BuildContext context) { + final isDesktop = Util.isDesktop; + return Stack( children: [ RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.symmetric( + horizontal: 20, + vertical: 10, + ) + : const EdgeInsets.all(12), child: Row( children: [ notification.changeNowId == null ? SvgPicture.asset( notification.iconAssetName, - width: 24, - height: 24, + width: isDesktop ? desktopIconSize : mobileIconSize, + height: isDesktop ? desktopIconSize : mobileIconSize, ) : Container( - width: 24, - height: 24, + width: isDesktop ? desktopIconSize : mobileIconSize, + height: isDesktop ? desktopIconSize : mobileIconSize, decoration: BoxDecoration( color: Colors.transparent, borderRadius: BorderRadius.circular(24), @@ -45,8 +58,8 @@ class NotificationCard extends StatelessWidget { color: Theme.of(context) .extension<StackColors>()! .accentColorDark, - width: 24, - height: 24, + width: isDesktop ? desktopIconSize : mobileIconSize, + height: isDesktop ? desktopIconSize : mobileIconSize, ), ), const SizedBox( @@ -56,9 +69,35 @@ class NotificationCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text( - notification.title, - style: STextStyles.titleBold12(context), + ConditionalParent( + condition: isDesktop && !notification.read, + builder: (child) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + child, + Text( + "New", + style: + STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorGreen, + ), + ) + ], + ), + child: Text( + notification.title, + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.titleBold12(context), + ), ), const SizedBox( height: 2, @@ -68,11 +107,25 @@ class NotificationCard extends StatelessWidget { children: [ Text( notification.description, - style: STextStyles.label(context), + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ) + : STextStyles.label(context), ), Text( extractPrettyDateString(notification.date), - style: STextStyles.label(context), + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ) + : STextStyles.label(context), ), ], ), diff --git a/lib/pages_desktop_specific/home/desktop_home_view.dart b/lib/pages_desktop_specific/home/desktop_home_view.dart index c0b0145f7..b1c35f00b 100644 --- a/lib/pages_desktop_specific/home/desktop_home_view.dart +++ b/lib/pages_desktop_specific/home/desktop_home_view.dart @@ -7,6 +7,9 @@ import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_v import 'package:stackwallet/pages_desktop_specific/home/notifications/desktop_notifications_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart'; +import 'package:stackwallet/providers/desktop/current_desktop_menu_item.dart'; +import 'package:stackwallet/providers/global/notifications_provider.dart'; +import 'package:stackwallet/providers/ui/unread_notifications_provider.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -20,7 +23,6 @@ class DesktopHomeView extends ConsumerStatefulWidget { } class _DesktopHomeViewState extends ConsumerState<DesktopHomeView> { - DesktopMenuItemId currentViewKey = DesktopMenuItemId.myStack; final Map<DesktopMenuItemId, Widget> contentViews = { DesktopMenuItemId.myStack: const Navigator( key: Key("desktopStackHomeKey"), @@ -58,10 +60,36 @@ class _DesktopHomeViewState extends ConsumerState<DesktopHomeView> { ), }; - void onMenuSelectionChanged(DesktopMenuItemId newKey) { - setState(() { - currentViewKey = newKey; - }); + void onMenuSelectionWillChange(DesktopMenuItemId newKey) { + // check for unread notifications and refresh provider before + // showing notifications view + if (newKey == DesktopMenuItemId.notifications) { + ref.refresh(unreadNotificationsStateProvider); + } + // mark notifications as read if leaving notifications view + if (ref.read(currentDesktopMenuItemProvider.state).state == + DesktopMenuItemId.notifications && + newKey != DesktopMenuItemId.notifications) { + final Set<int> unreadNotificationIds = + ref.read(unreadNotificationsStateProvider.state).state; + + if (unreadNotificationIds.isNotEmpty) { + List<Future<void>> futures = []; + for (int i = 0; i < unreadNotificationIds.length - 1; i++) { + futures.add(ref + .read(notificationsProvider) + .markAsRead(unreadNotificationIds.elementAt(i), false)); + } + + // wait for multiple to update if any + Future.wait(futures).then((_) { + // only notify listeners once + ref + .read(notificationsProvider) + .markAsRead(unreadNotificationIds.last, true); + }); + } + } } @override @@ -71,14 +99,16 @@ class _DesktopHomeViewState extends ConsumerState<DesktopHomeView> { child: Row( children: [ DesktopMenu( - onSelectionChanged: onMenuSelectionChanged, + // onSelectionChanged: onMenuSelectionChanged, + onSelectionWillChange: onMenuSelectionWillChange, ), Container( width: 1, color: Theme.of(context).extension<StackColors>()!.background, ), Expanded( - child: contentViews[currentViewKey]!, + child: contentViews[ + ref.watch(currentDesktopMenuItemProvider.state).state]!, ), ], ), diff --git a/lib/pages_desktop_specific/home/desktop_menu.dart b/lib/pages_desktop_specific/home/desktop_menu.dart index cfa1a0ff0..bdaa1d6ce 100644 --- a/lib/pages_desktop_specific/home/desktop_menu.dart +++ b/lib/pages_desktop_specific/home/desktop_menu.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_menu_item.dart'; +import 'package:stackwallet/providers/desktop/current_desktop_menu_item.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -21,10 +22,12 @@ enum DesktopMenuItemId { class DesktopMenu extends ConsumerStatefulWidget { const DesktopMenu({ Key? key, - required this.onSelectionChanged, + this.onSelectionChanged, + this.onSelectionWillChange, }) : super(key: key); final void Function(DesktopMenuItemId)? onSelectionChanged; + final void Function(DesktopMenuItemId)? onSelectionWillChange; @override ConsumerState<DesktopMenu> createState() => _DesktopMenuState(); @@ -35,12 +38,12 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { static const minimizedWidth = 72.0; double _width = expandedWidth; - DesktopMenuItemId selectedMenuItem = DesktopMenuItemId.myStack; void updateSelectedMenuItem(DesktopMenuItemId idKey) { - setState(() { - selectedMenuItem = idKey; - }); + widget.onSelectionWillChange?.call(idKey); + + ref.read(currentDesktopMenuItemProvider.state).state = idKey; + widget.onSelectionChanged?.call(idKey); } @@ -95,7 +98,10 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { Assets.svg.walletDesktop, width: 20, height: 20, - color: DesktopMenuItemId.myStack == selectedMenuItem + color: DesktopMenuItemId.myStack == + ref + .watch(currentDesktopMenuItemProvider.state) + .state ? Theme.of(context) .extension<StackColors>()! .textDark @@ -106,7 +112,8 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { ), label: "My Stack", value: DesktopMenuItemId.myStack, - group: selectedMenuItem, + group: + ref.watch(currentDesktopMenuItemProvider.state).state, onChanged: updateSelectedMenuItem, iconOnly: _width == minimizedWidth, ), @@ -118,7 +125,7 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { // Assets.svg.exchangeDesktop, // width: 20, // height: 20, - // color: DesktopMenuItemId.exchange == selectedMenuItem + // color: DesktopMenuItemId.exchange == ref.watch(currentDesktopMenuItemProvider.state).state // ? Theme.of(context) // .extension<StackColors>()! // .textDark @@ -129,7 +136,7 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { // ), // label: "Exchange", // value: DesktopMenuItemId.exchange, - // group: selectedMenuItem, + // group: ref.watch(currentDesktopMenuItemProvider.state).state, // onChanged: updateSelectedMenuItem, // iconOnly: _width == minimizedWidth, // ), @@ -141,19 +148,22 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { Assets.svg.bell, width: 20, height: 20, - color: - DesktopMenuItemId.notifications == selectedMenuItem - ? Theme.of(context) - .extension<StackColors>()! - .textDark - : Theme.of(context) - .extension<StackColors>()! - .textDark - .withOpacity(0.8), + color: DesktopMenuItemId.notifications == + ref + .watch(currentDesktopMenuItemProvider.state) + .state + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textDark + .withOpacity(0.8), ), label: "Notifications", value: DesktopMenuItemId.notifications, - group: selectedMenuItem, + group: + ref.watch(currentDesktopMenuItemProvider.state).state, onChanged: updateSelectedMenuItem, iconOnly: _width == minimizedWidth, ), @@ -165,7 +175,10 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { Assets.svg.addressBookDesktop, width: 20, height: 20, - color: DesktopMenuItemId.addressBook == selectedMenuItem + color: DesktopMenuItemId.addressBook == + ref + .watch(currentDesktopMenuItemProvider.state) + .state ? Theme.of(context) .extension<StackColors>()! .textDark @@ -176,7 +189,8 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { ), label: "Address Book", value: DesktopMenuItemId.addressBook, - group: selectedMenuItem, + group: + ref.watch(currentDesktopMenuItemProvider.state).state, onChanged: updateSelectedMenuItem, iconOnly: _width == minimizedWidth, ), @@ -188,7 +202,10 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { Assets.svg.gear, width: 20, height: 20, - color: DesktopMenuItemId.settings == selectedMenuItem + color: DesktopMenuItemId.settings == + ref + .watch(currentDesktopMenuItemProvider.state) + .state ? Theme.of(context) .extension<StackColors>()! .textDark @@ -199,7 +216,8 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { ), label: "Settings", value: DesktopMenuItemId.settings, - group: selectedMenuItem, + group: + ref.watch(currentDesktopMenuItemProvider.state).state, onChanged: updateSelectedMenuItem, iconOnly: _width == minimizedWidth, ), @@ -211,7 +229,10 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { Assets.svg.messageQuestion, width: 20, height: 20, - color: DesktopMenuItemId.support == selectedMenuItem + color: DesktopMenuItemId.support == + ref + .watch(currentDesktopMenuItemProvider.state) + .state ? Theme.of(context) .extension<StackColors>()! .textDark @@ -222,7 +243,8 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { ), label: "Support", value: DesktopMenuItemId.support, - group: selectedMenuItem, + group: + ref.watch(currentDesktopMenuItemProvider.state).state, onChanged: updateSelectedMenuItem, iconOnly: _width == minimizedWidth, ), @@ -234,7 +256,10 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { Assets.svg.aboutDesktop, width: 20, height: 20, - color: DesktopMenuItemId.about == selectedMenuItem + color: DesktopMenuItemId.about == + ref + .watch(currentDesktopMenuItemProvider.state) + .state ? Theme.of(context) .extension<StackColors>()! .textDark @@ -245,7 +270,8 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { ), label: "About", value: DesktopMenuItemId.about, - group: selectedMenuItem, + group: + ref.watch(currentDesktopMenuItemProvider.state).state, onChanged: updateSelectedMenuItem, iconOnly: _width == minimizedWidth, ), @@ -262,7 +288,8 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { ), label: "Exit", value: 7, - group: selectedMenuItem, + group: + ref.watch(currentDesktopMenuItemProvider.state).state, onChanged: (_) { // todo: save stuff/ notify before exit? exit(0); diff --git a/lib/pages_desktop_specific/home/notifications/desktop_notifications_view.dart b/lib/pages_desktop_specific/home/notifications/desktop_notifications_view.dart index c8e688aa6..0c51f899d 100644 --- a/lib/pages_desktop_specific/home/notifications/desktop_notifications_view.dart +++ b/lib/pages_desktop_specific/home/notifications/desktop_notifications_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/notifications/notification_card.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/providers/ui/unread_notifications_provider.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; @@ -47,10 +48,25 @@ class _DesktopNotificationsViewState ), ) : ListView.builder( + primary: false, itemCount: notifications.length, itemBuilder: (context, index) { - return NotificationCard( - notification: notifications[index], + final notification = notifications[index]; + if (notification.read == false) { + ref + .read(unreadNotificationsStateProvider.state) + .state + .add(notification.id); + } + + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 5, + ), + child: NotificationCard( + notification: notification, + ), ); }, ), diff --git a/lib/providers/desktop/current_desktop_menu_item.dart b/lib/providers/desktop/current_desktop_menu_item.dart new file mode 100644 index 000000000..6a58db6a0 --- /dev/null +++ b/lib/providers/desktop/current_desktop_menu_item.dart @@ -0,0 +1,5 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages_desktop_specific/home/desktop_menu.dart'; + +final currentDesktopMenuItemProvider = + StateProvider<DesktopMenuItemId>((ref) => DesktopMenuItemId.myStack); From 48bfabf74e67331383bb2e5e3b635d9c1316da72 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 14 Nov 2022 10:49:45 -0600 Subject: [PATCH 237/426] update desktop directory paths for swb --- .../stack_backup_views/helpers/swb_file_system.dart | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart index 82d3fd97d..e88e11dfe 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart @@ -26,11 +26,20 @@ class SWBFileSystem { } debugPrint(rootPath!.absolute.toString()); - Directory sampleFolder = - Directory('${rootPath!.path}Documents/Stack_backups'); + late Directory sampleFolder; + if (Platform.isIOS) { sampleFolder = Directory(rootPath!.path); + } else if (Platform.isAndroid) { + sampleFolder = Directory('${rootPath!.path}Documents/Stack_backups'); + } else if (Platform.isLinux) { + sampleFolder = Directory('${rootPath!.path}/Stack_backups'); + } else if (Platform.isWindows) { + sampleFolder = Directory('${rootPath!.path}/Stack_backups'); + } else if (Platform.isMacOS) { + sampleFolder = Directory('${rootPath!.path}/Stack_backups'); } + try { if (!sampleFolder.existsSync()) { sampleFolder.createSync(recursive: true); From ceaaa0a4f065f6543c48dada95d5c8e4edc10230 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 14 Nov 2022 11:25:34 -0600 Subject: [PATCH 238/426] linter warning clean up and update process dialog popups --- .../backup_and_restore_settings.dart | 8 +- .../create_auto_backup.dart | 228 ++++++++++++------ .../enable_backup_dialog.dart | 5 +- 3 files changed, 162 insertions(+), 79 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart index 0df7c8975..47d1ffdc1 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart @@ -7,6 +7,7 @@ import 'package:stackwallet/pages/settings_views/global_settings_view/stack_back import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart'; +import 'package:stackwallet/providers/global/auto_swb_service_provider.dart'; import 'package:stackwallet/providers/global/locale_provider.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -15,6 +16,7 @@ import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; @@ -23,9 +25,6 @@ import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../../../../providers/global/auto_swb_service_provider.dart'; -import '../../../../widgets/custom_buttons/blue_text_button.dart'; - class BackupRestoreSettings extends ConsumerStatefulWidget { const BackupRestoreSettings({Key? key}) : super(key: key); @@ -99,7 +98,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { useSafeArea: false, barrierDismissible: true, builder: (context) { - return CreateAutoBackup(); + return const CreateAutoBackup(); }, ); } @@ -428,6 +427,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { width: 190, label: "Edit auto backup", onPressed: () { + Navigator.of(context).pop(); createAutoBackup(); }, ), diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart index 02d33fb95..e383d6fe9 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -22,8 +23,8 @@ import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; -import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.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/progress_bar.dart'; @@ -119,10 +120,9 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType "); - bool isEnabledAutoBackup = ref.watch(prefsChangeNotifierProvider - .select((value) => value.isAutoBackupEnabled)); + // bool isEnabledAutoBackup = ref.watch(prefsChangeNotifierProvider + // .select((value) => value.isAutoBackupEnabled)); - String? selectedItem = "Every 10 minutes"; final isDesktop = Util.isDesktop; return DesktopDialog( maxHeight: 680, @@ -140,25 +140,7 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> { textAlign: TextAlign.center, ), ), - Padding( - padding: const EdgeInsets.all(20.0), - child: AppBarIconButton( - color: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - size: 40, - icon: SvgPicture.asset( - Assets.svg.x, - color: Theme.of(context).extension<StackColors>()!.textDark, - width: 22, - height: 22, - ), - onPressed: () { - int count = 0; - Navigator.of(context).popUntil((_) => count++ >= 2); - }, - ), - ), + const DesktopDialogCloseButton(), ], ), const SizedBox( @@ -487,7 +469,7 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> { child: isDesktop ? DropdownButtonHideUnderline( child: DropdownButton2( - offset: Offset(0, -10), + offset: const Offset(0, -10), isExpanded: true, dropdownElevation: 0, value: _currentDropDownValue, @@ -570,12 +552,8 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> { Expanded( child: SecondaryButton( label: "Cancel", - onPressed: () { - int count = 0; - !isEnabledAutoBackup - ? Navigator.of(context).popUntil((_) => count++ >= 2) - : Navigator.of(context).pop(); - }, + desktopMed: true, + onPressed: Navigator.of(context).pop, ), ), const SizedBox( @@ -583,6 +561,7 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> { ), Expanded( child: PrimaryButton( + desktopMed: true, label: "Enable Auto Backup", enabled: shouldEnableCreate, onPressed: !shouldEnableCreate @@ -595,44 +574,89 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> { passphraseRepeatController.text; if (pathToSave.isEmpty) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Directory not chosen", - context: context, + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Directory not chosen", + context: context, + ), ); return; } if (!(await Directory(pathToSave).exists())) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Directory does not exist", - context: context, + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Directory does not exist", + context: context, + ), ); return; } if (passphrase.isEmpty) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "A passphrase is required", - context: context, + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "A passphrase is required", + context: context, + ), ); return; } if (passphrase != repeatPassphrase) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Passphrase does not match", - context: context, + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Passphrase does not match", + context: context, + ), ); return; } - showDialog<dynamic>( - context: context, - barrierDismissible: false, - builder: (_) => const StackDialog( - title: "Encrypting initial backup", - message: "This shouldn't take long", + unawaited( + showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) { + if (Util.isDesktop) { + return DesktopDialog( + maxHeight: double.infinity, + maxWidth: 450, + child: Padding( + padding: const EdgeInsets.all( + 32, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Encrypting initial backup", + style: STextStyles.desktopH3( + context), + ), + const SizedBox( + height: 40, + ), + Text( + "This shouldn't take long", + style: STextStyles + .desktopTextExtraExtraSmall( + context), + ), + ], + ), + ), + ); + } else { + return const StackDialog( + title: "Encrypting initial backup", + message: "This shouldn't take long", + ); + } + }, ), ); @@ -653,10 +677,12 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> { .log("$err\n$s", level: LogLevel.Error); // pop encryption progress dialog Navigator.of(context).pop(); - showFloatingFlushBar( - type: FlushBarType.warning, - message: err, - context: context, + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: err, + context: context, + ), ); return; } catch (e, s) { @@ -664,10 +690,12 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> { .log("$e\n$s", level: LogLevel.Error); // pop encryption progress dialog Navigator.of(context).pop(); - showFloatingFlushBar( - type: FlushBarType.warning, - message: "$e", - context: context, + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "$e", + context: context, + ), ); return; } @@ -698,9 +726,7 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> { if (mounted) { // pop encryption progress dialog - int count = 0; - Navigator.of(context) - .popUntil((_) => count++ >= 2); + Navigator.of(context).pop(); if (result) { ref @@ -717,22 +743,76 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> { await showDialog<dynamic>( context: context, barrierDismissible: false, - builder: (_) => Platform.isAndroid - ? StackOkDialog( - title: - "Stack Auto Backup enabled and saved to:", - message: fileToSave, - ) - : const StackOkDialog( - title: "Stack Auto Backup enabled!"), + builder: (context) { + if (Platform.isAndroid) { + return StackOkDialog( + title: + "Stack Auto Backup enabled and saved to:", + message: fileToSave, + ); + } else if (Util.isDesktop) { + return DesktopDialog( + maxHeight: double.infinity, + maxWidth: 500, + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Text( + "Stack Auto Backup enabled!", + style: + STextStyles.desktopH3( + context), + ), + const DesktopDialogCloseButton(), + ], + ), + const SizedBox( + height: 40, + ), + Row( + children: [ + const Spacer(), + Expanded( + child: PrimaryButton( + label: "Ok", + desktopMed: true, + onPressed: () { + Navigator.of(context) + .pop(); + }, + ), + ), + ], + ) + ], + ), + ), + ); + } else { + return const StackOkDialog( + title: "Stack Auto Backup enabled!", + ); + } + }, ); if (mounted) { passphraseController.text = ""; passphraseRepeatController.text = ""; - int count = 0; - Navigator.of(context) - .popUntil((_) => count++ >= 2); + Navigator.of(context).pop(); } } else { await showDialog<dynamic>( diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart index 963fb4441..6496253d5 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart @@ -18,7 +18,7 @@ class EnableBackupDialog extends StatelessWidget { useSafeArea: false, barrierDismissible: true, builder: (context) { - return CreateAutoBackup(); + return const CreateAutoBackup(); }, ); } @@ -59,6 +59,7 @@ class EnableBackupDialog extends StatelessWidget { children: [ Expanded( child: SecondaryButton( + desktopMed: true, label: "Cancel", onPressed: () { Navigator.of(context).pop(); @@ -70,8 +71,10 @@ class EnableBackupDialog extends StatelessWidget { ), Expanded( child: PrimaryButton( + desktopMed: true, label: "Continue", onPressed: () { + Navigator.of(context).pop(); createAutoBackup(); }, ), From f46c0dacf9f22c8ec62acd6bd46933791b5bd7c3 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 14 Nov 2022 08:26:36 -0700 Subject: [PATCH 239/426] fixed desktop sizing error --- .../desktop_address_book.dart | 245 ++++++++++-------- 1 file changed, 134 insertions(+), 111 deletions(-) diff --git a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart index 367671a3e..e375bbcc7 100644 --- a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart +++ b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/models/contact.dart'; +import 'package:stackwallet/pages/address_book_views/subviews/add_address_book_entry_view.dart'; import 'package:stackwallet/pages/address_book_views/subviews/address_book_filter_view.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -11,6 +12,7 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; @@ -42,7 +44,7 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { useSafeArea: false, barrierDismissible: true, builder: (context) { - return DesktopDialog( + return const DesktopDialog( maxHeight: 609, maxWidth: 576, child: AddressBookFilterView(), @@ -51,6 +53,21 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { ); } + Future<void> newContact() async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return const DesktopDialog( + maxHeight: 609, + maxWidth: 576, + child: AddAddressBookEntryView(), + ); + }, + ); + } + @override void initState() { _searchController = TextEditingController(); @@ -71,6 +88,7 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); final hasWallets = ref.watch(walletsChangeNotifierProvider).hasWallets; + final size = MediaQuery.of(context).size; return Column( mainAxisSize: MainAxisSize.min, @@ -93,127 +111,132 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { const SizedBox(height: 53), Padding( padding: const EdgeInsets.symmetric(horizontal: 24), - child: Row( - children: [ - SizedBox( - height: 60, - width: 489, - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: false, - enableSuggestions: false, - controller: _searchController, - focusNode: _searchFocusNode, - onChanged: (newString) { - setState(() => filter = newString); - }, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search...", - _searchFocusNode, - context, - ).copyWith( - labelStyle: STextStyles.fieldLabel(context) - .copyWith(fontSize: 16), - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, + child: RoundedContainer( + color: Theme.of(context).extension<StackColors>()!.background, + child: Row( + children: [ + SizedBox( + height: 60, + width: size.width - 800, + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: false, + enableSuggestions: false, + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: (newString) { + setState(() => filter = newString); + }, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search...", + _searchFocusNode, + context, + ).copyWith( + labelStyle: STextStyles.fieldLabel(context) + .copyWith(fontSize: 16), + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, - ), - ), - suffixIcon: _searchController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchController.text = ""; - filter = ""; - }); - }, - ), - ], + suffixIcon: _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + filter = ""; + }); + }, + ), + ], + ), ), - ), - ) - : null, + ) + : null, + ), ), ), ), - ), - const SizedBox(width: 20), - TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getDesktopMenuButtonColorSelected(context), - onPressed: () { - selectCryptocurrency(); - }, - child: SizedBox( - width: 200, - height: 56, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: SvgPicture.asset(Assets.svg.filter), - ), - Text( - "Filter", - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, + const SizedBox(width: 20), + TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getDesktopMenuButtonColorSelected(context), + onPressed: () { + selectCryptocurrency(); + }, + child: SizedBox( + width: 200, + height: 56, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: SvgPicture.asset(Assets.svg.filter), ), - ), - ], + Text( + "Filter", + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + ], + ), ), ), - ), - const SizedBox(width: 20), - TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () {}, - child: SizedBox( - width: 200, - height: 56, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: SvgPicture.asset(Assets.svg.circlePlus), - ), - Text( - "Add new", - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .popupBG, + const SizedBox(width: 20), + TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + onPressed: () { + newContact(); + }, + child: SizedBox( + width: 200, + height: 56, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: SvgPicture.asset(Assets.svg.circlePlus), ), - ), - ], + Text( + "Add new", + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .popupBG, + ), + ), + ], + ), ), ), - ), - ], + ], + ), ), ), Padding( From e91c99883b24f8171db4c296845742f7712f552b Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 14 Nov 2022 11:43:00 -0700 Subject: [PATCH 240/426] edit auto backup navigation route error --- .../backup_and_restore/backup_and_restore_settings.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart index 47d1ffdc1..7d86999ac 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart @@ -427,7 +427,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { width: 190, label: "Edit auto backup", onPressed: () { - Navigator.of(context).pop(); + // Navigator.of(context).pop(); createAutoBackup(); }, ), From 27b6293072b8287769d3c72f2171008ee7bd5d9b Mon Sep 17 00:00:00 2001 From: Dan Miller <dan@cypherstack.com> Date: Mon, 14 Nov 2022 11:09:25 -0800 Subject: [PATCH 241/426] Add markdown python lib dependency to linux build script comments. --- scripts/linux/build_secure_storage_deps.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/linux/build_secure_storage_deps.sh b/scripts/linux/build_secure_storage_deps.sh index 378f7a604..7a725d65c 100755 --- a/scripts/linux/build_secure_storage_deps.sh +++ b/scripts/linux/build_secure_storage_deps.sh @@ -20,7 +20,7 @@ cd "$LINUX_DIRECTORY" || exit # Build libSecret # sudo apt install meson libgirepository1.0-dev valac xsltproc gi-docgen docbook-xsl # sudo apt install python3-pip -#pip3 install --user meson --upgrade +#pip3 install --user meson markdown --upgrade # pip3 install --user gi-docgen cd build || exit git -C libsecret pull || git clone https://gitlab.gnome.org/GNOME/libsecret.git libsecret From 29ec03fb87572538f67b6d7832cca8896e8d4706 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 14 Nov 2022 11:40:12 -0600 Subject: [PATCH 242/426] disable swb auto backup desktop popup --- .../backup_and_restore_settings.dart | 169 ++++++++++-------- 1 file changed, 91 insertions(+), 78 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart index 7d86999ac..53ce55424 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart @@ -19,8 +19,10 @@ import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.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'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -149,61 +151,80 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { ), ) : DesktopDialog( - maxHeight: 270, - child: Padding( - padding: EdgeInsets.symmetric(vertical: 20, horizontal: 32), - child: Column( - children: [ - Text( - "Disable Auto Backup", - style: STextStyles.desktopH3(context), - ), - const SizedBox(height: 24), - SizedBox( - width: 600, - child: Text( - "You are turning off Auto Backup. You can turn it back on at any time. " - "Your previous Auto Backup file will not be deleted. Remember to backup your wallets " - "manually so you don't lose important information.", - style: STextStyles.desktopTextSmall(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, + maxHeight: double.infinity, + maxWidth: 580, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Disable Auto Backup", + style: STextStyles.desktopH3(context), ), ), + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, ), - const Spacer(), - Row( - mainAxisAlignment: MainAxisAlignment.center, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - SecondaryButton( - width: 248, - desktopMed: true, - enabled: true, - label: "Back", - onPressed: () { - Navigator.of(context).pop(); - }, + SizedBox( + width: 600, + child: Text( + "You are turning off Auto Backup. You can turn it back on at any time. Your previous Auto Backup file will not be deleted. Remember to backup your wallets manually so you don't lose important information.", + style: STextStyles.desktopTextSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + ), + ), + ), + const SizedBox( + height: 48, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: SecondaryButton( + desktopMed: true, + label: "Cancel", + onPressed: Navigator.of(context).pop, + ), + ), + const SizedBox(width: 16), + Expanded( + child: PrimaryButton( + desktopMed: true, + label: "Disable", + onPressed: () { + ref + .read(prefsChangeNotifierProvider) + .isAutoBackupEnabled = false; + Navigator.of(context).pop(); + }, + ), + ), + ], ), - const SizedBox(width: 20), - PrimaryButton( - width: 248, - desktopMed: true, - enabled: true, - label: "Disable", - onPressed: () { - Navigator.of(context).pop(); - setState(() { - ref - .watch(prefsChangeNotifierProvider) - .isAutoBackupEnabled = false; - }); - }, - ) ], ), - ], - ), + ), + ], ), ); }, @@ -372,40 +393,32 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( + RoundedContainer( width: 403, color: Theme.of(context) .extension<StackColors>()! .background, - child: Padding( - padding: - const EdgeInsets.all(8.0), - child: Column( - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - Text( - "Backed up ${prettySinceLastBackupString(ref.watch(prefsChangeNotifierProvider.select((value) => value.lastAutoBackup)))}", - style: STextStyles - .itemSubtitle( - context), - ), - BlueTextButton( - text: "Back up now", - onTap: () { - ref - .read( - autoSWBServiceProvider) - .doBackup(); - }, - ), - ], - ), - ], - ), + child: Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Text( + "Backed up ${prettySinceLastBackupString(ref.watch(prefsChangeNotifierProvider.select((value) => value.lastAutoBackup)))}", + style: + STextStyles.itemSubtitle( + context), + ), + BlueTextButton( + text: "Back up now", + onTap: () { + ref + .read( + autoSWBServiceProvider) + .doBackup(); + }, + ), + ], ), ), const SizedBox( From bdf100842437628e60f50994cdbd33406c6108c7 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 14 Nov 2022 12:19:26 -0600 Subject: [PATCH 243/426] desktop edit auto swb functionality --- .../edit_auto_backup_view.dart | 1170 +++++++++-------- .../backup_and_restore_settings.dart | 42 +- .../create_auto_backup.dart | 2 +- 3 files changed, 691 insertions(+), 523 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart index 4d3c6ca99..3b6d87b52 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart @@ -1,6 +1,8 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -15,6 +17,7 @@ import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/format.dart'; @@ -22,7 +25,10 @@ import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/progress_bar.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; @@ -51,6 +57,14 @@ class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> { late final SWBFileSystem stackFileSystem; final zxcvbn = Zxcvbn(); + late BackupFrequencyType _currentDropDownValue; + + final List<BackupFrequencyType> _dropDownItems = [ + BackupFrequencyType.everyTenMinutes, + BackupFrequencyType.everyAppStart, + BackupFrequencyType.afterClosingAWallet, + ]; + String passwordFeedback = "Add another word or two. Uncommon words are better. Use a few words, avoid common phrases. No need for symbols, digits, or uppercase letters."; @@ -66,6 +80,157 @@ class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> { passwordRepeatController.text.isNotEmpty; } + void onSavePressed() async { + final String pathToSave = fileLocationController.text; + final String passphrase = passwordController.text; + final String repeatPassphrase = passwordRepeatController.text; + + if (pathToSave.isEmpty) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Directory not chosen", + context: context, + ), + ); + return; + } + if (!(await Directory(pathToSave).exists())) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Directory does not exist", + context: context, + ), + ); + return; + } + if (passphrase.isEmpty) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "A passphrase is required", + context: context, + ), + ); + return; + } + if (passphrase != repeatPassphrase) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Passphrase does not match", + context: context, + ), + ); + return; + } + + unawaited( + showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => const StackDialog( + title: "Updating Auto Backup", + message: "This shouldn't take long", + ), + ), + ); + // make sure the dialog is able to be displayed for at least 1 second + final fut = Future<void>.delayed(const Duration(seconds: 1)); + + String adkString; + int adkVersion; + try { + final adk = await compute(generateAdk, passphrase); + adkString = Format.uint8listToString(adk.item2); + adkVersion = adk.item1; + } on Exception catch (e, s) { + String err = getErrorMessageFromSWBException(e); + Logging.instance.log("$err\n$s", level: LogLevel.Error); + // pop encryption progress dialog + Navigator.of(context).pop(); + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: err, + context: context, + ), + ); + return; + } catch (e, s) { + Logging.instance.log("$e\n$s", level: LogLevel.Error); + // pop encryption progress dialog + Navigator.of(context).pop(); + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "$e", + context: context, + ), + ); + return; + } + + await secureStore.write(key: "auto_adk_string", value: adkString); + await secureStore.write( + key: "auto_adk_version_string", value: adkVersion.toString()); + + final DateTime now = DateTime.now(); + final String fileToSave = createAutoBackupFilename(pathToSave, now); + + final backup = await SWB.createStackWalletJSON( + secureStorage: ref.read(secureStoreProvider), + ); + + bool result = await SWB.encryptStackWalletWithADK( + fileToSave, + adkString, + jsonEncode(backup), + adkVersion: adkVersion, + ); + + // this future should already be complete unless there was an error encrypting + await Future.wait([fut]); + + if (mounted) { + // pop encryption progress dialog + Navigator.of(context).pop(); + + if (result) { + ref.read(prefsChangeNotifierProvider).autoBackupLocation = pathToSave; + ref.read(prefsChangeNotifierProvider).lastAutoBackup = now; + + ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled = true; + + await showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => Platform.isAndroid + ? StackOkDialog( + title: "Stack Auto Backup saved to:", + message: fileToSave, + ) + : const StackOkDialog(title: "Stack Auto Backup saved"), + ); + if (mounted) { + passwordController.text = ""; + passwordRepeatController.text = ""; + + Navigator.of(context) + .popUntil(ModalRoute.withName(AutoBackupView.routeName)); + } + } else { + await showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => + const StackOkDialog(title: "Failed to update Auto Backup"), + ); + } + } + } + @override void initState() { secureStore = ref.read(secureStoreProvider); @@ -77,6 +242,9 @@ class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> { fileLocationController.text = ref.read(prefsChangeNotifierProvider).autoBackupLocation ?? ""; + _currentDropDownValue = + ref.read(prefsChangeNotifierProvider).backupFrequencyType; + passwordFocusNode = FocusNode(); passwordRepeatFocusNode = FocusNode(); @@ -110,547 +278,509 @@ class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + final isDesktop = Util.isDesktop; + + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Edit Auto Backup", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Edit Auto Backup", - style: STextStyles.navBarTitle(context), + body: Padding( + padding: const EdgeInsets.all(16), + child: LayoutBuilder(builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: child, + ), + ), + ); + }), ), ), - body: Padding( - padding: const EdgeInsets.all(16), - child: LayoutBuilder(builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, + child: Column( + crossAxisAlignment: + isDesktop ? CrossAxisAlignment.start : CrossAxisAlignment.stretch, + children: [ + if (!isDesktop) + Text( + "Create your backup", + style: STextStyles.smallMed12(context), + ), + if (isDesktop) + Text( + "Choose file location", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark3, ), - child: IntrinsicHeight( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - "Create your backup", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 10, - ), - if (!Platform.isAndroid) - TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - onTap: Platform.isAndroid - ? null - : () async { - try { - await stackFileSystem.prepareStorage(); + textAlign: TextAlign.left, + ), + const SizedBox( + height: 10, + ), + if (!Platform.isAndroid) + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + onTap: Platform.isAndroid + ? null + : () async { + try { + await stackFileSystem.prepareStorage(); - if (mounted) { - await stackFileSystem.pickDir(context); - } - - if (mounted) { - setState(() { - fileLocationController.text = - stackFileSystem.dirPath ?? ""; - }); - } - } catch (e, s) { - Logging.instance - .log("$e\n$s", level: LogLevel.Error); - } - }, - controller: fileLocationController, - style: STextStyles.field(context), - decoration: InputDecoration( - hintText: "Save to...", - hintStyle: STextStyles.fieldLabel(context), - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox( - width: 16, - ), - SvgPicture.asset( - Assets.svg.folder, - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, - width: 16, - height: 16, - ), - const SizedBox( - width: 12, - ), - ], - ), - ), - ), - key: const Key( - "createBackupSaveToFileLocationTextFieldKey"), - readOnly: true, - toolbarOptions: const ToolbarOptions( - copy: true, - cut: false, - paste: false, - selectAll: false, - ), - onChanged: (newValue) {}, - ), - if (!Platform.isAndroid) - const SizedBox( - height: 10, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("createBackupPasswordFieldKey1"), - focusNode: passwordFocusNode, - controller: passwordController, - style: STextStyles.field(context), - obscureText: hidePassword, - enableSuggestions: false, - autocorrect: false, - decoration: standardInputDecoration( - "Create passphrase", - passwordFocusNode, - context, - ).copyWith( - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox( - width: 16, - ), - GestureDetector( - key: const Key( - "createBackupPasswordFieldShowPasswordButtonKey"), - onTap: () async { - setState(() { - hidePassword = !hidePassword; - }); - }, - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, - width: 16, - height: 16, - ), - ), - const SizedBox( - width: 12, - ), - ], - ), - ), - ), - onChanged: (newValue) { - if (newValue.isEmpty) { - setState(() { - passwordFeedback = ""; - }); - return; - } - final result = zxcvbn.evaluate(newValue); - String suggestionsAndTips = ""; - for (var sug - in result.feedback.suggestions!.toSet()) { - suggestionsAndTips += "$sug\n"; - } - suggestionsAndTips += result.feedback.warning!; - String feedback = - // "Password Strength: ${((result.score! / 4.0) * 100).toInt()}%\n" - suggestionsAndTips; - - passwordStrength = result.score! / 4; - - // hack fix to format back string returned from zxcvbn - if (feedback.contains("phrasesNo need")) { - feedback = feedback.replaceFirst( - "phrasesNo need", "phrases\nNo need"); - } - - if (feedback.endsWith("\n")) { - feedback = - feedback.substring(0, feedback.length - 2); - } + if (mounted) { + await stackFileSystem.pickDir(context); + } + if (mounted) { setState(() { - passwordFeedback = feedback; + fileLocationController.text = + stackFileSystem.dirPath ?? ""; + }); + } + } catch (e, s) { + Logging.instance.log("$e\n$s", level: LogLevel.Error); + } + }, + controller: fileLocationController, + style: STextStyles.field(context), + decoration: InputDecoration( + hintText: "Save to...", + hintStyle: STextStyles.fieldLabel(context), + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox( + width: 16, + ), + SvgPicture.asset( + Assets.svg.folder, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 16, + height: 16, + ), + const SizedBox( + width: 12, + ), + ], + ), + ), + ), + key: const Key("createBackupSaveToFileLocationTextFieldKey"), + readOnly: true, + toolbarOptions: const ToolbarOptions( + copy: true, + cut: false, + paste: false, + selectAll: false, + ), + onChanged: (newValue) {}, + ), + if (isDesktop) + const SizedBox( + height: 24, + ), + if (isDesktop) + Text( + "Create a passphrase", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark3, + ), + textAlign: TextAlign.left, + ), + if (!Platform.isAndroid) + const SizedBox( + height: 10, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("createBackupPasswordFieldKey1"), + focusNode: passwordFocusNode, + controller: passwordController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Create passphrase", + passwordFocusNode, + context, + ).copyWith( + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox( + width: 16, + ), + GestureDetector( + key: const Key( + "createBackupPasswordFieldShowPasswordButtonKey"), + onTap: () async { + setState(() { + hidePassword = !hidePassword; }); }, - ), - ), - if (passwordFocusNode.hasFocus || - passwordRepeatFocusNode.hasFocus || - passwordController.text.isNotEmpty) - Padding( - padding: EdgeInsets.only( - left: 12, - right: 12, - top: passwordFeedback.isNotEmpty ? 4 : 0, - ), - child: passwordFeedback.isNotEmpty - ? Text( - passwordFeedback, - style: STextStyles.infoSmall(context), - ) - : null, - ), - if (passwordFocusNode.hasFocus || - passwordRepeatFocusNode.hasFocus || - passwordController.text.isNotEmpty) - Padding( - padding: const EdgeInsets.only( - left: 12, - right: 12, - top: 10, - ), - child: ProgressBar( - key: const Key("createStackBackUpProgressBar"), - width: MediaQuery.of(context).size.width - 32 - 24, - height: 5, - fillColor: passwordStrength < 0.51 - ? Theme.of(context) - .extension<StackColors>()! - .accentColorRed - : passwordStrength < 1 - ? Theme.of(context) - .extension<StackColors>()! - .accentColorYellow - : Theme.of(context) - .extension<StackColors>()! - .accentColorGreen, - backgroundColor: Theme.of(context) + child: SvgPicture.asset( + hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash, + color: Theme.of(context) .extension<StackColors>()! - .buttonBackSecondary, - percent: - passwordStrength < 0.25 ? 0.03 : passwordStrength, + .textDark3, + width: 16, + height: 16, ), ), - const SizedBox( - height: 10, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + const SizedBox( + width: 12, ), - child: TextField( - key: const Key("createBackupPasswordFieldKey2"), - focusNode: passwordRepeatFocusNode, - controller: passwordRepeatController, - style: STextStyles.field(context), - obscureText: hidePassword, - enableSuggestions: false, - autocorrect: false, - decoration: standardInputDecoration( - "Confirm passphrase", - passwordRepeatFocusNode, - context, - ).copyWith( - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox( - width: 16, - ), - GestureDetector( - key: const Key( - "createBackupPasswordFieldShowPasswordButtonKey"), - onTap: () async { - setState(() { - hidePassword = !hidePassword; - }); - }, - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, - width: 16, - height: 16, - ), - ), - const SizedBox( - width: 12, - ), - ], - ), - ), - ), - onChanged: (newValue) { - setState(() {}); - // TODO: ? check if passwords match? - }, - ), - ), - const SizedBox( - height: 32, - ), - Text( - "Auto Backup frequency", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 10, - ), - Stack( - children: [ - TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - readOnly: true, - textInputAction: TextInputAction.none, - ), - Positioned.fill( - child: RawMaterialButton( - splashColor: Theme.of(context) - .extension<StackColors>()! - .highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - showModalBottomSheet<dynamic>( - backgroundColor: Colors.transparent, - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - builder: (_) => - const BackupFrequencyTypeSelectSheet(), - ); - }, - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 12.0), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - Format.prettyFrequencyType(ref.watch( - prefsChangeNotifierProvider.select( - (value) => - value.backupFrequencyType))), - style: STextStyles.itemSubtitle12(context), - ), - Padding( - padding: const EdgeInsets.only(right: 4.0), - child: SvgPicture.asset( - Assets.svg.chevronDown, - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle2, - width: 12, - height: 6, - ), - ), - ], - ), - ), - ), - ) - ], - ), - const Spacer(), - const SizedBox( - height: 10, - ), - TextButton( - style: shouldEnableCreate - ? Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context) - : Theme.of(context) - .extension<StackColors>()! - .getPrimaryDisabledButtonColor(context), - onPressed: !shouldEnableCreate - ? null - : () async { - final String pathToSave = - fileLocationController.text; - final String passphrase = passwordController.text; - final String repeatPassphrase = - passwordRepeatController.text; + ], + ), + ), + ), + onChanged: (newValue) { + if (newValue.isEmpty) { + setState(() { + passwordFeedback = ""; + }); + return; + } + final result = zxcvbn.evaluate(newValue); + String suggestionsAndTips = ""; + for (var sug in result.feedback.suggestions!.toSet()) { + suggestionsAndTips += "$sug\n"; + } + suggestionsAndTips += result.feedback.warning!; + String feedback = + // "Password Strength: ${((result.score! / 4.0) * 100).toInt()}%\n" + suggestionsAndTips; - if (pathToSave.isEmpty) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Directory not chosen", - context: context, - ); - return; - } - if (!(await Directory(pathToSave).exists())) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Directory does not exist", - context: context, - ); - return; - } - if (passphrase.isEmpty) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "A passphrase is required", - context: context, - ); - return; - } - if (passphrase != repeatPassphrase) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Passphrase does not match", - context: context, - ); - return; - } + passwordStrength = result.score! / 4; - showDialog<dynamic>( - context: context, - barrierDismissible: false, - builder: (_) => const StackDialog( - title: "Updating Auto Backup", - message: "This shouldn't take long", - ), - ); - // make sure the dialog is able to be displayed for at least 1 second - final fut = Future<void>.delayed( - const Duration(seconds: 1)); + // hack fix to format back string returned from zxcvbn + if (feedback.contains("phrasesNo need")) { + feedback = feedback.replaceFirst( + "phrasesNo need", "phrases\nNo need"); + } - String adkString; - int adkVersion; - try { - final adk = - await compute(generateAdk, passphrase); - adkString = Format.uint8listToString(adk.item2); - adkVersion = adk.item1; - } on Exception catch (e, s) { - String err = getErrorMessageFromSWBException(e); - Logging.instance - .log("$err\n$s", level: LogLevel.Error); - // pop encryption progress dialog - Navigator.of(context).pop(); - showFloatingFlushBar( - type: FlushBarType.warning, - message: err, - context: context, - ); - return; - } catch (e, s) { - Logging.instance - .log("$e\n$s", level: LogLevel.Error); - // pop encryption progress dialog - Navigator.of(context).pop(); - showFloatingFlushBar( - type: FlushBarType.warning, - message: "$e", - context: context, - ); - return; - } + if (feedback.endsWith("\n")) { + feedback = feedback.substring(0, feedback.length - 2); + } - await secureStore.write( - key: "auto_adk_string", value: adkString); - await secureStore.write( - key: "auto_adk_version_string", - value: adkVersion.toString()); - - final DateTime now = DateTime.now(); - final String fileToSave = - createAutoBackupFilename(pathToSave, now); - - final backup = await SWB.createStackWalletJSON( - secureStorage: ref.read(secureStoreProvider), - ); - - bool result = await SWB.encryptStackWalletWithADK( - fileToSave, - adkString, - jsonEncode(backup), - adkVersion: adkVersion, - ); - - // this future should already be complete unless there was an error encrypting - await Future.wait([fut]); - - if (mounted) { - // pop encryption progress dialog - Navigator.of(context).pop(); - - if (result) { - ref - .read(prefsChangeNotifierProvider) - .autoBackupLocation = pathToSave; - ref - .read(prefsChangeNotifierProvider) - .lastAutoBackup = now; - - ref - .read(prefsChangeNotifierProvider) - .isAutoBackupEnabled = true; - - await showDialog<dynamic>( - context: context, - barrierDismissible: false, - builder: (_) => Platform.isAndroid - ? StackOkDialog( - title: - "Stack Auto Backup saved to:", - message: fileToSave, - ) - : const StackOkDialog( - title: "Stack Auto Backup saved"), - ); - if (mounted) { - passwordController.text = ""; - passwordRepeatController.text = ""; - - Navigator.of(context).popUntil( - ModalRoute.withName( - AutoBackupView.routeName)); - } - } else { - await showDialog<dynamic>( - context: context, - barrierDismissible: false, - builder: (_) => const StackOkDialog( - title: "Failed to update Auto Backup"), - ); - } - } - }, - child: Text( - "Save", - style: STextStyles.button(context), - ), + setState(() { + passwordFeedback = feedback; + }); + }, + ), + ), + if (passwordFocusNode.hasFocus || + passwordRepeatFocusNode.hasFocus || + passwordController.text.isNotEmpty) + Padding( + padding: EdgeInsets.only( + left: 12, + right: 12, + top: passwordFeedback.isNotEmpty ? 4 : 0, + ), + child: passwordFeedback.isNotEmpty + ? Text( + passwordFeedback, + style: STextStyles.infoSmall(context), ) - ], + : null, + ), + if (passwordFocusNode.hasFocus || + passwordRepeatFocusNode.hasFocus || + passwordController.text.isNotEmpty) + Padding( + padding: const EdgeInsets.only( + left: 12, + right: 12, + top: 10, + ), + child: ProgressBar( + key: const Key("createStackBackUpProgressBar"), + width: isDesktop + ? 492 + : MediaQuery.of(context).size.width - 32 - 24, + height: 5, + fillColor: passwordStrength < 0.51 + ? Theme.of(context).extension<StackColors>()!.accentColorRed + : passwordStrength < 1 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorYellow + : Theme.of(context) + .extension<StackColors>()! + .accentColorGreen, + backgroundColor: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + percent: passwordStrength < 0.25 ? 0.03 : passwordStrength, + ), + ), + SizedBox( + height: isDesktop ? 16 : 10, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("createBackupPasswordFieldKey2"), + focusNode: passwordRepeatFocusNode, + controller: passwordRepeatController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Confirm passphrase", + passwordRepeatFocusNode, + context, + ).copyWith( + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox( + width: 16, + ), + GestureDetector( + key: const Key( + "createBackupPasswordFieldShowPasswordButtonKey"), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 16, + height: 16, + ), + ), + const SizedBox( + width: 12, + ), + ], + ), + ), + ), + onChanged: (newValue) { + setState(() {}); + // TODO: ? check if passwords match? + }, + ), + ), + SizedBox( + height: isDesktop ? 24 : 32, + ), + Text( + "Auto Backup frequency", + style: isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of(context).extension<StackColors>()!.textDark3, + ) + : STextStyles.smallMed12(context), + ), + const SizedBox( + height: 10, + ), + if (isDesktop) + DropdownButtonHideUnderline( + child: DropdownButton2( + offset: const Offset(0, -10), + isExpanded: true, + dropdownElevation: 0, + value: _currentDropDownValue, + items: [ + ..._dropDownItems.map( + (e) { + String message = ""; + switch (e) { + case BackupFrequencyType.everyTenMinutes: + message = "Every 10 minutes"; + break; + case BackupFrequencyType.everyAppStart: + message = "Every app startup"; + break; + case BackupFrequencyType.afterClosingAWallet: + message = "After closing a cryptocurrency wallet"; + break; + } + + return DropdownMenuItem( + value: e, + child: Text(message), + ); + }, + ), + ], + onChanged: (value) { + if (value is BackupFrequencyType) { + if (ref + .read(prefsChangeNotifierProvider) + .backupFrequencyType != + value) { + ref + .read(prefsChangeNotifierProvider) + .backupFrequencyType = value; + } + setState(() { + _currentDropDownValue = value; + }); + } + }, + icon: SvgPicture.asset( + Assets.svg.chevronDown, + width: 10, + height: 5, + color: Theme.of(context).extension<StackColors>()!.textDark3, + ), + buttonPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + buttonDecoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + dropdownDecoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), ), ), - ); - }), + if (!isDesktop) + Stack( + children: [ + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + readOnly: true, + textInputAction: TextInputAction.none, + ), + Positioned.fill( + child: RawMaterialButton( + splashColor: + Theme.of(context).extension<StackColors>()!.highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + showModalBottomSheet<dynamic>( + backgroundColor: Colors.transparent, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + builder: (_) => const BackupFrequencyTypeSelectSheet(), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + Format.prettyFrequencyType(ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.backupFrequencyType))), + style: STextStyles.itemSubtitle12(context), + ), + Padding( + padding: const EdgeInsets.only(right: 4.0), + child: SvgPicture.asset( + Assets.svg.chevronDown, + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle2, + width: 12, + height: 6, + ), + ), + ], + ), + ), + ), + ) + ], + ), + if (!isDesktop) const Spacer(), + SizedBox( + height: isDesktop ? 24 : 10, + ), + if (isDesktop) + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + desktopMed: true, + onPressed: Navigator.of(context).pop, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Save", + desktopMed: true, + enabled: shouldEnableCreate, + onPressed: onSavePressed, + ), + ), + ], + ), + if (!isDesktop) + TextButton( + style: shouldEnableCreate + ? Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context) + : Theme.of(context) + .extension<StackColors>()! + .getPrimaryDisabledButtonColor(context), + onPressed: !shouldEnableCreate ? null : onSavePressed, + child: Text( + "Save", + style: STextStyles.button(context), + ), + ) + ], ), ); } diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart index 53ce55424..663c3f975 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:intl/intl.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart'; @@ -105,6 +106,44 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { ); } + Future<void> editAutoBackup() async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Edit auto backup", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + const Padding( + padding: EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: EditAutoBackupView(), + ), + ], + ), + ), + ); + } + Future<void> attemptDisable() async { final result = await showDialog<bool?>( context: context, @@ -440,8 +479,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { width: 190, label: "Edit auto backup", onPressed: () { - // Navigator.of(context).pop(); - createAutoBackup(); + editAutoBackup(); }, ), ], diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart index e383d6fe9..5e7e86fe1 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart @@ -383,7 +383,7 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> { ), ), const SizedBox( - height: 10, + height: 16, ), ClipRRect( borderRadius: BorderRadius.circular( From e053764554ef5ab32226110a0ed18a9d6ac1c5c5 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 14 Nov 2022 13:24:40 -0600 Subject: [PATCH 244/426] basic desktop change passphrase functionality --- .../home/settings_menu/security_settings.dart | 820 +++++++++--------- lib/utilities/desktop_password_service.dart | 42 + 2 files changed, 474 insertions(+), 388 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/security_settings.dart b/lib/pages_desktop_specific/home/settings_menu/security_settings.dart index 9ee9b5bfc..d752ece38 100644 --- a/lib/pages_desktop_specific/home/settings_menu/security_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/security_settings.dart @@ -1,9 +1,13 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; -import 'package:flutter/src/widgets/framework.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/providers/desktop/storage_crypto_handler_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; @@ -47,6 +51,63 @@ class _SecuritySettings extends ConsumerState<SecuritySettings> { String passwordFeedback = "Add another word or two. Uncommon words are better. Use a few words, avoid common phrases. No need for symbols, digits, or uppercase letters."; + Future<bool> attemptChangePW() async { + final String pw = passwordCurrentController.text; + final String pwNew = passwordController.text; + final String pwNewRepeat = passwordRepeatController.text; + + final verified = + await ref.read(storageCryptoHandlerProvider).verifyPassphrase(pw); + + if (verified) { + if (pwNew != pwNewRepeat) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "New passphrase does not match!", + context: context, + ), + ); + return false; + } else { + final success = + await ref.read(storageCryptoHandlerProvider).changePassphrase( + pw, + pwNew, + ); + + if (success) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "Passphrase successfully changed", + context: context, + ), + ); + return true; + } else { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Passphrase change failed", + context: context, + ), + ); + return false; + } + } + } else { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Current passphrase is not valid!", + context: context, + ), + ); + return false; + } + } + @override void initState() { passwordCurrentController = TextEditingController(); @@ -78,411 +139,394 @@ class _SecuritySettings extends ConsumerState<SecuritySettings> { debugPrint("BUILD: $runtimeType"); return Column( children: [ - Padding( - padding: const EdgeInsets.only( - right: 30, - ), - child: RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SvgPicture.asset( - Assets.svg.circleLock, - width: 48, - height: 48, - ), - Center( - child: Padding( - padding: const EdgeInsets.all(10), - child: RichText( - textAlign: TextAlign.start, - text: TextSpan( - children: [ - TextSpan( - text: "Change Password", - style: STextStyles.desktopTextSmall(context), - ), - TextSpan( - text: - "\n\nProtect your Stack Wallet with a strong password. Stack Wallet does not store " - "your password, and is therefore NOT able to restore it. Keep your password safe and secure.", - style: - STextStyles.desktopTextExtraExtraSmall(context), - ), - ], - ), - ), - ), - ), - Column( + Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + radiusMultiplier: 2, + padding: const EdgeInsets.all(24), + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: EdgeInsets.all( - 10, - ), - child: changePassword - ? SizedBox( - width: 512, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Current password", - style: - STextStyles.desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark3), - textAlign: TextAlign.left, - ), - const SizedBox(height: 10), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + SvgPicture.asset( + Assets.svg.circleLock, + width: 48, + height: 48, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 16, + ), + Text( + "Change Password", + style: STextStyles.desktopTextSmall(context), + ), + const SizedBox( + height: 8, + ), + Text( + "Protect your Stack Wallet with a strong password. Stack Wallet does not store " + "your password, and is therefore NOT able to restore it. Keep your password safe and secure.", + style: + STextStyles.desktopTextExtraExtraSmall(context), + ), + const SizedBox( + height: 20, + ), + changePassword + ? SizedBox( + width: 512, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Current password", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + textAlign: TextAlign.left, ), - child: TextField( - key: const Key( - "desktopSecurityRestoreFromFilePasswordFieldKey"), - focusNode: passwordCurrentFocusNode, - controller: passwordCurrentController, - style: STextStyles.field(context), - obscureText: hidePassword, - enableSuggestions: false, - autocorrect: false, - decoration: standardInputDecoration( - "Enter current password", - passwordCurrentFocusNode, - context, - ).copyWith( - labelStyle: - STextStyles.fieldLabel(context), - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox( - width: 16, - ), - GestureDetector( - key: const Key( - "desktopSecurityRestoreFromFilePasswordFieldShowPasswordButtonKey"), - onTap: () async { - setState(() { - hidePassword = - !hidePassword; - }); - }, - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, - width: 16, - height: 16, - ), - ), - const SizedBox( - width: 12, - ), - ], - ), - ), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - onChanged: (newValue) {}, - ), - ), - const SizedBox(height: 16), - Text( - "New password", - style: - STextStyles.desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark3), - textAlign: TextAlign.left, - ), - const SizedBox(height: 10), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key( - "desktopSecurityCreateNewPasswordFieldKey1"), - focusNode: passwordFocusNode, - controller: passwordController, - style: STextStyles.field(context), - obscureText: hidePassword, - enableSuggestions: false, - autocorrect: false, - decoration: standardInputDecoration( - "Enter new password", - passwordFocusNode, - context, - ).copyWith( - labelStyle: - STextStyles.fieldLabel(context), - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox( - width: 16, - ), - GestureDetector( - key: const Key( - "desktopSecurityCreateNewPasswordButtonKey1"), - onTap: () async { - setState(() { - hidePassword = - !hidePassword; - }); - }, - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, - width: 16, - height: 16, - ), - ), - const SizedBox( - width: 12, - ), - ], - ), - ), - ), - onChanged: (newValue) { - if (newValue.isEmpty) { - setState(() { - passwordFeedback = ""; - }); - return; - } - final result = - zxcvbn.evaluate(newValue); - String suggestionsAndTips = ""; - for (var sug in result - .feedback.suggestions! - .toSet()) { - suggestionsAndTips += "$sug\n"; - } - suggestionsAndTips += - result.feedback.warning!; - String feedback = - // "Password Strength: ${((result.score! / 4.0) * 100).toInt()}%\n" - suggestionsAndTips; - - passwordStrength = result.score! / 4; - - // hack fix to format back string returned from zxcvbn - if (feedback - .contains("phrasesNo need")) { - feedback = feedback.replaceFirst( - "phrasesNo need", - "phrases\nNo need"); - } - - if (feedback.endsWith("\n")) { - feedback = feedback.substring( - 0, feedback.length - 2); - } - - setState(() { - passwordFeedback = feedback; - }); - }, - ), - ), - if (passwordFocusNode.hasFocus || - passwordRepeatFocusNode.hasFocus || - passwordController.text.isNotEmpty) - Padding( - padding: EdgeInsets.only( - left: 12, - right: 12, - top: - passwordFeedback.isNotEmpty ? 4 : 0, - ), - child: passwordFeedback.isNotEmpty - ? Text( - passwordFeedback, - style: STextStyles.infoSmall( - context), - ) - : null, - ), - if (passwordFocusNode.hasFocus || - passwordRepeatFocusNode.hasFocus || - passwordController.text.isNotEmpty) - Padding( - padding: const EdgeInsets.only( - left: 12, - right: 12, - top: 10, - ), - child: ProgressBar( + child: TextField( key: const Key( - "desktopSecurityCreateStackBackUpProgressBar"), - width: 450, - height: 5, - fillColor: passwordStrength < 0.51 - ? Theme.of(context) - .extension<StackColors>()! - .accentColorRed - : passwordStrength < 1 - ? Theme.of(context) - .extension<StackColors>()! - .accentColorYellow - : Theme.of(context) - .extension<StackColors>()! - .accentColorGreen, - backgroundColor: Theme.of(context) - .extension<StackColors>()! - .buttonBackSecondary, - percent: passwordStrength < 0.25 - ? 0.03 - : passwordStrength, - ), - ), - const SizedBox(height: 16), - Text( - "Confirm new password", - style: - STextStyles.desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark3), - textAlign: TextAlign.left, - ), - const SizedBox(height: 10), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key( - "desktopSecurityCreateNewPasswordFieldKey2"), - focusNode: passwordRepeatFocusNode, - controller: passwordRepeatController, - style: STextStyles.field(context), - obscureText: hidePassword, - enableSuggestions: false, - autocorrect: false, - decoration: standardInputDecoration( - "Confirm new password", - passwordRepeatFocusNode, - context, - ).copyWith( - labelStyle: - STextStyles.fieldLabel(context), - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox( - width: 16, - ), - GestureDetector( - key: const Key( - "desktopSecurityCreateNewPasswordButtonKey2"), - onTap: () async { - setState(() { - hidePassword = - !hidePassword; - }); - }, - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, + "desktopSecurityRestoreFromFilePasswordFieldKey"), + focusNode: passwordCurrentFocusNode, + controller: passwordCurrentController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Enter current password", + passwordCurrentFocusNode, + context, + ).copyWith( + labelStyle: + STextStyles.fieldLabel(context), + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox( width: 16, - height: 16, ), - ), - const SizedBox( - width: 12, - ), - ], + GestureDetector( + key: const Key( + "desktopSecurityRestoreFromFilePasswordFieldShowPasswordButtonKey"), + onTap: () async { + setState(() { + hidePassword = + !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension< + StackColors>()! + .textDark3, + width: 16, + height: 16, + ), + ), + const SizedBox( + width: 12, + ), + ], + ), ), ), + onChanged: (newValue) { + setState(() {}); + }, ), - onChanged: (newValue) { - setState(() {}); - }, ), - ), - const SizedBox(height: 20), - PrimaryButton( - width: 142, - desktopMed: true, - enabled: shouldEnableSave, - label: "Save changes", - onPressed: () { - setState(() { - changePassword = false; - }); - }, - ) - ], + const SizedBox(height: 16), + Text( + "New password", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + textAlign: TextAlign.left, + ), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key( + "desktopSecurityCreateNewPasswordFieldKey1"), + focusNode: passwordFocusNode, + controller: passwordController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Enter new password", + passwordFocusNode, + context, + ).copyWith( + labelStyle: + STextStyles.fieldLabel(context), + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox( + width: 16, + ), + GestureDetector( + key: const Key( + "desktopSecurityCreateNewPasswordButtonKey1"), + onTap: () async { + setState(() { + hidePassword = + !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension< + StackColors>()! + .textDark3, + width: 16, + height: 16, + ), + ), + const SizedBox( + width: 12, + ), + ], + ), + ), + ), + onChanged: (newValue) { + if (newValue.isEmpty) { + setState(() { + passwordFeedback = ""; + }); + return; + } + final result = + zxcvbn.evaluate(newValue); + String suggestionsAndTips = ""; + for (var sug in result + .feedback.suggestions! + .toSet()) { + suggestionsAndTips += "$sug\n"; + } + suggestionsAndTips += + result.feedback.warning!; + String feedback = + // "Password Strength: ${((result.score! / 4.0) * 100).toInt()}%\n" + suggestionsAndTips; + + passwordStrength = result.score! / 4; + + // hack fix to format back string returned from zxcvbn + if (feedback + .contains("phrasesNo need")) { + feedback = feedback.replaceFirst( + "phrasesNo need", + "phrases\nNo need"); + } + + if (feedback.endsWith("\n")) { + feedback = feedback.substring( + 0, feedback.length - 2); + } + + setState(() { + passwordFeedback = feedback; + }); + }, + ), + ), + if (passwordFocusNode.hasFocus || + passwordRepeatFocusNode.hasFocus || + passwordController.text.isNotEmpty) + Padding( + padding: EdgeInsets.only( + left: 12, + right: 12, + top: passwordFeedback.isNotEmpty + ? 4 + : 0, + ), + child: passwordFeedback.isNotEmpty + ? Text( + passwordFeedback, + style: STextStyles.infoSmall( + context), + ) + : null, + ), + if (passwordFocusNode.hasFocus || + passwordRepeatFocusNode.hasFocus || + passwordController.text.isNotEmpty) + Padding( + padding: const EdgeInsets.only( + left: 12, + right: 12, + top: 10, + ), + child: ProgressBar( + key: const Key( + "desktopSecurityCreateStackBackUpProgressBar"), + width: 450, + height: 5, + fillColor: passwordStrength < 0.51 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorRed + : passwordStrength < 1 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorYellow + : Theme.of(context) + .extension<StackColors>()! + .accentColorGreen, + backgroundColor: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + percent: passwordStrength < 0.25 + ? 0.03 + : passwordStrength, + ), + ), + const SizedBox(height: 16), + Text( + "Confirm new password", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + textAlign: TextAlign.left, + ), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key( + "desktopSecurityCreateNewPasswordFieldKey2"), + focusNode: passwordRepeatFocusNode, + controller: passwordRepeatController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Confirm new password", + passwordRepeatFocusNode, + context, + ).copyWith( + labelStyle: + STextStyles.fieldLabel(context), + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox( + width: 16, + ), + GestureDetector( + key: const Key( + "desktopSecurityCreateNewPasswordButtonKey2"), + onTap: () async { + setState(() { + hidePassword = + !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension< + StackColors>()! + .textDark3, + width: 16, + height: 16, + ), + ), + const SizedBox( + width: 12, + ), + ], + ), + ), + ), + onChanged: (newValue) { + setState(() {}); + }, + ), + ), + const SizedBox(height: 20), + PrimaryButton( + width: 160, + desktopMed: true, + enabled: shouldEnableSave, + label: "Save changes", + onPressed: () async { + final didChangePW = + await attemptChangePW(); + if (didChangePW) { + setState(() { + changePassword = false; + }); + } + }, + ) + ], + ), + ) + : PrimaryButton( + width: 210, + desktopMed: true, + enabled: true, + label: "Set up new password", + onPressed: () { + setState(() { + changePassword = true; + }); + }, ), - ) - : PrimaryButton( - width: 192, - desktopMed: true, - enabled: true, - label: "Set up new password", - onPressed: () { - setState(() { - changePassword = true; - }); - }, - ), + ], ), ], ), - ], + ), ), - ), + const SizedBox( + width: 40, + ), + ], ), ], ); } } - -class NewPasswordButton extends ConsumerWidget { - const NewPasswordButton({ - Key? key, - }) : super(key: key); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return SizedBox( - width: 200, - height: 48, - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () {}, - child: Text( - "Set up new password", - style: STextStyles.button(context), - ), - ), - ); - } -} diff --git a/lib/utilities/desktop_password_service.dart b/lib/utilities/desktop_password_service.dart index f20525873..9ef83932b 100644 --- a/lib/utilities/desktop_password_service.dart +++ b/lib/utilities/desktop_password_service.dart @@ -115,6 +115,48 @@ class DPS { } } + Future<bool> changePassphrase( + String passphraseOld, + String passphraseNew, + ) async { + final box = await Hive.openBox<String>(DB.boxNameDesktopData); + final keyBlob = DB.instance.get<String>( + boxName: DB.boxNameDesktopData, + key: _kKeyBlobKey, + ); + await box.close(); + + if (keyBlob == null) { + // no passphrase key blob found so any passphrase is technically bad + return false; + } + + if (!(await verifyPassphrase(passphraseOld))) { + return false; + } + + try { + await _handler!.resetPassphrase(passphraseNew); + + final box = await Hive.openBox<String>(DB.boxNameDesktopData); + await DB.instance.put<String>( + boxName: DB.boxNameDesktopData, + key: _kKeyBlobKey, + value: await _handler!.getKeyBlob(), + ); + await box.close(); + + // successfully updated passphrase + return true; + } catch (e, s) { + Logging.instance.log( + "${_getMessageFromException(e)}\n$s", + level: LogLevel.Warning, + ); + return false; + } + } + Future<bool> hasPassword() async { final keyBlob = DB.instance.get<String>( boxName: DB.boxNameDesktopData, From 9df0569bb16fb17583f97c83227d7b83b5cd0068 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 14 Nov 2022 13:35:14 -0600 Subject: [PATCH 245/426] add passphrase check before displaying wallet seed --- .../unlock_wallet_keys_desktop.dart | 59 ++++++++++--------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart index e65820737..23360c98c 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart @@ -1,10 +1,15 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart'; +import 'package:stackwallet/providers/desktop/storage_crypto_handler_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; @@ -196,36 +201,32 @@ class _UnlockWalletKeysDesktopState enabled: continueEnabled, onPressed: continueEnabled ? () async { - // todo: check password - // Navigator.of(context).pop(); - final words = await ref - .read(walletsChangeNotifierProvider) - .getManager(widget.walletId) - .mnemonic; + final verified = await ref + .read(storageCryptoHandlerProvider) + .verifyPassphrase(passwordController.text); - await Navigator.of(context).pushReplacementNamed( - WalletKeysDesktopPopup.routeName, - arguments: words, - ); - // - // await showDialog<void>( - // context: context, - // barrierDismissible: false, - // builder: (context) => Navigator( - // initialRoute: WalletKeysDesktopPopup.routeName, - // onGenerateRoute: RouteGenerator.generateRoute, - // onGenerateInitialRoutes: (_, __) { - // return [ - // RouteGenerator.generateRoute( - // RouteSettings( - // name: WalletKeysDesktopPopup.routeName, - // arguments: words, - // ), - // ) - // ]; - // }, - // ), - // ); + if (verified) { + final words = await ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .mnemonic; + + if (mounted) { + await Navigator.of(context) + .pushReplacementNamed( + WalletKeysDesktopPopup.routeName, + arguments: words, + ); + } + } else { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Invalid passphrase!", + context: context, + ), + ); + } } : null, ), From 9417d78c8170b64a27acf23d24347f2822eff138 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 14 Nov 2022 13:29:43 -0700 Subject: [PATCH 246/426] wip: new contact emoji selection and crypto selection --- .../subviews/add_address_book_entry_view.dart | 858 ++++++++++-------- 1 file changed, 501 insertions(+), 357 deletions(-) diff --git a/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart b/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart index 74f3dfde8..588ca5b26 100644 --- a/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart +++ b/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart @@ -15,15 +15,17 @@ import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:stackwallet/widgets/emoji_select_sheet.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; -import 'package:stackwallet/utilities/util.dart'; - class AddAddressBookEntryView extends ConsumerStatefulWidget { const AddAddressBookEntryView({ Key? key, @@ -108,395 +110,537 @@ class _AddAddressBookEntryViewState Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "New contact", - style: STextStyles.navBarTitle(context), - ), - actions: [ - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("addAddressBookEntryFavoriteButtonKey"), - size: 36, - shadows: const [], - color: Theme.of(context).extension<StackColors>()!.background, - icon: SvgPicture.asset( - Assets.svg.star, - color: _isFavorite - ? Theme.of(context) - .extension<StackColors>()! - .favoriteStarActive - : Theme.of(context) - .extension<StackColors>()! - .favoriteStarInactive, - width: 20, - height: 20, - ), - onPressed: () { - setState(() { - _isFavorite = !_isFavorite; - }); + final isDesktop = Util.isDesktop; + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } }, ), - ), - ), - ], - ), - body: LayoutBuilder( - builder: (context, constraint) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: SingleChildScrollView( - controller: scrollController, - padding: const EdgeInsets.only( - // top: 8, - left: 4, - right: 4, - bottom: 16, + title: Text( + "New contact", + style: STextStyles.navBarTitle(context), ), - child: ConstrainedBox( - constraints: BoxConstraints( - // subtract top and bottom padding set in parent - minHeight: constraint.maxHeight - 16, // - 8, - ), - child: IntrinsicHeight( - child: Column( - children: [ - const SizedBox( - height: 4, + actions: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("addAddressBookEntryFavoriteButtonKey"), + size: 36, + shadows: const [], + color: Theme.of(context) + .extension<StackColors>()! + .background, + icon: SvgPicture.asset( + Assets.svg.star, + color: _isFavorite + ? Theme.of(context) + .extension<StackColors>()! + .favoriteStarActive + : Theme.of(context) + .extension<StackColors>()! + .favoriteStarInactive, + width: 20, + height: 20, ), - GestureDetector( - onTap: () { - if (_selectedEmoji != null) { + onPressed: () { + setState(() { + _isFavorite = !_isFavorite; + }); + }, + ), + ), + ), + ], + ), + body: child); + }, + child: ConditionalParent( + condition: isDesktop, + builder: (child) { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(32), + child: Row( + children: [ + Text( + "New contact", + style: STextStyles.desktopH3(context), + textAlign: TextAlign.center, + ), + const SizedBox(width: 10), + AppBarIconButton( + key: + const Key("addAddressBookEntryFavoriteButtonKey"), + size: 36, + shadows: const [], + color: Theme.of(context) + .extension<StackColors>()! + .background, + icon: SvgPicture.asset( + Assets.svg.star, + color: _isFavorite + ? Theme.of(context) + .extension<StackColors>()! + .favoriteStarActive + : Theme.of(context) + .extension<StackColors>()! + .favoriteStarInactive, + width: 20, + height: 20, + ), + onPressed: () { setState(() { - _selectedEmoji = null; + _isFavorite = !_isFavorite; }); - return; - } - showModalBottomSheet<dynamic>( - backgroundColor: Colors.transparent, - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - builder: (_) => const EmojiSelectSheet(), - ).then((value) { - if (value is Emoji) { + }, + ), + ], + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded(child: child), + ], + ); + }, + child: LayoutBuilder( + builder: (context, constraint) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: SingleChildScrollView( + controller: scrollController, + padding: const EdgeInsets.only( + // top: 8, + left: 4, + right: 4, + bottom: 16, + ), + child: ConstrainedBox( + constraints: BoxConstraints( + // subtract top and bottom padding set in parent + minHeight: constraint.maxHeight - 16, // - 8, + ), + child: IntrinsicHeight( + child: Column( + children: [ + const SizedBox( + height: 4, + ), + GestureDetector( + onTap: () { + if (_selectedEmoji != null) { setState(() { - _selectedEmoji = value; + _selectedEmoji = null; }); + return; } - }); - }, - child: SizedBox( - height: 48, - width: 48, - child: Stack( - children: [ - Container( - height: 48, - width: 48, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - color: Theme.of(context) - .extension<StackColors>()! - .textFieldActiveBG, - ), - child: Center( - child: _selectedEmoji == null - ? SvgPicture.asset( - Assets.svg.user, - height: 24, - width: 24, - ) - : Text( - _selectedEmoji!.char, - style: - STextStyles.pageTitleH1(context), + + ///TODO if desktop make dialog + !isDesktop + ? showModalBottomSheet<dynamic>( + backgroundColor: Colors.transparent, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + builder: (_) => const EmojiSelectSheet(), + ).then((value) { + if (value is Emoji) { + setState(() { + _selectedEmoji = value; + }); + } + }) + : showDialog<dynamic>( + context: context, + builder: (context) { + return DesktopDialog( + maxHeight: 700, + maxWidth: 700, + child: Column( + children: [ + Row( + children: [ + Padding( + padding: + const EdgeInsets.all(32), + child: Text( + "Select emoji", + style: + STextStyles.desktopH3( + context), + textAlign: TextAlign.center, + ), + ), + ], + ), + Expanded( + child: LayoutBuilder( + builder: + (context, constraints) { + return SingleChildScrollView( + scrollDirection: + Axis.vertical, + child: ConstrainedBox( + constraints: + BoxConstraints( + minHeight: constraints + .maxHeight, + minWidth: constraints + .maxWidth, + ), + child: IntrinsicHeight( + child: Column( + children: const [ + Padding( + padding: EdgeInsets + .symmetric( + horizontal: + 32), + // child: + // EmojiSelectSheet(), + ), + ], + ), + ), + ), + ); + }, + ), + ), + ], ), - ), - ), - Align( - alignment: Alignment.bottomRight, - child: Container( - height: 14, - width: 14, + ); + }).then((value) { + if (value is Emoji) { + setState(() { + _selectedEmoji = value; + }); + } + }); + }, + child: SizedBox( + height: 48, + width: 48, + child: Stack( + children: [ + Container( + height: 48, + width: 48, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(14), - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), + borderRadius: BorderRadius.circular(24), + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveBG, + ), child: Center( child: _selectedEmoji == null ? SvgPicture.asset( - Assets.svg.plus, - color: Theme.of(context) - .extension<StackColors>()! - .textWhite, - width: 12, - height: 12, + Assets.svg.user, + height: 24, + width: 24, ) - : SvgPicture.asset( - Assets.svg.thickX, - color: Theme.of(context) - .extension<StackColors>()! - .textWhite, - width: 8, - height: 8, + : Text( + _selectedEmoji!.char, + style: STextStyles.pageTitleH1( + context), ), ), ), - ) - ], - ), - ), - ), - const SizedBox( - height: 8, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: nameController, - focusNode: nameFocusNode, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter contact name", - nameFocusNode, - context, - ).copyWith( - suffixIcon: ref - .read(contactNameIsNotEmptyStateProvider - .state) - .state - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - nameController.text = ""; - }); - }, - ), - ], - ), + Align( + alignment: Alignment.bottomRight, + child: Container( + height: 14, + width: 14, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + child: Center( + child: _selectedEmoji == null + ? SvgPicture.asset( + Assets.svg.plus, + color: Theme.of(context) + .extension<StackColors>()! + .textWhite, + width: 12, + height: 12, + ) + : SvgPicture.asset( + Assets.svg.thickX, + color: Theme.of(context) + .extension<StackColors>()! + .textWhite, + width: 8, + height: 8, + ), ), - ) - : null, + ), + ) + ], + ), ), - onChanged: (newValue) { - ref - .read(contactNameIsNotEmptyStateProvider.state) - .state = newValue.isNotEmpty; - }, ), - ), - if (forms.length <= 1) const SizedBox( height: 8, ), - if (forms.length <= 1) forms[0], - if (forms.length > 1) - for (int i = 0; i < forms.length; i++) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 12, - ), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Address ${i + 1}", - style: STextStyles.smallMed12(context), - ), - BlueTextButton( - onTap: () { - _removeForm(forms[i].id); - }, - text: "Remove", - ), - ], - ), - const SizedBox( - height: 8, - ), - forms[i], - ], + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - const SizedBox( - height: 16, - ), - BlueTextButton( - onTap: () { - _addForm(); - scrollController.animateTo( - scrollController.position.maxScrollExtent + 500, - duration: const Duration(milliseconds: 500), - curve: Curves.easeInOut, - ); - }, - text: "+ Add another address", - ), - // GestureDetector( - // - // child: Text( - // "+ Add another address", - // style: STextStyles.largeMedium14(context), - // ), - // ), - const SizedBox( - height: 16, - ), - const Spacer(), - Row( - children: [ - Expanded( - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - ), - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: nameController, + focusNode: nameFocusNode, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Enter contact name", + nameFocusNode, + context, + ).copyWith( + suffixIcon: ref + .read(contactNameIsNotEmptyStateProvider + .state) + .state + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + nameController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, ), + onChanged: (newValue) { + ref + .read( + contactNameIsNotEmptyStateProvider.state) + .state = newValue.isNotEmpty; + }, ), + ), + if (forms.length <= 1) const SizedBox( - width: 16, + height: 8, ), - Expanded( - child: Builder( - builder: (context) { - bool nameExists = ref - .watch(contactNameIsNotEmptyStateProvider - .state) - .state; - - bool validForms = ref.watch( - validContactStateProvider(forms - .map((e) => e.id) - .toList(growable: false))); - - bool shouldEnableSave = - validForms && nameExists; - - return TextButton( - style: shouldEnableSave - ? Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context) - : Theme.of(context) - .extension<StackColors>()! - .getPrimaryDisabledButtonColor( - context), - onPressed: shouldEnableSave - ? () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration(milliseconds: 75), - ); - } - List<ContactAddressEntry> entries = - []; - for (int i = 0; - i < forms.length; - i++) { - entries.add(ref - .read(addressEntryDataProvider( - forms[i].id)) - .buildAddressEntry()); - } - Contact contact = Contact( - emojiChar: _selectedEmoji?.char, - name: nameController.text, - addresses: entries, - isFavorite: _isFavorite, - ); - - if (await ref - .read(addressBookServiceProvider) - .addContact(contact)) { - if (mounted) { - Navigator.of(context).pop(); - } - // TODO show success notification - } else { - // TODO show error notification - } - } - : null, - child: Text( - "Save", - style: STextStyles.button(context).copyWith( - color: shouldEnableSave - ? Theme.of(context) - .extension<StackColors>()! - .buttonTextPrimary - : Theme.of(context) - .extension<StackColors>()! - .buttonTextPrimaryDisabled, + if (forms.length <= 1) forms[0], + if (forms.length > 1) + for (int i = 0; i < forms.length; i++) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 12, + ), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Address ${i + 1}", + style: STextStyles.smallMed12(context), ), - ), - ); - }, + BlueTextButton( + onTap: () { + _removeForm(forms[i].id); + }, + text: "Remove", + ), + ], + ), + const SizedBox( + height: 8, + ), + forms[i], + ], ), - ), - ], - ), - ], + const SizedBox( + height: 16, + ), + BlueTextButton( + onTap: () { + _addForm(); + scrollController.animateTo( + scrollController.position.maxScrollExtent + 500, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + ); + }, + text: "+ Add another address", + ), + // GestureDetector( + // + // child: Text( + // "+ Add another address", + // style: STextStyles.largeMedium14(context), + // ), + // ), + const SizedBox( + height: 16, + ), + const Spacer(), + Row( + children: [ + Expanded( + child: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Cancel", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: Builder( + builder: (context) { + bool nameExists = ref + .watch(contactNameIsNotEmptyStateProvider + .state) + .state; + + bool validForms = ref.watch( + validContactStateProvider(forms + .map((e) => e.id) + .toList(growable: false))); + + bool shouldEnableSave = + validForms && nameExists; + + return TextButton( + style: shouldEnableSave + ? Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor( + context) + : Theme.of(context) + .extension<StackColors>()! + .getPrimaryDisabledButtonColor( + context), + onPressed: shouldEnableSave + ? () async { + if (FocusScope.of(context) + .hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration( + milliseconds: 75), + ); + } + List<ContactAddressEntry> entries = + []; + for (int i = 0; + i < forms.length; + i++) { + entries.add(ref + .read( + addressEntryDataProvider( + forms[i].id)) + .buildAddressEntry()); + } + Contact contact = Contact( + emojiChar: _selectedEmoji?.char, + name: nameController.text, + addresses: entries, + isFavorite: _isFavorite, + ); + + if (await ref + .read( + addressBookServiceProvider) + .addContact(contact)) { + if (mounted) { + Navigator.of(context).pop(); + } + // TODO show success notification + } else { + // TODO show error notification + } + } + : null, + child: Text( + "Save", + style: + STextStyles.button(context).copyWith( + color: shouldEnableSave + ? Theme.of(context) + .extension<StackColors>()! + .buttonTextPrimary + : Theme.of(context) + .extension<StackColors>()! + .buttonTextPrimaryDisabled, + ), + ), + ); + }, + ), + ), + ], + ), + ], + ), ), ), ), - ), - ); - }, + ); + }, + ), ), ); } From e0555f53a437d37e8c5c951de2c9c7f90448b7eb Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 14 Nov 2022 16:00:09 -0700 Subject: [PATCH 247/426] WIP: desktop address book displays contacts --- .../address_book_views/address_book_view.dart | 553 +++++++++--------- .../desktop_address_book.dart | 31 +- 2 files changed, 288 insertions(+), 296 deletions(-) diff --git a/lib/pages/address_book_views/address_book_view.dart b/lib/pages/address_book_views/address_book_view.dart index c9dd72d72..08c548627 100644 --- a/lib/pages/address_book_views/address_book_view.dart +++ b/lib/pages/address_book_views/address_book_view.dart @@ -13,7 +13,9 @@ import 'package:stackwallet/utilities/constants.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/utilities/util.dart'; import 'package:stackwallet/widgets/address_book_card.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; @@ -21,8 +23,6 @@ import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; -import 'package:stackwallet/utilities/util.dart'; - class AddressBookView extends ConsumerStatefulWidget { const AddressBookView({Key? key, this.coin}) : super(key: key); @@ -103,288 +103,287 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> { final addressBookEntriesFuture = ref.watch( addressBookServiceProvider.select((value) => value.addressBookEntries)); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Address book", - style: STextStyles.navBarTitle(context), - ), - actions: [ - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, + final isDesktop = Util.isDesktop; + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("addressBookFilterViewButton"), - size: 36, - shadows: const [], - color: Theme.of(context).extension<StackColors>()!.background, - icon: SvgPicture.asset( - Assets.svg.filter, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - width: 20, - height: 20, + title: Text( + "Address book", + style: STextStyles.navBarTitle(context), + ), + actions: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, ), - onPressed: () { - Navigator.of(context).pushNamed( - AddressBookFilterView.routeName, - ); - }, - ), - ), - ), - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("addressBookAddNewContactViewButton"), - size: 36, - shadows: const [], - color: Theme.of(context).extension<StackColors>()!.background, - icon: SvgPicture.asset( - Assets.svg.plus, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - width: 20, - height: 20, - ), - onPressed: () { - Navigator.of(context).pushNamed( - AddAddressBookEntryView.routeName, - ); - }, - ), - ), - ), - ], - ), - body: LayoutBuilder( - builder: (builderContext, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: _searchController, - focusNode: _searchFocusNode, - onChanged: (value) { - setState(() { - _searchTerm = value; - }); - }, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - _searchFocusNode, - context, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, - ), - ), - suffixIcon: _searchController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchController.text = ""; - }); - }, - ), - ], - ), - ), - ) - : null, - ), - ), - ), - const SizedBox( - height: 16, - ), - Text( - "Favorites", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 12, - ), - FutureBuilder( - future: addressBookEntriesFuture, - builder: (_, AsyncSnapshot<List<Contact>> snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - _cacheFav = snapshot.data!; - } - if (_cacheFav == null) { - // TODO proper loading animation - return const LoadingIndicator(); - } else { - if (_cacheFav!.isNotEmpty) { - return RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: Column( - children: [ - ..._cacheFav! - .where((element) => element.addresses - .where((e) => ref.watch( - addressBookFilterProvider - .select((value) => value - .coins - .contains(e.coin)))) - .isNotEmpty) - .where((e) => - e.isFavorite && - ref - .read( - addressBookServiceProvider) - .matches(_searchTerm, e)) - .where( - (element) => element.isFavorite) - .map( - (e) => AddressBookCard( - key: Key( - "favContactCard_${e.id}_key"), - contactId: e.id, - ), - ), - ], - ), - ); - } else { - return RoundedWhiteContainer( - child: Center( - child: Text( - "Your favorite contacts will appear here", - style: STextStyles.itemSubtitle(context), - ), - ), - ); - } - } - }, - ), - const SizedBox( - height: 16, - ), - Text( - "All contacts", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 12, - ), - FutureBuilder( - future: addressBookEntriesFuture, - builder: (_, AsyncSnapshot<List<Contact>> snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - _cache = snapshot.data!; - } - if (_cache == null) { - // TODO proper loading animation - return const LoadingIndicator(); - } else { - if (_cache!.isNotEmpty) { - return RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: Column( - children: [ - ..._cache! - .where((element) => element.addresses - .where((e) => ref.watch( - addressBookFilterProvider - .select((value) => value - .coins - .contains(e.coin)))) - .isNotEmpty) - .where((e) => ref - .read(addressBookServiceProvider) - .matches(_searchTerm, e)) - .where( - (element) => !element.isFavorite) - .map( - (e) => AddressBookCard( - key: Key( - "contactCard_${e.id}_key"), - contactId: e.id, - ), - ), - ], - ), - ); - } else { - return RoundedWhiteContainer( - child: Center( - child: Text( - "Your contacts will appear here", - style: STextStyles.itemSubtitle(context), - ), - ), - ); - } - } - }, - ), - ], + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("addressBookFilterViewButton"), + size: 36, + shadows: const [], + color: + Theme.of(context).extension<StackColors>()!.background, + icon: SvgPicture.asset( + Assets.svg.filter, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + width: 20, + height: 20, ), + onPressed: () { + Navigator.of(context).pushNamed( + AddressBookFilterView.routeName, + ); + }, ), ), ), + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("addressBookAddNewContactViewButton"), + size: 36, + shadows: const [], + color: + Theme.of(context).extension<StackColors>()!.background, + icon: SvgPicture.asset( + Assets.svg.plus, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + width: 20, + height: 20, + ), + onPressed: () { + Navigator.of(context).pushNamed( + AddAddressBookEntryView.routeName, + ); + }, + ), + ), + ), + ], + ), + body: LayoutBuilder( + builder: (builderContext, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), + ), + ), + ), + ); + }, + ), + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - ); - }, + child: !isDesktop + ? TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: (value) { + setState(() { + _searchTerm = value; + }); + }, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), + ), + suffixIcon: _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ) + : null, + ), + if (!isDesktop) const SizedBox(height: 16), + Text( + "Favorites", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 12, + ), + FutureBuilder( + future: addressBookEntriesFuture, + builder: (_, AsyncSnapshot<List<Contact>> snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData) { + _cacheFav = snapshot.data!; + } + if (_cacheFav == null) { + // TODO proper loading animation + return const LoadingIndicator(); + } else { + if (_cacheFav!.isNotEmpty) { + return RoundedWhiteContainer( + padding: EdgeInsets.all(!isDesktop ? 0 : 15), + child: Column( + children: [ + ..._cacheFav! + .where((element) => element.addresses + .where((e) => ref.watch( + addressBookFilterProvider.select((value) => + value.coins.contains(e.coin)))) + .isNotEmpty) + .where((e) => + e.isFavorite && + ref + .read(addressBookServiceProvider) + .matches(_searchTerm, e)) + .where((element) => element.isFavorite) + .map( + (e) => AddressBookCard( + key: Key("favContactCard_${e.id}_key"), + contactId: e.id, + ), + ), + ], + ), + ); + } else { + return RoundedWhiteContainer( + child: Center( + child: Text( + "Your favorite contacts will appear here", + style: STextStyles.itemSubtitle(context), + ), + ), + ); + } + } + }, + ), + const SizedBox( + height: 16, + ), + Text( + "All contacts", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 12, + ), + FutureBuilder( + future: addressBookEntriesFuture, + builder: (_, AsyncSnapshot<List<Contact>> snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData) { + _cache = snapshot.data!; + } + if (_cache == null) { + // TODO proper loading animation + return const LoadingIndicator(); + } else { + if (_cache!.isNotEmpty) { + return RoundedWhiteContainer( + padding: EdgeInsets.all(!isDesktop ? 0 : 15), + child: Column( + children: [ + ..._cache! + .where((element) => element.addresses + .where((e) => ref.watch( + addressBookFilterProvider.select((value) => + value.coins.contains(e.coin)))) + .isNotEmpty) + .where((e) => ref + .read(addressBookServiceProvider) + .matches(_searchTerm, e)) + .where((element) => !element.isFavorite) + .map( + (e) => AddressBookCard( + key: Key("desktopContactCard_${e.id}_key"), + contactId: e.id, + ), + ), + ], + ), + ); + } else { + return RoundedWhiteContainer( + child: Center( + child: Text( + "Your contacts will appear here", + style: STextStyles.itemSubtitle(context), + ), + ), + ); + } + } + }, + ), + ], ), ); } diff --git a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart index e375bbcc7..475650722 100644 --- a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart +++ b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/models/contact.dart'; +import 'package:stackwallet/pages/address_book_views/address_book_view.dart'; import 'package:stackwallet/pages/address_book_views/subviews/add_address_book_entry_view.dart'; import 'package:stackwallet/pages/address_book_views/subviews/address_book_filter_view.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; @@ -9,11 +10,11 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; -import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; @@ -36,7 +37,7 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { late bool hasContacts = false; - String filter = ""; + String _searchTerm = ""; Future<void> selectCryptocurrency() async { await showDialog<dynamic>( @@ -123,25 +124,25 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { Constants.size.circularBorderRadius, ), child: TextField( - autocorrect: false, - enableSuggestions: false, + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, controller: _searchController, focusNode: _searchFocusNode, - onChanged: (newString) { - setState(() => filter = newString); + onChanged: (value) { + setState(() { + _searchTerm = value; + }); }, style: STextStyles.field(context), decoration: standardInputDecoration( - "Search...", + "Search", _searchFocusNode, context, ).copyWith( - labelStyle: STextStyles.fieldLabel(context) - .copyWith(fontSize: 16), prefixIcon: Padding( padding: const EdgeInsets.symmetric( horizontal: 10, - vertical: 16, + vertical: 20, ), child: SvgPicture.asset( Assets.svg.search, @@ -160,7 +161,6 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { onTap: () async { setState(() { _searchController.text = ""; - filter = ""; }); }, ), @@ -243,14 +243,7 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 26), child: SizedBox( width: 489, - child: RoundedWhiteContainer( - child: Center( - child: Text( - "Your contacts will appear here", - style: STextStyles.itemSubtitle(context), - ), - ), - ), + child: AddressBookView(), ), ), ], From 3a7f1f9c49854c035e66c4254050a4c7dbfa67e7 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 14 Nov 2022 19:27:36 -0700 Subject: [PATCH 248/426] layout fix for new contact --- .../subviews/add_address_book_entry_view.dart | 659 ++++++++++++------ .../new_contact_address_entry_form.dart | 6 +- 2 files changed, 464 insertions(+), 201 deletions(-) diff --git a/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart b/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart index 588ca5b26..5835c80cd 100644 --- a/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart +++ b/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart @@ -224,7 +224,11 @@ class _AddAddressBookEntryViewState const DesktopDialogCloseButton(), ], ), - Expanded(child: child), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: child, + )), ], ); }, @@ -248,216 +252,473 @@ class _AddAddressBookEntryViewState child: IntrinsicHeight( child: Column( children: [ - const SizedBox( - height: 4, - ), - GestureDetector( - onTap: () { - if (_selectedEmoji != null) { - setState(() { - _selectedEmoji = null; - }); - return; - } + if (!isDesktop) const SizedBox(height: 4), + isDesktop + ? Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + onTap: () { + if (_selectedEmoji != null) { + setState(() { + _selectedEmoji = null; + }); + return; + } - ///TODO if desktop make dialog - !isDesktop - ? showModalBottomSheet<dynamic>( - backgroundColor: Colors.transparent, - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - builder: (_) => const EmojiSelectSheet(), - ).then((value) { - if (value is Emoji) { - setState(() { - _selectedEmoji = value; - }); - } - }) - : showDialog<dynamic>( - context: context, - builder: (context) { - return DesktopDialog( - maxHeight: 700, - maxWidth: 700, - child: Column( - children: [ - Row( - children: [ - Padding( - padding: - const EdgeInsets.all(32), - child: Text( - "Select emoji", - style: - STextStyles.desktopH3( - context), - textAlign: TextAlign.center, - ), + ///TODO if desktop make dialog + !isDesktop + ? showModalBottomSheet<dynamic>( + backgroundColor: + Colors.transparent, + context: context, + shape: + const RoundedRectangleBorder( + borderRadius: + BorderRadius.vertical( + top: Radius.circular(20), ), - ], - ), - Expanded( - child: LayoutBuilder( - builder: - (context, constraints) { - return SingleChildScrollView( - scrollDirection: - Axis.vertical, - child: ConstrainedBox( - constraints: - BoxConstraints( - minHeight: constraints - .maxHeight, - minWidth: constraints - .maxWidth, - ), - child: IntrinsicHeight( - child: Column( - children: const [ - Padding( - padding: EdgeInsets - .symmetric( - horizontal: - 32), - // child: - // EmojiSelectSheet(), + ), + builder: (_) => + const EmojiSelectSheet(), + ).then((value) { + if (value is Emoji) { + setState(() { + _selectedEmoji = value; + }); + } + }) + : showDialog<dynamic>( + context: context, + builder: (context) { + return DesktopDialog( + maxHeight: 700, + maxWidth: 700, + child: Column( + children: [ + Row( + children: [ + Padding( + padding: + const EdgeInsets + .all(32), + child: Text( + "Select emoji", + style: STextStyles + .desktopH3( + context), + textAlign: + TextAlign + .center, ), - ], + ), + ], + ), + Expanded( + child: LayoutBuilder( + builder: (context, + constraints) { + return SingleChildScrollView( + scrollDirection: + Axis.vertical, + child: + ConstrainedBox( + constraints: + BoxConstraints( + minHeight: + constraints + .maxHeight, + minWidth: + constraints + .maxWidth, + ), + child: + IntrinsicHeight( + child: Column( + children: const [ + Padding( + padding: + EdgeInsets.symmetric(horizontal: 32), + // child: + // EmojiSelectSheet(), + ), + ], + ), + ), + ), + ); + }, ), ), + ], + ), + ); + }).then((value) { + if (value is Emoji) { + setState(() { + _selectedEmoji = value; + }); + } + }); + }, + child: SizedBox( + height: 56, + width: 56, + child: Stack( + children: [ + Container( + height: 56, + width: 56, + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(24), + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveBG, + ), + child: Center( + child: _selectedEmoji == null + ? SvgPicture.asset( + Assets.svg.user, + height: 30, + width: 30, + ) + : Text( + _selectedEmoji!.char, + style: STextStyles + .pageTitleH1(context), ), - ); - }, + ), + ), + Align( + alignment: Alignment.bottomRight, + child: Container( + height: 14, + width: 14, + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(14), + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + child: Center( + child: _selectedEmoji == null + ? SvgPicture.asset( + Assets.svg.plus, + color: Theme.of(context) + .extension< + StackColors>()! + .textWhite, + width: 12, + height: 12, + ) + : SvgPicture.asset( + Assets.svg.thickX, + color: Theme.of(context) + .extension< + StackColors>()! + .textWhite, + width: 8, + height: 8, + ), ), ), - ], - ), - ); - }).then((value) { - if (value is Emoji) { - setState(() { - _selectedEmoji = value; - }); - } - }); - }, - child: SizedBox( - height: 48, - width: 48, - child: Stack( - children: [ - Container( - height: 48, - width: 48, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - color: Theme.of(context) - .extension<StackColors>()! - .textFieldActiveBG, - ), - child: Center( - child: _selectedEmoji == null - ? SvgPicture.asset( - Assets.svg.user, - height: 24, - width: 24, ) - : Text( - _selectedEmoji!.char, - style: STextStyles.pageTitleH1( - context), - ), - ), - ), - Align( - alignment: Alignment.bottomRight, - child: Container( - height: 14, - width: 14, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(14), - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - child: Center( - child: _selectedEmoji == null - ? SvgPicture.asset( - Assets.svg.plus, - color: Theme.of(context) - .extension<StackColors>()! - .textWhite, - width: 12, - height: 12, - ) - : SvgPicture.asset( - Assets.svg.thickX, - color: Theme.of(context) - .extension<StackColors>()! - .textWhite, - width: 8, - height: 8, - ), + ], + ), ), ), - ) - ], - ), - ), - ), - const SizedBox( - height: 8, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: nameController, - focusNode: nameFocusNode, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter contact name", - nameFocusNode, - context, - ).copyWith( - suffixIcon: ref - .read(contactNameIsNotEmptyStateProvider - .state) - .state - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - nameController.text = ""; - }); - }, - ), - ], - ), + const SizedBox(width: 8), + SizedBox( + width: isDesktop ? 450 : null, + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - ) - : null, - ), - onChanged: (newValue) { - ref - .read( - contactNameIsNotEmptyStateProvider.state) - .state = newValue.isNotEmpty; - }, - ), - ), + child: TextField( + autocorrect: + Util.isDesktop ? false : true, + enableSuggestions: + Util.isDesktop ? false : true, + controller: nameController, + focusNode: nameFocusNode, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Enter contact name", + nameFocusNode, + context, + ).copyWith( + labelStyle: + STextStyles.fieldLabel(context), + suffixIcon: ref + .read( + contactNameIsNotEmptyStateProvider + .state) + .state + ? Padding( + padding: + const EdgeInsets.only( + right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + nameController + .text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + onChanged: (newValue) { + ref + .read( + contactNameIsNotEmptyStateProvider + .state) + .state = newValue.isNotEmpty; + }, + ), + ), + ), + ], + ) + : Column( + children: [ + GestureDetector( + onTap: () { + if (_selectedEmoji != null) { + setState(() { + _selectedEmoji = null; + }); + return; + } + + ///TODO if desktop make dialog + !isDesktop + ? showModalBottomSheet<dynamic>( + backgroundColor: + Colors.transparent, + context: context, + shape: + const RoundedRectangleBorder( + borderRadius: + BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + builder: (_) => + const EmojiSelectSheet(), + ).then((value) { + if (value is Emoji) { + setState(() { + _selectedEmoji = value; + }); + } + }) + : showDialog<dynamic>( + context: context, + builder: (context) { + return DesktopDialog( + maxHeight: 700, + maxWidth: 700, + child: Column( + children: [ + Row( + children: [ + Padding( + padding: + const EdgeInsets + .all(32), + child: Text( + "Select emoji", + style: STextStyles + .desktopH3( + context), + textAlign: + TextAlign + .center, + ), + ), + ], + ), + Expanded( + child: LayoutBuilder( + builder: (context, + constraints) { + return SingleChildScrollView( + scrollDirection: + Axis.vertical, + child: + ConstrainedBox( + constraints: + BoxConstraints( + minHeight: + constraints + .maxHeight, + minWidth: + constraints + .maxWidth, + ), + child: + IntrinsicHeight( + child: Column( + children: const [ + Padding( + padding: + EdgeInsets.symmetric(horizontal: 32), + // child: + // EmojiSelectSheet(), + ), + ], + ), + ), + ), + ); + }, + ), + ), + ], + ), + ); + }).then((value) { + if (value is Emoji) { + setState(() { + _selectedEmoji = value; + }); + } + }); + }, + child: SizedBox( + height: 48, + width: 48, + child: Stack( + children: [ + Container( + height: 48, + width: 48, + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(24), + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveBG, + ), + child: Center( + child: _selectedEmoji == null + ? SvgPicture.asset( + Assets.svg.user, + height: 24, + width: 24, + ) + : Text( + _selectedEmoji!.char, + style: STextStyles + .pageTitleH1(context), + ), + ), + ), + Align( + alignment: Alignment.bottomRight, + child: Container( + height: 14, + width: 14, + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(14), + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + child: Center( + child: _selectedEmoji == null + ? SvgPicture.asset( + Assets.svg.plus, + color: Theme.of(context) + .extension< + StackColors>()! + .textWhite, + width: 12, + height: 12, + ) + : SvgPicture.asset( + Assets.svg.thickX, + color: Theme.of(context) + .extension< + StackColors>()! + .textWhite, + width: 8, + height: 8, + ), + ), + ), + ) + ], + ), + ), + ), + const SizedBox(height: 8), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: + Util.isDesktop ? false : true, + enableSuggestions: + Util.isDesktop ? false : true, + controller: nameController, + focusNode: nameFocusNode, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Enter contact name", + nameFocusNode, + context, + ).copyWith( + suffixIcon: ref + .read( + contactNameIsNotEmptyStateProvider + .state) + .state + ? Padding( + padding: const EdgeInsets.only( + right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + nameController + .text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + onChanged: (newValue) { + ref + .read( + contactNameIsNotEmptyStateProvider + .state) + .state = newValue.isNotEmpty; + }, + ), + ), + ], + ), + if (!isDesktop) const SizedBox(height: 8), if (forms.length <= 1) const SizedBox( height: 8, diff --git a/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart b/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart index ce98cee10..b6cf0aad4 100644 --- a/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart +++ b/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart @@ -14,14 +14,13 @@ import 'package:stackwallet/utilities/enums/coin_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/utilities/util.dart'; import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart'; import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; -import 'package:stackwallet/utilities/util.dart'; - class NewContactAddressEntryForm extends ConsumerStatefulWidget { const NewContactAddressEntryForm({ Key? key, @@ -70,6 +69,7 @@ class _NewContactAddressEntryFormState @override Widget build(BuildContext context) { + final isDesktop = Util.isDesktop; return Column( children: [ TextField( @@ -168,6 +168,7 @@ class _NewContactAddressEntryFormState addressLabelFocusNode, context, ).copyWith( + labelStyle: isDesktop ? STextStyles.fieldLabel(context) : null, suffixIcon: addressLabelController.text.isNotEmpty ? Padding( padding: const EdgeInsets.only(right: 0), @@ -212,6 +213,7 @@ class _NewContactAddressEntryFormState addressFocusNode, context, ).copyWith( + labelStyle: isDesktop ? STextStyles.fieldLabel(context) : null, suffixIcon: UnconstrainedBox( child: Row( children: [ From e372db770860164485ae2ffd2c34fc2e14fc5ec6 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 14 Nov 2022 20:55:11 -0700 Subject: [PATCH 249/426] height size issue --- .../address_book_views/address_book_view.dart | 351 +++++++++--------- 1 file changed, 183 insertions(+), 168 deletions(-) diff --git a/lib/pages/address_book_views/address_book_view.dart b/lib/pages/address_book_views/address_book_view.dart index 08c548627..50e51110b 100644 --- a/lib/pages/address_book_views/address_book_view.dart +++ b/lib/pages/address_book_views/address_book_view.dart @@ -209,181 +209,196 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> { ), ); }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: !isDesktop - ? TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: _searchController, - focusNode: _searchFocusNode, - onChanged: (value) { - setState(() { - _searchTerm = value; - }); - }, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - _searchFocusNode, - context, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: MediaQuery.of(context).size.height - 271, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: !isDesktop + ? TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: (value) { + setState(() { + _searchTerm = value; + }); + }, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), ), + suffixIcon: _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, ), - suffixIcon: _searchController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchController.text = ""; - }); - }, - ), - ], + ) + : null, + ), + if (!isDesktop) const SizedBox(height: 16), + Text( + "Favorites", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 12, + ), + FutureBuilder( + future: addressBookEntriesFuture, + builder: (_, AsyncSnapshot<List<Contact>> snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData) { + _cacheFav = snapshot.data!; + } + if (_cacheFav == null) { + // TODO proper loading animation + return const LoadingIndicator(); + } else { + if (_cacheFav!.isNotEmpty) { + return RoundedWhiteContainer( + padding: EdgeInsets.all(!isDesktop ? 0 : 15), + child: Column( + children: [ + ..._cacheFav! + .where((element) => element.addresses + .where((e) => ref.watch( + addressBookFilterProvider.select( + (value) => + value.coins.contains(e.coin)))) + .isNotEmpty) + .where((e) => + e.isFavorite && + ref + .read(addressBookServiceProvider) + .matches(_searchTerm, e)) + .where((element) => element.isFavorite) + .map( + (e) => AddressBookCard( + key: Key("favContactCard_${e.id}_key"), + contactId: e.id, ), ), - ) - : null, - ), - ) - : null, - ), - if (!isDesktop) const SizedBox(height: 16), - Text( - "Favorites", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 12, - ), - FutureBuilder( - future: addressBookEntriesFuture, - builder: (_, AsyncSnapshot<List<Contact>> snapshot) { - if (snapshot.connectionState == ConnectionState.done && - snapshot.hasData) { - _cacheFav = snapshot.data!; - } - if (_cacheFav == null) { - // TODO proper loading animation - return const LoadingIndicator(); - } else { - if (_cacheFav!.isNotEmpty) { - return RoundedWhiteContainer( - padding: EdgeInsets.all(!isDesktop ? 0 : 15), - child: Column( - children: [ - ..._cacheFav! - .where((element) => element.addresses - .where((e) => ref.watch( - addressBookFilterProvider.select((value) => - value.coins.contains(e.coin)))) - .isNotEmpty) - .where((e) => - e.isFavorite && - ref - .read(addressBookServiceProvider) - .matches(_searchTerm, e)) - .where((element) => element.isFavorite) - .map( - (e) => AddressBookCard( - key: Key("favContactCard_${e.id}_key"), - contactId: e.id, - ), - ), - ], - ), - ); - } else { - return RoundedWhiteContainer( - child: Center( - child: Text( - "Your favorite contacts will appear here", - style: STextStyles.itemSubtitle(context), + ], ), - ), - ); - } - } - }, - ), - const SizedBox( - height: 16, - ), - Text( - "All contacts", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 12, - ), - FutureBuilder( - future: addressBookEntriesFuture, - builder: (_, AsyncSnapshot<List<Contact>> snapshot) { - if (snapshot.connectionState == ConnectionState.done && - snapshot.hasData) { - _cache = snapshot.data!; - } - if (_cache == null) { - // TODO proper loading animation - return const LoadingIndicator(); - } else { - if (_cache!.isNotEmpty) { - return RoundedWhiteContainer( - padding: EdgeInsets.all(!isDesktop ? 0 : 15), - child: Column( - children: [ - ..._cache! - .where((element) => element.addresses - .where((e) => ref.watch( - addressBookFilterProvider.select((value) => - value.coins.contains(e.coin)))) - .isNotEmpty) - .where((e) => ref - .read(addressBookServiceProvider) - .matches(_searchTerm, e)) - .where((element) => !element.isFavorite) - .map( - (e) => AddressBookCard( - key: Key("desktopContactCard_${e.id}_key"), - contactId: e.id, - ), - ), - ], - ), - ); - } else { - return RoundedWhiteContainer( - child: Center( - child: Text( - "Your contacts will appear here", - style: STextStyles.itemSubtitle(context), + ); + } else { + return RoundedWhiteContainer( + child: Center( + child: Text( + "Your favorite contacts will appear here", + style: STextStyles.itemSubtitle(context), + ), ), - ), - ); + ); + } } - } - }, - ), - ], + }, + ), + const SizedBox( + height: 16, + ), + Text( + "All contacts", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 12, + ), + FutureBuilder( + future: addressBookEntriesFuture, + builder: (_, AsyncSnapshot<List<Contact>> snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData) { + _cache = snapshot.data!; + } + if (_cache == null) { + // TODO proper loading animation + return const LoadingIndicator(); + } else { + if (_cache!.isNotEmpty) { + return Column( + children: [ + RoundedWhiteContainer( + padding: EdgeInsets.all(!isDesktop ? 0 : 15), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + ..._cache! + .where((element) => element.addresses + .where((e) => ref.watch( + addressBookFilterProvider.select( + (value) => value.coins + .contains(e.coin)))) + .isNotEmpty) + .where((e) => ref + .read(addressBookServiceProvider) + .matches(_searchTerm, e)) + .where((element) => !element.isFavorite) + .map( + (e) => AddressBookCard( + key: Key( + "desktopContactCard_${e.id}_key"), + contactId: e.id, + ), + ), + ], + ), + ), + ), + ], + ); + } else { + return RoundedWhiteContainer( + child: Center( + child: Text( + "Your contacts will appear here", + style: STextStyles.itemSubtitle(context), + ), + ), + ); + } + } + }, + ), + ], + ), ), ); } From 591edeca63a2b7899e908ef9d493d2dc3ef09a6c Mon Sep 17 00:00:00 2001 From: Dan Miller <dan@cypherstack.com> Date: Mon, 14 Nov 2022 20:40:44 -0800 Subject: [PATCH 250/426] Fix RFC6068 mailto link for support on desktop plaftorm. --- .../settings_views/global_settings_view/support_view.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/support_view.dart b/lib/pages/settings_views/global_settings_view/support_view.dart index 20aeedf61..6bf3544ae 100644 --- a/lib/pages/settings_views/global_settings_view/support_view.dart +++ b/lib/pages/settings_views/global_settings_view/support_view.dart @@ -330,7 +330,7 @@ class SupportView extends StatelessWidget { onPressed: () { if (!isDesktop) { launchUrl( - Uri.parse("mailto://support@stackwallet.com"), + Uri.parse("mailto:support@stackwallet.com"), mode: LaunchMode.externalApplication, ); } @@ -367,7 +367,7 @@ class SupportView extends StatelessWidget { text: isDesktop ? "support@stackwallet.com" : "", onTap: () { launchUrl( - Uri.parse("mailto://support@stackwallet.com"), + Uri.parse("mailto:support@stackwallet.com"), mode: LaunchMode.externalApplication, ); }, From 4a908564980fc4e426df61479a36192447aaa9e1 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Tue, 15 Nov 2022 12:12:38 -0700 Subject: [PATCH 251/426] WIP: node settings scrollable --- .../home/settings_menu/nodes_settings.dart | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart b/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart index 1d0317037..2343e3990 100644 --- a/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart @@ -11,8 +11,10 @@ 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/utilities/util.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; class NodesSettings extends ConsumerStatefulWidget { const NodesSettings({Key? key}) : super(key: key); @@ -29,6 +31,8 @@ class _NodesSettings extends ConsumerState<NodesSettings> { late final TextEditingController searchNodeController; late final FocusNode searchNodeFocusNode; + late final ScrollController nodeScrollController; + String filter = ""; @override @@ -39,6 +43,8 @@ class _NodesSettings extends ConsumerState<NodesSettings> { searchNodeController = TextEditingController(); searchNodeFocusNode = FocusNode(); + nodeScrollController = ScrollController(); + super.initState(); } @@ -134,6 +140,26 @@ class _NodesSettings extends ConsumerState<NodesSettings> { height: 16, ), ), + suffixIcon: searchNodeController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + searchNodeController.text = ""; + filter = ""; + }); + }, + ), + ], + ), + ), + ) + : null, ), ), ), @@ -145,6 +171,9 @@ class _NodesSettings extends ConsumerState<NodesSettings> { borderColor: Theme.of(context).extension<StackColors>()!.background, child: ListView.separated( + controller: nodeScrollController, + physics: const AlwaysScrollableScrollPhysics(), + scrollDirection: Axis.vertical, primary: false, shrinkWrap: true, itemBuilder: (context, index) { From 8adec7ba5cba884d14181b78ad7056d5714cf21b Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Tue, 15 Nov 2022 12:29:08 -0700 Subject: [PATCH 252/426] auto frequency dark mode text fix --- .../stack_backup_views/edit_auto_backup_view.dart | 8 +++++++- .../backup_and_restore/create_auto_backup.dart | 6 +++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart index 3b6d87b52..76d280980 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart @@ -424,6 +424,7 @@ class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> { passwordFocusNode, context, ).copyWith( + labelStyle: isDesktop ? STextStyles.fieldLabel(context) : null, suffixIcon: UnconstrainedBox( child: Row( children: [ @@ -555,6 +556,7 @@ class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> { passwordRepeatFocusNode, context, ).copyWith( + labelStyle: isDesktop ? STextStyles.fieldLabel(context) : null, suffixIcon: UnconstrainedBox( child: Row( children: [ @@ -631,7 +633,11 @@ class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> { return DropdownMenuItem( value: e, - child: Text(message), + child: Text( + message, + style: + STextStyles.desktopTextExtraExtraSmall(context), + ), ); }, ), diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart index 5e7e86fe1..663136dba 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart @@ -492,7 +492,11 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> { return DropdownMenuItem( value: e, - child: Text(message), + child: Text( + message, + style: STextStyles.desktopTextExtraExtraSmall( + context), + ), ); }, ), From a5d925fb98e8ab0c00cf37cafec6d20ba4f4d214 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Tue, 15 Nov 2022 13:42:48 -0700 Subject: [PATCH 253/426] WIP: manual backup nav route error --- .../create_backup_view.dart | 121 ++++++++++++++++-- 1 file changed, 109 insertions(+), 12 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart index b7ee6b4be..821504bf8 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart @@ -18,6 +18,7 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.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/progress_bar.dart'; @@ -523,7 +524,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { if (mounted) { // pop encryption progress dialog - Navigator.of(context).pop(); + if (!isDesktop) Navigator.of(context).pop(); if (result) { await showDialog<dynamic>( @@ -607,14 +608,52 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { return; } - unawaited(showDialog<dynamic>( - context: context, - barrierDismissible: false, - builder: (_) => const StackDialog( - title: "Encrypting backup", - message: "This shouldn't take long", + unawaited( + showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) { + if (Util.isDesktop) { + return DesktopDialog( + maxHeight: double.infinity, + maxWidth: 450, + child: Padding( + padding: const EdgeInsets.all( + 32, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Encrypting initial backup", + style: + STextStyles.desktopH3( + context), + ), + const SizedBox( + height: 40, + ), + Text( + "This shouldn't take long", + style: STextStyles + .desktopTextExtraExtraSmall( + context), + ), + ], + ), + ), + ); + } else { + return const StackDialog( + title: "Encrypting initial backup", + message: "This shouldn't take long", + ); + } + }, ), - )); + ); // make sure the dialog is able to be displayed for at least 1 second await Future<void>.delayed( const Duration(seconds: 1)); @@ -637,7 +676,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { if (mounted) { // pop encryption progress dialog - Navigator.of(context).pop(); + if (!isDesktop) Navigator.of(context).pop(); if (result) { await showDialog<dynamic>( @@ -648,9 +687,67 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { title: "Backup saved to:", message: fileToSave, ) - : const StackOkDialog( - title: - "Backup creation succeeded"), + : !isDesktop + ? const StackOkDialog( + title: + "Backup creation succeeded") + : DesktopDialog( + maxHeight: double.infinity, + maxWidth: 500, + child: Padding( + padding: + const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: Column( + mainAxisSize: + MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + const SizedBox( + height: 26), + Text( + "Stack backup saved to: \n", + style: STextStyles + .desktopH3( + context), + ), + Text( + fileToSave, + style: STextStyles + .desktopTextExtraExtraSmall( + context), + ), + const SizedBox( + height: 40, + ), + Row( + children: [ + // const Spacer(), + Expanded( + child: + PrimaryButton( + label: "Ok", + desktopMed: + true, + onPressed: + () { + // Navigator.of( + // context) + // .pop(); + }, + ), + ), + ], + ) + ], + ), + ), + ), ); passwordController.text = ""; passwordRepeatController.text = ""; From 2ec1bda6f2ff3b51f893ed8d4793249a20f1a715 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Tue, 15 Nov 2022 13:45:06 -0700 Subject: [PATCH 254/426] desktop address book contact buttons --- lib/widgets/address_book_card.dart | 180 ++++++++++++++--------------- 1 file changed, 88 insertions(+), 92 deletions(-) diff --git a/lib/widgets/address_book_card.dart b/lib/widgets/address_book_card.dart index cebcf166f..c9ac86052 100644 --- a/lib/widgets/address_book_card.dart +++ b/lib/widgets/address_book_card.dart @@ -9,7 +9,6 @@ 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/utilities/util.dart'; -import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class AddressBookCard extends ConsumerStatefulWidget { @@ -59,111 +58,108 @@ class _AddressBookCardState extends ConsumerState<AddressBookCard> { } } - return ConditionalParent( - condition: !isDesktop, - child: Row( - children: [ - Container( - width: 32, - height: 32, - decoration: BoxDecoration( - color: contact.id == "default" - ? Theme.of(context) - .extension<StackColors>()! - .myStackContactIconBG - : Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - borderRadius: BorderRadius.circular(32), + return RoundedWhiteContainer( + padding: const EdgeInsets.all(4), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + showDialog<void>( + context: context, + useSafeArea: true, + barrierDismissible: true, + builder: (_) => ContactPopUp( + contactId: contact.id, ), - child: contact.id == "default" - ? Center( - child: SvgPicture.asset( - Assets.svg.stackIcon(context), - width: 20, - ), - ) - : contact.emojiChar != null + ); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: contact.id == "default" + ? Theme.of(context) + .extension<StackColors>()! + .myStackContactIconBG + : Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular(32), + ), + child: contact.id == "default" ? Center( - child: Text(contact.emojiChar!), - ) - : Center( child: SvgPicture.asset( - Assets.svg.user, - width: 18, + Assets.svg.stackIcon(context), + width: 20, ), - ), - ), - const SizedBox( - width: 12, - ), - if (isDesktop) - Text( - contact.name, - style: STextStyles.itemSubtitle12(context), - ), - if (isDesktop) - const SizedBox( - width: 16, - ), - if (isDesktop) - Text( - coinsString, - style: STextStyles.label(context), - ), - if (!isDesktop) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ + ) + : contact.emojiChar != null + ? Center( + child: Text(contact.emojiChar!), + ) + : Center( + child: SvgPicture.asset( + Assets.svg.user, + width: 18, + ), + ), + ), + const SizedBox( + width: 12, + ), + if (isDesktop) Text( contact.name, style: STextStyles.itemSubtitle12(context), ), + if (isDesktop) const SizedBox( - height: 4, + width: 16, ), + if (isDesktop) Text( coinsString, style: STextStyles.label(context), ), - ], - ), - if (isDesktop) const Spacer(), - if (isDesktop) - SvgPicture.asset( - widget.indicatorDown == true - ? Assets.svg.chevronDown - : Assets.svg.chevronUp, - width: 10, - height: 5, - color: Theme.of(context).extension<StackColors>()!.textSubtitle2, - ), - ], - ), - builder: (child) => RoundedWhiteContainer( - padding: const EdgeInsets.all(4), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - padding: const EdgeInsets.all(0), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - showDialog<void>( - context: context, - useSafeArea: true, - barrierDismissible: true, - builder: (_) => ContactPopUp( - contactId: contact.id, - ), - ); - }, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: child, + if (!isDesktop) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + contact.name, + style: STextStyles.itemSubtitle12(context), + ), + const SizedBox( + height: 4, + ), + Text( + coinsString, + style: STextStyles.label(context), + ), + ], + ), + if (isDesktop) const Spacer(), + // if (isDesktop) + // SvgPicture.asset( + // widget.indicatorDown == true + // ? Assets.svg.chevronDown + // : Assets.svg.chevronUp, + // width: 10, + // height: 5, + // color: + // Theme.of(context).extension<StackColors>()!.textSubtitle2, + // ), + ], ), ), ), From d326c10f4236a071e55a7bb8eb63873a57103f06 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 15 Nov 2022 11:59:53 -0600 Subject: [PATCH 255/426] desktop login loading indicator --- .../desktop_login_view.dart | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/pages_desktop_specific/desktop_login_view.dart b/lib/pages_desktop_specific/desktop_login_view.dart index 93a281bf8..363c1fb0d 100644 --- a/lib/pages_desktop_specific/desktop_login_view.dart +++ b/lib/pages_desktop_specific/desktop_login_view.dart @@ -17,6 +17,7 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; class DesktopLoginView extends ConsumerStatefulWidget { @@ -45,6 +46,15 @@ class _DesktopLoginViewState extends ConsumerState<DesktopLoginView> { Future<void> login() async { try { + unawaited( + showDialog( + context: context, + builder: (context) => const LoadingIndicator( + width: 200, + ), + ), + ); + await ref .read(storageCryptoHandlerProvider) .initFromExisting(passwordController.text); @@ -55,6 +65,9 @@ class _DesktopLoginViewState extends ConsumerState<DesktopLoginView> { // if no errors passphrase is correct if (mounted) { + // pop loading indicator + Navigator.of(context).pop(); + unawaited( Navigator.of(context).pushNamedAndRemoveUntil( DesktopHomeView.routeName, @@ -63,6 +76,9 @@ class _DesktopLoginViewState extends ConsumerState<DesktopLoginView> { ); } } catch (e) { + // pop loading indicator + Navigator.of(context).pop(); + await showFloatingFlushBar( type: FlushBarType.warning, message: e.toString(), From a48975940d2b1eeef817c4353718c246663c477f Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 15 Nov 2022 12:16:12 -0600 Subject: [PATCH 256/426] desktop wallet keys and net info button highlight --- .../wallet_view/desktop_wallet_view.dart | 4 +- .../sub_widgets/network_info_button.dart | 39 +++++++++++++------ .../sub_widgets/wallet_keys_button.dart | 22 +++++------ 3 files changed, 39 insertions(+), 26 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index 97efb2f68..81b531632 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -292,13 +292,13 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { eventBus: eventBus, ), const SizedBox( - width: 32, + width: 2, ), WalletKeysButton( walletId: walletId, ), const SizedBox( - width: 32, + width: 12, ), ], ), diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart index c001f9bf3..59d20a9df 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart @@ -98,25 +98,26 @@ class _NetworkInfoButtonState extends ConsumerState<NetworkInfoButton> { Widget _buildNetworkIcon(WalletSyncStatus status, BuildContext context) { const size = 24.0; + final color = _getColor(status, context); switch (status) { case WalletSyncStatus.unableToSync: return SvgPicture.asset( Assets.svg.radioProblem, - color: Theme.of(context).extension<StackColors>()!.accentColorRed, + color: color, width: size, height: size, ); case WalletSyncStatus.synced: return SvgPicture.asset( Assets.svg.radio, - color: Theme.of(context).extension<StackColors>()!.accentColorGreen, + color: color, width: size, height: size, ); case WalletSyncStatus.syncing: return SvgPicture.asset( Assets.svg.radioSyncing, - color: Theme.of(context).extension<StackColors>()!.accentColorYellow, + color: color, width: size, height: size, ); @@ -125,35 +126,46 @@ class _NetworkInfoButtonState extends ConsumerState<NetworkInfoButton> { Widget _buildText(WalletSyncStatus status, BuildContext context) { String label; - Color color; switch (status) { case WalletSyncStatus.unableToSync: label = "Unable to sync"; - color = Theme.of(context).extension<StackColors>()!.accentColorRed; break; case WalletSyncStatus.synced: label = "Synchronised"; - color = Theme.of(context).extension<StackColors>()!.accentColorGreen; break; case WalletSyncStatus.syncing: label = "Synchronising"; - color = Theme.of(context).extension<StackColors>()!.accentColorYellow; break; } return Text( label, style: STextStyles.desktopMenuItemSelected(context).copyWith( - color: color, + color: _getColor(status, context), ), ); } + Color _getColor(WalletSyncStatus status, BuildContext context) { + switch (status) { + case WalletSyncStatus.unableToSync: + return Theme.of(context).extension<StackColors>()!.accentColorRed; + case WalletSyncStatus.synced: + return Theme.of(context).extension<StackColors>()!.accentColorGreen; + case WalletSyncStatus.syncing: + return Theme.of(context).extension<StackColors>()!.accentColorYellow; + } + } + @override Widget build(BuildContext context) { - return GestureDetector( - onTap: () { + return RawMaterialButton( + hoverColor: _getColor(_currentSyncStatus, context).withOpacity(0.1), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(1000), + ), + onPressed: () { if (Util.isDesktop) { // showDialog<void>( // context: context, @@ -265,8 +277,11 @@ class _NetworkInfoButtonState extends ConsumerState<NetworkInfoButton> { ); } }, - child: Container( - color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 16, + horizontal: 32, + ), child: Row( children: [ _buildNetworkIcon(_currentSyncStatus, context), diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_button.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_button.dart index 649433d52..32994dcb9 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_button.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_button.dart @@ -16,8 +16,11 @@ class WalletKeysButton extends StatelessWidget { @override Widget build(BuildContext context) { - return GestureDetector( - onTap: () { + return RawMaterialButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(1000), + ), + onPressed: () { showDialog<void>( context: context, barrierDismissible: false, @@ -36,17 +39,12 @@ class WalletKeysButton extends StatelessWidget { }, ), ); - - // showDialog<void>( - // context: context, - // barrierDismissible: false, - // builder: (context) => UnlockWalletKeysDesktop( - // walletId: walletId, - // ), - // ); }, - child: Container( - color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 19, + horizontal: 32, + ), child: Row( children: [ SvgPicture.asset( From 5211617082c1c458c7cf85ce19d2f64697a53cf5 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 15 Nov 2022 14:56:05 -0600 Subject: [PATCH 257/426] WIP desktop fee dropdown --- .../sub_widgets/desktop_fee_dropdown.dart | 369 ++++++++++++++++++ .../wallet_view/sub_widgets/desktop_send.dart | 72 ++-- 2 files changed, 407 insertions(+), 34 deletions(-) create mode 100644 lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart new file mode 100644 index 000000000..9acb3a6f9 --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart @@ -0,0 +1,369 @@ +import 'package:decimal/decimal.dart'; +import 'package:dropdown_button2/dropdown_button2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/models/models.dart'; +import 'package:stackwallet/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/providers/ui/fee_rate_type_state_provider.dart'; +import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart'; +import 'package:stackwallet/services/coins/firo/firo_wallet.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/fee_rate_type_enum.dart'; +import 'package:stackwallet/utilities/format.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/animated_text.dart'; + +class DesktopFeeDropDown extends ConsumerStatefulWidget { + const DesktopFeeDropDown({ + Key? key, + required this.walletId, + }) : super(key: key); + + final String walletId; + + @override + ConsumerState<DesktopFeeDropDown> createState() => _DesktopFeeDropDownState(); +} + +class _DesktopFeeDropDownState extends ConsumerState<DesktopFeeDropDown> { + late final String walletId; + + FeeObject? feeObject; + FeeRateType feeRateType = FeeRateType.average; + + final stringsToLoopThrough = [ + "Calculating", + "Calculating.", + "Calculating..", + "Calculating...", + ]; + + Future<Decimal> feeFor({ + required int amount, + required FeeRateType feeRateType, + required int feeRate, + required Coin coin, + }) async { + switch (feeRateType) { + case FeeRateType.fast: + if (ref.read(feeSheetSessionCacheProvider).fast[amount] == null) { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(walletId); + + if ((coin == Coin.firo || coin == Coin.firoTestNet) && + ref.read(publicPrivateBalanceStateProvider.state).state != + "Private") { + ref.read(feeSheetSessionCacheProvider).fast[amount] = + Format.satoshisToAmount(await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate)); + } else { + ref.read(feeSheetSessionCacheProvider).fast[amount] = + Format.satoshisToAmount( + await manager.estimateFeeFor(amount, feeRate)); + } + } + return ref.read(feeSheetSessionCacheProvider).fast[amount]!; + + case FeeRateType.average: + if (ref.read(feeSheetSessionCacheProvider).average[amount] == null) { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(walletId); + + if ((coin == Coin.firo || coin == Coin.firoTestNet) && + ref.read(publicPrivateBalanceStateProvider.state).state != + "Private") { + ref.read(feeSheetSessionCacheProvider).average[amount] = + Format.satoshisToAmount(await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate)); + } else { + ref.read(feeSheetSessionCacheProvider).average[amount] = + Format.satoshisToAmount( + await manager.estimateFeeFor(amount, feeRate)); + } + } + return ref.read(feeSheetSessionCacheProvider).average[amount]!; + + case FeeRateType.slow: + if (ref.read(feeSheetSessionCacheProvider).slow[amount] == null) { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(walletId); + + if ((coin == Coin.firo || coin == Coin.firoTestNet) && + ref.read(publicPrivateBalanceStateProvider.state).state != + "Private") { + ref.read(feeSheetSessionCacheProvider).slow[amount] = + Format.satoshisToAmount(await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate)); + } else { + ref.read(feeSheetSessionCacheProvider).slow[amount] = + Format.satoshisToAmount( + await manager.estimateFeeFor(amount, feeRate)); + } + } + return ref.read(feeSheetSessionCacheProvider).slow[amount]!; + } + } + + String estimatedTimeToBeIncludedInNextBlock( + int targetBlockTime, int estimatedNumberOfBlocks) { + int time = targetBlockTime * estimatedNumberOfBlocks; + + int hours = (time / 3600).floor(); + if (hours > 1) { + return "~$hours hours"; + } else if (hours == 1) { + return "~$hours hour"; + } + + // less than an hour + + final string = (time / 60).toStringAsFixed(1); + + if (string == "1.0") { + return "~1 minute"; + } else { + if (string.endsWith(".0")) { + return "~${(time / 60).floor()} minutes"; + } + return "~$string minutes"; + } + } + + @override + void initState() { + walletId = widget.walletId; + super.initState(); + } + + String? labelSlow; + String? labelAverage; + String? labelFast; + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + + final manager = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletId))); + + return FutureBuilder( + future: manager.fees, + builder: (context, AsyncSnapshot<FeeObject> snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData) { + feeObject = snapshot.data!; + } + return DropdownButtonHideUnderline( + child: DropdownButton2( + offset: const Offset(0, -10), + isExpanded: true, + dropdownElevation: 0, + value: ref.watch(feeRateTypeStateProvider.state).state, + // selectedItemBuilder: (s) { + // return [ + // ...FeeRateType.values.map( + // (e) => DropdownMenuItem( + // value: e, + // child: FeeDropDownChild( + // feeObject: feeObject, + // feeRateType: e, + // walletId: walletId, + // amount: amount, + // feeFor: feeFor, + // isSelected: true, + // ), + // ), + // ), + // ]; + // }, + items: [ + ...FeeRateType.values.map( + (e) => DropdownMenuItem( + value: e, + child: FeeDropDownChild( + feeObject: feeObject, + feeRateType: e, + walletId: walletId, + feeFor: feeFor, + isSelected: false, + ), + ), + ), + ], + onChanged: (newRateType) { + if (newRateType is FeeRateType) { + ref.read(feeRateTypeStateProvider.state).state = newRateType; + } + }, + icon: SvgPicture.asset( + Assets.svg.chevronDown, + width: 12, + height: 6, + color: Theme.of(context).extension<StackColors>()!.textDark3, + ), + buttonPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + buttonDecoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + dropdownDecoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + ); + }); + } +} + +final sendAmountProvider = + StateProvider.autoDispose<Decimal>((_) => Decimal.zero); + +class FeeDropDownChild extends ConsumerWidget { + const FeeDropDownChild({ + Key? key, + required this.feeObject, + required this.feeRateType, + required this.walletId, + required this.feeFor, + required this.isSelected, + }) : super(key: key); + + final FeeObject? feeObject; + final FeeRateType feeRateType; + final String walletId; + final Future<Decimal> Function({ + required int amount, + required FeeRateType feeRateType, + required int feeRate, + required Coin coin, + }) feeFor; + final bool isSelected; + + static const stringsToLoopThrough = [ + "Calculating", + "Calculating.", + "Calculating..", + "Calculating...", + ]; + + String estimatedTimeToBeIncludedInNextBlock( + int targetBlockTime, int estimatedNumberOfBlocks) { + int time = targetBlockTime * estimatedNumberOfBlocks; + + int hours = (time / 3600).floor(); + if (hours > 1) { + return "~$hours hours"; + } else if (hours == 1) { + return "~$hours hour"; + } + + // less than an hour + + final string = (time / 60).toStringAsFixed(1); + + if (string == "1.0") { + return "~1 minute"; + } else { + if (string.endsWith(".0")) { + return "~${(time / 60).floor()} minutes"; + } + return "~$string minutes"; + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + debugPrint("BUILD: $runtimeType : $feeRateType"); + + final manager = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletId))); + + if (feeObject == null) { + return AnimatedText( + stringsToLoopThrough: stringsToLoopThrough, + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: + Theme.of(context).extension<StackColors>()!.textFieldActiveText, + ), + ); + } else { + return FutureBuilder( + future: feeFor( + coin: manager.coin, + feeRateType: FeeRateType.fast, + feeRate: feeRateType == FeeRateType.fast + ? feeObject!.fast + : feeRateType == FeeRateType.slow + ? feeObject!.slow + : feeObject!.medium, + amount: Format.decimalAmountToSatoshis( + ref.watch(sendAmountProvider.state).state, + ), + ), + builder: (_, AsyncSnapshot<Decimal> snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "${feeRateType.prettyName} (~${snapshot.data!} ${manager.coin.ticker})", + style: + STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveText, + ), + textAlign: TextAlign.left, + ), + if (feeObject != null) + Text( + estimatedTimeToBeIncludedInNextBlock( + Constants.targetBlockTimeInSeconds(manager.coin), + feeRateType == FeeRateType.fast + ? feeObject!.numberOfBlocksFast + : feeRateType == FeeRateType.slow + ? feeObject!.numberOfBlocksSlow + : feeObject!.numberOfBlocksAverage, + ), + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveSearchIconRight, + ), + ), + ], + ); + } else { + return AnimatedText( + stringsToLoopThrough: stringsToLoopThrough, + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveText, + ), + ); + } + }, + ); + } + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index 071122def..cd19c53f8 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -13,6 +13,7 @@ import 'package:stackwallet/pages/send_view/sub_widgets/building_transaction_dia import 'package:stackwallet/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/fee_rate_type_state_provider.dart'; import 'package:stackwallet/providers/ui/preview_tx_button_state_provider.dart'; @@ -94,11 +95,6 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { late VoidCallback onCryptoAmountChanged; Future<void> previewSend() async { - // wait for keyboard to disappear - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration(milliseconds: 100), - ); final manager = ref.read(walletsChangeNotifierProvider).getManager(walletId); @@ -794,35 +790,25 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { _addressToggleFlag = true; } - // _cryptoFocus.addListener(() { - // if (!_cryptoFocus.hasFocus && !_baseFocus.hasFocus) { - // if (_amountToSend == null) { - // setState(() { - // _calculateFeesFuture = calculateFees(0); - // }); - // } else { - // setState(() { - // _calculateFeesFuture = - // calculateFees(Format.decimalAmountToSatoshis(_amountToSend!)); - // }); - // } - // } - // }); - // - // _baseFocus.addListener(() { - // if (!_cryptoFocus.hasFocus && !_baseFocus.hasFocus) { - // if (_amountToSend == null) { - // setState(() { - // _calculateFeesFuture = calculateFees(0); - // }); - // } else { - // setState(() { - // _calculateFeesFuture = - // calculateFees(Format.decimalAmountToSatoshis(_amountToSend!)); - // }); - // } - // } - // }); + _cryptoFocus.addListener(() { + if (!_cryptoFocus.hasFocus && !_baseFocus.hasFocus) { + if (_amountToSend == null) { + ref.refresh(sendAmountProvider); + } else { + ref.read(sendAmountProvider.state).state = _amountToSend!; + } + } + }); + + _baseFocus.addListener(() { + if (!_cryptoFocus.hasFocus && !_baseFocus.hasFocus) { + if (_amountToSend == null) { + ref.refresh(sendAmountProvider); + } else { + ref.read(sendAmountProvider.state).state = _amountToSend!; + } + } + }); super.initState(); } @@ -1371,6 +1357,24 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { ), ), ), + const SizedBox( + height: 20, + ), + Text( + "Transaction fee (estimated)", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveSearchIconRight, + ), + textAlign: TextAlign.left, + ), + const SizedBox( + height: 10, + ), + DesktopFeeDropDown( + walletId: walletId, + ), const SizedBox( height: 36, ), From aead30504e36b2ceb7ad42dbb56ab50bb6945f14 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Tue, 15 Nov 2022 14:12:55 -0700 Subject: [PATCH 258/426] alignment for settings pages icons --- assets/svg/configuration.svg | 11 + .../desktop_address_book.dart | 4 +- .../advanced_settings/advanced_settings.dart | 11 +- .../settings_menu/appearance_settings.dart | 11 +- .../currency_settings/currency_settings.dart | 25 +- .../language_settings/language_settings.dart | 11 +- .../home/settings_menu/nodes_settings.dart | 11 +- .../home/settings_menu/security_settings.dart | 765 +++++++++--------- .../syncing_preferences_settings.dart | 14 +- lib/utilities/assets.dart | 1 + pubspec.yaml | 1 + 11 files changed, 442 insertions(+), 423 deletions(-) create mode 100644 assets/svg/configuration.svg diff --git a/assets/svg/configuration.svg b/assets/svg/configuration.svg new file mode 100644 index 000000000..516bbf320 --- /dev/null +++ b/assets/svg/configuration.svg @@ -0,0 +1,11 @@ +<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="48" height="48" rx="24" fill="#E0E3E3"/> +<g clip-path="url(#clip0_5813_29086)"> +<path d="M13.5 30.5625C13.5 29.8365 14.0878 29.25 14.8125 29.25H17.0544C17.5605 28.0893 18.7172 27.2812 20.0625 27.2812C21.4078 27.2812 22.5275 28.0893 23.0689 29.25H33.1875C33.9135 29.25 34.5 29.8365 34.5 30.5625C34.5 31.2885 33.9135 31.875 33.1875 31.875H23.0689C22.5275 33.0357 21.4078 33.8438 20.0625 33.8438C18.7172 33.8438 17.5605 33.0357 17.0544 31.875H14.8125C14.0878 31.875 13.5 31.2885 13.5 30.5625ZM21.375 30.5625C21.375 29.8365 20.7885 29.25 20.0625 29.25C19.3365 29.25 18.75 29.8365 18.75 30.5625C18.75 31.2885 19.3365 31.875 20.0625 31.875C20.7885 31.875 21.375 31.2885 21.375 30.5625ZM27.9375 20.7188C29.2828 20.7188 30.4025 21.5268 30.9439 22.6875H33.1875C33.9135 22.6875 34.5 23.274 34.5 24C34.5 24.726 33.9135 25.3125 33.1875 25.3125H30.9439C30.4025 26.4732 29.2828 27.2812 27.9375 27.2812C26.5922 27.2812 25.4355 26.4732 24.9311 25.3125H14.8125C14.0878 25.3125 13.5 24.726 13.5 24C13.5 23.274 14.0878 22.6875 14.8125 22.6875H24.9311C25.4355 21.5268 26.5922 20.7188 27.9375 20.7188ZM29.25 24C29.25 23.274 28.6635 22.6875 27.9375 22.6875C27.2115 22.6875 26.625 23.274 26.625 24C26.625 24.726 27.2115 25.3125 27.9375 25.3125C28.6635 25.3125 29.25 24.726 29.25 24ZM33.1875 16.125C33.9135 16.125 34.5 16.7128 34.5 17.4375C34.5 18.1635 33.9135 18.75 33.1875 18.75H24.3814C23.84 19.9107 22.7203 20.7188 21.375 20.7188C20.0297 20.7188 18.873 19.9107 18.3686 18.75H14.8125C14.0878 18.75 13.5 18.1635 13.5 17.4375C13.5 16.7128 14.0878 16.125 14.8125 16.125H18.3686C18.873 14.9663 20.0297 14.1562 21.375 14.1562C22.7203 14.1562 23.84 14.9663 24.3814 16.125H33.1875ZM20.0625 17.4375C20.0625 18.1635 20.649 18.75 21.375 18.75C22.101 18.75 22.6875 18.1635 22.6875 17.4375C22.6875 16.7128 22.101 16.125 21.375 16.125C20.649 16.125 20.0625 16.7128 20.0625 17.4375Z" fill="black"/> +</g> +<defs> +<clipPath id="clip0_5813_29086"> +<rect width="21" height="21" fill="white" transform="translate(13.5 13.5)"/> +</clipPath> +</defs> +</svg> diff --git a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart index 475650722..407ed9897 100644 --- a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart +++ b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart @@ -239,8 +239,8 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { ), ), ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 26), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 24, vertical: 26), child: SizedBox( width: 489, child: AddressBookView(), diff --git a/lib/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart b/lib/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart index 8e89e3f67..0991a14ca 100644 --- a/lib/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart @@ -35,10 +35,13 @@ class _AdvancedSettings extends ConsumerState<AdvancedSettings> { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SvgPicture.asset( - Assets.svg.circleLanguage, - width: 48, - height: 48, + Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + Assets.svg.circleSliders, + width: 48, + height: 48, + ), ), Column( crossAxisAlignment: CrossAxisAlignment.stretch, diff --git a/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart b/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart index bf6fc81e2..27a10ca80 100644 --- a/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart @@ -52,10 +52,13 @@ class _AppearanceOptionSettings child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SvgPicture.asset( - Assets.svg.circleSun, - width: 48, - height: 48, + Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + Assets.svg.circleSun, + width: 48, + height: 48, + ), ), Column( crossAxisAlignment: CrossAxisAlignment.stretch, diff --git a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart index dab613f63..b1581d971 100644 --- a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart @@ -32,10 +32,13 @@ class _CurrencySettings extends ConsumerState<CurrencySettings> { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SvgPicture.asset( - Assets.svg.circleDollarSign, - width: 48, - height: 48, + Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + Assets.svg.circleDollarSign, + width: 48, + height: 48, + ), ), Center( child: Padding( @@ -67,7 +70,7 @@ class _CurrencySettings extends ConsumerState<CurrencySettings> { padding: EdgeInsets.all( 10, ), - child: NewPasswordButton(), + child: changeCurrency(), ), ], ), @@ -80,19 +83,11 @@ class _CurrencySettings extends ConsumerState<CurrencySettings> { } } -class NewPasswordButton extends ConsumerWidget { - const NewPasswordButton({ +class changeCurrency extends ConsumerWidget { + const changeCurrency({ Key? key, }) : super(key: key); Future<void> chooseCurrency(BuildContext context) async { - // await showDialog<dynamic>( - // context: context, - // useSafeArea: false, - // barrierDismissible: true, - // builder: (context) { - // return CurrencyDialog(); - // }, - // ); await showDialog<dynamic>( context: context, useSafeArea: false, diff --git a/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart b/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart index 0f66d7dd5..2047d4eff 100644 --- a/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart @@ -31,10 +31,13 @@ class _LanguageOptionSettings extends ConsumerState<LanguageOptionSettings> { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SvgPicture.asset( - Assets.svg.circleLanguage, - width: 48, - height: 48, + Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + Assets.svg.circleLanguage, + width: 48, + height: 48, + ), ), Column( crossAxisAlignment: CrossAxisAlignment.stretch, diff --git a/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart b/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart index 2343e3990..0f539f9f3 100644 --- a/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart @@ -79,10 +79,13 @@ class _NodesSettings extends ConsumerState<NodesSettings> { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - SvgPicture.asset( - Assets.svg.circleNode, - width: 48, - height: 48, + Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + Assets.svg.circleNode, + width: 48, + height: 48, + ), ), Column( crossAxisAlignment: CrossAxisAlignment.stretch, diff --git a/lib/pages_desktop_specific/home/settings_menu/security_settings.dart b/lib/pages_desktop_specific/home/settings_menu/security_settings.dart index d752ece38..01a6c9c9b 100644 --- a/lib/pages_desktop_specific/home/settings_menu/security_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/security_settings.dart @@ -139,392 +139,389 @@ class _SecuritySettings extends ConsumerState<SecuritySettings> { debugPrint("BUILD: $runtimeType"); return Column( children: [ - Row( - children: [ - Expanded( - child: RoundedWhiteContainer( - radiusMultiplier: 2, - padding: const EdgeInsets.all(24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SvgPicture.asset( - Assets.svg.circleLock, - width: 48, - height: 48, - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 16, - ), - Text( - "Change Password", - style: STextStyles.desktopTextSmall(context), - ), - const SizedBox( - height: 8, - ), - Text( - "Protect your Stack Wallet with a strong password. Stack Wallet does not store " - "your password, and is therefore NOT able to restore it. Keep your password safe and secure.", - style: - STextStyles.desktopTextExtraExtraSmall(context), - ), - const SizedBox( - height: 20, - ), - changePassword - ? SizedBox( - width: 512, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Current password", - style: STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark3), - textAlign: TextAlign.left, - ), - const SizedBox(height: 10), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key( - "desktopSecurityRestoreFromFilePasswordFieldKey"), - focusNode: passwordCurrentFocusNode, - controller: passwordCurrentController, - style: STextStyles.field(context), - obscureText: hidePassword, - enableSuggestions: false, - autocorrect: false, - decoration: standardInputDecoration( - "Enter current password", - passwordCurrentFocusNode, - context, - ).copyWith( - labelStyle: - STextStyles.fieldLabel(context), - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox( - width: 16, - ), - GestureDetector( - key: const Key( - "desktopSecurityRestoreFromFilePasswordFieldShowPasswordButtonKey"), - onTap: () async { - setState(() { - hidePassword = - !hidePassword; - }); - }, - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension< - StackColors>()! - .textDark3, - width: 16, - height: 16, - ), - ), - const SizedBox( - width: 12, - ), - ], - ), - ), - ), - onChanged: (newValue) { - setState(() {}); - }, - ), - ), - const SizedBox(height: 16), - Text( - "New password", - style: STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark3), - textAlign: TextAlign.left, - ), - const SizedBox(height: 10), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key( - "desktopSecurityCreateNewPasswordFieldKey1"), - focusNode: passwordFocusNode, - controller: passwordController, - style: STextStyles.field(context), - obscureText: hidePassword, - enableSuggestions: false, - autocorrect: false, - decoration: standardInputDecoration( - "Enter new password", - passwordFocusNode, - context, - ).copyWith( - labelStyle: - STextStyles.fieldLabel(context), - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox( - width: 16, - ), - GestureDetector( - key: const Key( - "desktopSecurityCreateNewPasswordButtonKey1"), - onTap: () async { - setState(() { - hidePassword = - !hidePassword; - }); - }, - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension< - StackColors>()! - .textDark3, - width: 16, - height: 16, - ), - ), - const SizedBox( - width: 12, - ), - ], - ), - ), - ), - onChanged: (newValue) { - if (newValue.isEmpty) { - setState(() { - passwordFeedback = ""; - }); - return; - } - final result = - zxcvbn.evaluate(newValue); - String suggestionsAndTips = ""; - for (var sug in result - .feedback.suggestions! - .toSet()) { - suggestionsAndTips += "$sug\n"; - } - suggestionsAndTips += - result.feedback.warning!; - String feedback = - // "Password Strength: ${((result.score! / 4.0) * 100).toInt()}%\n" - suggestionsAndTips; - - passwordStrength = result.score! / 4; - - // hack fix to format back string returned from zxcvbn - if (feedback - .contains("phrasesNo need")) { - feedback = feedback.replaceFirst( - "phrasesNo need", - "phrases\nNo need"); - } - - if (feedback.endsWith("\n")) { - feedback = feedback.substring( - 0, feedback.length - 2); - } - - setState(() { - passwordFeedback = feedback; - }); - }, - ), - ), - if (passwordFocusNode.hasFocus || - passwordRepeatFocusNode.hasFocus || - passwordController.text.isNotEmpty) - Padding( - padding: EdgeInsets.only( - left: 12, - right: 12, - top: passwordFeedback.isNotEmpty - ? 4 - : 0, - ), - child: passwordFeedback.isNotEmpty - ? Text( - passwordFeedback, - style: STextStyles.infoSmall( - context), - ) - : null, - ), - if (passwordFocusNode.hasFocus || - passwordRepeatFocusNode.hasFocus || - passwordController.text.isNotEmpty) - Padding( - padding: const EdgeInsets.only( - left: 12, - right: 12, - top: 10, - ), - child: ProgressBar( - key: const Key( - "desktopSecurityCreateStackBackUpProgressBar"), - width: 450, - height: 5, - fillColor: passwordStrength < 0.51 - ? Theme.of(context) - .extension<StackColors>()! - .accentColorRed - : passwordStrength < 1 - ? Theme.of(context) - .extension<StackColors>()! - .accentColorYellow - : Theme.of(context) - .extension<StackColors>()! - .accentColorGreen, - backgroundColor: Theme.of(context) - .extension<StackColors>()! - .buttonBackSecondary, - percent: passwordStrength < 0.25 - ? 0.03 - : passwordStrength, - ), - ), - const SizedBox(height: 16), - Text( - "Confirm new password", - style: STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark3), - textAlign: TextAlign.left, - ), - const SizedBox(height: 10), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key( - "desktopSecurityCreateNewPasswordFieldKey2"), - focusNode: passwordRepeatFocusNode, - controller: passwordRepeatController, - style: STextStyles.field(context), - obscureText: hidePassword, - enableSuggestions: false, - autocorrect: false, - decoration: standardInputDecoration( - "Confirm new password", - passwordRepeatFocusNode, - context, - ).copyWith( - labelStyle: - STextStyles.fieldLabel(context), - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox( - width: 16, - ), - GestureDetector( - key: const Key( - "desktopSecurityCreateNewPasswordButtonKey2"), - onTap: () async { - setState(() { - hidePassword = - !hidePassword; - }); - }, - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension< - StackColors>()! - .textDark3, - width: 16, - height: 16, - ), - ), - const SizedBox( - width: 12, - ), - ], - ), - ), - ), - onChanged: (newValue) { - setState(() {}); - }, - ), - ), - const SizedBox(height: 20), - PrimaryButton( - width: 160, - desktopMed: true, - enabled: shouldEnableSave, - label: "Save changes", - onPressed: () async { - final didChangePW = - await attemptChangePW(); - if (didChangePW) { - setState(() { - changePassword = false; - }); - } - }, - ) - ], - ), - ) - : PrimaryButton( - width: 210, - desktopMed: true, - enabled: true, - label: "Set up new password", - onPressed: () { - setState(() { - changePassword = true; - }); - }, - ), - ], - ), - ], + Padding( + padding: const EdgeInsets.only( + right: 30, + ), + child: RoundedWhiteContainer( + // radiusMultiplier: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + Assets.svg.circleLock, + width: 48, + height: 48, + ), ), - ), + Padding( + padding: + const EdgeInsets.only(left: 10, right: 10, bottom: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 16, + ), + Text( + "Change Password", + style: STextStyles.desktopTextSmall(context), + ), + const SizedBox( + height: 8, + ), + Text( + "Protect your Stack Wallet with a strong password. Stack Wallet does not store " + "your password, and is therefore NOT able to restore it. Keep your password safe and secure.", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + const SizedBox( + height: 20, + ), + changePassword + ? SizedBox( + width: 512, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Current password", + style: + STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + textAlign: TextAlign.left, + ), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key( + "desktopSecurityRestoreFromFilePasswordFieldKey"), + focusNode: passwordCurrentFocusNode, + controller: passwordCurrentController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Enter current password", + passwordCurrentFocusNode, + context, + ).copyWith( + labelStyle: + STextStyles.fieldLabel(context), + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox( + width: 16, + ), + GestureDetector( + key: const Key( + "desktopSecurityRestoreFromFilePasswordFieldShowPasswordButtonKey"), + onTap: () async { + setState(() { + hidePassword = + !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 16, + height: 16, + ), + ), + const SizedBox( + width: 12, + ), + ], + ), + ), + ), + onChanged: (newValue) { + setState(() {}); + }, + ), + ), + const SizedBox(height: 16), + Text( + "New password", + style: + STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + textAlign: TextAlign.left, + ), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key( + "desktopSecurityCreateNewPasswordFieldKey1"), + focusNode: passwordFocusNode, + controller: passwordController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Enter new password", + passwordFocusNode, + context, + ).copyWith( + labelStyle: + STextStyles.fieldLabel(context), + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox( + width: 16, + ), + GestureDetector( + key: const Key( + "desktopSecurityCreateNewPasswordButtonKey1"), + onTap: () async { + setState(() { + hidePassword = + !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 16, + height: 16, + ), + ), + const SizedBox( + width: 12, + ), + ], + ), + ), + ), + onChanged: (newValue) { + if (newValue.isEmpty) { + setState(() { + passwordFeedback = ""; + }); + return; + } + final result = + zxcvbn.evaluate(newValue); + String suggestionsAndTips = ""; + for (var sug in result + .feedback.suggestions! + .toSet()) { + suggestionsAndTips += "$sug\n"; + } + suggestionsAndTips += + result.feedback.warning!; + String feedback = + // "Password Strength: ${((result.score! / 4.0) * 100).toInt()}%\n" + suggestionsAndTips; + + passwordStrength = result.score! / 4; + + // hack fix to format back string returned from zxcvbn + if (feedback + .contains("phrasesNo need")) { + feedback = feedback.replaceFirst( + "phrasesNo need", + "phrases\nNo need"); + } + + if (feedback.endsWith("\n")) { + feedback = feedback.substring( + 0, feedback.length - 2); + } + + setState(() { + passwordFeedback = feedback; + }); + }, + ), + ), + if (passwordFocusNode.hasFocus || + passwordRepeatFocusNode.hasFocus || + passwordController.text.isNotEmpty) + Padding( + padding: EdgeInsets.only( + left: 12, + right: 12, + top: + passwordFeedback.isNotEmpty ? 4 : 0, + ), + child: passwordFeedback.isNotEmpty + ? Text( + passwordFeedback, + style: STextStyles.infoSmall( + context), + ) + : null, + ), + if (passwordFocusNode.hasFocus || + passwordRepeatFocusNode.hasFocus || + passwordController.text.isNotEmpty) + Padding( + padding: const EdgeInsets.only( + left: 12, + right: 12, + top: 10, + ), + child: ProgressBar( + key: const Key( + "desktopSecurityCreateStackBackUpProgressBar"), + width: 450, + height: 5, + fillColor: passwordStrength < 0.51 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorRed + : passwordStrength < 1 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorYellow + : Theme.of(context) + .extension<StackColors>()! + .accentColorGreen, + backgroundColor: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + percent: passwordStrength < 0.25 + ? 0.03 + : passwordStrength, + ), + ), + const SizedBox(height: 16), + Text( + "Confirm new password", + style: + STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3), + textAlign: TextAlign.left, + ), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key( + "desktopSecurityCreateNewPasswordFieldKey2"), + focusNode: passwordRepeatFocusNode, + controller: passwordRepeatController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Confirm new password", + passwordRepeatFocusNode, + context, + ).copyWith( + labelStyle: + STextStyles.fieldLabel(context), + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox( + width: 16, + ), + GestureDetector( + key: const Key( + "desktopSecurityCreateNewPasswordButtonKey2"), + onTap: () async { + setState(() { + hidePassword = + !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 16, + height: 16, + ), + ), + const SizedBox( + width: 12, + ), + ], + ), + ), + ), + onChanged: (newValue) { + setState(() {}); + }, + ), + ), + const SizedBox(height: 20), + PrimaryButton( + width: 160, + desktopMed: true, + enabled: shouldEnableSave, + label: "Save changes", + onPressed: () async { + final didChangePW = + await attemptChangePW(); + if (didChangePW) { + setState(() { + changePassword = false; + }); + } + }, + ) + ], + ), + ) + : PrimaryButton( + width: 210, + desktopMed: true, + enabled: true, + label: "Set up new password", + onPressed: () { + setState(() { + changePassword = true; + }); + }, + ), + ], + ), + ), + ], ), - const SizedBox( - width: 40, - ), - ], + ), ), ], ); diff --git a/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart b/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart index 7f6aac260..618ee56da 100644 --- a/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart @@ -2,13 +2,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/src/widgets/framework.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.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/rounded_white_container.dart'; -import '../../../pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart'; - class SyncingPreferencesSettings extends ConsumerStatefulWidget { const SyncingPreferencesSettings({Key? key}) : super(key: key); @@ -34,10 +33,13 @@ class _SyncingPreferencesSettings child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SvgPicture.asset( - Assets.svg.circleArrowRotate, - width: 48, - height: 48, + Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + Assets.svg.circleArrowRotate, + width: 48, + height: 48, + ), ), Column( crossAxisAlignment: CrossAxisAlignment.stretch, diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index e76a17c12..38d720969 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -59,6 +59,7 @@ class _SVG { String txExchangeFailed(BuildContext context) => "assets/svg/${Theme.of(context).extension<StackColors>()!.themeType.name}/tx-exchange-icon-failed.svg"; + String get circleSliders => "assets/svg/configuration.svg"; String get circlePlus => "assets/svg/plus-circle.svg"; String get framedGear => "assets/svg/framed-gear.svg"; String get framedAddressBook => "assets/svg/framed-address-book.svg"; diff --git a/pubspec.yaml b/pubspec.yaml index 10a00ff2e..bba5f6ed2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -315,6 +315,7 @@ flutter: - assets/svg/keys.svg - assets/svg/arrow-down.svg - assets/svg/plus-circle.svg + - assets/svg/configuration.svg # coin icons - assets/svg/coin_icons/Bitcoin.svg - assets/svg/coin_icons/Litecoin.svg From f11119119affc02e8a427dadac434f613669542d Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Tue, 15 Nov 2022 14:32:44 -0700 Subject: [PATCH 259/426] changed all settings buttons to PrimaryButton --- .../currency_settings/currency_settings.dart | 108 ++++++++---------- .../language_settings/language_settings.dart | 62 ++++------ .../home/settings_menu/security_settings.dart | 8 +- .../syncing_preferences_settings.dart | 35 ++---- 4 files changed, 79 insertions(+), 134 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart index b1581d971..7ad8b38b9 100644 --- a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart @@ -7,6 +7,7 @@ import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.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/rounded_white_container.dart'; class CurrencySettings extends ConsumerStatefulWidget { @@ -18,6 +19,41 @@ class CurrencySettings extends ConsumerStatefulWidget { ConsumerState<CurrencySettings> createState() => _CurrencySettings(); } +Future<void> chooseCurrency(BuildContext context) async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return DesktopDialog( + maxHeight: 800, + maxWidth: 600, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(32), + child: Text( + "Select currency", + style: STextStyles.desktopH3(context), + textAlign: TextAlign.center, + ), + ), + const DesktopDialogCloseButton(), + ], + ), + const Expanded( + child: BaseCurrencySettingsView(), + ), + ], + ), + ); + }, + ); +} + class _CurrencySettings extends ConsumerState<CurrencySettings> { @override Widget build(BuildContext context) { @@ -65,12 +101,20 @@ class _CurrencySettings extends ConsumerState<CurrencySettings> { ), Column( crossAxisAlignment: CrossAxisAlignment.start, - children: const [ + children: [ Padding( padding: EdgeInsets.all( 10, ), - child: changeCurrency(), + child: PrimaryButton( + width: 210, + desktopMed: true, + enabled: true, + label: "Change currency", + onPressed: () { + chooseCurrency(context); + }, + ), ), ], ), @@ -82,63 +126,3 @@ class _CurrencySettings extends ConsumerState<CurrencySettings> { ); } } - -class changeCurrency extends ConsumerWidget { - const changeCurrency({ - Key? key, - }) : super(key: key); - Future<void> chooseCurrency(BuildContext context) async { - await showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return DesktopDialog( - maxHeight: 800, - maxWidth: 600, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.all(32), - child: Text( - "Select currency", - style: STextStyles.desktopH3(context), - textAlign: TextAlign.center, - ), - ), - const DesktopDialogCloseButton(), - ], - ), - const Expanded( - child: BaseCurrencySettingsView(), - ), - ], - ), - ); - }, - ); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return SizedBox( - width: 200, - height: 48, - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () { - chooseCurrency(context); - }, - child: Text( - "Change currency", - style: STextStyles.button(context), - ), - ), - ); - } -} diff --git a/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart b/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart index 2047d4eff..96ce5cf61 100644 --- a/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart @@ -4,7 +4,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages_desktop_specific/home/settings_menu/language_settings/language_dialog.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/desktop/primary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class LanguageOptionSettings extends ConsumerStatefulWidget { @@ -17,6 +17,17 @@ class LanguageOptionSettings extends ConsumerStatefulWidget { _LanguageOptionSettings(); } +Future<void> chooseLanguage(BuildContext context) async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return const LanguageDialog(); + }, + ); +} + class _LanguageOptionSettings extends ConsumerState<LanguageOptionSettings> { @override Widget build(BuildContext context) { @@ -66,12 +77,20 @@ class _LanguageOptionSettings extends ConsumerState<LanguageOptionSettings> { ), Column( crossAxisAlignment: CrossAxisAlignment.start, - children: const [ + children: [ Padding( padding: EdgeInsets.all( 10, ), - child: ChangeLanguageButton(), + child: PrimaryButton( + width: 210, + desktopMed: true, + enabled: true, + label: "Change language", + onPressed: () { + chooseLanguage(context); + }, + ), ), ], ), @@ -83,40 +102,3 @@ class _LanguageOptionSettings extends ConsumerState<LanguageOptionSettings> { ); } } - -class ChangeLanguageButton extends ConsumerWidget { - const ChangeLanguageButton({ - Key? key, - }) : super(key: key); - - Future<void> chooseLanguage(BuildContext context) async { - await showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return const LanguageDialog(); - }, - ); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return SizedBox( - width: 200, - height: 48, - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () { - chooseLanguage(context); - }, - child: Text( - "Change language", - style: STextStyles.button(context), - ), - ), - ); - } -} diff --git a/lib/pages_desktop_specific/home/settings_menu/security_settings.dart b/lib/pages_desktop_specific/home/settings_menu/security_settings.dart index 01a6c9c9b..de3505ace 100644 --- a/lib/pages_desktop_specific/home/settings_menu/security_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/security_settings.dart @@ -157,20 +157,16 @@ class _SecuritySettings extends ConsumerState<SecuritySettings> { ), ), Padding( - padding: - const EdgeInsets.only(left: 10, right: 10, bottom: 10), + padding: const EdgeInsets.all(10), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox( - height: 16, - ), Text( "Change Password", style: STextStyles.desktopTextSmall(context), ), const SizedBox( - height: 8, + height: 16, ), Text( "Protect your Stack Wallet with a strong password. Stack Wallet does not store " diff --git a/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart b/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart index 618ee56da..b245f8d43 100644 --- a/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart @@ -5,7 +5,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.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/desktop/primary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class SyncingPreferencesSettings extends ConsumerStatefulWidget { @@ -75,12 +75,18 @@ class _SyncingPreferencesSettings ), Column( crossAxisAlignment: CrossAxisAlignment.start, - children: const [ + children: [ Padding( padding: EdgeInsets.all( 10, ), - child: ChangePrefButton(), + child: PrimaryButton( + width: 210, + desktopMed: true, + enabled: true, + label: "Change preferences", + onPressed: () {}, + ), ), ], ), @@ -92,26 +98,3 @@ class _SyncingPreferencesSettings ); } } - -class ChangePrefButton extends ConsumerWidget { - const ChangePrefButton({ - Key? key, - }) : super(key: key); - @override - Widget build(BuildContext context, WidgetRef ref) { - return SizedBox( - width: 200, - height: 48, - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () {}, - child: Text( - "Change preferences", - style: STextStyles.button(context), - ), - ), - ); - } -} From 1afc468d28b0ed4b8775a9692a848728c4a4e401 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 15 Nov 2022 15:41:55 -0600 Subject: [PATCH 260/426] desktop nodes scroll layout fix --- .../home/settings_menu/nodes_settings.dart | 395 +++++++++--------- 1 file changed, 198 insertions(+), 197 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart b/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart index 0f539f9f3..e9417d2a7 100644 --- a/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart @@ -2,8 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/coin_nodes_view.dart'; -import 'package:stackwallet/providers/global/node_service_provider.dart'; -import 'package:stackwallet/providers/global/prefs_provider.dart'; +import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -52,12 +51,15 @@ class _NodesSettings extends ConsumerState<NodesSettings> { void dispose() { searchNodeController.dispose(); searchNodeFocusNode.dispose(); + nodeScrollController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + bool showTestNet = ref.watch( prefsChangeNotifierProvider.select((value) => value.showTestNetCoins), ); @@ -66,217 +68,216 @@ class _NodesSettings extends ConsumerState<NodesSettings> { ? _coins : _coins.sublist(0, _coins.length - kTestNetCoinCount); - debugPrint("BUILD: $runtimeType"); - return Column( - mainAxisSize: MainAxisSize.min, + return Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.only( - right: 30, - ), - child: RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: SvgPicture.asset( - Assets.svg.circleNode, - width: 48, - height: 48, - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.all(10), - child: RichText( - textAlign: TextAlign.start, - text: TextSpan( - children: [ - TextSpan( - text: "Nodes", - style: STextStyles.desktopTextSmall(context), - ), - TextSpan( - text: "\n\nSelect a coin to see nodes", - style: STextStyles.desktopTextExtraExtraSmall( - context), - ), - ], - ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + right: 32, + bottom: 32, + ), + child: RoundedWhiteContainer( + radiusMultiplier: 2, + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset( + Assets.svg.circleNode, + width: 48, + height: 48, ), - ), - ], - ), - Padding( - padding: const EdgeInsets.all(10.0), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: searchNodeController, - focusNode: searchNodeFocusNode, - onChanged: (newString) { - setState(() => filter = newString); - }, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - searchNodeFocusNode, - context, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, - ), - ), - suffixIcon: searchNodeController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - searchNodeController.text = ""; - filter = ""; - }); - }, - ), - ], - ), - ), - ) - : null, + const SizedBox( + height: 16, ), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(10.0), - child: RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - borderColor: - Theme.of(context).extension<StackColors>()!.background, - child: ListView.separated( - controller: nodeScrollController, - physics: const AlwaysScrollableScrollPhysics(), - scrollDirection: Axis.vertical, - primary: false, - shrinkWrap: true, - itemBuilder: (context, index) { - final coin = coins[index]; - final count = ref - .watch(nodeServiceChangeNotifierProvider - .select((value) => value.getNodesFor(coin))) - .length; - - return Padding( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + Text( + "Nodes", + style: STextStyles.desktopTextSmall(context), + ), + const SizedBox( + height: 8, + ), + Text( + "Select a coin to see nodes", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + const SizedBox( + height: 20, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: searchNodeController, + focusNode: searchNodeFocusNode, + onChanged: (newString) { + setState(() => filter = newString); + }, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + searchNodeFocusNode, + context, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, ), ), - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - onPressed: () { - showDialog<void>( - context: context, - builder: (context) => Navigator( - initialRoute: CoinNodesView.routeName, - onGenerateRoute: RouteGenerator.generateRoute, - onGenerateInitialRoutes: (_, __) { - return [ - FadePageRoute( - CoinNodesView( - coin: coin, - rootNavigator: true, - ), - const RouteSettings( - name: CoinNodesView.routeName, - ), - ), - ]; - }, - ), - ); - }, - child: Padding( - padding: const EdgeInsets.all( - 12.0, - ), - child: Row( - children: [ - Row( - children: [ - SvgPicture.asset( - Assets.svg.iconFor(coin: coin), - width: 24, - height: 24, - ), - const SizedBox( - width: 12, - ), - Column( - crossAxisAlignment: - CrossAxisAlignment.start, + suffixIcon: searchNodeController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( children: [ - Text( - "${coin.prettyName} nodes", - style: STextStyles.titleBold12( - context), - ), - Text( - count > 1 - ? "$count nodes" - : "Default", - style: STextStyles.label(context), + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + searchNodeController.text = ""; + filter = ""; + }); + }, ), ], ), - ], - ), - Expanded( - child: SvgPicture.asset( - Assets.svg.chevronRight, - alignment: Alignment.centerRight, ), + ) + : null, + ), + ), + ), + ], + ), + const SizedBox( + height: 20, + ), + Flexible( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: ListView.separated( + controller: nodeScrollController, + physics: const AlwaysScrollableScrollPhysics(), + scrollDirection: Axis.vertical, + primary: false, + shrinkWrap: true, + itemBuilder: (context, index) { + final coin = coins[index]; + final count = ref + .watch(nodeServiceChangeNotifierProvider + .select((value) => value.getNodesFor(coin))) + .length; + + return Padding( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + onPressed: () { + showDialog<void>( + context: context, + builder: (context) => Navigator( + initialRoute: CoinNodesView.routeName, + onGenerateRoute: + RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + FadePageRoute( + CoinNodesView( + coin: coin, + rootNavigator: true, + ), + const RouteSettings( + name: CoinNodesView.routeName, + ), + ), + ]; + }, ), - ], + ); + }, + child: Padding( + padding: const EdgeInsets.all( + 12.0, + ), + child: Row( + children: [ + Row( + children: [ + SvgPicture.asset( + Assets.svg.iconFor(coin: coin), + width: 24, + height: 24, + ), + const SizedBox( + width: 12, + ), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "${coin.prettyName} nodes", + style: STextStyles.titleBold12( + context), + ), + Text( + count > 1 + ? "$count nodes" + : "Default", + style: STextStyles.label(context), + ), + ], + ), + ], + ), + Expanded( + child: SvgPicture.asset( + Assets.svg.chevronRight, + alignment: Alignment.centerRight, + ), + ), + ], + ), ), ), - ), - ); - }, - separatorBuilder: (context, index) => Container( - height: 1, - color: Theme.of(context) - .extension<StackColors>()! - .background, + ); + }, + separatorBuilder: (context, index) => Container( + height: 1, + color: Theme.of(context) + .extension<StackColors>()! + .background, + ), + itemCount: coins.length, ), - itemCount: coins.length, ), ), - ), - ], + ], + ), ), ), ), From 6cfbeb180e69db1e46444211d0764efe26090105 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 15 Nov 2022 15:51:53 -0600 Subject: [PATCH 261/426] desktop nodes search --- .../home/settings_menu/nodes_settings.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart b/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart index e9417d2a7..012f7b47a 100644 --- a/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart @@ -34,6 +34,18 @@ class _NodesSettings extends ConsumerState<NodesSettings> { String filter = ""; + List<Coin> _search(String filter, List<Coin> coins) { + if (filter.isEmpty) { + return coins; + } + return coins + .where((coin) => + coin.prettyName.contains(filter) || + coin.name.contains(filter) || + coin.ticker.toLowerCase().contains(filter.toLowerCase())) + .toList(); + } + @override void initState() { _coins = _coins.toList(); @@ -68,6 +80,8 @@ class _NodesSettings extends ConsumerState<NodesSettings> { ? _coins : _coins.sublist(0, _coins.length - kTestNetCoinCount); + coins = _search(filter, coins); + return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ From a57651a766a2a5e77b3b1f5ca3ffcacd3f4dfb1d Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Tue, 15 Nov 2022 14:53:27 -0700 Subject: [PATCH 262/426] layout fix + correct currency description --- .../settings_menu/currency_settings/currency_settings.dart | 5 ++--- .../home/settings_menu/syncing_preferences_settings.dart | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart index 7ad8b38b9..ecc19ebb7 100644 --- a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart @@ -4,7 +4,6 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/currency_view.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/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; @@ -89,8 +88,8 @@ class _CurrencySettings extends ConsumerState<CurrencySettings> { ), TextSpan( text: - "\n\nProtect your Stack Wallet with a strong password. Stack Wallet does not store " - "your password, and is therefore NOT able to restore it. Keep your password safe and secure.", + "\n\nSelect a fiat currency to evaluate your crypto assets. We use CoinGecko conversion rates " + "when displaying your balance and transaction amounts.", style: STextStyles.desktopTextExtraExtraSmall(context), ), diff --git a/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart b/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart index b245f8d43..3d8bebb81 100644 --- a/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart @@ -56,7 +56,7 @@ class _SyncingPreferencesSettings ), TextSpan( text: - "\nSet up your syncing preferences for all wallets in your Stack.", + "\n\nSet up your syncing preferences for all wallets in your Stack.", style: STextStyles.desktopTextExtraExtraSmall( context), ), @@ -69,7 +69,7 @@ class _SyncingPreferencesSettings ///TODO: ONLY SHOW SYNC OPTIONS ON BUTTON PRESS Column( - children: [ + children: const [ SyncingOptionsView(), ], ), @@ -77,7 +77,7 @@ class _SyncingPreferencesSettings crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: EdgeInsets.all( + padding: const EdgeInsets.all( 10, ), child: PrimaryButton( From 824d8036789f0cbc1ba0b6ae5090554cf4ff2561 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Tue, 15 Nov 2022 14:54:12 -0700 Subject: [PATCH 263/426] cursor on hover of wallet --- .../home/my_stack_view/my_wallets.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/my_wallets.dart b/lib/pages_desktop_specific/home/my_stack_view/my_wallets.dart index 550db293e..13170d07e 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/my_wallets.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/my_wallets.dart @@ -53,7 +53,10 @@ class _MyWalletsState extends ConsumerState<MyWallets> { height: 20, ), const Expanded( - child: WalletSummaryTable(), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: WalletSummaryTable(), + ), ), ], ), From 19d070c61832659d7971aa1b860c41df400f4a7b Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Tue, 15 Nov 2022 15:00:07 -0700 Subject: [PATCH 264/426] send/receive pointer finger on hover --- .../sub_widgets/send_receive_tab_menu.dart | 48 +++++++++++-------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/send_receive_tab_menu.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/send_receive_tab_menu.dart index 54dca9a4c..251e4f301 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/send_receive_tab_menu.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/send_receive_tab_menu.dart @@ -48,16 +48,20 @@ class _SendReceiveTabMenuState extends State<SendReceiveTabMenu> { const SizedBox( height: 16, ), - Text( - "Send", - style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: _selectedIndex == 0 - ? Theme.of(context) - .extension<StackColors>()! - .accentColorBlue - : Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, + MouseRegion( + cursor: SystemMouseCursors.click, + child: Text( + "Send", + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: _selectedIndex == 0 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), ), ), const SizedBox( @@ -90,16 +94,20 @@ class _SendReceiveTabMenuState extends State<SendReceiveTabMenu> { const SizedBox( height: 16, ), - Text( - "Receive", - style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: _selectedIndex == 1 - ? Theme.of(context) - .extension<StackColors>()! - .accentColorBlue - : Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, + MouseRegion( + cursor: SystemMouseCursors.click, + child: Text( + "Receive", + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: _selectedIndex == 1 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), ), ), const SizedBox( From 15c51e32698c0436c672c832ac5c8b6adec609b8 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 15 Nov 2022 16:21:52 -0600 Subject: [PATCH 265/426] support view buttons --- .../global_settings_view/support_view.dart | 444 ++++++------------ 1 file changed, 145 insertions(+), 299 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/support_view.dart b/lib/pages/settings_views/global_settings_view/support_view.dart index 20aeedf61..9c3c41296 100644 --- a/lib/pages/settings_views/global_settings_view/support_view.dart +++ b/lib/pages/settings_views/global_settings_view/support_view.dart @@ -17,14 +17,13 @@ class SupportView extends StatelessWidget { }) : super(key: key); static const String routeName = "/support"; - final double iconSize = 28; @override Widget build(BuildContext context) { - final isDesktop = Util.isDesktop; - debugPrint("BUILD: $runtimeType"); + final isDesktop = Util.isDesktop; + return ConditionalParent( condition: !isDesktop, builder: (child) { @@ -64,321 +63,168 @@ class SupportView extends StatelessWidget { : const SizedBox( height: 12, ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - if (!isDesktop) { - launchUrl( - Uri.parse("https://t.me/stackwallet"), - mode: LaunchMode.externalApplication, - ); - } - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - SvgPicture.asset( - Assets.socials.telegram, - width: iconSize, - height: iconSize, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - ), - const SizedBox( - width: 12, - ), - Text( - "Telegram", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - ], - ), - BlueTextButton( - text: isDesktop ? "@stackwallet" : "", - onTap: () { - launchUrl( - Uri.parse("https://t.me/stackwallet"), - mode: LaunchMode.externalApplication, - ); - }, - ), - ], - ), - ), - ), + AboutItem( + linkUrl: "https://t.me/stackwallet", + label: "Telegram", + buttonText: "@stackwallet", + iconAsset: Assets.socials.telegram, + isDesktop: isDesktop, ), const SizedBox( height: 8, ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - if (!isDesktop) { - launchUrl( - Uri.parse("https://discord.gg/RZMG3yUm"), - mode: LaunchMode.externalApplication, - ); - } - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - SvgPicture.asset( - Assets.socials.discord, - width: iconSize, - height: iconSize, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - ), - const SizedBox( - width: 12, - ), - Text( - "Discord", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - ], - ), - BlueTextButton( - text: isDesktop ? "Stack Wallet" : "", - onTap: () { - launchUrl( - Uri.parse( - "https://discord.gg/RZMG3yUm"), //expired link? - mode: LaunchMode.externalApplication, - ); - }, - ), - ], - ), - ), - ), + AboutItem( + linkUrl: "https://discord.gg/RZMG3yUm", + label: "Discord", + buttonText: "Stack Wallet", + iconAsset: Assets.socials.discord, + isDesktop: isDesktop, ), const SizedBox( height: 8, ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - if (!isDesktop) { - launchUrl( - Uri.parse("https://www.reddit.com/r/stackwallet/"), - mode: LaunchMode.externalApplication, - ); - } - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - SvgPicture.asset( - Assets.socials.reddit, - width: iconSize, - height: iconSize, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - ), - const SizedBox( - width: 12, - ), - Text( - "Reddit", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - ], - ), - BlueTextButton( - text: isDesktop ? "r/stackwallet" : "", - onTap: () { - launchUrl( - Uri.parse("https://www.reddit.com/r/stackwallet/"), - mode: LaunchMode.externalApplication, - ); - }, - ), - ], - ), - ), - ), + AboutItem( + linkUrl: "https://www.reddit.com/r/stackwallet/", + label: "Reddit", + buttonText: "r/stackwallet", + iconAsset: Assets.socials.reddit, + isDesktop: isDesktop, ), const SizedBox( height: 8, ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - if (!isDesktop) { - launchUrl( - Uri.parse("https://twitter.com/stack_wallet"), - mode: LaunchMode.externalApplication, - ); - } - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - SvgPicture.asset( - Assets.socials.twitter, - width: iconSize, - height: iconSize, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - ), - const SizedBox( - width: 12, - ), - Text( - "Twitter", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - ], - ), - BlueTextButton( - text: isDesktop ? "@stack_wallet" : "", - onTap: () { - launchUrl( - Uri.parse("https://twitter.com/stack_wallet"), - mode: LaunchMode.externalApplication, - ); - }, - ), - ], - ), - ), - ), + AboutItem( + linkUrl: "https://twitter.com/stack_wallet", + label: "Twitter", + buttonText: "@stack_wallet", + iconAsset: Assets.socials.twitter, + isDesktop: isDesktop, ), const SizedBox( height: 8, ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - if (!isDesktop) { - launchUrl( - Uri.parse("mailto://support@stackwallet.com"), - mode: LaunchMode.externalApplication, - ); - } - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - SvgPicture.asset( - Assets.svg.envelope, - width: iconSize, - height: iconSize, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - ), - const SizedBox( - width: 12, - ), - Text( - "Email", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - ], - ), - BlueTextButton( - text: isDesktop ? "support@stackwallet.com" : "", - onTap: () { - launchUrl( - Uri.parse("mailto://support@stackwallet.com"), - mode: LaunchMode.externalApplication, - ); - }, - ), - ], - ), - ), - ), + AboutItem( + linkUrl: "mailto://support@stackwallet.com", + label: "Email", + buttonText: "support@stackwallet.com", + iconAsset: Assets.svg.envelope, + isDesktop: isDesktop, ), ], ), ); } } + +class AboutItem extends StatelessWidget { + const AboutItem({ + Key? key, + required this.linkUrl, + required this.label, + required this.buttonText, + required this.iconAsset, + required this.isDesktop, + }) : super(key: key); + + final String linkUrl; + final String label; + final String buttonText; + final String iconAsset; + final bool isDesktop; + + @override + Widget build(BuildContext context) { + final double iconSize = isDesktop ? 20 : 28; + + return RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: ConditionalParent( + condition: !isDesktop, + builder: (child) => RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + launchUrl( + Uri.parse(linkUrl), + mode: LaunchMode.externalApplication, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: child, + ), + ), + child: Padding( + padding: isDesktop + ? const EdgeInsets.symmetric( + horizontal: 20, + vertical: 15, + ) + : const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + ConditionalParent( + condition: isDesktop, + builder: (child) => Container( + width: 40, + height: 40, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10000), + color: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + ), + child: Center( + child: child, + ), + ), + child: SvgPicture.asset( + iconAsset, + width: iconSize, + height: iconSize, + color: Theme.of(context) + .extension<StackColors>()! + .bottomNavIconIcon, + ), + ), + const SizedBox( + width: 12, + ), + Text( + label, + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + ], + ), + if (isDesktop) + BlueTextButton( + text: buttonText, + onTap: () { + launchUrl( + Uri.parse(linkUrl), + mode: LaunchMode.externalApplication, + ); + }, + ), + ], + ), + ), + ), + ); + } +} From 3eae7d0fabccca7e96715eeebf23567b9084e6ac Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Tue, 15 Nov 2022 15:22:45 -0700 Subject: [PATCH 266/426] password eye icon pointer cursor --- .../create_password/create_password_view.dart | 56 ++++++++++--------- .../desktop_login_view.dart | 21 ++++--- 2 files changed, 43 insertions(+), 34 deletions(-) diff --git a/lib/pages_desktop_specific/create_password/create_password_view.dart b/lib/pages_desktop_specific/create_password/create_password_view.dart index c29fc3de6..8e752f508 100644 --- a/lib/pages_desktop_specific/create_password/create_password_view.dart +++ b/lib/pages_desktop_specific/create_password/create_password_view.dart @@ -203,15 +203,18 @@ class _CreatePasswordViewState extends ConsumerState<CreatePasswordView> { height: 32, width: 32, child: Center( - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, - width: 24, - height: 19, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 24, + height: 19, + ), ), ), ), @@ -354,22 +357,25 @@ class _CreatePasswordViewState extends ConsumerState<CreatePasswordView> { height: 32, width: 32, child: Center( - child: SvgPicture.asset( - fieldsMatch && passwordStrength == 1 - ? Assets.svg.checkCircle - : hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: fieldsMatch && - passwordStrength == 1 - ? Theme.of(context) - .extension<StackColors>()! - .accentColorGreen - : Theme.of(context) - .extension<StackColors>()! - .textDark3, - width: 24, - height: 19, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: SvgPicture.asset( + fieldsMatch && passwordStrength == 1 + ? Assets.svg.checkCircle + : hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: fieldsMatch && + passwordStrength == 1 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorGreen + : Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 24, + height: 19, + ), ), ), ), diff --git a/lib/pages_desktop_specific/desktop_login_view.dart b/lib/pages_desktop_specific/desktop_login_view.dart index 363c1fb0d..ebd1c334b 100644 --- a/lib/pages_desktop_specific/desktop_login_view.dart +++ b/lib/pages_desktop_specific/desktop_login_view.dart @@ -175,15 +175,18 @@ class _DesktopLoginViewState extends ConsumerState<DesktopLoginView> { hidePassword = !hidePassword; }); }, - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, - width: 24, - height: 24, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 24, + height: 24, + ), ), ), const SizedBox( From 1818b00ac66c25cb099cb0d3a72a3cfce6c7dcd1 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 15 Nov 2022 16:29:12 -0600 Subject: [PATCH 267/426] slightly adjust mouse region --- .../sub_widgets/send_receive_tab_menu.dart | 112 +++++++++--------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/send_receive_tab_menu.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/send_receive_tab_menu.dart index 251e4f301..b2f6156c2 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/send_receive_tab_menu.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/send_receive_tab_menu.dart @@ -39,18 +39,18 @@ class _SendReceiveTabMenuState extends State<SendReceiveTabMenu> { return Row( children: [ Expanded( - child: GestureDetector( - onTap: () => _onChanged(0), - child: Container( - color: Colors.transparent, - child: Column( - children: [ - const SizedBox( - height: 16, - ), - MouseRegion( - cursor: SystemMouseCursors.click, - child: Text( + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () => _onChanged(0), + child: Container( + color: Colors.transparent, + child: Column( + children: [ + const SizedBox( + height: 16, + ), + Text( "Send", style: STextStyles.desktopTextExtraSmall(context).copyWith( @@ -63,40 +63,40 @@ class _SendReceiveTabMenuState extends State<SendReceiveTabMenu> { .textSubtitle1, ), ), - ), - const SizedBox( - height: 19, - ), - Container( - height: 2, - decoration: BoxDecoration( - color: _selectedIndex == 0 - ? Theme.of(context) - .extension<StackColors>()! - .accentColorBlue - : Theme.of(context) - .extension<StackColors>()! - .background, + const SizedBox( + height: 19, ), - ), - ], + Container( + height: 2, + decoration: BoxDecoration( + color: _selectedIndex == 0 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Theme.of(context) + .extension<StackColors>()! + .background, + ), + ), + ], + ), ), ), ), ), Expanded( - child: GestureDetector( - onTap: () => _onChanged(1), - child: Container( - color: Colors.transparent, - child: Column( - children: [ - const SizedBox( - height: 16, - ), - MouseRegion( - cursor: SystemMouseCursors.click, - child: Text( + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () => _onChanged(1), + child: Container( + color: Colors.transparent, + child: Column( + children: [ + const SizedBox( + height: 16, + ), + Text( "Receive", style: STextStyles.desktopTextExtraSmall(context).copyWith( @@ -109,23 +109,23 @@ class _SendReceiveTabMenuState extends State<SendReceiveTabMenu> { .textSubtitle1, ), ), - ), - const SizedBox( - height: 19, - ), - Container( - height: 2, - decoration: BoxDecoration( - color: _selectedIndex == 1 - ? Theme.of(context) - .extension<StackColors>()! - .accentColorBlue - : Theme.of(context) - .extension<StackColors>()! - .background, + const SizedBox( + height: 19, ), - ), - ], + Container( + height: 2, + decoration: BoxDecoration( + color: _selectedIndex == 1 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Theme.of(context) + .extension<StackColors>()! + .background, + ), + ), + ], + ), ), ), ), From b4488fceed20ab882992c2a331025a39b4174de7 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Tue, 15 Nov 2022 15:41:58 -0700 Subject: [PATCH 268/426] US spelling adjustment --- .../wallet_view/sub_widgets/network_info_button.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart index 59d20a9df..5195a4b9b 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart @@ -132,10 +132,10 @@ class _NetworkInfoButtonState extends ConsumerState<NetworkInfoButton> { label = "Unable to sync"; break; case WalletSyncStatus.synced: - label = "Synchronised"; + label = "Synchronized"; break; case WalletSyncStatus.syncing: - label = "Synchronising"; + label = "Synchronizing"; break; } From 07cf1f3f92a304aa2ecd1c931454f0655ae7b951 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 15 Nov 2022 16:44:23 -0600 Subject: [PATCH 269/426] Add MouseRegion to Expandable widget and clean up duplications --- .../home/my_stack_view/my_wallets.dart | 5 +- lib/widgets/expandable.dart | 13 ++- lib/widgets/table_view/table_view.dart | 3 +- .../wallet_info_row/wallet_info_row.dart | 93 ++++++++++--------- 4 files changed, 59 insertions(+), 55 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/my_wallets.dart b/lib/pages_desktop_specific/home/my_stack_view/my_wallets.dart index 13170d07e..550db293e 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/my_wallets.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/my_wallets.dart @@ -53,10 +53,7 @@ class _MyWalletsState extends ConsumerState<MyWallets> { height: 20, ), const Expanded( - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: WalletSummaryTable(), - ), + child: WalletSummaryTable(), ), ], ), diff --git a/lib/widgets/expandable.dart b/lib/widgets/expandable.dart index a0c2be5a4..47726d6d6 100644 --- a/lib/widgets/expandable.dart +++ b/lib/widgets/expandable.dart @@ -89,11 +89,14 @@ class _ExpandableState extends State<Expandable> with TickerProviderStateMixin { return Column( mainAxisSize: MainAxisSize.min, children: [ - GestureDetector( - onTap: toggle, - child: Container( - color: Colors.transparent, - child: widget.header, + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: toggle, + child: Container( + color: Colors.transparent, + child: widget.header, + ), ), ), SizeTransition( diff --git a/lib/widgets/table_view/table_view.dart b/lib/widgets/table_view/table_view.dart index 7e8693f0d..8c2d470bd 100644 --- a/lib/widgets/table_view/table_view.dart +++ b/lib/widgets/table_view/table_view.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:stackwallet/widgets/table_view/table_view_row.dart'; class TableView extends StatefulWidget { const TableView({ @@ -9,7 +8,7 @@ class TableView extends StatefulWidget { this.shrinkWrap = false, }) : super(key: key); - final List<TableViewRow> rows; + final List<Widget> rows; final double rowSpacing; final bool shrinkWrap; diff --git a/lib/widgets/wallet_info_row/wallet_info_row.dart b/lib/widgets/wallet_info_row/wallet_info_row.dart index d5e42e814..fe006a67b 100644 --- a/lib/widgets/wallet_info_row/wallet_info_row.dart +++ b/lib/widgets/wallet_info_row/wallet_info_row.dart @@ -26,53 +26,58 @@ class WalletInfoRow extends ConsumerWidget { .getManagerProvider(walletId)); if (Util.isDesktop) { - return GestureDetector( - onTap: onPressed, - child: Container( - color: Colors.transparent, - child: Row( - children: [ - Expanded( - flex: 4, - child: Row( - children: [ - WalletInfoCoinIcon(coin: manager.coin), - const SizedBox( - width: 12, - ), - Text( - manager.walletName, - style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: - Theme.of(context).extension<StackColors>()!.textDark, + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: onPressed, + child: Container( + color: Colors.transparent, + child: Row( + children: [ + Expanded( + flex: 4, + child: Row( + children: [ + WalletInfoCoinIcon(coin: manager.coin), + const SizedBox( + width: 12, ), - ), - ], + Text( + manager.walletName, + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + ], + ), ), - ), - Expanded( - flex: 4, - child: WalletInfoRowBalanceFuture( - walletId: walletId, + Expanded( + flex: 4, + child: WalletInfoRowBalanceFuture( + walletId: walletId, + ), ), - ), - Expanded( - flex: 6, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - SvgPicture.asset( - Assets.svg.chevronRight, - width: 20, - height: 20, - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ) - ], - ), - ) - ], + Expanded( + flex: 6, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + SvgPicture.asset( + Assets.svg.chevronRight, + width: 20, + height: 20, + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ) + ], + ), + ) + ], + ), ), ), ); From cebbfcf82d3a54b3ec4b740878afbd653a42abfc Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 15 Nov 2022 17:45:16 -0600 Subject: [PATCH 270/426] desktop currency update on save only --- .../global_settings_view/currency_view.dart | 76 +++++++++++++------ .../currency_settings/currency_settings.dart | 2 +- 2 files changed, 55 insertions(+), 23 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/currency_view.dart b/lib/pages/settings_views/global_settings_view/currency_view.dart index 4f2c3258c..4e8fd5f6e 100644 --- a/lib/pages/settings_views/global_settings_view/currency_view.dart +++ b/lib/pages/settings_views/global_settings_view/currency_view.dart @@ -14,11 +14,10 @@ import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; -import '../../../widgets/rounded_white_container.dart'; - class BaseCurrencySettingsView extends ConsumerStatefulWidget { const BaseCurrencySettingsView({Key? key}) : super(key: key); @@ -37,14 +36,20 @@ class _CurrencyViewState extends ConsumerState<BaseCurrencySettingsView> { final _searchFocusNode = FocusNode(); void onTap(int index) { - if (currenciesWithoutSelected[index] == current || current.isEmpty) { - // ignore if already selected currency - return; + if (Util.isDesktop) { + setState(() { + current = currenciesWithoutSelected[index]; + }); + } else { + if (currenciesWithoutSelected[index] == current || current.isEmpty) { + // ignore if already selected currency + return; + } + current = currenciesWithoutSelected[index]; + currenciesWithoutSelected.remove(current); + currenciesWithoutSelected.insert(0, current); + ref.read(prefsChangeNotifierProvider).currency = current; } - current = currenciesWithoutSelected[index]; - currenciesWithoutSelected.remove(current); - currenciesWithoutSelected.insert(0, current); - ref.read(prefsChangeNotifierProvider).currency = current; } BorderRadius? _borderRadius(int index) { @@ -82,6 +87,15 @@ class _CurrencyViewState extends ConsumerState<BaseCurrencySettingsView> { @override void initState() { _searchController = TextEditingController(); + if (Util.isDesktop) { + currenciesWithoutSelected = + ref.read(baseCurrenciesProvider).map.keys.toList(); + current = ref.read(prefsChangeNotifierProvider).currency; + if (current.isNotEmpty) { + currenciesWithoutSelected.remove(current); + currenciesWithoutSelected.insert(0, current); + } + } super.initState(); } @@ -94,20 +108,25 @@ class _CurrencyViewState extends ConsumerState<BaseCurrencySettingsView> { @override Widget build(BuildContext context) { - current = ref - .watch(prefsChangeNotifierProvider.select((value) => value.currency)); - - currenciesWithoutSelected = ref - .watch(baseCurrenciesProvider.select((value) => value.map)) - .keys - .toList(); - if (current.isNotEmpty) { - currenciesWithoutSelected.remove(current); - currenciesWithoutSelected.insert(0, current); - } - currenciesWithoutSelected = _filtered(); final isDesktop = Util.isDesktop; + if (!isDesktop) { + current = ref + .watch(prefsChangeNotifierProvider.select((value) => value.currency)); + + currenciesWithoutSelected = ref + .watch(baseCurrenciesProvider.select((value) => value.map)) + .keys + .toList(); + + if (current.isNotEmpty) { + currenciesWithoutSelected.remove(current); + currenciesWithoutSelected.insert(0, current); + } + } + + currenciesWithoutSelected = _filtered(); + return ConditionalParent( condition: !isDesktop, builder: (child) { @@ -181,7 +200,20 @@ class _CurrencyViewState extends ConsumerState<BaseCurrencySettingsView> { child: PrimaryButton( label: "Save changes", desktopMed: true, - onPressed: Navigator.of(context).pop, + onPressed: () { + ref.read(prefsChangeNotifierProvider).currency = + current; + + if (ref + .read(prefsChangeNotifierProvider) + .externalCalls) { + ref + .read(priceAnd24hChangeNotifierProvider) + .updatePrice(); + } + + Navigator.of(context).pop(); + }, ), ), ], diff --git a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart index ecc19ebb7..ad7d749ee 100644 --- a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart @@ -102,7 +102,7 @@ class _CurrencySettings extends ConsumerState<CurrencySettings> { crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: EdgeInsets.all( + padding: const EdgeInsets.all( 10, ), child: PrimaryButton( From f7b5462029f7bf9c3a19e4a2d5d4ac443a9b5cfb Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 15 Nov 2022 17:55:34 -0600 Subject: [PATCH 271/426] increase min window height for desktop --- lib/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/main.dart b/lib/main.dart index 21abd9df7..b1f917f58 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -76,7 +76,7 @@ void main() async { if (Util.isDesktop) { setWindowTitle('Stack Wallet'); - setWindowMinSize(const Size(1200, 900)); + setWindowMinSize(const Size(1200, 1100)); setWindowMaxSize(Size.infinite); } From 1b5ced50613c05c02cdb0bd0bde7208760b94b44 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 15 Nov 2022 19:27:08 -0600 Subject: [PATCH 272/426] desktop save receiving qr image to file --- .../generate_receiving_uri_qr_code_view.dart | 61 +++++++++++++++---- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart b/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart index ae615bd96..75d0deba0 100644 --- a/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart +++ b/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart @@ -1,9 +1,11 @@ +import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; import 'dart:ui' as ui; // import 'package:document_file_save_plus/document_file_save_plus.dart'; import 'package:decimal/decimal.dart'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_svg/svg.dart'; @@ -71,19 +73,53 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> { await image.toByteData(format: ui.ImageByteFormat.png); Uint8List pngBytes = byteData!.buffer.asUint8List(); - // if (shouldSaveInsteadOfShare) { - // await DocumentFileSavePlus.saveFile( - // pngBytes, - // "receive_qr_code_${DateTime.now().toLocal().toIso8601String()}.png", - // "image/png"); - // } else { - final tempDir = await getTemporaryDirectory(); - final file = await File("${tempDir.path}/qrcode.png").create(); - await file.writeAsBytes(pngBytes); + if (shouldSaveInsteadOfShare) { + if (Util.isDesktop) { + final dir = Directory("${Platform.environment['HOME']}"); + if (!dir.existsSync()) { + throw Exception( + "Home dir not found while trying to open filepicker on QR image save"); + } + final path = await FilePicker.platform.saveFile( + fileName: "qrcode.png", + initialDirectory: dir.path, + ); - await Share.shareFiles(["${tempDir.path}/qrcode.png"], - text: "Receive URI QR Code"); - // } + if (path != null) { + final file = File(path); + if (file.existsSync()) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "$path already exists!", + context: context, + ), + ); + } else { + await file.writeAsBytes(pngBytes); + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "$path saved!", + context: context, + ), + ); + } + } + } else { + // await DocumentFileSavePlus.saveFile( + // pngBytes, + // "receive_qr_code_${DateTime.now().toLocal().toIso8601String()}.png", + // "image/png"); + } + } else { + final tempDir = await getTemporaryDirectory(); + final file = await File("${tempDir.path}/qrcode.png").create(); + await file.writeAsBytes(pngBytes); + + await Share.shareFiles(["${tempDir.path}/qrcode.png"], + text: "Receive URI QR Code"); + } } catch (e) { debugPrint(e.toString()); } @@ -567,6 +603,7 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> { desktopMed: true, onPressed: () async { // TODO: add save functionality instead of share + // save works on linux at the moment await _capturePng(true); }, label: "Save", From 11bae1740db7eb132bb755717d45dee6ee4c80b1 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 15 Nov 2022 19:52:20 -0600 Subject: [PATCH 273/426] desktop qr save only button --- .../generate_receiving_uri_qr_code_view.dart | 42 +++++++++++-------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart b/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart index 75d0deba0..981def830 100644 --- a/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart +++ b/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart @@ -547,6 +547,7 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> { borderColor: Theme.of(context) .extension<StackColors>()! .background, + width: isDesktop ? 370 : null, child: Column( children: [ Text( @@ -578,26 +579,31 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> { height: 12, ), Row( + mainAxisAlignment: isDesktop + ? MainAxisAlignment.center + : MainAxisAlignment.start, children: [ - SecondaryButton( - width: 170, - desktopMed: true, - onPressed: () async { - await _capturePng(false); - }, - label: "Share", - icon: SvgPicture.asset( - Assets.svg.share, - width: 20, - height: 20, - color: Theme.of(context) - .extension<StackColors>()! - .buttonTextSecondary, + if (!isDesktop) + SecondaryButton( + width: 170, + desktopMed: true, + onPressed: () async { + await _capturePng(false); + }, + label: "Share", + icon: SvgPicture.asset( + Assets.svg.share, + width: 20, + height: 20, + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, + ), + ), + if (!isDesktop) + const SizedBox( + width: 16, ), - ), - const SizedBox( - width: 16, - ), PrimaryButton( width: 170, desktopMed: true, From b64246228cadb58d4d9c32faf4b9fd005d9f4112 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 15 Nov 2022 19:58:32 -0600 Subject: [PATCH 274/426] update addressbookcard test --- test/widget_tests/address_book_card_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/widget_tests/address_book_card_test.dart b/test/widget_tests/address_book_card_test.dart index 07b1387df..7c53d8d50 100644 --- a/test/widget_tests/address_book_card_test.dart +++ b/test/widget_tests/address_book_card_test.dart @@ -70,7 +70,7 @@ void main() { await widgetTester.tap(find.byType(RawMaterialButton)); expect(find.byType(ContactPopUp), findsOneWidget); } else if (Util.isDesktop) { - expect(find.byType(RawMaterialButton), findsNothing); + expect(find.byType(RawMaterialButton), findsOneWidget); } }); } From bfc1603197abca97426b06c7599b12409d10f2ed Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Wed, 16 Nov 2022 08:11:25 -0700 Subject: [PATCH 275/426] fixed setting container corner rounding and padding --- .../advanced_settings/advanced_settings.dart | 1 + .../home/settings_menu/appearance_settings.dart | 1 + .../backup_and_restore/backup_and_restore_settings.dart | 3 +++ .../currency_settings/currency_settings.dart | 1 + .../language_settings/language_settings.dart | 1 + .../home/settings_menu/nodes_settings.dart | 8 ++++---- .../home/settings_menu/security_settings.dart | 2 +- .../home/settings_menu/syncing_preferences_settings.dart | 1 + 8 files changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart b/lib/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart index 0991a14ca..b4ff3fe6a 100644 --- a/lib/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart @@ -32,6 +32,7 @@ class _AdvancedSettings extends ConsumerState<AdvancedSettings> { right: 30, ), child: RoundedWhiteContainer( + radiusMultiplier: 2, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart b/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart index 27a10ca80..7a9ed557f 100644 --- a/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart @@ -49,6 +49,7 @@ class _AppearanceOptionSettings right: 30, ), child: RoundedWhiteContainer( + radiusMultiplier: 2, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart index 663c3f975..7d70d4d0f 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart @@ -338,6 +338,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { right: 30, ), child: RoundedWhiteContainer( + radiusMultiplier: 2, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -501,6 +502,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { right: 30, ), child: RoundedWhiteContainer( + radiusMultiplier: 2, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -583,6 +585,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { bottom: 40, ), child: RoundedWhiteContainer( + radiusMultiplier: 2, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart index ad7d749ee..4c4225ce4 100644 --- a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart @@ -64,6 +64,7 @@ class _CurrencySettings extends ConsumerState<CurrencySettings> { right: 30, ), child: RoundedWhiteContainer( + radiusMultiplier: 2, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart b/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart index 96ce5cf61..08aeb9bc3 100644 --- a/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart @@ -39,6 +39,7 @@ class _LanguageOptionSettings extends ConsumerState<LanguageOptionSettings> { right: 30, ), child: RoundedWhiteContainer( + radiusMultiplier: 2, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart b/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart index 012f7b47a..a7e95d33a 100644 --- a/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/nodes_settings.dart @@ -88,12 +88,12 @@ class _NodesSettings extends ConsumerState<NodesSettings> { Expanded( child: Padding( padding: const EdgeInsets.only( - right: 32, - bottom: 32, + right: 30, + // bottom: 32, ), child: RoundedWhiteContainer( radiusMultiplier: 2, - padding: const EdgeInsets.all(24), + padding: const EdgeInsets.all(20), child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -114,7 +114,7 @@ class _NodesSettings extends ConsumerState<NodesSettings> { style: STextStyles.desktopTextSmall(context), ), const SizedBox( - height: 8, + height: 16, ), Text( "Select a coin to see nodes", diff --git a/lib/pages_desktop_specific/home/settings_menu/security_settings.dart b/lib/pages_desktop_specific/home/settings_menu/security_settings.dart index de3505ace..9f870440b 100644 --- a/lib/pages_desktop_specific/home/settings_menu/security_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/security_settings.dart @@ -144,7 +144,7 @@ class _SecuritySettings extends ConsumerState<SecuritySettings> { right: 30, ), child: RoundedWhiteContainer( - // radiusMultiplier: 2, + radiusMultiplier: 2, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart b/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart index 3d8bebb81..408b93e15 100644 --- a/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart @@ -30,6 +30,7 @@ class _SyncingPreferencesSettings right: 30, ), child: RoundedWhiteContainer( + radiusMultiplier: 2, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ From 0f3a0b18a4a156a7fdb79f162dde21c326fb4036 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Wed, 16 Nov 2022 09:53:51 -0700 Subject: [PATCH 276/426] manual backup ok button on dialog works --- .../create_backup_view.dart | 142 +++++++++--------- 1 file changed, 74 insertions(+), 68 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart index 821504bf8..55fc08087 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart @@ -654,9 +654,10 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { }, ), ); + // make sure the dialog is able to be displayed for at least 1 second - await Future<void>.delayed( - const Duration(seconds: 1)); + final fut = Future<void>.delayed( + const Duration(seconds: 2)); final DateTime now = DateTime.now(); final String fileToSave = @@ -674,81 +675,86 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { jsonEncode(backup), ); + await Future.wait([fut]); + if (mounted) { // pop encryption progress dialog if (!isDesktop) Navigator.of(context).pop(); if (result) { await showDialog<dynamic>( - context: context, - barrierDismissible: false, - builder: (_) => Platform.isAndroid - ? StackOkDialog( + context: context, + barrierDismissible: false, + builder: (context) { + if (Platform.isAndroid) { + return StackOkDialog( title: "Backup saved to:", message: fileToSave, - ) - : !isDesktop - ? const StackOkDialog( - title: - "Backup creation succeeded") - : DesktopDialog( - maxHeight: double.infinity, - maxWidth: 500, - child: Padding( - padding: - const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: Column( - mainAxisSize: - MainAxisSize.min, - crossAxisAlignment: - CrossAxisAlignment - .start, - children: [ - const SizedBox( - height: 26), - Text( - "Stack backup saved to: \n", - style: STextStyles - .desktopH3( - context), - ), - Text( - fileToSave, - style: STextStyles - .desktopTextExtraExtraSmall( - context), - ), - const SizedBox( - height: 40, - ), - Row( - children: [ - // const Spacer(), - Expanded( - child: - PrimaryButton( - label: "Ok", - desktopMed: - true, - onPressed: - () { - // Navigator.of( - // context) - // .pop(); - }, - ), - ), - ], - ) - ], - ), - ), + ); + } else if (isDesktop) { + return DesktopDialog( + maxHeight: double.infinity, + maxWidth: 500, + child: Padding( + padding: + const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, ), - ); + child: Column( + mainAxisSize: + MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + const SizedBox( + height: 26), + Text( + "Stack backup saved to: \n", + style: STextStyles + .desktopH3(context), + ), + Text( + fileToSave, + style: STextStyles + .desktopTextExtraExtraSmall( + context), + ), + const SizedBox( + height: 40, + ), + Row( + children: [ + // const Spacer(), + Expanded( + child: + PrimaryButton( + label: "Ok", + desktopMed: true, + onPressed: () { + int count = 0; + Navigator.of( + context) + .popUntil((_) => + count++ >= + 2); + }, + ), + ), + ], + ) + ], + ), + ), + ); + } else { + return const StackOkDialog( + title: + "Backup creation succeeded"); + } + }); passwordController.text = ""; passwordRepeatController.text = ""; setState(() {}); From e9a5cc85aec3e07b72a0b1eee81d03c0ea4454f6 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 16 Nov 2022 11:15:41 -0600 Subject: [PATCH 277/426] add delay for ui to update properly --- .../stack_backup_views/create_backup_view.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart index 55fc08087..02556beb9 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart @@ -655,9 +655,12 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { ), ); + await Future<void>.delayed( + const Duration(seconds: 1)); + // make sure the dialog is able to be displayed for at least 1 second final fut = Future<void>.delayed( - const Duration(seconds: 2)); + const Duration(seconds: 1)); final DateTime now = DateTime.now(); final String fileToSave = From c50b2054fe8025fd804220695e0b6f1d88e62bb7 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 16 Nov 2022 11:23:42 -0600 Subject: [PATCH 278/426] no notifications fix --- .../desktop_notifications_view.dart | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/pages_desktop_specific/home/notifications/desktop_notifications_view.dart b/lib/pages_desktop_specific/home/notifications/desktop_notifications_view.dart index 0c51f899d..b9c03ff19 100644 --- a/lib/pages_desktop_specific/home/notifications/desktop_notifications_view.dart +++ b/lib/pages_desktop_specific/home/notifications/desktop_notifications_view.dart @@ -39,13 +39,20 @@ class _DesktopNotificationsViewState ), ), body: notifications.isEmpty - ? RoundedWhiteContainer( - child: Center( - child: Text( - "Notifications will appear here", - style: STextStyles.desktopTextExtraExtraSmall(context), + ? Column( + children: [ + Padding( + padding: const EdgeInsets.all(24), + child: RoundedWhiteContainer( + child: Center( + child: Text( + "Notifications will appear here", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + ), ), - ), + ], ) : ListView.builder( primary: false, From 0ce2477cf81fcc1a278a41748b9f2cd5424fb97f Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 16 Nov 2022 12:08:19 -0600 Subject: [PATCH 279/426] desktop addressbook layout --- .../desktop_address_book.dart | 176 ++++++++---------- 1 file changed, 74 insertions(+), 102 deletions(-) diff --git a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart index 407ed9897..ec40e5f60 100644 --- a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart +++ b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart @@ -13,8 +13,10 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; -import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; @@ -89,37 +91,31 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); final hasWallets = ref.watch(walletsChangeNotifierProvider).hasWallets; - final size = MediaQuery.of(context).size; - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - DesktopAppBar( - isCompactHeight: true, - leading: Row( - children: [ - const SizedBox( - width: 24, - ), - Text( - "Address Book", - style: STextStyles.desktopH3(context), - ) - ], - ), + return DesktopScaffold( + appBar: DesktopAppBar( + isCompactHeight: true, + leading: Row( + children: [ + const SizedBox( + width: 24, + ), + Text( + "Address Book", + style: STextStyles.desktopH3(context), + ) + ], ), - const SizedBox(height: 53), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: RoundedContainer( - color: Theme.of(context).extension<StackColors>()!.background, - child: Row( - children: [ - SizedBox( - height: 60, - width: size.width - 800, - child: ClipRRect( + ), + body: Padding( + padding: const EdgeInsets.all(24), + child: Row( + children: [ + Expanded( + flex: 6, + child: Column( + children: [ + ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), @@ -172,81 +168,57 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { ), ), ), - ), - const SizedBox(width: 20), - TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getDesktopMenuButtonColorSelected(context), - onPressed: () { - selectCryptocurrency(); - }, - child: SizedBox( - width: 200, - height: 56, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: SvgPicture.asset(Assets.svg.filter), - ), - Text( - "Filter", - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ), - ), - ], - ), + const SizedBox( + height: 24, ), - ), - const SizedBox(width: 20), - TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () { - newContact(); - }, - child: SizedBox( - width: 200, - height: 56, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: SvgPicture.asset(Assets.svg.circlePlus), - ), - Text( - "Add new", - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .popupBG, - ), - ), - ], - ), - ), - ), - ], + const AddressBookView(), + ], + ), ), - ), + const SizedBox( + width: 20, + ), + Expanded( + flex: 5, + child: Column( + children: [ + Row( + children: [ + SecondaryButton( + width: 184, + label: "Filter", + desktopMed: true, + icon: SvgPicture.asset( + Assets.svg.filter, + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, + ), + onPressed: selectCryptocurrency, + ), + const SizedBox( + width: 20, + ), + PrimaryButton( + width: 184, + label: "Add new", + desktopMed: true, + icon: SvgPicture.asset( + Assets.svg.circlePlus, + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextPrimary, + ), + onPressed: newContact, + ), + ], + ), + ], + ), + ), + ], ), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 24, vertical: 26), - child: SizedBox( - width: 489, - child: AddressBookView(), - ), - ), - ], + ), ); } } From f66b780e53ca89731f7dad19324e28a7f2ea76d4 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 16 Nov 2022 12:09:19 -0600 Subject: [PATCH 280/426] desktop popup edge color fix --- lib/widgets/desktop/desktop_dialog.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/widgets/desktop/desktop_dialog.dart b/lib/widgets/desktop/desktop_dialog.dart index d11124ba6..59c59c575 100644 --- a/lib/widgets/desktop/desktop_dialog.dart +++ b/lib/widgets/desktop/desktop_dialog.dart @@ -25,6 +25,7 @@ class DesktopDialog extends StatelessWidget { maxHeight: maxHeight, ), child: Material( + color: Colors.transparent, borderRadius: BorderRadius.circular( 20, ), From 016b90e904e3e9c6651c42a9c0b50415bdb6839b Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 16 Nov 2022 12:22:09 -0600 Subject: [PATCH 281/426] support view full buttons desktop --- .../global_settings_view/support_view.dart | 57 ++++++++----------- 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/support_view.dart b/lib/pages/settings_views/global_settings_view/support_view.dart index 02d281b66..9ff50345c 100644 --- a/lib/pages/settings_views/global_settings_view/support_view.dart +++ b/lib/pages/settings_views/global_settings_view/support_view.dart @@ -7,7 +7,6 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; -import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -138,30 +137,20 @@ class AboutItem extends StatelessWidget { return RoundedWhiteContainer( padding: const EdgeInsets.all(0), - child: ConditionalParent( - condition: !isDesktop, - builder: (child) => RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - launchUrl( - Uri.parse(linkUrl), - mode: LaunchMode.externalApplication, - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), - child: child, + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), ), + onPressed: () { + launchUrl( + Uri.parse(linkUrl), + mode: LaunchMode.externalApplication, + ); + }, child: Padding( padding: isDesktop ? const EdgeInsets.symmetric( @@ -212,15 +201,19 @@ class AboutItem extends StatelessWidget { ], ), if (isDesktop) - BlueTextButton( - text: buttonText, - onTap: () { - launchUrl( - Uri.parse(linkUrl), - mode: LaunchMode.externalApplication, - ); - }, - ), + Text( + buttonText, + style: STextStyles.desktopTextExtraExtraSmall(context), + ) + // BlueTextButton( + // text: buttonText, + // onTap: () { + // launchUrl( + // Uri.parse(linkUrl), + // mode: LaunchMode.externalApplication, + // ); + // }, + // ), ], ), ), From 2c88b017f3d399b72e0bb3aec9f4b26e5be200b9 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 16 Nov 2022 12:23:12 -0600 Subject: [PATCH 282/426] updated discord link --- lib/pages/settings_views/global_settings_view/support_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/settings_views/global_settings_view/support_view.dart b/lib/pages/settings_views/global_settings_view/support_view.dart index 9ff50345c..ee00bdb00 100644 --- a/lib/pages/settings_views/global_settings_view/support_view.dart +++ b/lib/pages/settings_views/global_settings_view/support_view.dart @@ -73,7 +73,7 @@ class SupportView extends StatelessWidget { height: 8, ), AboutItem( - linkUrl: "https://discord.gg/RZMG3yUm", + linkUrl: "https://discord.com/invite/mRPZuXx3At", label: "Discord", buttonText: "Stack Wallet", iconAsset: Assets.socials.discord, From be70d75d7551bc7b7cadf3227aeaf741512add9d Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 16 Nov 2022 12:27:57 -0600 Subject: [PATCH 283/426] mouse cursor for desktop favorites card --- .../sub_widgets/favorite_card.dart | 357 +++++++++--------- 1 file changed, 183 insertions(+), 174 deletions(-) diff --git a/lib/pages/wallets_view/sub_widgets/favorite_card.dart b/lib/pages/wallets_view/sub_widgets/favorite_card.dart index 8ce8add17..7749f264d 100644 --- a/lib/pages/wallets_view/sub_widgets/favorite_card.dart +++ b/lib/pages/wallets_view/sub_widgets/favorite_card.dart @@ -13,6 +13,7 @@ import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:tuple/tuple.dart'; class FavoriteCard extends ConsumerStatefulWidget { @@ -54,190 +55,198 @@ class _FavoriteCardState extends ConsumerState<FavoriteCard> { final externalCalls = ref.watch( prefsChangeNotifierProvider.select((value) => value.externalCalls)); - return GestureDetector( - onTap: () { - if (Util.isDesktop) { - Navigator.of(context).pushNamed( - DesktopWalletView.routeName, - arguments: walletId, - ); - } else { - Navigator.of(context).pushNamed( - WalletView.routeName, - arguments: Tuple2( - walletId, - managerProvider, - ), - ); - } - }, - child: SizedBox( - width: widget.width, - height: widget.height, - child: CardOverlayStack( - background: Stack( - children: [ - Container( - width: widget.width, - height: widget.height, - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .colorForCoin(coin), - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), + return ConditionalParent( + condition: Util.isDesktop, + builder: (child) => MouseRegion( + cursor: SystemMouseCursors.click, + child: child, + ), + child: GestureDetector( + onTap: () { + if (Util.isDesktop) { + Navigator.of(context).pushNamed( + DesktopWalletView.routeName, + arguments: walletId, + ); + } else { + Navigator.of(context).pushNamed( + WalletView.routeName, + arguments: Tuple2( + walletId, + managerProvider, ), - Column( - children: [ - const Spacer(), - SizedBox( - height: widget.width * 0.3, - child: Row( - children: [ - const Spacer( - flex: 9, - ), - SvgPicture.asset( - Assets.svg.ellipse2, - height: widget.width * 0.3, - ), - // ), - const Spacer( - flex: 2, - ), - ], - ), - ), - ], - ), - Row( - children: [ - const Spacer( - flex: 5, - ), - SizedBox( - width: widget.width * 0.45, - child: Column( - children: [ - SvgPicture.asset( - Assets.svg.ellipse1, - width: widget.width * 0.45, - ), - const Spacer(), - ], - ), - ), - const Spacer( - flex: 1, - ), - ], - ), - ], - ), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + ); + } + }, + child: SizedBox( + width: widget.width, + height: widget.height, + child: CardOverlayStack( + background: Stack( children: [ - Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Text( - ref.watch(managerProvider - .select((value) => value.walletName)), - style: STextStyles.itemSubtitle12(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textFavoriteCard, - ), - overflow: TextOverflow.fade, - ), - ), - SvgPicture.asset( - Assets.svg.iconFor(coin: coin), - width: 24, - height: 24, - ), - ], + Container( + width: widget.width, + height: widget.height, + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .colorForCoin(coin), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), ), - FutureBuilder( - future: ref.watch( - managerProvider.select((value) => value.totalBalance)), - builder: (builderContext, AsyncSnapshot<Decimal> snapshot) { - if (snapshot.connectionState == ConnectionState.done && - snapshot.hasData) { - if (snapshot.data != null) { - _cachedBalance = snapshot.data!; - if (externalCalls) { - _cachedFiatValue = _cachedBalance * - ref - .watch( - priceAnd24hChangeNotifierProvider.select( - (value) => value.getPrice(coin), - ), - ) - .item1; - } - } - } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FittedBox( - fit: BoxFit.scaleDown, - child: Text( - "${Format.localizedStringAsFixed( - decimalPlaces: 8, - value: _cachedBalance, - locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), - )} ${coin.ticker}", - style: STextStyles.titleBold12(context).copyWith( - fontSize: 16, - color: Theme.of(context) - .extension<StackColors>()! - .textFavoriteCard, - ), + Column( + children: [ + const Spacer(), + SizedBox( + height: widget.width * 0.3, + child: Row( + children: [ + const Spacer( + flex: 9, ), - ), - if (externalCalls) - const SizedBox( - height: 4, + SvgPicture.asset( + Assets.svg.ellipse2, + height: widget.width * 0.3, ), - if (externalCalls) - Text( - "${Format.localizedStringAsFixed( - decimalPlaces: 2, - value: _cachedFiatValue, - locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), - )} ${ref.watch( - prefsChangeNotifierProvider - .select((value) => value.currency), - )}", - style: STextStyles.itemSubtitle12(context).copyWith( - fontSize: 10, - color: Theme.of(context) - .extension<StackColors>()! - .textFavoriteCard, - ), + // ), + const Spacer( + flex: 2, ), - ], - ); - }, + ], + ), + ), + ], + ), + Row( + children: [ + const Spacer( + flex: 5, + ), + SizedBox( + width: widget.width * 0.45, + child: Column( + children: [ + SvgPicture.asset( + Assets.svg.ellipse1, + width: widget.width * 0.45, + ), + const Spacer(), + ], + ), + ), + const Spacer( + flex: 1, + ), + ], ), ], ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + ref.watch(managerProvider + .select((value) => value.walletName)), + style: STextStyles.itemSubtitle12(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFavoriteCard, + ), + overflow: TextOverflow.fade, + ), + ), + SvgPicture.asset( + Assets.svg.iconFor(coin: coin), + width: 24, + height: 24, + ), + ], + ), + ), + FutureBuilder( + future: ref.watch( + managerProvider.select((value) => value.totalBalance)), + builder: (builderContext, AsyncSnapshot<Decimal> snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData) { + if (snapshot.data != null) { + _cachedBalance = snapshot.data!; + if (externalCalls) { + _cachedFiatValue = _cachedBalance * + ref + .watch( + priceAnd24hChangeNotifierProvider.select( + (value) => value.getPrice(coin), + ), + ) + .item1; + } + } + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FittedBox( + fit: BoxFit.scaleDown, + child: Text( + "${Format.localizedStringAsFixed( + decimalPlaces: 8, + value: _cachedBalance, + locale: ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ), + )} ${coin.ticker}", + style: STextStyles.titleBold12(context).copyWith( + fontSize: 16, + color: Theme.of(context) + .extension<StackColors>()! + .textFavoriteCard, + ), + ), + ), + if (externalCalls) + const SizedBox( + height: 4, + ), + if (externalCalls) + Text( + "${Format.localizedStringAsFixed( + decimalPlaces: 2, + value: _cachedFiatValue, + locale: ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ), + )} ${ref.watch( + prefsChangeNotifierProvider + .select((value) => value.currency), + )}", + style: + STextStyles.itemSubtitle12(context).copyWith( + fontSize: 10, + color: Theme.of(context) + .extension<StackColors>()! + .textFavoriteCard, + ), + ), + ], + ); + }, + ), + ], + ), + ), ), ), ), From dfc52e3e7e6a3f3169b3e3c410ccbd0920a9e963 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Wed, 16 Nov 2022 12:29:51 -0700 Subject: [PATCH 284/426] unsuccessful login lag fixed --- lib/pages_desktop_specific/desktop_login_view.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/pages_desktop_specific/desktop_login_view.dart b/lib/pages_desktop_specific/desktop_login_view.dart index ebd1c334b..f60ce2240 100644 --- a/lib/pages_desktop_specific/desktop_login_view.dart +++ b/lib/pages_desktop_specific/desktop_login_view.dart @@ -55,6 +55,8 @@ class _DesktopLoginViewState extends ConsumerState<DesktopLoginView> { ), ); + await Future<void>.delayed(const Duration(seconds: 1)); + await ref .read(storageCryptoHandlerProvider) .initFromExisting(passwordController.text); @@ -79,6 +81,8 @@ class _DesktopLoginViewState extends ConsumerState<DesktopLoginView> { // pop loading indicator Navigator.of(context).pop(); + await Future<void>.delayed(const Duration(seconds: 1)); + await showFloatingFlushBar( type: FlushBarType.warning, message: e.toString(), From 5f863bb35a49f661bea9a4b3bfeea19e1989f4a1 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Wed, 16 Nov 2022 13:18:32 -0700 Subject: [PATCH 285/426] QR button on desktop Send commented out --- .../wallet_view/sub_widgets/desktop_send.dart | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index cd19c53f8..ec9a1620a 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -40,7 +40,6 @@ import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/icon_widgets/addressbook_icon.dart'; import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart'; -import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; @@ -1247,12 +1246,12 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { }, child: const AddressBookIcon(), ), - if (sendToController.text.isEmpty) - TextFieldIconButton( - key: const Key("sendViewScanQrButtonKey"), - onTap: scanQr, - child: const QrCodeIcon(), - ) + // if (sendToController.text.isEmpty) + // TextFieldIconButton( + // key: const Key("sendViewScanQrButtonKey"), + // onTap: scanQr, + // child: const QrCodeIcon(), + // ) ], ), ), From bd0b01efcd71d9cee38cd26eb3f7fb37e2c7fcdf Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Wed, 16 Nov 2022 15:51:49 -0700 Subject: [PATCH 286/426] desktop debug shows search bar when scrolling --- .../advanced_settings/debug_info_dialog.dart | 122 ++++++++---------- 1 file changed, 57 insertions(+), 65 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/advanced_settings/debug_info_dialog.dart b/lib/pages_desktop_specific/home/settings_menu/advanced_settings/debug_info_dialog.dart index cf687e3e7..3235a5549 100644 --- a/lib/pages_desktop_specific/home/settings_menu/advanced_settings/debug_info_dialog.dart +++ b/lib/pages_desktop_specific/home/settings_menu/advanced_settings/debug_info_dialog.dart @@ -108,10 +108,65 @@ class _DebugInfoDialog extends ConsumerState<DebugInfoDialog> { const DesktopDialogCloseButton(), ], ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 32), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: searchDebugController, + focusNode: searchDebugFocusNode, + onChanged: (newString) { + setState(() => _searchTerm = newString); + }, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + searchDebugFocusNode, + context, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), + ), + suffixIcon: searchDebugController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + searchDebugController.text = ""; + _searchTerm = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + ), Expanded( // flex: 24, child: NestedScrollView( - floatHeaderSlivers: true, + // floatHeaderSlivers: true, headerSliverBuilder: (context, innerBoxIsScrolled) { return [ SliverOverlapAbsorber( @@ -122,70 +177,7 @@ class _DebugInfoDialog extends ConsumerState<DebugInfoDialog> { padding: const EdgeInsets.symmetric( vertical: 16, horizontal: 32), child: Column( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: - Util.isDesktop ? false : true, - controller: searchDebugController, - focusNode: searchDebugFocusNode, - onChanged: (newString) { - setState(() => _searchTerm = newString); - }, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - searchDebugFocusNode, - context, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, - ), - ), - suffixIcon: searchDebugController - .text.isNotEmpty - ? Padding( - padding: - const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - searchDebugController - .text = ""; - _searchTerm = ""; - }); - }, - ), - ], - ), - ), - ) - : null, - ), - ), - ), - ), - const SizedBox( - height: 12, - ), - ], + children: const [], ), ), ), From 2936249bd6190be934a3623457ce2bb679b1cccc Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Wed, 16 Nov 2022 16:36:50 -0700 Subject: [PATCH 287/426] textfields clear on send --- .../wallet_view/sub_widgets/desktop_send.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index ec9a1620a..af2c2517a 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -248,6 +248,13 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { label: "Yes", onPressed: () { Navigator.of(context).pop(true); + + setState(() { + sendToController.text = ""; + cryptoAmountController.text = ""; + baseAmountController.text = ""; + noteController.text = ""; + }); }, ), ), From fc9e4d35dd3d7da66d10a587a052558feee0397d Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 07:02:56 -0600 Subject: [PATCH 288/426] remove loading future --- .../address_book_views/address_book_view.dart | 221 +++++++++--------- 1 file changed, 112 insertions(+), 109 deletions(-) diff --git a/lib/pages/address_book_views/address_book_view.dart b/lib/pages/address_book_views/address_book_view.dart index 50e51110b..fd2a995cc 100644 --- a/lib/pages/address_book_views/address_book_view.dart +++ b/lib/pages/address_book_views/address_book_view.dart @@ -18,7 +18,6 @@ import 'package:stackwallet/widgets/address_book_card.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; -import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; @@ -38,9 +37,9 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> { late TextEditingController _searchController; final _searchFocusNode = FocusNode(); - - List<Contact>? _cache; - List<Contact>? _cacheFav; + // + // List<Contact>? _cache; + // List<Contact>? _cacheFav; String _searchTerm = ""; @@ -100,8 +99,10 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - final addressBookEntriesFuture = ref.watch( - addressBookServiceProvider.select((value) => value.addressBookEntries)); + // final addressBookEntriesFuture = ref.watch( + // addressBookServiceProvider.select((value) => value.addressBookEntries)); + final contacts = + ref.watch(addressBookServiceProvider.select((value) => value.contacts)); final isDesktop = Util.isDesktop; return ConditionalParent( @@ -279,57 +280,58 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> { const SizedBox( height: 12, ), - FutureBuilder( - future: addressBookEntriesFuture, - builder: (_, AsyncSnapshot<List<Contact>> snapshot) { - if (snapshot.connectionState == ConnectionState.done && - snapshot.hasData) { - _cacheFav = snapshot.data!; - } - if (_cacheFav == null) { - // TODO proper loading animation - return const LoadingIndicator(); - } else { - if (_cacheFav!.isNotEmpty) { - return RoundedWhiteContainer( - padding: EdgeInsets.all(!isDesktop ? 0 : 15), - child: Column( - children: [ - ..._cacheFav! - .where((element) => element.addresses - .where((e) => ref.watch( - addressBookFilterProvider.select( - (value) => - value.coins.contains(e.coin)))) - .isNotEmpty) - .where((e) => - e.isFavorite && - ref - .read(addressBookServiceProvider) - .matches(_searchTerm, e)) - .where((element) => element.isFavorite) - .map( - (e) => AddressBookCard( - key: Key("favContactCard_${e.id}_key"), - contactId: e.id, - ), - ), - ], - ), - ); - } else { - return RoundedWhiteContainer( - child: Center( - child: Text( - "Your favorite contacts will appear here", - style: STextStyles.itemSubtitle(context), + // FutureBuilder( + // future: addressBookEntriesFuture, + // builder: (_, AsyncSnapshot<List<Contact>> snapshot) { + // if (snapshot.connectionState == ConnectionState.done && + // snapshot.hasData) { + // _cacheFav = snapshot.data!; + // } + // if (_cacheFav == null) { + // // TODO proper loading animation + // return const LoadingIndicator(); + // } else { + // if (_cacheFav!.isNotEmpty) { + // return + RoundedWhiteContainer( + padding: EdgeInsets.all(!isDesktop ? 0 : 15), + child: Column( + children: [ + ...contacts + .where((element) => element.addresses + .where((e) => ref.watch(addressBookFilterProvider + .select((value) => value.coins.contains(e.coin)))) + .isNotEmpty) + .where((e) => + e.isFavorite && + ref + .read(addressBookServiceProvider) + .matches(_searchTerm, e)) + .where((element) => element.isFavorite) + .map( + (e) => AddressBookCard( + key: Key("favContactCard_${e.id}_key"), + contactId: e.id, ), ), - ); - } - } - }, - ), + ], + ), + ) + // ; + // } else { + // return RoundedWhiteContainer( + // child: Center( + // child: Text( + // "Your favorite contacts will appear here", + // style: STextStyles.itemSubtitle(context), + // ), + // ), + // ); + // } + // } + // }, + // ) + , const SizedBox( height: 16, ), @@ -340,63 +342,64 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> { const SizedBox( height: 12, ), - FutureBuilder( - future: addressBookEntriesFuture, - builder: (_, AsyncSnapshot<List<Contact>> snapshot) { - if (snapshot.connectionState == ConnectionState.done && - snapshot.hasData) { - _cache = snapshot.data!; - } - if (_cache == null) { - // TODO proper loading animation - return const LoadingIndicator(); - } else { - if (_cache!.isNotEmpty) { - return Column( + // FutureBuilder( + // future: addressBookEntriesFuture, + // builder: (_, AsyncSnapshot<List<Contact>> snapshot) { + // if (snapshot.connectionState == ConnectionState.done && + // snapshot.hasData) { + // _cache = snapshot.data!; + // } + // if (_cache == null) { + // // TODO proper loading animation + // return const LoadingIndicator(); + // } else { + // if (_cache!.isNotEmpty) { + // return + Column( + children: [ + RoundedWhiteContainer( + padding: EdgeInsets.all(!isDesktop ? 0 : 15), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( children: [ - RoundedWhiteContainer( - padding: EdgeInsets.all(!isDesktop ? 0 : 15), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - ..._cache! - .where((element) => element.addresses - .where((e) => ref.watch( - addressBookFilterProvider.select( - (value) => value.coins - .contains(e.coin)))) - .isNotEmpty) - .where((e) => ref - .read(addressBookServiceProvider) - .matches(_searchTerm, e)) - .where((element) => !element.isFavorite) - .map( - (e) => AddressBookCard( - key: Key( - "desktopContactCard_${e.id}_key"), - contactId: e.id, - ), - ), - ], + ...contacts + .where((element) => element.addresses + .where((e) => ref.watch( + addressBookFilterProvider.select((value) => + value.coins.contains(e.coin)))) + .isNotEmpty) + .where((e) => ref + .read(addressBookServiceProvider) + .matches(_searchTerm, e)) + .where((element) => !element.isFavorite) + .map( + (e) => AddressBookCard( + key: Key("desktopContactCard_${e.id}_key"), + contactId: e.id, + ), ), - ), - ), ], - ); - } else { - return RoundedWhiteContainer( - child: Center( - child: Text( - "Your contacts will appear here", - style: STextStyles.itemSubtitle(context), - ), - ), - ); - } - } - }, - ), + ), + ), + ), + ], + ) + // ; + // } else { + // return RoundedWhiteContainer( + // child: Center( + // child: Text( + // "Your contacts will appear here", + // style: STextStyles.itemSubtitle(context), + // ), + // ), + // ); + // } + // } + // }, + // ) + , ], ), ), From 7e2160d7ccf37fca61f918771ed7337b71fbd153 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 07:03:09 -0600 Subject: [PATCH 289/426] fix duplicate keys error --- .../address_book_views/subviews/contact_details_view.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages/address_book_views/subviews/contact_details_view.dart b/lib/pages/address_book_views/subviews/contact_details_view.dart index c0c10b3b1..a48a535c6 100644 --- a/lib/pages/address_book_views/subviews/contact_details_view.dart +++ b/lib/pages/address_book_views/subviews/contact_details_view.dart @@ -469,7 +469,7 @@ class _ContactDetailsViewState extends ConsumerState<ContactDetailsView> { ..._cachedTransactions.map( (e) => TransactionCard( key: Key( - "contactDetailsTransaction_${e.item2.txid}_cardKey"), + "contactDetailsTransaction_${e.item1}_${e.item2.txid}_cardKey"), transaction: e.item2, walletId: e.item1, ), @@ -499,7 +499,7 @@ class _ContactDetailsViewState extends ConsumerState<ContactDetailsView> { ..._cachedTransactions.map( (e) => TransactionCard( key: Key( - "contactDetailsTransaction_${e.item2.txid}_cardKey"), + "contactDetailsTransaction_${e.item1}_${e.item2.txid}_cardKey"), transaction: e.item2, walletId: e.item1, ), From e0ef78685ddf9450ac62140544b74c4c8ce8b068 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 07:10:28 -0600 Subject: [PATCH 290/426] empty contacts list fix --- .../address_book_views/address_book_view.dart | 175 +++++++----------- 1 file changed, 69 insertions(+), 106 deletions(-) diff --git a/lib/pages/address_book_views/address_book_view.dart b/lib/pages/address_book_views/address_book_view.dart index fd2a995cc..147e677e0 100644 --- a/lib/pages/address_book_views/address_book_view.dart +++ b/lib/pages/address_book_views/address_book_view.dart @@ -99,8 +99,6 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - // final addressBookEntriesFuture = ref.watch( - // addressBookServiceProvider.select((value) => value.addressBookEntries)); final contacts = ref.watch(addressBookServiceProvider.select((value) => value.contacts)); @@ -280,58 +278,41 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> { const SizedBox( height: 12, ), - // FutureBuilder( - // future: addressBookEntriesFuture, - // builder: (_, AsyncSnapshot<List<Contact>> snapshot) { - // if (snapshot.connectionState == ConnectionState.done && - // snapshot.hasData) { - // _cacheFav = snapshot.data!; - // } - // if (_cacheFav == null) { - // // TODO proper loading animation - // return const LoadingIndicator(); - // } else { - // if (_cacheFav!.isNotEmpty) { - // return - RoundedWhiteContainer( - padding: EdgeInsets.all(!isDesktop ? 0 : 15), - child: Column( - children: [ - ...contacts - .where((element) => element.addresses - .where((e) => ref.watch(addressBookFilterProvider - .select((value) => value.coins.contains(e.coin)))) - .isNotEmpty) - .where((e) => - e.isFavorite && - ref - .read(addressBookServiceProvider) - .matches(_searchTerm, e)) - .where((element) => element.isFavorite) - .map( - (e) => AddressBookCard( - key: Key("favContactCard_${e.id}_key"), - contactId: e.id, + if (contacts.isNotEmpty) + RoundedWhiteContainer( + padding: EdgeInsets.all(!isDesktop ? 0 : 15), + child: Column( + children: [ + ...contacts + .where((element) => element.addresses + .where((e) => ref.watch( + addressBookFilterProvider.select( + (value) => value.coins.contains(e.coin)))) + .isNotEmpty) + .where((e) => + e.isFavorite && + ref + .read(addressBookServiceProvider) + .matches(_searchTerm, e)) + .where((element) => element.isFavorite) + .map( + (e) => AddressBookCard( + key: Key("favContactCard_${e.id}_key"), + contactId: e.id, + ), ), - ), - ], + ], + ), + ), + if (contacts.isEmpty) + RoundedWhiteContainer( + child: Center( + child: Text( + "Your favorite contacts will appear here", + style: STextStyles.itemSubtitle(context), + ), + ), ), - ) - // ; - // } else { - // return RoundedWhiteContainer( - // child: Center( - // child: Text( - // "Your favorite contacts will appear here", - // style: STextStyles.itemSubtitle(context), - // ), - // ), - // ); - // } - // } - // }, - // ) - , const SizedBox( height: 16, ), @@ -342,64 +323,46 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> { const SizedBox( height: 12, ), - // FutureBuilder( - // future: addressBookEntriesFuture, - // builder: (_, AsyncSnapshot<List<Contact>> snapshot) { - // if (snapshot.connectionState == ConnectionState.done && - // snapshot.hasData) { - // _cache = snapshot.data!; - // } - // if (_cache == null) { - // // TODO proper loading animation - // return const LoadingIndicator(); - // } else { - // if (_cache!.isNotEmpty) { - // return - Column( - children: [ - RoundedWhiteContainer( - padding: EdgeInsets.all(!isDesktop ? 0 : 15), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - ...contacts - .where((element) => element.addresses - .where((e) => ref.watch( - addressBookFilterProvider.select((value) => - value.coins.contains(e.coin)))) - .isNotEmpty) - .where((e) => ref - .read(addressBookServiceProvider) - .matches(_searchTerm, e)) - .where((element) => !element.isFavorite) - .map( - (e) => AddressBookCard( - key: Key("desktopContactCard_${e.id}_key"), - contactId: e.id, + if (contacts.isNotEmpty) + Column( + children: [ + RoundedWhiteContainer( + padding: EdgeInsets.all(!isDesktop ? 0 : 15), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + ...contacts + .where((element) => element.addresses + .where((e) => ref.watch( + addressBookFilterProvider.select( + (value) => + value.coins.contains(e.coin)))) + .isNotEmpty) + .where((e) => ref + .read(addressBookServiceProvider) + .matches(_searchTerm, e)) + .map( + (e) => AddressBookCard( + key: Key("desktopContactCard_${e.id}_key"), + contactId: e.id, + ), ), - ), - ], + ], + ), ), ), + ], + ), + if (contacts.isEmpty) + RoundedWhiteContainer( + child: Center( + child: Text( + "Your contacts will appear here", + style: STextStyles.itemSubtitle(context), + ), ), - ], - ) - // ; - // } else { - // return RoundedWhiteContainer( - // child: Center( - // child: Text( - // "Your contacts will appear here", - // style: STextStyles.itemSubtitle(context), - // ), - // ), - // ); - // } - // } - // }, - // ) - , + ), ], ), ), From 7cc3c71b0d1949401d79dcbe453b0dd3783a8a27 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 07:22:53 -0600 Subject: [PATCH 291/426] desktop addressbook search --- .../address_book_views/address_book_view.dart | 306 +++++++++--------- .../desktop_address_book.dart | 14 +- 2 files changed, 158 insertions(+), 162 deletions(-) diff --git a/lib/pages/address_book_views/address_book_view.dart b/lib/pages/address_book_views/address_book_view.dart index 147e677e0..35e2601e2 100644 --- a/lib/pages/address_book_views/address_book_view.dart +++ b/lib/pages/address_book_views/address_book_view.dart @@ -23,11 +23,16 @@ import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; class AddressBookView extends ConsumerStatefulWidget { - const AddressBookView({Key? key, this.coin}) : super(key: key); + const AddressBookView({ + Key? key, + this.coin, + this.filterTerm, + }) : super(key: key); static const String routeName = "/addressBook"; final Coin? coin; + final String? filterTerm; @override ConsumerState<AddressBookView> createState() => _AddressBookViewState(); @@ -37,9 +42,6 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> { late TextEditingController _searchController; final _searchFocusNode = FocusNode(); - // - // List<Contact>? _cache; - // List<Contact>? _cacheFav; String _searchTerm = ""; @@ -198,7 +200,12 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> { child: IntrinsicHeight( child: Padding( padding: const EdgeInsets.all(4), - child: child, + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: MediaQuery.of(context).size.height - 271, + ), + child: child, + ), ), ), ), @@ -208,163 +215,156 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> { ), ); }, - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: MediaQuery.of(context).size.height - 271, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: !isDesktop - ? TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: _searchController, - focusNode: _searchFocusNode, - onChanged: (value) { - setState(() { - _searchTerm = value; - }); - }, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - _searchFocusNode, - context, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: !isDesktop + ? TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: (value) { + setState(() { + _searchTerm = value; + }); + }, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, ), - suffixIcon: _searchController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchController.text = ""; - }); - }, - ), - ], - ), - ), - ) - : null, ), - ) - : null, - ), - if (!isDesktop) const SizedBox(height: 16), - Text( - "Favorites", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 12, - ), - if (contacts.isNotEmpty) - RoundedWhiteContainer( - padding: EdgeInsets.all(!isDesktop ? 0 : 15), - child: Column( - children: [ - ...contacts - .where((element) => element.addresses - .where((e) => ref.watch( - addressBookFilterProvider.select( - (value) => value.coins.contains(e.coin)))) - .isNotEmpty) - .where((e) => - e.isFavorite && - ref - .read(addressBookServiceProvider) - .matches(_searchTerm, e)) - .where((element) => element.isFavorite) - .map( - (e) => AddressBookCard( - key: Key("favContactCard_${e.id}_key"), - contactId: e.id, - ), - ), - ], - ), - ), - if (contacts.isEmpty) - RoundedWhiteContainer( - child: Center( - child: Text( - "Your favorite contacts will appear here", - style: STextStyles.itemSubtitle(context), - ), - ), - ), - const SizedBox( - height: 16, - ), - Text( - "All contacts", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 12, - ), - if (contacts.isNotEmpty) - Column( - children: [ - RoundedWhiteContainer( - padding: EdgeInsets.all(!isDesktop ? 0 : 15), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - ...contacts - .where((element) => element.addresses - .where((e) => ref.watch( - addressBookFilterProvider.select( - (value) => - value.coins.contains(e.coin)))) - .isNotEmpty) - .where((e) => ref - .read(addressBookServiceProvider) - .matches(_searchTerm, e)) - .map( - (e) => AddressBookCard( - key: Key("desktopContactCard_${e.id}_key"), - contactId: e.id, + suffixIcon: _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + }); + }, + ), + ], ), ), - ], - ), + ) + : null, ), - ), + ) + : null, + ), + if (!isDesktop) const SizedBox(height: 16), + Text( + "Favorites", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 12, + ), + if (contacts.isNotEmpty) + RoundedWhiteContainer( + padding: EdgeInsets.all(!isDesktop ? 0 : 15), + child: Column( + children: [ + ...contacts + .where((element) => element.addresses + .where((e) => ref.watch(addressBookFilterProvider + .select((value) => value.coins.contains(e.coin)))) + .isNotEmpty) + .where((e) => + e.isFavorite && + ref + .read(addressBookServiceProvider) + .matches(widget.filterTerm ?? _searchTerm, e)) + .where((element) => element.isFavorite) + .map( + (e) => AddressBookCard( + key: Key("favContactCard_${e.id}_key"), + contactId: e.id, + ), + ), ], ), - if (contacts.isEmpty) - RoundedWhiteContainer( - child: Center( - child: Text( - "Your contacts will appear here", - style: STextStyles.itemSubtitle(context), - ), + ), + if (contacts.isEmpty) + RoundedWhiteContainer( + child: Center( + child: Text( + "Your favorite contacts will appear here", + style: STextStyles.itemSubtitle(context), ), ), - ], - ), + ), + const SizedBox( + height: 16, + ), + Text( + "All contacts", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 12, + ), + if (contacts.isNotEmpty) + Column( + children: [ + RoundedWhiteContainer( + padding: EdgeInsets.all(!isDesktop ? 0 : 15), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + ...contacts + .where((element) => element.addresses + .where((e) => ref.watch( + addressBookFilterProvider.select((value) => + value.coins.contains(e.coin)))) + .isNotEmpty) + .where((e) => ref + .read(addressBookServiceProvider) + .matches(widget.filterTerm ?? _searchTerm, e)) + .map( + (e) => AddressBookCard( + key: Key("desktopContactCard_${e.id}_key"), + contactId: e.id, + ), + ), + ], + ), + ), + ), + ], + ), + if (contacts.isEmpty) + RoundedWhiteContainer( + child: Center( + child: Text( + "Your contacts will appear here", + style: STextStyles.itemSubtitle(context), + ), + ), + ), + ], ), ); } diff --git a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart index ec40e5f60..d561de946 100644 --- a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart +++ b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart @@ -1,11 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/models/contact.dart'; import 'package:stackwallet/pages/address_book_views/address_book_view.dart'; import 'package:stackwallet/pages/address_book_views/subviews/add_address_book_entry_view.dart'; import 'package:stackwallet/pages/address_book_views/subviews/address_book_filter_view.dart'; -import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -34,11 +32,6 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { late final FocusNode _searchFocusNode; - List<Contact>? _cache; - List<Contact>? _cacheFav; - - late bool hasContacts = false; - String _searchTerm = ""; Future<void> selectCryptocurrency() async { @@ -90,7 +83,6 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - final hasWallets = ref.watch(walletsChangeNotifierProvider).hasWallets; return DesktopScaffold( appBar: DesktopAppBar( @@ -171,7 +163,11 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { const SizedBox( height: 24, ), - const AddressBookView(), + Expanded( + child: AddressBookView( + filterTerm: _searchTerm, + ), + ), ], ), ), From 49103c86f1b63bd56563a0aa1167bcda5ee69cd8 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 09:00:10 -0600 Subject: [PATCH 292/426] desktop addressbook layout fix --- .../desktop_address_book.dart | 442 +++++++++++++----- 1 file changed, 338 insertions(+), 104 deletions(-) diff --git a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart index d561de946..abb797aac 100644 --- a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart +++ b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart @@ -1,23 +1,31 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/pages/address_book_views/address_book_view.dart'; +import 'package:stackwallet/models/contact.dart'; +import 'package:stackwallet/models/contact_address_entry.dart'; import 'package:stackwallet/pages/address_book_views/subviews/add_address_book_entry_view.dart'; import 'package:stackwallet/pages/address_book_views/subviews/address_book_filter_view.dart'; +import 'package:stackwallet/providers/global/address_book_service_provider.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/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/address_book_card.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; +import '../../../providers/providers.dart'; +import '../../../providers/ui/address_book_providers/address_book_filter_provider.dart'; + class DesktopAddressBook extends ConsumerStatefulWidget { const DesktopAddressBook({Key? key}) : super(key: key); @@ -69,6 +77,46 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { _searchController = TextEditingController(); _searchFocusNode = FocusNode(); + ref.refresh(addressBookFilterProvider); + + // if (widget.coin == null) { + List<Coin> coins = Coin.values.where((e) => !(e == Coin.epicCash)).toList(); + coins.remove(Coin.firoTestNet); + + bool showTestNet = ref.read(prefsChangeNotifierProvider).showTestNetCoins; + + if (showTestNet) { + ref.read(addressBookFilterProvider).addAll(coins, false); + } else { + ref.read(addressBookFilterProvider).addAll( + coins.getRange(0, coins.length - kTestNetCoinCount + 1), false); + } + // } else { + // ref.read(addressBookFilterProvider).add(widget.coin!, false); + // } + + WidgetsBinding.instance.addPostFrameCallback((_) async { + List<ContactAddressEntry> addresses = []; + final managers = ref.read(walletsChangeNotifierProvider).managers; + for (final manager in managers) { + addresses.add( + ContactAddressEntry( + coin: manager.coin, + address: await manager.currentReceivingAddress, + label: "Current Receiving", + other: manager.walletName, + ), + ); + } + final self = Contact( + name: "My Stack", + addresses: addresses, + isFavorite: true, + id: "default", + ); + await ref.read(addressBookServiceProvider).editContact(self); + }); + super.initState(); } @@ -83,6 +131,32 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); + final contacts = + ref.watch(addressBookServiceProvider.select((value) => value.contacts)); + + final allContacts = contacts + .where((element) => element.addresses + .where((e) => ref.watch(addressBookFilterProvider + .select((value) => value.coins.contains(e.coin)))) + .isNotEmpty) + .where((e) => + ref.read(addressBookServiceProvider).matches(_searchTerm, e)); + + final favorites = contacts + .where((element) => element.addresses + .where((e) => ref.watch(addressBookFilterProvider + .select((value) => value.coins.contains(e.coin)))) + .isNotEmpty) + .where((e) => + e.isFavorite && + ref.read(addressBookServiceProvider).matches(_searchTerm, e)) + .where((element) => element.isFavorite); + + print("========================================================="); + print("contacts: ${contacts.length}"); + print("favorites: ${favorites.length}"); + print("allContacts: ${allContacts.length}"); + print("========================================================="); return DesktopScaffold( appBar: DesktopAppBar( @@ -100,121 +174,281 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { ), ), body: Padding( - padding: const EdgeInsets.all(24), - child: Row( + padding: const EdgeInsets.only( + left: 24, + right: 24, + bottom: 24, + ), + child: DesktopAddressBookScaffold( + controlsLeft: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: (value) { + setState(() { + _searchTerm = value; + }); + }, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 20, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), + ), + suffixIcon: _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + controlsRight: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + SecondaryButton( + width: 184, + label: "Filter", + desktopMed: true, + icon: SvgPicture.asset( + Assets.svg.filter, + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, + ), + onPressed: selectCryptocurrency, + ), + const SizedBox( + width: 20, + ), + PrimaryButton( + width: 184, + label: "Add new", + desktopMed: true, + icon: SvgPicture.asset( + Assets.svg.circlePlus, + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextPrimary, + ), + onPressed: newContact, + ), + ], + ), + filterItems: Container(), + upperLabel: favorites.isEmpty && allContacts.isEmpty + ? null + : Text( + favorites.isEmpty ? "All contacts" : "Favorites", + style: STextStyles.smallMed12(context), + ), + lowerLabel: favorites.isEmpty + ? null + : Padding( + padding: const EdgeInsets.only( + top: 20, + bottom: 12, + ), + child: Text( + "All contacts", + style: STextStyles.smallMed12(context), + ), + ), + favorites: favorites.isNotEmpty + ? RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: Column( + children: [ + ...favorites.map( + (e) => AddressBookCard( + key: Key("favContactCard_${e.id}_key"), + contactId: e.id, + ), + ), + ], + ), + ) + : RoundedWhiteContainer( + child: Center( + child: Text( + "Your favorite contacts will appear here", + style: STextStyles.itemSubtitle(context), + ), + ), + ), + all: allContacts.isNotEmpty + ? Column( + children: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + ...allContacts.map( + (e) => AddressBookCard( + key: Key("desktopContactCard_${e.id}_key"), + contactId: e.id, + ), + ), + ], + ), + ), + ), + ], + ) + : RoundedWhiteContainer( + child: Center( + child: Text( + "Your contacts will appear here", + style: STextStyles.itemSubtitle(context), + ), + ), + ), + details: Container( + color: Colors.purple, + ), + ), + ), + ); + } +} + +class DesktopAddressBookScaffold extends StatelessWidget { + const DesktopAddressBookScaffold({ + Key? key, + required this.controlsLeft, + required this.controlsRight, + required this.filterItems, + required this.upperLabel, + required this.lowerLabel, + required this.favorites, + required this.all, + required this.details, + }) : super(key: key); + + final Widget? controlsLeft; + final Widget? controlsRight; + final Widget? filterItems; + final Widget? upperLabel; + final Widget? lowerLabel; + final Widget? favorites; + final Widget? all; + final Widget? details; + + static const double weirdRowHeight = 30; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, children: [ Expanded( flex: 6, - child: Column( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: _searchController, - focusNode: _searchFocusNode, - onChanged: (value) { - setState(() { - _searchTerm = value; - }); - }, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - _searchFocusNode, - context, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 20, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, - ), - ), - suffixIcon: _searchController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchController.text = ""; - }); - }, - ), - ], - ), - ), - ) - : null, - ), - ), - ), - const SizedBox( - height: 24, - ), - Expanded( - child: AddressBookView( - filterTerm: _searchTerm, - ), - ), - ], - ), + child: controlsLeft ?? Container(), ), const SizedBox( width: 20, ), Expanded( flex: 5, - child: Column( - children: [ - Row( - children: [ - SecondaryButton( - width: 184, - label: "Filter", - desktopMed: true, - icon: SvgPicture.asset( - Assets.svg.filter, - color: Theme.of(context) - .extension<StackColors>()! - .buttonTextSecondary, - ), - onPressed: selectCryptocurrency, - ), - const SizedBox( - width: 20, - ), - PrimaryButton( - width: 184, - label: "Add new", - desktopMed: true, - icon: SvgPicture.asset( - Assets.svg.circlePlus, - color: Theme.of(context) - .extension<StackColors>()! - .buttonTextPrimary, - ), - onPressed: newContact, - ), - ], - ), - ], - ), + child: controlsRight ?? Container(), ), ], ), - ), + const SizedBox( + height: 20, + ), + Row( + children: [ + Expanded( + child: filterItems ?? Container(), + ), + ], + ), + Expanded( + child: Row( + children: [ + Expanded( + flex: 6, + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: weirdRowHeight, + child: upperLabel, + ), + favorites ?? Container(), + lowerLabel ?? Container(), + all ?? Container(), + ], + ), + ), + ), + ); + }, + ), + ), + const SizedBox( + width: 20, + ), + Expanded( + flex: 5, + child: Column( + children: [ + const SizedBox( + height: weirdRowHeight, + ), + Expanded( + child: details ?? Container(), + ), + ], + ), + ), + ], + ), + ) + ], ); } } From 8c0a6f5669de3d0bf4aa8b6c5329ff4d09e6bc30 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 09:04:54 -0600 Subject: [PATCH 293/426] address book search fixes --- .../address_book_views/address_book_view.dart | 1 + .../desktop_address_book.dart | 51 +++++++++---------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/lib/pages/address_book_views/address_book_view.dart b/lib/pages/address_book_views/address_book_view.dart index 35e2601e2..cdc9fb5b7 100644 --- a/lib/pages/address_book_views/address_book_view.dart +++ b/lib/pages/address_book_views/address_book_view.dart @@ -261,6 +261,7 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> { onTap: () async { setState(() { _searchController.text = ""; + _searchTerm = ""; }); }, ), diff --git a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart index abb797aac..51b075284 100644 --- a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart +++ b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart @@ -152,12 +152,6 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { ref.read(addressBookServiceProvider).matches(_searchTerm, e)) .where((element) => element.isFavorite); - print("========================================================="); - print("contacts: ${contacts.length}"); - print("favorites: ${favorites.length}"); - print("allContacts: ${allContacts.length}"); - print("========================================================="); - return DesktopScaffold( appBar: DesktopAppBar( isCompactHeight: true, @@ -222,6 +216,7 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { onTap: () async { setState(() { _searchController.text = ""; + _searchTerm = ""; }); }, ), @@ -284,8 +279,18 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { style: STextStyles.smallMed12(context), ), ), - favorites: favorites.isNotEmpty - ? RoundedWhiteContainer( + favorites: favorites.isEmpty + ? contacts.isNotEmpty + ? null + : RoundedWhiteContainer( + child: Center( + child: Text( + "Your favorite contacts will appear here", + style: STextStyles.itemSubtitle(context), + ), + ), + ) + : RoundedWhiteContainer( padding: const EdgeInsets.all(0), child: Column( children: [ @@ -297,17 +302,19 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { ), ], ), - ) - : RoundedWhiteContainer( - child: Center( - child: Text( - "Your favorite contacts will appear here", - style: STextStyles.itemSubtitle(context), - ), - ), ), - all: allContacts.isNotEmpty - ? Column( + all: allContacts.isEmpty + ? contacts.isNotEmpty + ? null + : RoundedWhiteContainer( + child: Center( + child: Text( + "Your contacts will appear here", + style: STextStyles.itemSubtitle(context), + ), + ), + ) + : Column( children: [ RoundedWhiteContainer( padding: const EdgeInsets.all(0), @@ -326,14 +333,6 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { ), ), ], - ) - : RoundedWhiteContainer( - child: Center( - child: Text( - "Your contacts will appear here", - style: STextStyles.itemSubtitle(context), - ), - ), ), details: Container( color: Colors.purple, From 72248d6a644807d6f7410c31529ae3a57869066f Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 10:12:19 -0600 Subject: [PATCH 294/426] expandable fix --- .../wallet_network_settings_view.dart | 10 +++++----- .../sub_widgets/contact_list_item.dart | 2 +- lib/widgets/expandable.dart | 6 +++--- lib/widgets/node_card.dart | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart index accf244eb..3044467aa 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart @@ -77,7 +77,7 @@ class _WalletNetworkSettingsViewState late double _percent; late int _blocksRemaining; - bool _advancedIsExpanded = true; + bool _advancedIsExpanded = false; Future<void> _attemptRescan() async { if (!Platform.isLinux) await Wakelock.enable(); @@ -855,8 +855,8 @@ class _WalletNetworkSettingsViewState ), SvgPicture.asset( _advancedIsExpanded - ? Assets.svg.chevronDown - : Assets.svg.chevronUp, + ? Assets.svg.chevronUp + : Assets.svg.chevronDown, width: 12, height: 6, color: Theme.of(context) @@ -877,11 +877,11 @@ class _WalletNetworkSettingsViewState text: "Rescan", onTap: () async { await Navigator.of(context).push( - FadePageRoute<void>( + FadePageRoute<void>( ConfirmFullRescanDialog( onConfirm: _attemptRescan, ), - const RouteSettings(), + const RouteSettings(), ), ); // await showDialog<dynamic>( diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/sub_widgets/contact_list_item.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/sub_widgets/contact_list_item.dart index e030f9882..7acfaae9e 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/sub_widgets/contact_list_item.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/sub_widgets/contact_list_item.dart @@ -58,7 +58,7 @@ class _ContactListItemState extends ConsumerState<ContactListItem> { ), child: AddressBookCard( contactId: contactId, - indicatorDown: _state == ExpandableState.expanded, + indicatorDown: _state, ), ), body: Column( diff --git a/lib/widgets/expandable.dart b/lib/widgets/expandable.dart index 47726d6d6..737f4ce7d 100644 --- a/lib/widgets/expandable.dart +++ b/lib/widgets/expandable.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; enum ExpandableState { - expanded, collapsed, + expanded, } class ExpandableController { @@ -45,11 +45,11 @@ class _ExpandableState extends State<Expandable> with TickerProviderStateMixin { Future<void> toggle() async { if (animation.isDismissed) { await animationController.forward(); - _toggleState = ExpandableState.collapsed; + _toggleState = ExpandableState.expanded; widget.onExpandChanged?.call(_toggleState); } else if (animation.isCompleted) { await animationController.reverse(); - _toggleState = ExpandableState.expanded; + _toggleState = ExpandableState.collapsed; widget.onExpandChanged?.call(_toggleState); } controller?.state = _toggleState; diff --git a/lib/widgets/node_card.dart b/lib/widgets/node_card.dart index 1da7e9012..c3fb36c70 100644 --- a/lib/widgets/node_card.dart +++ b/lib/widgets/node_card.dart @@ -46,7 +46,7 @@ class NodeCard extends ConsumerStatefulWidget { class _NodeCardState extends ConsumerState<NodeCard> { String _status = "Disconnected"; late final String nodeId; - bool _advancedIsExpanded = true; + bool _advancedIsExpanded = false; Future<void> _notifyWalletsOfUpdatedNode(WidgetRef ref) async { final managers = ref @@ -367,8 +367,8 @@ class _NodeCardState extends ConsumerState<NodeCard> { if (isDesktop) SvgPicture.asset( _advancedIsExpanded - ? Assets.svg.chevronDown - : Assets.svg.chevronUp, + ? Assets.svg.chevronUp + : Assets.svg.chevronDown, width: 12, height: 6, color: Theme.of(context) From df810c2a1449458e046aa994960b2ccdd5cbeecb Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 10:12:38 -0600 Subject: [PATCH 295/426] "send from" contacts fix --- .../address_book_address_chooser.dart | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart index 9f309a08e..92dd9f6fc 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart @@ -200,12 +200,19 @@ class _AddressBookAddressChooserState extends State<AddressBookAddressChooser> { final favorites = pullOutFavorites(contacts); - return ListView.builder( + final totalLength = favorites.length + + contacts.length + + 2; // +2 for "fav" and "all" headers + + return ListView.separated( primary: false, shrinkWrap: true, - itemCount: favorites.length + - contacts.length + - 2, // +2 for "fav" and "all" headers + itemCount: totalLength, + separatorBuilder: (context, index) { + return const SizedBox( + height: 10, + ); + }, itemBuilder: (context, index) { if (index == 0) { return Padding( @@ -220,7 +227,7 @@ class _AddressBookAddressChooserState extends State<AddressBookAddressChooser> { STextStyles.desktopTextExtraExtraSmall(context), ), ); - } else if (index <= favorites.length) { + } else if (index < favorites.length + 1) { final id = favorites[index - 1].id; return ContactListItem( key: Key("contactContactListItem_${id}_key"), @@ -241,7 +248,7 @@ class _AddressBookAddressChooserState extends State<AddressBookAddressChooser> { ), ); } else { - final id = contacts[index - favorites.length - 1].id; + final id = contacts[index - favorites.length - 2].id; return ContactListItem( key: Key("contactContactListItem_${id}_key"), contactId: id, From b988342bb146d1085f64527b4ce28f7765532c6f Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 10:12:59 -0600 Subject: [PATCH 296/426] "send from" contact card fix --- lib/widgets/address_book_card.dart | 185 +++++++++++++++-------------- 1 file changed, 95 insertions(+), 90 deletions(-) diff --git a/lib/widgets/address_book_card.dart b/lib/widgets/address_book_card.dart index c9ac86052..329e35fdf 100644 --- a/lib/widgets/address_book_card.dart +++ b/lib/widgets/address_book_card.dart @@ -9,6 +9,8 @@ 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/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/expandable.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class AddressBookCard extends ConsumerStatefulWidget { @@ -19,7 +21,7 @@ class AddressBookCard extends ConsumerStatefulWidget { }) : super(key: key); final String contactId; - final bool? indicatorDown; + final ExpandableState? indicatorDown; @override ConsumerState<AddressBookCard> createState() => _AddressBookCardState(); @@ -58,108 +60,111 @@ class _AddressBookCardState extends ConsumerState<AddressBookCard> { } } - return RoundedWhiteContainer( - padding: const EdgeInsets.all(4), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - padding: const EdgeInsets.all(0), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - showDialog<void>( - context: context, - useSafeArea: true, - barrierDismissible: true, - builder: (_) => ContactPopUp( - contactId: contact.id, + return ConditionalParent( + condition: !isDesktop, + child: Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: contact.id == "default" + ? Theme.of(context) + .extension<StackColors>()! + .myStackContactIconBG + : Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular(32), ), - ); - }, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - Container( - width: 32, - height: 32, - decoration: BoxDecoration( - color: contact.id == "default" - ? Theme.of(context) - .extension<StackColors>()! - .myStackContactIconBG - : Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - borderRadius: BorderRadius.circular(32), - ), - child: contact.id == "default" + child: contact.id == "default" + ? Center( + child: SvgPicture.asset( + Assets.svg.stackIcon(context), + width: 20, + ), + ) + : contact.emojiChar != null ? Center( - child: SvgPicture.asset( - Assets.svg.stackIcon(context), - width: 20, - ), + child: Text(contact.emojiChar!), ) - : contact.emojiChar != null - ? Center( - child: Text(contact.emojiChar!), - ) - : Center( - child: SvgPicture.asset( - Assets.svg.user, - width: 18, - ), - ), - ), - const SizedBox( - width: 12, - ), - if (isDesktop) + : Center( + child: SvgPicture.asset( + Assets.svg.user, + width: 18, + ), + ), + ), + const SizedBox( + width: 12, + ), + if (isDesktop) + Text( + contact.name, + style: STextStyles.itemSubtitle12(context), + ), + if (isDesktop) + const SizedBox( + width: 16, + ), + if (isDesktop) + Text( + coinsString, + style: STextStyles.label(context), + ), + if (!isDesktop) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ Text( contact.name, style: STextStyles.itemSubtitle12(context), ), - if (isDesktop) const SizedBox( - width: 16, + height: 4, ), - if (isDesktop) Text( coinsString, style: STextStyles.label(context), ), - if (!isDesktop) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - contact.name, - style: STextStyles.itemSubtitle12(context), - ), - const SizedBox( - height: 4, - ), - Text( - coinsString, - style: STextStyles.label(context), - ), - ], - ), - if (isDesktop) const Spacer(), - // if (isDesktop) - // SvgPicture.asset( - // widget.indicatorDown == true - // ? Assets.svg.chevronDown - // : Assets.svg.chevronUp, - // width: 10, - // height: 5, - // color: - // Theme.of(context).extension<StackColors>()!.textSubtitle2, - // ), - ], + ], + ), + if (isDesktop) const Spacer(), + if (isDesktop) + SvgPicture.asset( + widget.indicatorDown == ExpandableState.collapsed + ? Assets.svg.chevronDown + : Assets.svg.chevronUp, + width: 10, + height: 5, + color: Theme.of(context).extension<StackColors>()!.textSubtitle2, + ), + ], + ), + builder: (child) => RoundedWhiteContainer( + padding: const EdgeInsets.all(4), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + showDialog<void>( + context: context, + useSafeArea: true, + barrierDismissible: true, + builder: (_) => ContactPopUp( + contactId: contact.id, + ), + ); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: child, ), ), ), From b6e4357c3c63a6567fbdf2809ffb0b9c4b22b4a7 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 10:23:12 -0600 Subject: [PATCH 297/426] wallet overview syncing/loading balance text color fix for darkmode --- .../wallet_view/sub_widgets/desktop_wallet_summary.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart index 7a9e93467..f4bfed976 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart @@ -243,7 +243,7 @@ class _WDesktopWalletSummaryState extends State<DesktopWalletSummary> { fontSize: 24, color: Theme.of(context) .extension<StackColors>()! - .textFavoriteCard, + .textDark, ), ), if (externalCalls) From 81d5f757b3d039528f523df3af8b8d1cca21a81e Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 11:10:26 -0600 Subject: [PATCH 298/426] WIP: desktop contact details --- .../desktop_address_book.dart | 252 ++++++++---------- .../desktop_address_book_scaffold.dart | 111 ++++++++ .../subwidgets/desktop_contact_details.dart | 191 +++++++++++++ lib/widgets/address_book_card.dart | 9 +- 4 files changed, 427 insertions(+), 136 deletions(-) create mode 100644 lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_book_scaffold.dart create mode 100644 lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart diff --git a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart index 51b075284..5e22a6089 100644 --- a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart +++ b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart @@ -5,7 +5,11 @@ import 'package:stackwallet/models/contact.dart'; import 'package:stackwallet/models/contact_address_entry.dart'; import 'package:stackwallet/pages/address_book_views/subviews/add_address_book_entry_view.dart'; import 'package:stackwallet/pages/address_book_views/subviews/address_book_filter_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_book_scaffold.dart'; +import 'package:stackwallet/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart'; import 'package:stackwallet/providers/global/address_book_service_provider.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/providers/ui/address_book_providers/address_book_filter_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -19,13 +23,11 @@ import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; -import '../../../providers/providers.dart'; -import '../../../providers/ui/address_book_providers/address_book_filter_provider.dart'; - class DesktopAddressBook extends ConsumerStatefulWidget { const DesktopAddressBook({Key? key}) : super(key: key); @@ -42,6 +44,8 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { String _searchTerm = ""; + String? currentContactId; + Future<void> selectCryptocurrency() async { await showDialog<dynamic>( context: context, @@ -139,8 +143,9 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { .where((e) => ref.watch(addressBookFilterProvider .select((value) => value.coins.contains(e.coin)))) .isNotEmpty) - .where((e) => - ref.read(addressBookServiceProvider).matches(_searchTerm, e)); + .where( + (e) => ref.read(addressBookServiceProvider).matches(_searchTerm, e)) + .toList(); final favorites = contacts .where((element) => element.addresses @@ -150,7 +155,8 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { .where((e) => e.isFavorite && ref.read(addressBookServiceProvider).matches(_searchTerm, e)) - .where((element) => element.isFavorite); + .where((element) => element.isFavorite) + .toList(); return DesktopScaffold( appBar: DesktopAppBar( @@ -294,12 +300,56 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { padding: const EdgeInsets.all(0), child: Column( children: [ - ...favorites.map( - (e) => AddressBookCard( - key: Key("favContactCard_${e.id}_key"), - contactId: e.id, + for (int i = 0; i < favorites.length; i++) + Column( + children: [ + if (i > 0) + Container( + color: Theme.of(context) + .extension<StackColors>()! + .background, + height: 1, + ), + Padding( + padding: const EdgeInsets.all(4), + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark + .withOpacity( + currentContactId == favorites[i].id + ? 0.08 + : 0, + ), + child: RawMaterialButton( + onPressed: () { + setState(() { + currentContactId = favorites[i].id; + }); + }, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 16, + ), + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: AddressBookCard( + key: Key( + "favContactCard_${favorites[i].id}_key"), + contactId: favorites[i].id, + desktopSendFrom: false, + ), + ), + ), + ), + ], ), - ), ], ), ), @@ -318,136 +368,70 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { children: [ RoundedWhiteContainer( padding: const EdgeInsets.all(0), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - ...allContacts.map( - (e) => AddressBookCard( - key: Key("desktopContactCard_${e.id}_key"), - contactId: e.id, - ), + child: Column( + children: [ + for (int i = 0; i < allContacts.length; i++) + Column( + children: [ + if (i > 0) + Container( + color: Theme.of(context) + .extension<StackColors>()! + .background, + height: 1, + ), + Padding( + padding: const EdgeInsets.all(4), + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark + .withOpacity( + currentContactId == allContacts[i].id + ? 0.08 + : 0, + ), + child: RawMaterialButton( + onPressed: () { + setState(() { + currentContactId = allContacts[i].id; + }); + }, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 16, + ), + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: AddressBookCard( + key: Key( + "favContactCard_${allContacts[i].id}_key"), + contactId: allContacts[i].id, + desktopSendFrom: false, + ), + ), + ), + ), + ], ), - ], - ), + ], ), ), ], ), - details: Container( - color: Colors.purple, - ), + details: currentContactId == null + ? Container() + : DesktopContactDetails( + contactId: currentContactId!, + ), ), ), ); } } - -class DesktopAddressBookScaffold extends StatelessWidget { - const DesktopAddressBookScaffold({ - Key? key, - required this.controlsLeft, - required this.controlsRight, - required this.filterItems, - required this.upperLabel, - required this.lowerLabel, - required this.favorites, - required this.all, - required this.details, - }) : super(key: key); - - final Widget? controlsLeft; - final Widget? controlsRight; - final Widget? filterItems; - final Widget? upperLabel; - final Widget? lowerLabel; - final Widget? favorites; - final Widget? all; - final Widget? details; - - static const double weirdRowHeight = 30; - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Expanded( - flex: 6, - child: controlsLeft ?? Container(), - ), - const SizedBox( - width: 20, - ), - Expanded( - flex: 5, - child: controlsRight ?? Container(), - ), - ], - ), - const SizedBox( - height: 20, - ), - Row( - children: [ - Expanded( - child: filterItems ?? Container(), - ), - ], - ), - Expanded( - child: Row( - children: [ - Expanded( - flex: 6, - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: weirdRowHeight, - child: upperLabel, - ), - favorites ?? Container(), - lowerLabel ?? Container(), - all ?? Container(), - ], - ), - ), - ), - ); - }, - ), - ), - const SizedBox( - width: 20, - ), - Expanded( - flex: 5, - child: Column( - children: [ - const SizedBox( - height: weirdRowHeight, - ), - Expanded( - child: details ?? Container(), - ), - ], - ), - ), - ], - ), - ) - ], - ); - } -} diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_book_scaffold.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_book_scaffold.dart new file mode 100644 index 000000000..f32ea1f7f --- /dev/null +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_book_scaffold.dart @@ -0,0 +1,111 @@ +import 'package:flutter/widgets.dart'; + +class DesktopAddressBookScaffold extends StatelessWidget { + const DesktopAddressBookScaffold({ + Key? key, + required this.controlsLeft, + required this.controlsRight, + required this.filterItems, + required this.upperLabel, + required this.lowerLabel, + required this.favorites, + required this.all, + required this.details, + }) : super(key: key); + + final Widget? controlsLeft; + final Widget? controlsRight; + final Widget? filterItems; + final Widget? upperLabel; + final Widget? lowerLabel; + final Widget? favorites; + final Widget? all; + final Widget? details; + + static const double weirdRowHeight = 30; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Expanded( + flex: 6, + child: controlsLeft ?? Container(), + ), + const SizedBox( + width: 20, + ), + Expanded( + flex: 5, + child: controlsRight ?? Container(), + ), + ], + ), + const SizedBox( + height: 20, + ), + Row( + children: [ + Expanded( + child: filterItems ?? Container(), + ), + ], + ), + Expanded( + child: Row( + children: [ + Expanded( + flex: 6, + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + primary: false, + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: weirdRowHeight, + child: upperLabel, + ), + favorites ?? Container(), + lowerLabel ?? Container(), + all ?? Container(), + ], + ), + ), + ), + ); + }, + ), + ), + const SizedBox( + width: 20, + ), + Expanded( + flex: 5, + child: Column( + children: [ + const SizedBox( + height: weirdRowHeight, + ), + Expanded( + child: details ?? Container(), + ), + ], + ), + ), + ], + ), + ) + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart new file mode 100644 index 000000000..5184b2293 --- /dev/null +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart @@ -0,0 +1,191 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/models/contact_address_entry.dart'; +import 'package:stackwallet/providers/global/address_book_service_provider.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_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; + +class DesktopContactDetails extends ConsumerStatefulWidget { + const DesktopContactDetails({ + Key? key, + required this.contactId, + }) : super(key: key); + + final String contactId; + + @override + ConsumerState<DesktopContactDetails> createState() => + _DesktopContactDetailsState(); +} + +class _DesktopContactDetailsState extends ConsumerState<DesktopContactDetails> { + @override + Widget build(BuildContext context) { + final contact = ref.watch(addressBookServiceProvider + .select((value) => value.getContactById(widget.contactId))); + + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular(32), + ), + child: contact.id == "default" + ? Center( + child: SvgPicture.asset( + Assets.svg.stackIcon(context), + width: 20, + ), + ) + : contact.emojiChar != null + ? Center( + child: Text(contact.emojiChar!), + ) + : Center( + child: SvgPicture.asset( + Assets.svg.user, + width: 18, + ), + ), + ), + const SizedBox( + width: 16, + ), + Text( + contact.name, + style: STextStyles.desktopTextSmall(context), + ), + ], + ), + SecondaryButton( + label: "Options", + onPressed: () {}, + ), + ], + ), + const SizedBox( + height: 24, + ), + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Addresses", + style: STextStyles.desktopTextExtraExtraSmall( + context), + ), + BlueTextButton( + text: "Add new", + onTap: () {}, + ), + ], + ), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + ...contact.addresses + .map((e) => AddressCard(entry: e)), + ], + ) + ], + ), + ), + ), + ); + }, + ), + ), + ], + ); + } +} + +class AddressCard extends StatelessWidget { + const AddressCard({ + Key? key, + required this.entry, + }) : super(key: key); + + final ContactAddressEntry entry; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + SvgPicture.asset( + Assets.svg.iconFor( + coin: entry.coin, + ), + height: 32, + width: 32, + ), + const SizedBox( + width: 16, + ), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText( + "${entry.label} ${entry.coin.ticker}", + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark, + ), + ), + const SizedBox( + height: 2, + ), + SelectableText( + entry.address, + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + const SizedBox( + height: 8, + ), + Row( + children: [ + BlueTextButton( + text: "Copy", + onTap: () {}, + ), + const SizedBox( + width: 16, + ), + BlueTextButton( + text: "Edit", + onTap: () {}, + ), + ], + ) + ], + ), + ], + ); + } +} diff --git a/lib/widgets/address_book_card.dart b/lib/widgets/address_book_card.dart index 329e35fdf..b79f89662 100644 --- a/lib/widgets/address_book_card.dart +++ b/lib/widgets/address_book_card.dart @@ -18,10 +18,12 @@ class AddressBookCard extends ConsumerStatefulWidget { Key? key, required this.contactId, this.indicatorDown, + this.desktopSendFrom = true, }) : super(key: key); final String contactId; final ExpandableState? indicatorDown; + final bool desktopSendFrom; @override ConsumerState<AddressBookCard> createState() => _AddressBookCardState(); @@ -30,10 +32,12 @@ class AddressBookCard extends ConsumerStatefulWidget { class _AddressBookCardState extends ConsumerState<AddressBookCard> { late final String contactId; late final bool isDesktop; + late final bool desktopSendFrom; @override void initState() { contactId = widget.contactId; + desktopSendFrom = widget.desktopSendFrom; isDesktop = Util.isDesktop; super.initState(); } @@ -107,6 +111,7 @@ class _AddressBookCardState extends ConsumerState<AddressBookCard> { const SizedBox( width: 16, ), + if (isDesktop && !desktopSendFrom) const Spacer(), if (isDesktop) Text( coinsString, @@ -129,8 +134,8 @@ class _AddressBookCardState extends ConsumerState<AddressBookCard> { ), ], ), - if (isDesktop) const Spacer(), - if (isDesktop) + if (isDesktop && desktopSendFrom) const Spacer(), + if (isDesktop && desktopSendFrom) SvgPicture.asset( widget.indicatorDown == ExpandableState.collapsed ? Assets.svg.chevronDown From 9063749eadcaf1ab414b37db7591d55049efcfe3 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Thu, 17 Nov 2022 10:42:11 -0700 Subject: [PATCH 299/426] anonymize button added to firo wallet --- .../wallet_view/desktop_wallet_view.dart | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index 81b531632..769f22157 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:decimal/decimal.dart'; import 'package:event_bus/event_bus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -15,6 +16,7 @@ import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_vie import 'package:stackwallet/providers/global/auto_swb_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/transaction_filter_provider.dart'; +import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart'; @@ -27,8 +29,11 @@ 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_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/custom_loading_overlay.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/hover_text_field.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -165,6 +170,75 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { } } + Future<void> attemptAnonymize() 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 = await firoWallet.availablePublicBalance(); + if (publicBalance <= Decimal.zero) { + shouldPop = true; + if (mounted) { + 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 (mounted) { + Navigator.of(context).popUntil( + ModalRoute.withName(DesktopWalletView.routeName), + ); + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "Anonymize transaction submitted", + context: context, + ), + ); + } + } catch (e) { + shouldPop = true; + if (mounted) { + Navigator.of(context).popUntil( + ModalRoute.withName(DesktopWalletView.routeName), + ); + await showDialog<dynamic>( + context: context, + builder: (_) => StackOkDialog( + title: "Anonymize all failed", + message: "Reason: $e", + ), + ); + } + } + } + @override void initState() { controller = TextEditingController(); @@ -333,6 +407,67 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { : WalletSyncStatus.synced, ), const Spacer(), + if (coin == Coin.firo) const SizedBox(width: 10), + if (coin == Coin.firo) + SecondaryButton( + width: 180, + desktopMed: true, + label: "Anonymize funds", + onPressed: () async { + await showDialog<void>( + 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: 180, + desktopMed: true, + label: "Cancel", + onPressed: () { + Navigator.of(context).pop(); + }, + ), + const SizedBox(width: 20), + PrimaryButton( + width: 180, + desktopMed: true, + label: "Continue", + onPressed: () { + Navigator.of(context).pop(); + + unawaited(attemptAnonymize()); + }, + ) + ], + ), + ], + ), + ), + ), + ); + }, + ), + if (coin == Coin.firo) const SizedBox(width: 16), SecondaryButton( width: 180, desktopMed: true, From 9e7c1ccf9dc20e08ac74120e7faf866e1db3f103 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 11:46:44 -0600 Subject: [PATCH 300/426] button size enum --- .../subviews/address_book_filter_view.dart | 4 +- .../generate_receiving_uri_qr_code_view.dart | 5 +- .../send_view/confirm_transaction_view.dart | 4 +- .../building_transaction_dialog.dart | 2 +- .../global_settings_view/currency_view.dart | 4 +- .../add_edit_node_view.dart | 8 +- .../manage_nodes_views/node_details_view.dart | 4 +- .../create_backup_view.dart | 8 +- .../dialogs/cancel_stack_restore_dialog.dart | 4 +- .../edit_auto_backup_view.dart | 4 +- .../restore_from_file_view.dart | 4 +- .../stack_restore_progress_view.dart | 8 +- .../sub_widgets/confirm_full_rescan.dart | 4 +- .../all_transactions_view.dart | 2 +- .../transaction_search_filter_view.dart | 4 +- .../desktop_address_book.dart | 4 +- .../wallet_view/desktop_wallet_view.dart | 2 +- .../sub_widgets/desktop_auth_send.dart | 4 +- .../sub_widgets/desktop_receive.dart | 2 +- .../wallet_view/sub_widgets/desktop_send.dart | 10 +-- .../backup_and_restore_settings.dart | 14 +-- .../create_auto_backup.dart | 7 +- .../enable_backup_dialog.dart | 4 +- .../currency_settings/currency_settings.dart | 2 +- .../language_settings/language_settings.dart | 2 +- .../home/settings_menu/security_settings.dart | 4 +- .../syncing_preferences_settings.dart | 2 +- lib/widgets/desktop/custom_text_button.dart | 10 +++ lib/widgets/desktop/primary_button.dart | 86 +++++++++++++++--- lib/widgets/desktop/secondary_button.dart | 89 ++++++++++++++++--- 30 files changed, 225 insertions(+), 86 deletions(-) diff --git a/lib/pages/address_book_views/subviews/address_book_filter_view.dart b/lib/pages/address_book_views/subviews/address_book_filter_view.dart index df779331e..c129251d5 100644 --- a/lib/pages/address_book_views/subviews/address_book_filter_view.dart +++ b/lib/pages/address_book_views/subviews/address_book_filter_view.dart @@ -159,7 +159,7 @@ class _AddressBookFilterViewState extends ConsumerState<AddressBookFilterView> { children: [ SecondaryButton( width: 248, - desktopMed: true, + buttonHeight: ButtonHeight.l, enabled: true, label: "Cancel", onPressed: () { @@ -169,7 +169,7 @@ class _AddressBookFilterViewState extends ConsumerState<AddressBookFilterView> { // const SizedBox(width: 16), PrimaryButton( width: 248, - desktopMed: true, + buttonHeight: ButtonHeight.l, enabled: true, label: "Apply", onPressed: () { diff --git a/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart b/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart index 981def830..05cedb148 100644 --- a/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart +++ b/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart @@ -530,7 +530,7 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> { }); } : onGeneratePressed, - desktopMed: true, + buttonHeight: ButtonHeight.l, ), if (isDesktop && didGenerate) Row( @@ -586,7 +586,6 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> { if (!isDesktop) SecondaryButton( width: 170, - desktopMed: true, onPressed: () async { await _capturePng(false); }, @@ -606,7 +605,7 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> { ), PrimaryButton( width: 170, - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: () async { // TODO: add save functionality instead of share // save works on linux at the moment diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index 0f1692c08..8f7afb0bb 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -148,7 +148,7 @@ class _ConfirmTransactionViewState const Spacer(), Expanded( child: PrimaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Ok", onPressed: Navigator.of(context).pop, ), @@ -780,7 +780,7 @@ class _ConfirmTransactionViewState : const EdgeInsets.all(0), child: PrimaryButton( label: "Send", - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: () async { final dynamic unlocked; 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 045218e54..1f6c95df6 100644 --- a/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart +++ b/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart @@ -77,7 +77,7 @@ class _RestoringDialogState extends State<BuildingTransactionDialog> height: 40, ), SecondaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Cancel", onPressed: () { onCancel.call(); diff --git a/lib/pages/settings_views/global_settings_view/currency_view.dart b/lib/pages/settings_views/global_settings_view/currency_view.dart index 4e8fd5f6e..dccf2d61b 100644 --- a/lib/pages/settings_views/global_settings_view/currency_view.dart +++ b/lib/pages/settings_views/global_settings_view/currency_view.dart @@ -189,7 +189,7 @@ class _CurrencyViewState extends ConsumerState<BaseCurrencySettingsView> { Expanded( child: SecondaryButton( label: "Cancel", - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: Navigator.of(context).pop, ), ), @@ -199,7 +199,7 @@ class _CurrencyViewState extends ConsumerState<BaseCurrencySettingsView> { Expanded( child: PrimaryButton( label: "Save changes", - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: () { ref.read(prefsChangeNotifierProvider).currency = current; diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index 890953caf..606c4481f 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -238,7 +238,7 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> { Expanded( child: SecondaryButton( label: "Cancel", - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: () => Navigator.of( context, rootNavigator: true, @@ -251,7 +251,7 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> { Expanded( child: PrimaryButton( label: "Save", - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: () => Navigator.of( context, rootNavigator: true, @@ -561,7 +561,7 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> { child: SecondaryButton( label: "Test connection", enabled: testConnectionEnabled, - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: testConnectionEnabled ? () async { await _testConnection(); @@ -578,7 +578,7 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> { child: PrimaryButton( label: "Save", enabled: saveEnabled, - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: saveEnabled ? attemptSave : null, ), ), diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart index a80a64147..3d49ae6f7 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart @@ -349,7 +349,7 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> { Expanded( child: SecondaryButton( label: "Test connection", - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: () async { await _testConnection(ref, context); }, @@ -364,7 +364,7 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> { child: !nodeId.startsWith("default") ? PrimaryButton( label: _desktopReadOnly ? "Edit" : "Save", - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: () async { final shouldSave = _desktopReadOnly == false; setState(() { diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart index 02556beb9..0609e4b1b 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart @@ -562,7 +562,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { Consumer(builder: (context, ref, __) { return PrimaryButton( width: 183, - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Create backup", enabled: shouldEnableCreate, onPressed: !shouldEnableCreate @@ -735,7 +735,9 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { child: PrimaryButton( label: "Ok", - desktopMed: true, + buttonHeight: + ButtonHeight + .l, onPressed: () { int count = 0; Navigator.of( @@ -778,7 +780,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { ), SecondaryButton( width: 183, - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Cancel", onPressed: () {}, ), 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 a9f4e134d..905cdea72 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 @@ -85,7 +85,7 @@ class CancelStackRestoreDialog extends StatelessWidget { children: [ SecondaryButton( width: 248, - desktopMed: true, + buttonHeight: ButtonHeight.l, enabled: true, label: "Keep restoring", onPressed: () { @@ -95,7 +95,7 @@ class CancelStackRestoreDialog extends StatelessWidget { const SizedBox(width: 20), PrimaryButton( width: 248, - desktopMed: true, + buttonHeight: ButtonHeight.l, enabled: true, label: "Cancel anyway", onPressed: () { diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart index 76d280980..310be9f2b 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart @@ -754,7 +754,7 @@ class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> { Expanded( child: SecondaryButton( label: "Cancel", - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: Navigator.of(context).pop, ), ), @@ -764,7 +764,7 @@ class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> { Expanded( child: PrimaryButton( label: "Save", - desktopMed: true, + buttonHeight: ButtonHeight.l, enabled: shouldEnableCreate, onPressed: onSavePressed, ), 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 c5ccfa6b3..9be6af4cb 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 @@ -389,7 +389,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { children: [ PrimaryButton( width: 183, - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Restore", enabled: !(passwordController.text.isEmpty || fileLocationController.text.isEmpty), @@ -566,7 +566,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { ), SecondaryButton( width: 183, - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Cancel", onPressed: () {}, ), 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 e34def23d..c7f53378d 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 @@ -108,7 +108,7 @@ class _StackRestoreProgressViewState // children: [ // SecondaryButton( // width: 248, - // desktopMed: true, + // buttonHeight: ButtonHeight.l, // enabled: true, // label: "Keep restoring", // onPressed: () { @@ -118,7 +118,7 @@ class _StackRestoreProgressViewState // const SizedBox(width: 16), // PrimaryButton( // width: 248, - // desktopMed: true, + // buttonHeight: ButtonHeight.l, // enabled: true, // label: "Cancel anyway", // onPressed: () { @@ -681,7 +681,7 @@ class _StackRestoreProgressViewState _success ? PrimaryButton( width: 248, - desktopMed: true, + buttonHeight: ButtonHeight.l, enabled: true, label: "Done", onPressed: () async { @@ -690,7 +690,7 @@ class _StackRestoreProgressViewState ) : SecondaryButton( width: 248, - desktopMed: true, + buttonHeight: ButtonHeight.l, enabled: true, label: "Cancel restore process", onPressed: () async { 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 950d8d79e..150af6ac5 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 @@ -58,7 +58,7 @@ class ConfirmFullRescanDialog extends StatelessWidget { children: [ Expanded( child: SecondaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: Navigator.of(context).pop, label: "Cancel", ), @@ -68,7 +68,7 @@ class ConfirmFullRescanDialog extends StatelessWidget { ), Expanded( child: PrimaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: () { Navigator.of(context).pop(); onConfirm.call(); diff --git a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart index d41877a9a..95dcc8126 100644 --- a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart +++ b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart @@ -385,7 +385,7 @@ class _TransactionDetailsViewState extends ConsumerState<AllTransactionsView> { ), if (isDesktop) SecondaryButton( - desktopMed: isDesktop, + buttonHeight: ButtonHeight.l, width: 200, label: "Filter", icon: SvgPicture.asset( diff --git a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart index abebb71e4..d135ea276 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart @@ -869,7 +869,7 @@ class _TransactionSearchViewState Expanded( child: SecondaryButton( label: "Cancel", - desktopMed: isDesktop, + buttonHeight: ButtonHeight.l, onPressed: () async { if (!isDesktop) { if (FocusScope.of(context).hasFocus) { @@ -919,7 +919,7 @@ class _TransactionSearchViewState ), Expanded( child: PrimaryButton( - desktopMed: isDesktop, + buttonHeight: ButtonHeight.l, onPressed: () async { await _onApplyPressed(); }, diff --git a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart index 5e22a6089..f028a3424 100644 --- a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart +++ b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart @@ -240,7 +240,7 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { SecondaryButton( width: 184, label: "Filter", - desktopMed: true, + buttonHeight: ButtonHeight.l, icon: SvgPicture.asset( Assets.svg.filter, color: Theme.of(context) @@ -255,7 +255,7 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { PrimaryButton( width: 184, label: "Add new", - desktopMed: true, + buttonHeight: ButtonHeight.l, icon: SvgPicture.asset( Assets.svg.circlePlus, color: Theme.of(context) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index 769f22157..952194244 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -470,7 +470,7 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { if (coin == Coin.firo) const SizedBox(width: 16), SecondaryButton( width: 180, - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: () { _onExchangePressed(context); }, diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart index 566a82b35..9f863c8a4 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart @@ -142,7 +142,7 @@ class _DesktopAuthSendState extends ConsumerState<DesktopAuthSend> { Expanded( child: SecondaryButton( label: "Cancel", - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: Navigator.of(context).pop, ), ), @@ -153,7 +153,7 @@ class _DesktopAuthSendState extends ConsumerState<DesktopAuthSend> { child: PrimaryButton( enabled: _confirmEnabled, label: "Confirm", - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: () async { // TODO show spinner while verifying passphrase diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart index 9a59c3ec1..3de4ed1e3 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart @@ -199,7 +199,7 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> { ), if (coin != Coin.epicCash) SecondaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: generateNewAddress, label: "Generate new address", ), diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index af2c2517a..336dd7b4e 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -145,7 +145,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { right: 32, ), child: SecondaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Ok", onPressed: () { Navigator.of(context).pop(); @@ -232,7 +232,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { children: [ Expanded( child: SecondaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Cancel", onPressed: () { Navigator.of(context).pop(false); @@ -244,7 +244,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { ), Expanded( child: PrimaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Yes", onPressed: () { Navigator.of(context).pop(true); @@ -399,7 +399,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { ), child: Expanded( child: SecondaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Yes", onPressed: () { Navigator.of( @@ -1385,7 +1385,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { height: 36, ), PrimaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Preview send", enabled: ref.watch(previewTxButtonStateProvider.state).state, onPressed: ref.watch(previewTxButtonStateProvider.state).state diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart index 7d70d4d0f..37f3f35a6 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart @@ -240,7 +240,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { children: [ Expanded( child: SecondaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Cancel", onPressed: Navigator.of(context).pop, ), @@ -248,7 +248,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { const SizedBox(width: 16), Expanded( child: PrimaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Disable", onPressed: () { ref @@ -422,7 +422,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { padding: const EdgeInsets.all(10), child: !isEnabledAutoBackup ? PrimaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, width: 200, label: "Enable auto backup", onPressed: () { @@ -467,7 +467,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { Row( children: [ PrimaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, width: 190, label: "Disable auto backup", onPressed: () { @@ -476,7 +476,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { ), const SizedBox(width: 16), SecondaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, width: 190, label: "Edit auto backup", onPressed: () { @@ -560,7 +560,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { child: CreateBackupView(), ) : PrimaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, width: 200, label: "Create manual backup", onPressed: () { @@ -642,7 +642,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { child: RestoreFromFileView(), ) : PrimaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, width: 200, label: "Restore backup", onPressed: () { diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart index 663136dba..df80da732 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/create_auto_backup.dart @@ -556,7 +556,7 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> { Expanded( child: SecondaryButton( label: "Cancel", - desktopMed: true, + buttonHeight: ButtonHeight.l, onPressed: Navigator.of(context).pop, ), ), @@ -565,7 +565,7 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> { ), Expanded( child: PrimaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Enable Auto Backup", enabled: shouldEnableCreate, onPressed: !shouldEnableCreate @@ -792,7 +792,8 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> { Expanded( child: PrimaryButton( label: "Ok", - desktopMed: true, + buttonHeight: + ButtonHeight.l, onPressed: () { Navigator.of(context) .pop(); diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart index 6496253d5..df9f18b52 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart @@ -59,7 +59,7 @@ class EnableBackupDialog extends StatelessWidget { children: [ Expanded( child: SecondaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Cancel", onPressed: () { Navigator.of(context).pop(); @@ -71,7 +71,7 @@ class EnableBackupDialog extends StatelessWidget { ), Expanded( child: PrimaryButton( - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Continue", onPressed: () { Navigator.of(context).pop(); diff --git a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart index 4c4225ce4..0740157ad 100644 --- a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart @@ -108,7 +108,7 @@ class _CurrencySettings extends ConsumerState<CurrencySettings> { ), child: PrimaryButton( width: 210, - desktopMed: true, + buttonHeight: ButtonHeight.l, enabled: true, label: "Change currency", onPressed: () { diff --git a/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart b/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart index 08aeb9bc3..db636ba17 100644 --- a/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart @@ -85,7 +85,7 @@ class _LanguageOptionSettings extends ConsumerState<LanguageOptionSettings> { ), child: PrimaryButton( width: 210, - desktopMed: true, + buttonHeight: ButtonHeight.l, enabled: true, label: "Change language", onPressed: () { diff --git a/lib/pages_desktop_specific/home/settings_menu/security_settings.dart b/lib/pages_desktop_specific/home/settings_menu/security_settings.dart index 9f870440b..f2853e6f5 100644 --- a/lib/pages_desktop_specific/home/settings_menu/security_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/security_settings.dart @@ -485,7 +485,7 @@ class _SecuritySettings extends ConsumerState<SecuritySettings> { const SizedBox(height: 20), PrimaryButton( width: 160, - desktopMed: true, + buttonHeight: ButtonHeight.l, enabled: shouldEnableSave, label: "Save changes", onPressed: () async { @@ -503,7 +503,7 @@ class _SecuritySettings extends ConsumerState<SecuritySettings> { ) : PrimaryButton( width: 210, - desktopMed: true, + buttonHeight: ButtonHeight.l, enabled: true, label: "Set up new password", onPressed: () { diff --git a/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart b/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart index 408b93e15..ae11f5582 100644 --- a/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart @@ -83,7 +83,7 @@ class _SyncingPreferencesSettings ), child: PrimaryButton( width: 210, - desktopMed: true, + buttonHeight: ButtonHeight.l, enabled: true, label: "Change preferences", onPressed: () {}, diff --git a/lib/widgets/desktop/custom_text_button.dart b/lib/widgets/desktop/custom_text_button.dart index b96a697b8..90b75c459 100644 --- a/lib/widgets/desktop/custom_text_button.dart +++ b/lib/widgets/desktop/custom_text_button.dart @@ -1,6 +1,16 @@ import 'package:flutter/material.dart'; import 'package:stackwallet/utilities/util.dart'; +enum ButtonHeight { + xxs, + xs, + s, + m, + l, + xl, + xxl, +} + class CustomTextButtonBase extends StatelessWidget { const CustomTextButtonBase({ Key? key, diff --git a/lib/widgets/desktop/primary_button.dart b/lib/widgets/desktop/primary_button.dart index f3c900c34..134ff36c3 100644 --- a/lib/widgets/desktop/primary_button.dart +++ b/lib/widgets/desktop/primary_button.dart @@ -4,6 +4,8 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/desktop/custom_text_button.dart'; +export 'package:stackwallet/widgets/desktop/custom_text_button.dart'; + class PrimaryButton extends StatelessWidget { const PrimaryButton({ Key? key, @@ -13,7 +15,7 @@ class PrimaryButton extends StatelessWidget { this.icon, this.onPressed, this.enabled = true, - this.desktopMed = false, + this.buttonHeight, }) : super(key: key); final double? width; @@ -22,23 +24,44 @@ class PrimaryButton extends StatelessWidget { final VoidCallback? onPressed; final bool enabled; final Widget? icon; - final bool desktopMed; + final ButtonHeight? buttonHeight; TextStyle getStyle(bool isDesktop, BuildContext context) { if (isDesktop) { - if (desktopMed) { - return STextStyles.desktopTextExtraSmall(context).copyWith( - color: enabled - ? Theme.of(context).extension<StackColors>()!.buttonTextPrimary - : Theme.of(context) - .extension<StackColors>()! - .buttonTextPrimaryDisabled, - ); - } else { + if (buttonHeight == null) { return enabled ? STextStyles.desktopButtonEnabled(context) : STextStyles.desktopButtonDisabled(context); } + + switch (buttonHeight!) { + case ButtonHeight.xxs: + case ButtonHeight.xs: + case ButtonHeight.s: + return STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: enabled + ? Theme.of(context).extension<StackColors>()!.buttonTextPrimary + : Theme.of(context) + .extension<StackColors>()! + .buttonTextPrimaryDisabled, + ); + + case ButtonHeight.m: + case ButtonHeight.l: + return STextStyles.desktopTextExtraSmall(context).copyWith( + color: enabled + ? Theme.of(context).extension<StackColors>()!.buttonTextPrimary + : Theme.of(context) + .extension<StackColors>()! + .buttonTextPrimaryDisabled, + ); + + case ButtonHeight.xl: + case ButtonHeight.xxl: + return enabled + ? STextStyles.desktopButtonEnabled(context) + : STextStyles.desktopButtonDisabled(context); + } } else { return STextStyles.button(context).copyWith( color: enabled @@ -50,12 +73,51 @@ class PrimaryButton extends StatelessWidget { } } + double? _getHeight() { + if (buttonHeight == null) { + return height; + } + + if (Util.isDesktop) { + switch (buttonHeight!) { + case ButtonHeight.xxs: + return 28; + case ButtonHeight.xs: + return 32; + case ButtonHeight.s: + return 40; + case ButtonHeight.m: + return 48; + case ButtonHeight.l: + return 56; + case ButtonHeight.xl: + return 70; + case ButtonHeight.xxl: + return 96; + } + } else { + switch (buttonHeight!) { + case ButtonHeight.xxs: + case ButtonHeight.xs: + case ButtonHeight.s: + case ButtonHeight.m: + return 28; + case ButtonHeight.l: + return 30; + case ButtonHeight.xl: + return 46; + case ButtonHeight.xxl: + return 56; + } + } + } + @override Widget build(BuildContext context) { final isDesktop = Util.isDesktop; return CustomTextButtonBase( - height: desktopMed ? 56 : height, + height: _getHeight(), width: width, textButton: TextButton( onPressed: enabled ? onPressed : null, diff --git a/lib/widgets/desktop/secondary_button.dart b/lib/widgets/desktop/secondary_button.dart index 8d5eae0ce..7cf8e9f72 100644 --- a/lib/widgets/desktop/secondary_button.dart +++ b/lib/widgets/desktop/secondary_button.dart @@ -4,6 +4,8 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/desktop/custom_text_button.dart'; +export 'package:stackwallet/widgets/desktop/custom_text_button.dart'; + class SecondaryButton extends StatelessWidget { const SecondaryButton({ Key? key, @@ -13,7 +15,7 @@ class SecondaryButton extends StatelessWidget { this.icon, this.onPressed, this.enabled = true, - this.desktopMed = false, + this.buttonHeight, }) : super(key: key); final double? width; @@ -22,23 +24,47 @@ class SecondaryButton extends StatelessWidget { final VoidCallback? onPressed; final bool enabled; final Widget? icon; - final bool desktopMed; + final ButtonHeight? buttonHeight; TextStyle getStyle(bool isDesktop, BuildContext context) { if (isDesktop) { - if (desktopMed) { - return STextStyles.desktopTextExtraSmall(context).copyWith( - color: enabled - ? Theme.of(context).extension<StackColors>()!.buttonTextSecondary - : Theme.of(context) - .extension<StackColors>()! - .buttonTextSecondaryDisabled, - ); - } else { + if (buttonHeight == null) { return enabled ? STextStyles.desktopButtonSecondaryEnabled(context) : STextStyles.desktopButtonSecondaryDisabled(context); } + switch (buttonHeight!) { + case ButtonHeight.xxs: + case ButtonHeight.xs: + case ButtonHeight.s: + return STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: enabled + ? Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondaryDisabled, + ); + + case ButtonHeight.m: + case ButtonHeight.l: + return STextStyles.desktopTextExtraSmall(context).copyWith( + color: enabled + ? Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondaryDisabled, + ); + + case ButtonHeight.xl: + case ButtonHeight.xxl: + return enabled + ? STextStyles.desktopButtonSecondaryEnabled(context) + : STextStyles.desktopButtonSecondaryDisabled(context); + } } else { return STextStyles.button(context).copyWith( color: enabled @@ -50,12 +76,51 @@ class SecondaryButton extends StatelessWidget { } } + double? _getHeight() { + if (buttonHeight == null) { + return height; + } + + if (Util.isDesktop) { + switch (buttonHeight!) { + case ButtonHeight.xxs: + return 28; + case ButtonHeight.xs: + return 32; + case ButtonHeight.s: + return 40; + case ButtonHeight.m: + return 48; + case ButtonHeight.l: + return 56; + case ButtonHeight.xl: + return 70; + case ButtonHeight.xxl: + return 96; + } + } else { + switch (buttonHeight!) { + case ButtonHeight.xxs: + case ButtonHeight.xs: + case ButtonHeight.s: + case ButtonHeight.m: + return 28; + case ButtonHeight.l: + return 30; + case ButtonHeight.xl: + return 46; + case ButtonHeight.xxl: + return 56; + } + } + } + @override Widget build(BuildContext context) { final isDesktop = Util.isDesktop; return CustomTextButtonBase( - height: desktopMed ? 56 : height, + height: _getHeight(), width: width, textButton: TextButton( onPressed: enabled ? onPressed : null, From 1d238c29f09207b975968c27707df5097de218ac Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 12:01:52 -0600 Subject: [PATCH 301/426] WIP: centralize button heights --- .../create_backup_view.dart | 4 +- .../restore_from_file_view.dart | 4 +- .../wallet_view/desktop_wallet_view.dart | 6 +- .../advanced_settings/advanced_settings.dart | 111 +++++------------- .../backup_and_restore_settings.dart | 10 +- .../currency_settings/currency_settings.dart | 2 +- .../language_settings/language_settings.dart | 4 +- .../home/settings_menu/security_settings.dart | 2 +- .../syncing_preferences_settings.dart | 2 +- lib/widgets/desktop/primary_button.dart | 4 +- lib/widgets/desktop/secondary_button.dart | 4 +- 11 files changed, 52 insertions(+), 101 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart index 0609e4b1b..a6241d25a 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart @@ -562,7 +562,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { Consumer(builder: (context, ref, __) { return PrimaryButton( width: 183, - buttonHeight: ButtonHeight.l, + buttonHeight: ButtonHeight.m, label: "Create backup", enabled: shouldEnableCreate, onPressed: !shouldEnableCreate @@ -780,7 +780,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { ), SecondaryButton( width: 183, - buttonHeight: ButtonHeight.l, + buttonHeight: ButtonHeight.m, label: "Cancel", onPressed: () {}, ), 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 9be6af4cb..d6571967d 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 @@ -389,7 +389,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { children: [ PrimaryButton( width: 183, - buttonHeight: ButtonHeight.l, + buttonHeight: ButtonHeight.m, label: "Restore", enabled: !(passwordController.text.isEmpty || fileLocationController.text.isEmpty), @@ -566,7 +566,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { ), SecondaryButton( width: 183, - buttonHeight: ButtonHeight.l, + buttonHeight: ButtonHeight.m, label: "Cancel", onPressed: () {}, ), diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index 952194244..d21a19aee 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -411,7 +411,7 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { if (coin == Coin.firo) SecondaryButton( width: 180, - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Anonymize funds", onPressed: () async { await showDialog<void>( @@ -441,7 +441,7 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { children: [ SecondaryButton( width: 180, - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Cancel", onPressed: () { Navigator.of(context).pop(); @@ -450,7 +450,7 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { const SizedBox(width: 20), PrimaryButton( width: 180, - desktopMed: true, + buttonHeight: ButtonHeight.l, label: "Continue", onPressed: () { Navigator.of(context).pop(); diff --git a/lib/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart b/lib/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart index b4ff3fe6a..621683e65 100644 --- a/lib/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/advanced_settings/advanced_settings.dart @@ -8,6 +8,7 @@ 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/draggable_switch_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'debug_info_dialog.dart'; @@ -143,7 +144,21 @@ class _AdvancedSettings extends ConsumerState<AdvancedSettings> { ), ], ), - const StackPrivacyButton(), + PrimaryButton( + label: "Change", + buttonHeight: ButtonHeight.xs, + width: 86, + onPressed: () async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return const StackPrivacyDialog(); + }, + ); + }, + ) ], ), ); @@ -172,7 +187,21 @@ class _AdvancedSettings extends ConsumerState<AdvancedSettings> { .textDark), textAlign: TextAlign.left, ), - ShowLogsButton(), + PrimaryButton( + buttonHeight: ButtonHeight.xs, + label: "Show logs", + width: 101, + onPressed: () async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return const DebugInfoDialog(); + }, + ); + }, + ), ], ), ), @@ -184,81 +213,3 @@ class _AdvancedSettings extends ConsumerState<AdvancedSettings> { ); } } - -class StackPrivacyButton extends ConsumerWidget { - const StackPrivacyButton({ - Key? key, - }) : super(key: key); - @override - Widget build(BuildContext context, WidgetRef ref) { - Future<void> changePrivacySettings() async { - await showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return StackPrivacyDialog(); - }, - ); - } - - return SizedBox( - width: 84, - height: 37, - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () { - // Navigator.of(context).pushNamed( - // StackPrivacyCalls.routeName, - // arguments: false, - // ); - changePrivacySettings(); - }, - child: Text( - "Change", - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith(color: Colors.white), - ), - ), - ); - } -} - -class ShowLogsButton extends ConsumerWidget { - const ShowLogsButton({ - Key? key, - }) : super(key: key); - @override - Widget build(BuildContext context, WidgetRef ref) { - Future<void> viewDebugLogs() async { - await showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return const DebugInfoDialog(); - }, - ); - } - - return SizedBox( - width: 101, - height: 37, - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () { - viewDebugLogs(); - }, - child: Text( - "Show logs", - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith(color: Colors.white), - ), - ), - ); - } -} diff --git a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart index 37f3f35a6..c82b5f923 100644 --- a/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/backup_and_restore/backup_and_restore_settings.dart @@ -422,7 +422,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { padding: const EdgeInsets.all(10), child: !isEnabledAutoBackup ? PrimaryButton( - buttonHeight: ButtonHeight.l, + buttonHeight: ButtonHeight.m, width: 200, label: "Enable auto backup", onPressed: () { @@ -467,7 +467,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { Row( children: [ PrimaryButton( - buttonHeight: ButtonHeight.l, + buttonHeight: ButtonHeight.m, width: 190, label: "Disable auto backup", onPressed: () { @@ -476,7 +476,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { ), const SizedBox(width: 16), SecondaryButton( - buttonHeight: ButtonHeight.l, + buttonHeight: ButtonHeight.m, width: 190, label: "Edit auto backup", onPressed: () { @@ -560,7 +560,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { child: CreateBackupView(), ) : PrimaryButton( - buttonHeight: ButtonHeight.l, + buttonHeight: ButtonHeight.m, width: 200, label: "Create manual backup", onPressed: () { @@ -642,7 +642,7 @@ class _BackupRestoreSettings extends ConsumerState<BackupRestoreSettings> { child: RestoreFromFileView(), ) : PrimaryButton( - buttonHeight: ButtonHeight.l, + buttonHeight: ButtonHeight.m, width: 200, label: "Restore backup", onPressed: () { diff --git a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart index 0740157ad..d9c20d8fa 100644 --- a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart @@ -108,7 +108,7 @@ class _CurrencySettings extends ConsumerState<CurrencySettings> { ), child: PrimaryButton( width: 210, - buttonHeight: ButtonHeight.l, + buttonHeight: ButtonHeight.m, enabled: true, label: "Change currency", onPressed: () { diff --git a/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart b/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart index db636ba17..acddcb055 100644 --- a/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart @@ -80,12 +80,12 @@ class _LanguageOptionSettings extends ConsumerState<LanguageOptionSettings> { crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: EdgeInsets.all( + padding: const EdgeInsets.all( 10, ), child: PrimaryButton( width: 210, - buttonHeight: ButtonHeight.l, + buttonHeight: ButtonHeight.m, enabled: true, label: "Change language", onPressed: () { diff --git a/lib/pages_desktop_specific/home/settings_menu/security_settings.dart b/lib/pages_desktop_specific/home/settings_menu/security_settings.dart index f2853e6f5..f6762afa1 100644 --- a/lib/pages_desktop_specific/home/settings_menu/security_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/security_settings.dart @@ -503,7 +503,7 @@ class _SecuritySettings extends ConsumerState<SecuritySettings> { ) : PrimaryButton( width: 210, - buttonHeight: ButtonHeight.l, + buttonHeight: ButtonHeight.m, enabled: true, label: "Set up new password", onPressed: () { diff --git a/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart b/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart index ae11f5582..815e506db 100644 --- a/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart @@ -83,7 +83,7 @@ class _SyncingPreferencesSettings ), child: PrimaryButton( width: 210, - buttonHeight: ButtonHeight.l, + buttonHeight: ButtonHeight.m, enabled: true, label: "Change preferences", onPressed: () {}, diff --git a/lib/widgets/desktop/primary_button.dart b/lib/widgets/desktop/primary_button.dart index 134ff36c3..9441168e7 100644 --- a/lib/widgets/desktop/primary_button.dart +++ b/lib/widgets/desktop/primary_button.dart @@ -81,9 +81,9 @@ class PrimaryButton extends StatelessWidget { if (Util.isDesktop) { switch (buttonHeight!) { case ButtonHeight.xxs: - return 28; - case ButtonHeight.xs: return 32; + case ButtonHeight.xs: + return 37; case ButtonHeight.s: return 40; case ButtonHeight.m: diff --git a/lib/widgets/desktop/secondary_button.dart b/lib/widgets/desktop/secondary_button.dart index 7cf8e9f72..62bd900dd 100644 --- a/lib/widgets/desktop/secondary_button.dart +++ b/lib/widgets/desktop/secondary_button.dart @@ -84,9 +84,9 @@ class SecondaryButton extends StatelessWidget { if (Util.isDesktop) { switch (buttonHeight!) { case ButtonHeight.xxs: - return 28; - case ButtonHeight.xs: return 32; + case ButtonHeight.xs: + return 37; case ButtonHeight.s: return 40; case ButtonHeight.m: From 95a9fade38c065fb0d578b8ea92f4c83ca6b1a1a Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 12:17:55 -0600 Subject: [PATCH 302/426] desktop contact address details --- .../desktop_address_book.dart | 7 +- .../subwidgets/desktop_address_card.dart | 75 ++++++++++++ .../subwidgets/desktop_contact_details.dart | 108 ++++++------------ 3 files changed, 116 insertions(+), 74 deletions(-) create mode 100644 lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart diff --git a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart index f028a3424..fd25617e7 100644 --- a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart +++ b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart @@ -427,8 +427,11 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { ), details: currentContactId == null ? Container() - : DesktopContactDetails( - contactId: currentContactId!, + : RoundedWhiteContainer( + padding: const EdgeInsets.all(24), + child: DesktopContactDetails( + contactId: currentContactId!, + ), ), ), ), diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart new file mode 100644 index 000000000..49b75a4a8 --- /dev/null +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/models/contact_address_entry.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_buttons/blue_text_button.dart'; + +class DesktopAddressCard extends StatelessWidget { + const DesktopAddressCard({ + Key? key, + required this.entry, + }) : super(key: key); + + final ContactAddressEntry entry; + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset( + Assets.svg.iconFor( + coin: entry.coin, + ), + height: 32, + width: 32, + ), + const SizedBox( + width: 16, + ), + Flexible( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText( + "${entry.label} (${entry.coin.ticker})", + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark, + ), + ), + const SizedBox( + height: 2, + ), + SelectableText( + entry.address, + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + const SizedBox( + height: 8, + ), + Row( + children: [ + BlueTextButton( + text: "Copy", + onTap: () {}, + ), + const SizedBox( + width: 16, + ), + BlueTextButton( + text: "Edit", + onTap: () {}, + ), + ], + ) + ], + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart index 5184b2293..bc19fe3bd 100644 --- a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:stackwallet/models/contact_address_entry.dart'; +import 'package:stackwallet/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart'; import 'package:stackwallet/providers/global/address_book_service_provider.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_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; class DesktopContactDetails extends ConsumerStatefulWidget { const DesktopContactDetails({ @@ -74,6 +74,8 @@ class _DesktopContactDetailsState extends ConsumerState<DesktopContactDetails> { ), SecondaryButton( label: "Options", + width: 86, + buttonHeight: ButtonHeight.xxs, onPressed: () {}, ), ], @@ -106,12 +108,38 @@ class _DesktopContactDetailsState extends ConsumerState<DesktopContactDetails> { ), ], ), - Column( - mainAxisSize: MainAxisSize.min, - children: [ - ...contact.addresses - .map((e) => AddressCard(entry: e)), - ], + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + for (int i = 0; i < contact.addresses.length; i++) + Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (i > 0) + Container( + color: Theme.of(context) + .extension<StackColors>()! + .background, + height: 1, + ), + Padding( + padding: const EdgeInsets.all(18), + child: DesktopAddressCard( + entry: contact.addresses[i], + ), + ), + ], + ), + ], + ), ) ], ), @@ -125,67 +153,3 @@ class _DesktopContactDetailsState extends ConsumerState<DesktopContactDetails> { ); } } - -class AddressCard extends StatelessWidget { - const AddressCard({ - Key? key, - required this.entry, - }) : super(key: key); - - final ContactAddressEntry entry; - - @override - Widget build(BuildContext context) { - return Row( - children: [ - SvgPicture.asset( - Assets.svg.iconFor( - coin: entry.coin, - ), - height: 32, - width: 32, - ), - const SizedBox( - width: 16, - ), - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SelectableText( - "${entry.label} ${entry.coin.ticker}", - style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: Theme.of(context).extension<StackColors>()!.textDark, - ), - ), - const SizedBox( - height: 2, - ), - SelectableText( - entry.address, - style: STextStyles.desktopTextExtraExtraSmall(context), - ), - const SizedBox( - height: 8, - ), - Row( - children: [ - BlueTextButton( - text: "Copy", - onTap: () {}, - ), - const SizedBox( - width: 16, - ), - BlueTextButton( - text: "Edit", - onTap: () {}, - ), - ], - ) - ], - ), - ], - ); - } -} From 682966dab83a391ef3224c67833cc0d333a0cba9 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Thu, 17 Nov 2022 14:08:06 -0700 Subject: [PATCH 303/426] desktop block explorer dialog --- .../transaction_details_view.dart | 200 ++++++++++++------ .../wallet_view/desktop_wallet_view.dart | 4 +- 2 files changed, 142 insertions(+), 62 deletions(-) diff --git a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart index 1c2fb8e5d..dc4e41152 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart @@ -30,6 +30,8 @@ import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.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/icon_widgets/copy_icon.dart'; import 'package:stackwallet/widgets/icon_widgets/pencil_icon.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -154,60 +156,136 @@ class _TransactionDetailsViewState Future<bool> showExplorerWarning(String explorer) async { final bool? shouldContinue = await showDialog<bool>( - context: context, - barrierDismissible: false, - builder: (_) => StackDialog( - title: "Attention", - message: - "You are about to view this transaction in a block explorer. The explorer may log your IP address and link it to the transaction. Only proceed if you trust $explorer.", - icon: Row( - children: [ - Consumer(builder: (_, ref, __) { - return Checkbox( - value: ref.watch(prefsChangeNotifierProvider - .select((value) => value.hideBlockExplorerWarning)), - onChanged: (value) { - if (value is bool) { - ref - .read(prefsChangeNotifierProvider) - .hideBlockExplorerWarning = value; - setState(() {}); - } + context: context, + barrierDismissible: false, + builder: (_) { + if (!isDesktop) { + return StackDialog( + title: "Attention", + message: + "You are about to view this transaction in a block explorer. The explorer may log your IP address and link it to the transaction. Only proceed if you trust $explorer.", + icon: Row( + children: [ + Consumer(builder: (_, ref, __) { + return Checkbox( + value: ref.watch(prefsChangeNotifierProvider + .select((value) => value.hideBlockExplorerWarning)), + onChanged: (value) { + if (value is bool) { + ref + .read(prefsChangeNotifierProvider) + .hideBlockExplorerWarning = value; + setState(() {}); + } + }, + ); + }), + Text( + "Never show again", + style: STextStyles.smallMed14(context), + ) + ], + ), + leftButton: TextButton( + onPressed: () { + Navigator.of(context).pop(false); }, - ); - }), - Text( - "Never show again", - style: STextStyles.smallMed14(context), - ) - ], - ), - leftButton: TextButton( - onPressed: () { - Navigator.of(context).pop(false); - }, - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) + child: Text( + "Cancel", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + rightButton: TextButton( + style: Theme.of(context) .extension<StackColors>()! - .accentColorDark), - ), - ), - rightButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () { - Navigator.of(context).pop(true); - }, - child: Text( - "Continue", - style: STextStyles.button(context), - ), - ), - ), - ); + .getPrimaryEnabledButtonColor(context), + onPressed: () { + Navigator.of(context).pop(true); + }, + child: Text( + "Continue", + style: STextStyles.button(context), + ), + ), + ); + } else { + return DesktopDialog( + maxWidth: 550, + maxHeight: 300, + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 32, vertical: 20), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Attention", + style: STextStyles.desktopH2(context), + ), + Row( + children: [ + Consumer(builder: (_, ref, __) { + return Checkbox( + value: ref.watch(prefsChangeNotifierProvider + .select((value) => + value.hideBlockExplorerWarning)), + onChanged: (value) { + if (value is bool) { + ref + .read(prefsChangeNotifierProvider) + .hideBlockExplorerWarning = value; + setState(() {}); + } + }, + ); + }), + Text( + "Never show again", + style: STextStyles.smallMed14(context), + ) + ], + ), + ], + ), + const SizedBox(height: 16), + Text( + "You are about to view this transaction in a block explorer. The explorer may log your IP address and link it to the transaction. Only proceed if you trust $explorer.", + style: STextStyles.desktopTextSmall(context), + ), + const SizedBox(height: 35), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SecondaryButton( + width: 200, + buttonHeight: ButtonHeight.l, + label: "Cancel", + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + const SizedBox(width: 20), + PrimaryButton( + width: 200, + buttonHeight: ButtonHeight.l, + label: "Continue", + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ], + ), + ], + ), + ), + ); + } + }); return shouldContinue ?? false; } @@ -995,15 +1073,17 @@ class _TransactionDetailsViewState .externalApplication, ); } catch (_) { - unawaited(showDialog<void>( - context: context, - builder: (_) => StackOkDialog( - title: - "Could not open in block explorer", - message: - "Failed to open \"${uri.toString()}\"", + unawaited( + showDialog<void>( + context: context, + builder: (_) => StackOkDialog( + title: + "Could not open in block explorer", + message: + "Failed to open \"${uri.toString()}\"", + ), ), - )); + ); } finally { // Future<void>.delayed( // const Duration(seconds: 1), diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index d21a19aee..a7de8fdf4 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -440,7 +440,7 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { mainAxisAlignment: MainAxisAlignment.center, children: [ SecondaryButton( - width: 180, + width: 200, buttonHeight: ButtonHeight.l, label: "Cancel", onPressed: () { @@ -449,7 +449,7 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { ), const SizedBox(width: 20), PrimaryButton( - width: 180, + width: 200, buttonHeight: ButtonHeight.l, label: "Continue", onPressed: () { From 11735cdaf7d8e5393687c2ec89893a72908d7d8a Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 12:56:10 -0600 Subject: [PATCH 304/426] desktop emoji select --- .../subviews/add_address_book_entry_view.dart | 132 ++++-------- lib/widgets/emoji_select_sheet.dart | 188 ++++++++++-------- 2 files changed, 151 insertions(+), 169 deletions(-) diff --git a/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart b/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart index 5835c80cd..0007f3d81 100644 --- a/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart +++ b/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart @@ -191,33 +191,33 @@ class _AddAddressBookEntryViewState style: STextStyles.desktopH3(context), textAlign: TextAlign.center, ), - const SizedBox(width: 10), - AppBarIconButton( - key: - const Key("addAddressBookEntryFavoriteButtonKey"), - size: 36, - shadows: const [], - color: Theme.of(context) - .extension<StackColors>()! - .background, - icon: SvgPicture.asset( - Assets.svg.star, - color: _isFavorite - ? Theme.of(context) - .extension<StackColors>()! - .favoriteStarActive - : Theme.of(context) - .extension<StackColors>()! - .favoriteStarInactive, - width: 20, - height: 20, - ), - onPressed: () { - setState(() { - _isFavorite = !_isFavorite; - }); - }, - ), + // const SizedBox(width: 10), + // AppBarIconButton( + // key: + // const Key("addAddressBookEntryFavoriteButtonKey"), + // size: 36, + // shadows: const [], + // color: Theme.of(context) + // .extension<StackColors>()! + // .background, + // icon: SvgPicture.asset( + // Assets.svg.star, + // color: _isFavorite + // ? Theme.of(context) + // .extension<StackColors>()! + // .favoriteStarActive + // : Theme.of(context) + // .extension<StackColors>()! + // .favoriteStarInactive, + // width: 20, + // height: 20, + // ), + // onPressed: () { + // setState(() { + // _isFavorite = !_isFavorite; + // }); + // }, + // ), ], ), ), @@ -225,10 +225,11 @@ class _AddAddressBookEntryViewState ], ), Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: child, - )), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: child, + ), + ), ], ); }, @@ -292,66 +293,17 @@ class _AddAddressBookEntryViewState : showDialog<dynamic>( context: context, builder: (context) { - return DesktopDialog( + return const DesktopDialog( maxHeight: 700, - maxWidth: 700, - child: Column( - children: [ - Row( - children: [ - Padding( - padding: - const EdgeInsets - .all(32), - child: Text( - "Select emoji", - style: STextStyles - .desktopH3( - context), - textAlign: - TextAlign - .center, - ), - ), - ], - ), - Expanded( - child: LayoutBuilder( - builder: (context, - constraints) { - return SingleChildScrollView( - scrollDirection: - Axis.vertical, - child: - ConstrainedBox( - constraints: - BoxConstraints( - minHeight: - constraints - .maxHeight, - minWidth: - constraints - .maxWidth, - ), - child: - IntrinsicHeight( - child: Column( - children: const [ - Padding( - padding: - EdgeInsets.symmetric(horizontal: 32), - // child: - // EmojiSelectSheet(), - ), - ], - ), - ), - ), - ); - }, - ), - ), - ], + maxWidth: 600, + child: Padding( + padding: EdgeInsets.only( + left: 32, + right: 20, + top: 32, + bottom: 32, + ), + child: EmojiSelectSheet(), ), ); }).then((value) { diff --git a/lib/widgets/emoji_select_sheet.dart b/lib/widgets/emoji_select_sheet.dart index 85a90fec8..7bf02e967 100644 --- a/lib/widgets/emoji_select_sheet.dart +++ b/lib/widgets/emoji_select_sheet.dart @@ -4,6 +4,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; class EmojiSelectSheet extends ConsumerWidget { const EmojiSelectSheet({ @@ -16,7 +19,9 @@ class EmojiSelectSheet extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final size = MediaQuery.of(context).size; + final isDesktop = Util.isDesktop; + + final size = isDesktop ? const Size(600, 700) : MediaQuery.of(context).size; final double maxHeight = size.height * 0.60; final double availableWidth = size.width - (2 * horizontalPadding); final int emojisPerRow = @@ -24,90 +29,115 @@ class EmojiSelectSheet extends ConsumerWidget { final itemCount = Emoji.all().length; - return Container( - decoration: BoxDecoration( - color: Theme.of(context).extension<StackColors>()!.popupBG, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(20), + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Container( + decoration: BoxDecoration( + color: Theme.of(context).extension<StackColors>()!.popupBG, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + child: LimitedBox( + maxHeight: maxHeight, + child: Padding( + padding: EdgeInsets.only( + left: horizontalPadding, + right: horizontalPadding, + top: 10, + bottom: 0, + ), + child: child, + ), ), ), - child: LimitedBox( - maxHeight: maxHeight, - child: Padding( - padding: EdgeInsets.only( - left: horizontalPadding, - right: horizontalPadding, - top: 10, - bottom: 0, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Center( - child: Container( - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (!isDesktop) + Center( + child: Container( + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - width: 60, - height: 4, ), + width: 60, + height: 4, ), - const SizedBox( - height: 36, - ), - Text( - "Select emoji", - style: STextStyles.pageTitleH2(context), - textAlign: TextAlign.left, - ), - const SizedBox( - height: 16, - ), - Flexible( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Flexible( - child: GridView.builder( - itemCount: itemCount, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: emojisPerRow, - ), - itemBuilder: (context, index) { - final emoji = Emoji.all()[index]; - return GestureDetector( - onTap: () { - Navigator.of(context).pop(emoji); - }, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(100), - color: Colors.transparent, - ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text(emoji.char), - ), - ), - ); - }, - ), - ) - ], - ), - ), - const SizedBox( - height: 24, - ), - ], + ), + if (!isDesktop) + const SizedBox( + height: 36, + ), + Text( + "Select emoji", + style: isDesktop + ? STextStyles.desktopH3(context) + : STextStyles.pageTitleH2(context), + textAlign: TextAlign.left, ), - ), + SizedBox( + height: isDesktop ? 28 : 16, + ), + Flexible( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: GridView.builder( + itemCount: itemCount, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: emojisPerRow, + ), + itemBuilder: (context, index) { + final emoji = Emoji.all()[index]; + return GestureDetector( + onTap: () { + Navigator.of(context).pop(emoji); + }, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100), + color: Colors.transparent, + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + emoji.char, + style: isDesktop + ? STextStyles.desktopTextSmall(context) + : null, + ), + ), + ), + ); + }, + ), + ) + ], + ), + ), + SizedBox( + height: isDesktop ? 20 : 24, + ), + if (isDesktop) + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + SecondaryButton( + label: "Cancel", + width: 248, + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of(context).pop, + ), + ], + ), + ], ), ); } From 134087bfc4c18c3482e49d6b1e7a16b9f0a78032 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 13:15:57 -0600 Subject: [PATCH 305/426] desktop add contact popup tweaks --- .../subviews/add_address_book_entry_view.dart | 375 ++++++------------ .../new_contact_address_entry_form.dart | 2 +- 2 files changed, 121 insertions(+), 256 deletions(-) diff --git a/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart b/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart index 0007f3d81..bb93c68d8 100644 --- a/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart +++ b/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart @@ -21,6 +21,8 @@ import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.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/emoji_select_sheet.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; @@ -191,33 +193,6 @@ class _AddAddressBookEntryViewState style: STextStyles.desktopH3(context), textAlign: TextAlign.center, ), - // const SizedBox(width: 10), - // AppBarIconButton( - // key: - // const Key("addAddressBookEntryFavoriteButtonKey"), - // size: 36, - // shadows: const [], - // color: Theme.of(context) - // .extension<StackColors>()! - // .background, - // icon: SvgPicture.asset( - // Assets.svg.star, - // color: _isFavorite - // ? Theme.of(context) - // .extension<StackColors>()! - // .favoriteStarActive - // : Theme.of(context) - // .extension<StackColors>()! - // .favoriteStarInactive, - // width: 20, - // height: 20, - // ), - // onPressed: () { - // setState(() { - // _isFavorite = !_isFavorite; - // }); - // }, - // ), ], ), ), @@ -226,7 +201,11 @@ class _AddAddressBookEntryViewState ), Expanded( child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), + padding: const EdgeInsets.only( + left: 10, + right: 10, + bottom: 32, + ), child: child, ), ), @@ -239,16 +218,17 @@ class _AddAddressBookEntryViewState padding: const EdgeInsets.symmetric(horizontal: 12), child: SingleChildScrollView( controller: scrollController, - padding: const EdgeInsets.only( + padding: EdgeInsets.only( // top: 8, left: 4, right: 4, - bottom: 16, + bottom: isDesktop ? 0 : 16, ), child: ConstrainedBox( constraints: BoxConstraints( // subtract top and bottom padding set in parent - minHeight: constraint.maxHeight - 16, // - 8, + minHeight: + constraint.maxHeight - (isDesktop ? 0 : 16), // - 8, ), child: IntrinsicHeight( child: Column( @@ -259,38 +239,21 @@ class _AddAddressBookEntryViewState mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - GestureDetector( - onTap: () { - if (_selectedEmoji != null) { - setState(() { - _selectedEmoji = null; - }); - return; - } + SizedBox( + height: 56, + width: 56, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + if (_selectedEmoji != null) { + setState(() { + _selectedEmoji = null; + }); + return; + } - ///TODO if desktop make dialog - !isDesktop - ? showModalBottomSheet<dynamic>( - backgroundColor: - Colors.transparent, - context: context, - shape: - const RoundedRectangleBorder( - borderRadius: - BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - builder: (_) => - const EmojiSelectSheet(), - ).then((value) { - if (value is Emoji) { - setState(() { - _selectedEmoji = value; - }); - } - }) - : showDialog<dynamic>( + showDialog<dynamic>( context: context, builder: (context) { return const DesktopDialog( @@ -307,77 +270,80 @@ class _AddAddressBookEntryViewState ), ); }).then((value) { - if (value is Emoji) { - setState(() { - _selectedEmoji = value; - }); - } - }); - }, - child: SizedBox( - height: 56, - width: 56, - child: Stack( - children: [ - Container( - height: 56, - width: 56, - decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(24), - color: Theme.of(context) - .extension<StackColors>()! - .textFieldActiveBG, - ), - child: Center( - child: _selectedEmoji == null - ? SvgPicture.asset( - Assets.svg.user, - height: 30, - width: 30, - ) - : Text( - _selectedEmoji!.char, - style: STextStyles - .pageTitleH1(context), - ), - ), - ), - Align( - alignment: Alignment.bottomRight, - child: Container( - height: 14, - width: 14, + if (value is Emoji) { + setState(() { + _selectedEmoji = value; + }); + } + }); + }, + child: Stack( + children: [ + Container( + height: 56, + width: 56, decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(14), - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), + borderRadius: + BorderRadius.circular(100), + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveBG, + ), child: Center( child: _selectedEmoji == null ? SvgPicture.asset( - Assets.svg.plus, - color: Theme.of(context) - .extension< - StackColors>()! - .textWhite, - width: 12, - height: 12, + Assets.svg.user, + height: 30, + width: 30, ) - : SvgPicture.asset( - Assets.svg.thickX, - color: Theme.of(context) - .extension< - StackColors>()! - .textWhite, - width: 8, - height: 8, + : Text( + _selectedEmoji!.char, + style: STextStyles + .pageTitleH1( + context), ), ), ), - ) - ], + Align( + alignment: Alignment.bottomRight, + child: Container( + height: 14, + width: 14, + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular( + 14), + color: Theme.of(context) + .extension< + StackColors>()! + .accentColorDark), + child: Center( + child: _selectedEmoji == null + ? SvgPicture.asset( + Assets.svg.plus, + color: Theme.of( + context) + .extension< + StackColors>()! + .textWhite, + width: 12, + height: 12, + ) + : SvgPicture.asset( + Assets.svg.thickX, + color: Theme.of( + context) + .extension< + StackColors>()! + .textWhite, + width: 8, + height: 8, + ), + ), + ), + ) + ], + ), ), ), ), @@ -453,100 +419,23 @@ class _AddAddressBookEntryViewState return; } - ///TODO if desktop make dialog - !isDesktop - ? showModalBottomSheet<dynamic>( - backgroundColor: - Colors.transparent, - context: context, - shape: - const RoundedRectangleBorder( - borderRadius: - BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - builder: (_) => - const EmojiSelectSheet(), - ).then((value) { - if (value is Emoji) { - setState(() { - _selectedEmoji = value; - }); - } - }) - : showDialog<dynamic>( - context: context, - builder: (context) { - return DesktopDialog( - maxHeight: 700, - maxWidth: 700, - child: Column( - children: [ - Row( - children: [ - Padding( - padding: - const EdgeInsets - .all(32), - child: Text( - "Select emoji", - style: STextStyles - .desktopH3( - context), - textAlign: - TextAlign - .center, - ), - ), - ], - ), - Expanded( - child: LayoutBuilder( - builder: (context, - constraints) { - return SingleChildScrollView( - scrollDirection: - Axis.vertical, - child: - ConstrainedBox( - constraints: - BoxConstraints( - minHeight: - constraints - .maxHeight, - minWidth: - constraints - .maxWidth, - ), - child: - IntrinsicHeight( - child: Column( - children: const [ - Padding( - padding: - EdgeInsets.symmetric(horizontal: 32), - // child: - // EmojiSelectSheet(), - ), - ], - ), - ), - ), - ); - }, - ), - ), - ], - ), - ); - }).then((value) { - if (value is Emoji) { - setState(() { - _selectedEmoji = value; - }); - } - }); + showModalBottomSheet<dynamic>( + backgroundColor: Colors.transparent, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + builder: (_) => + const EmojiSelectSheet(), + ).then((value) { + if (value is Emoji) { + setState(() { + _selectedEmoji = value; + }); + } + }); }, child: SizedBox( height: 48, @@ -734,22 +623,16 @@ class _AddAddressBookEntryViewState Row( children: [ Expanded( - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - ), + child: SecondaryButton( + label: "Cancel", + buttonHeight: isDesktop ? ButtonHeight.m : null, onPressed: () async { - if (FocusScope.of(context).hasFocus) { + if (!isDesktop && + FocusScope.of(context).hasFocus) { FocusScope.of(context).unfocus(); await Future<void>.delayed( - const Duration(milliseconds: 75)); + const Duration(milliseconds: 75), + ); } if (mounted) { Navigator.of(context).pop(); @@ -776,16 +659,11 @@ class _AddAddressBookEntryViewState bool shouldEnableSave = validForms && nameExists; - return TextButton( - style: shouldEnableSave - ? Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor( - context) - : Theme.of(context) - .extension<StackColors>()! - .getPrimaryDisabledButtonColor( - context), + return PrimaryButton( + label: "Save", + buttonHeight: + isDesktop ? ButtonHeight.m : null, + enabled: shouldEnableSave, onPressed: shouldEnableSave ? () async { if (FocusScope.of(context) @@ -827,19 +705,6 @@ class _AddAddressBookEntryViewState } } : null, - child: Text( - "Save", - style: - STextStyles.button(context).copyWith( - color: shouldEnableSave - ? Theme.of(context) - .extension<StackColors>()! - .buttonTextPrimary - : Theme.of(context) - .extension<StackColors>()! - .buttonTextPrimaryDisabled, - ), - ), ); }, ), diff --git a/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart b/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart index b6cf0aad4..f49547858 100644 --- a/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart +++ b/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart @@ -253,7 +253,7 @@ class _NewContactAddressEntryFormState }, child: const ClipboardIcon(), ), - if (ref.watch(addressEntryDataProvider(widget.id) + if (!Util.isDesktop && ref.watch(addressEntryDataProvider(widget.id) .select((value) => value.address)) == null) TextFieldIconButton( From 0503999fa708aa0b7ab38d39c044d87435e045de Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 15:24:28 -0600 Subject: [PATCH 306/426] WIP coin dropdown --- .../new_contact_address_entry_form.dart | 245 ++++++++++++------ 1 file changed, 172 insertions(+), 73 deletions(-) diff --git a/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart b/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart index f49547858..25cff073b 100644 --- a/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart +++ b/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart @@ -1,8 +1,10 @@ +import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/address_book_views/subviews/coin_select_sheet.dart'; +import 'package:stackwallet/providers/providers.dart'; // import 'package:stackwallet/providers/global/should_show_lockscreen_on_resume_state_provider.dart'; import 'package:stackwallet/providers/ui/address_book_providers/address_entry_data_provider.dart'; import 'package:stackwallet/utilities/address_utils.dart'; @@ -47,6 +49,8 @@ class _NewContactAddressEntryFormState late final FocusNode addressLabelFocusNode; late final FocusNode addressFocusNode; + List<Coin> coins = []; + @override void initState() { addressLabelController = TextEditingController() @@ -55,6 +59,7 @@ class _NewContactAddressEntryFormState ..text = ref.read(addressEntryDataProvider(widget.id)).address ?? ""; addressLabelFocusNode = FocusNode(); addressFocusNode = FocusNode(); + coins = [...Coin.values]; super.initState(); } @@ -70,86 +75,179 @@ class _NewContactAddressEntryFormState @override Widget build(BuildContext context) { final isDesktop = Util.isDesktop; + bool showTestNet = ref.watch( + prefsChangeNotifierProvider.select((value) => value.showTestNetCoins), + ); + if (isDesktop) { + coins = [...Coin.values]; + + coins.remove(Coin.firoTestNet); + if (showTestNet) { + coins = coins.sublist(0, coins.length - kTestNetCoinCount); + } + } + return Column( children: [ - TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - readOnly: true, - style: STextStyles.field(context), - decoration: InputDecoration( - hintText: "Select cryptocurrency", - hintStyle: STextStyles.fieldLabel(context), - prefixIcon: Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: RawMaterialButton( - splashColor: - Theme.of(context).extension<StackColors>()!.highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + if (isDesktop) + DropdownButtonHideUnderline( + child: DropdownButton2<Coin>( + hint: Text( + "Select cryptocurrency", + style: STextStyles.fieldLabel(context), + ), + offset: const Offset(0, -10), + isExpanded: true, + dropdownElevation: 0, + value: ref.watch(addressEntryDataProvider(widget.id) + .select((value) => value.coin)), + onChanged: (value) { + if (value is Coin) { + ref.read(addressEntryDataProvider(widget.id)).coin = value; + } + }, + icon: SvgPicture.asset( + Assets.svg.chevronDown, + width: 10, + height: 5, + color: Theme.of(context).extension<StackColors>()!.textDark3, + ), + buttonPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + buttonDecoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + dropdownDecoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + items: [ + ...coins.map( + (coin) => DropdownMenuItem<Coin>( + value: coin, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.iconFor(coin: coin), + height: 24, + width: 24, + ), + const SizedBox( + width: 12, + ), + Text( + coin.prettyName, + style: + STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + ], + ), ), ), - onPressed: () { - showModalBottomSheet<dynamic>( - backgroundColor: Colors.transparent, - context: context, - builder: (_) => const CoinSelectSheet(), - ).then((value) { - if (value is Coin) { - ref.read(addressEntryDataProvider(widget.id)).coin = - value; - } - }); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ref.watch(addressEntryDataProvider(widget.id) - .select((value) => value.coin)) == - null - ? Text( - "Select cryptocurrency", - style: STextStyles.fieldLabel(context), - ) - : Row( - children: [ - SvgPicture.asset( - Assets.svg.iconFor( - coin: ref.watch( - addressEntryDataProvider(widget.id) - .select((value) => value.coin))!), - height: 20, - width: 20, - ), - const SizedBox( - width: 12, - ), - Text( - ref - .watch(addressEntryDataProvider(widget.id) - .select((value) => value.coin))! - .prettyName, - style: STextStyles.itemSubtitle12(context), - ), - ], - ), - SvgPicture.asset( - Assets.svg.chevronDown, - width: 8, - height: 4, - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle2, + ), + ], + ), + ), + if (!isDesktop) + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + readOnly: true, + style: STextStyles.field(context), + decoration: InputDecoration( + hintText: "Select cryptocurrency", + hintStyle: STextStyles.fieldLabel(context), + prefixIcon: Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: RawMaterialButton( + splashColor: + Theme.of(context).extension<StackColors>()!.highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - ], + ), + onPressed: () { + showModalBottomSheet<dynamic>( + backgroundColor: Colors.transparent, + context: context, + builder: (_) => const CoinSelectSheet(), + ).then((value) { + if (value is Coin) { + ref.read(addressEntryDataProvider(widget.id)).coin = + value; + } + }); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ref.watch(addressEntryDataProvider(widget.id) + .select((value) => value.coin)) == + null + ? Text( + "Select cryptocurrency", + style: STextStyles.fieldLabel(context), + ) + : Row( + children: [ + SvgPicture.asset( + Assets.svg.iconFor( + coin: ref.watch( + addressEntryDataProvider(widget.id) + .select( + (value) => value.coin))!), + height: 20, + width: 20, + ), + const SizedBox( + width: 12, + ), + Text( + ref + .watch( + addressEntryDataProvider(widget.id) + .select((value) => value.coin))! + .prettyName, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + if (!isDesktop) + SvgPicture.asset( + Assets.svg.chevronDown, + width: 8, + height: 4, + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle2, + ), + ], + ), ), ), ), ), ), - ), const SizedBox( height: 8, ), @@ -253,9 +351,10 @@ class _NewContactAddressEntryFormState }, child: const ClipboardIcon(), ), - if (!Util.isDesktop && ref.watch(addressEntryDataProvider(widget.id) - .select((value) => value.address)) == - null) + if (!Util.isDesktop && + ref.watch(addressEntryDataProvider(widget.id) + .select((value) => value.address)) == + null) TextFieldIconButton( key: const Key("addAddressBookEntryScanQrButtonKey"), onTap: () async { From 8799a9cfa2a993379e34a91cef9427b4fe272740 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 15:34:48 -0600 Subject: [PATCH 307/426] my stack contact tweaks --- .../subviews/add_address_book_entry_view.dart | 2 +- .../subwidgets/desktop_address_card.dart | 18 +++++++++++------- .../subwidgets/desktop_contact_details.dart | 11 +++++++---- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart b/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart index bb93c68d8..2759e9cb1 100644 --- a/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart +++ b/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart @@ -559,7 +559,7 @@ class _AddAddressBookEntryViewState ), ], ), - if (!isDesktop) const SizedBox(height: 8), + const SizedBox(height: 8), if (forms.length <= 1) const SizedBox( height: 8, diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart index 49b75a4a8..405b9107c 100644 --- a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart @@ -11,9 +11,11 @@ class DesktopAddressCard extends StatelessWidget { const DesktopAddressCard({ Key? key, required this.entry, + required this.contactId, }) : super(key: key); final ContactAddressEntry entry; + final String contactId; @override Widget build(BuildContext context) { @@ -57,13 +59,15 @@ class DesktopAddressCard extends StatelessWidget { text: "Copy", onTap: () {}, ), - const SizedBox( - width: 16, - ), - BlueTextButton( - text: "Edit", - onTap: () {}, - ), + if (contactId != "default") + const SizedBox( + width: 16, + ), + if (contactId != "default") + BlueTextButton( + text: "Edit", + onTap: () {}, + ), ], ) ], diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart index bc19fe3bd..ada330a14 100644 --- a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart @@ -40,16 +40,18 @@ class _DesktopContactDetailsState extends ConsumerState<DesktopContactDetails> { width: 32, height: 32, decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, + color: contact.id == "default" + ? Colors.transparent + : Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, borderRadius: BorderRadius.circular(32), ), child: contact.id == "default" ? Center( child: SvgPicture.asset( Assets.svg.stackIcon(context), - width: 20, + width: 32, ), ) : contact.emojiChar != null @@ -134,6 +136,7 @@ class _DesktopContactDetailsState extends ConsumerState<DesktopContactDetails> { padding: const EdgeInsets.all(18), child: DesktopAddressCard( entry: contact.addresses[i], + contactId: contact.id, ), ), ], From 51c98f90e94d9bbdeb8fe75828ec8f3a848ece29 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 16:01:11 -0600 Subject: [PATCH 308/426] contact tx history --- .../desktop_address_book.dart | 7 +- .../desktop_address_book_scaffold.dart | 3 +- .../subwidgets/desktop_address_card.dart | 18 +- .../subwidgets/desktop_contact_details.dart | 334 ++++++++++++------ 4 files changed, 248 insertions(+), 114 deletions(-) diff --git a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart index fd25617e7..f028a3424 100644 --- a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart +++ b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart @@ -427,11 +427,8 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { ), details: currentContactId == null ? Container() - : RoundedWhiteContainer( - padding: const EdgeInsets.all(24), - child: DesktopContactDetails( - contactId: currentContactId!, - ), + : DesktopContactDetails( + contactId: currentContactId!, ), ), ), diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_book_scaffold.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_book_scaffold.dart index f32ea1f7f..36e44e6d4 100644 --- a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_book_scaffold.dart +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_book_scaffold.dart @@ -56,6 +56,7 @@ class DesktopAddressBookScaffold extends StatelessWidget { ), Expanded( child: Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( flex: 6, @@ -96,7 +97,7 @@ class DesktopAddressBookScaffold extends StatelessWidget { const SizedBox( height: weirdRowHeight, ), - Expanded( + Flexible( child: details ?? Container(), ), ], diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart index 405b9107c..f00e0c137 100644 --- a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart @@ -1,8 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/models/contact_address_entry.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; @@ -12,10 +16,12 @@ class DesktopAddressCard extends StatelessWidget { Key? key, required this.entry, required this.contactId, + this.clipboard = const ClipboardWrapper(), }) : super(key: key); final ContactAddressEntry entry; final String contactId; + final ClipboardInterface clipboard; @override Widget build(BuildContext context) { @@ -57,7 +63,17 @@ class DesktopAddressCard extends StatelessWidget { children: [ BlueTextButton( text: "Copy", - onTap: () {}, + onTap: () { + clipboard.setData( + ClipboardData(text: entry.address), + ); + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ); + }, ), if (contactId != "default") const SizedBox( diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart index ada330a14..53597826c 100644 --- a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart @@ -1,14 +1,21 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/models/contact.dart'; +import 'package:stackwallet/models/paymint/transactions_model.dart'; import 'package:stackwallet/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart'; import 'package:stackwallet/providers/global/address_book_service_provider.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/services/coins/manager.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/blue_text_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/transaction_card.dart'; +import 'package:tuple/tuple.dart'; class DesktopContactDetails extends ConsumerStatefulWidget { const DesktopContactDetails({ @@ -24,132 +31,245 @@ class DesktopContactDetails extends ConsumerStatefulWidget { } class _DesktopContactDetailsState extends ConsumerState<DesktopContactDetails> { + List<Tuple2<String, Transaction>> _cachedTransactions = []; + + bool _contactHasAddress(String address, Contact contact) { + for (final entry in contact.addresses) { + if (entry.address == address) { + return true; + } + } + return false; + } + + Future<List<Tuple2<String, Transaction>>> _filteredTransactionsByContact( + List<Manager> managers, + ) async { + final contact = + ref.read(addressBookServiceProvider).getContactById(widget.contactId); + + // TODO: optimise + + List<Tuple2<String, Transaction>> result = []; + for (final manager in managers) { + final transactions = (await manager.transactionData) + .getAllTransactions() + .values + .toList() + .where((e) => _contactHasAddress(e.address, contact)); + + for (final tx in transactions) { + result.add(Tuple2(manager.walletId, tx)); + } + } + // sort by date + result.sort((a, b) => b.item2.timestamp - a.item2.timestamp); + + return result; + } + @override Widget build(BuildContext context) { final contact = ref.watch(addressBookServiceProvider .select((value) => value.getContactById(widget.contactId))); - return Column( + return Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Container( - width: 32, - height: 32, - decoration: BoxDecoration( - color: contact.id == "default" - ? Colors.transparent - : Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - borderRadius: BorderRadius.circular(32), - ), - child: contact.id == "default" - ? Center( - child: SvgPicture.asset( - Assets.svg.stackIcon(context), - width: 32, - ), - ) - : contact.emojiChar != null - ? Center( - child: Text(contact.emojiChar!), - ) - : Center( - child: SvgPicture.asset( - Assets.svg.user, - width: 18, - ), - ), - ), - const SizedBox( - width: 16, - ), - Text( - contact.name, - style: STextStyles.desktopTextSmall(context), - ), - ], - ), - SecondaryButton( - label: "Options", - width: 86, - buttonHeight: ButtonHeight.xxs, - onPressed: () {}, - ), - ], - ), - const SizedBox( - height: 24, - ), Expanded( - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Column( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Addresses", - style: STextStyles.desktopTextExtraExtraSmall( - context), - ), - BlueTextButton( - text: "Add new", - onTap: () {}, - ), - ], + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: contact.id == "default" + ? Colors.transparent + : Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular(32), + ), + child: contact.id == "default" + ? Center( + child: SvgPicture.asset( + Assets.svg.stackIcon(context), + width: 32, + ), + ) + : contact.emojiChar != null + ? Center( + child: Text(contact.emojiChar!), + ) + : Center( + child: SvgPicture.asset( + Assets.svg.user, + width: 18, + ), + ), ), const SizedBox( - height: 12, + width: 16, ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - borderColor: Theme.of(context) - .extension<StackColors>()! - .background, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - for (int i = 0; i < contact.addresses.length; i++) - Column( + Text( + contact.name, + style: STextStyles.desktopTextSmall(context), + ), + ], + ), + SecondaryButton( + label: "Options", + width: 86, + buttonHeight: ButtonHeight.xxs, + onPressed: () {}, + ), + ], + ), + const SizedBox( + height: 24, + ), + Flexible( + child: ListView( + primary: false, + shrinkWrap: true, + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Addresses", + style: + STextStyles.desktopTextExtraExtraSmall(context), + ), + BlueTextButton( + text: "Add new", + onTap: () {}, + ), + ], + ), + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + for (int i = 0; i < contact.addresses.length; i++) + Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (i > 0) + Container( + color: Theme.of(context) + .extension<StackColors>()! + .background, + height: 1, + ), + Padding( + padding: const EdgeInsets.all(18), + child: DesktopAddressCard( + entry: contact.addresses[i], + contactId: contact.id, + ), + ), + ], + ), + ], + ), + ), + Text( + "Transaction history", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + FutureBuilder( + future: _filteredTransactionsByContact( + ref.watch(walletsChangeNotifierProvider).managers), + builder: (_, + AsyncSnapshot<List<Tuple2<String, Transaction>>> + snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + _cachedTransactions = snapshot.data!; + + if (_cachedTransactions.isNotEmpty) { + return RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: Column( mainAxisSize: MainAxisSize.min, children: [ - if (i > 0) - Container( - color: Theme.of(context) - .extension<StackColors>()! - .background, - height: 1, - ), - Padding( - padding: const EdgeInsets.all(18), - child: DesktopAddressCard( - entry: contact.addresses[i], - contactId: contact.id, + ..._cachedTransactions.map( + (e) => TransactionCard( + key: Key( + "contactDetailsTransaction_${e.item1}_${e.item2.txid}_cardKey"), + transaction: e.item2, + walletId: e.item1, ), ), ], ), - ], - ), - ) - ], - ), + ); + } else { + return RoundedWhiteContainer( + child: Center( + child: Text( + "No transactions found", + style: STextStyles.itemSubtitle(context), + ), + ), + ); + } + } else { + // TODO: proper loading animation + if (_cachedTransactions.isEmpty) { + return const LoadingIndicator(); + } else { + return RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ..._cachedTransactions.map( + (e) => TransactionCard( + key: Key( + "contactDetailsTransaction_${e.item1}_${e.item2.txid}_cardKey"), + transaction: e.item2, + walletId: e.item1, + ), + ), + ], + ), + ); + } + } + }, + ), + ], ), ), - ); - }, + ], + ), ), ), ], From 494849364317cb55e46e8c7e40d786dc37ede959 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 16:18:00 -0600 Subject: [PATCH 309/426] contact tx history --- .../subwidgets/desktop_contact_details.dart | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart index 53597826c..fb094d766 100644 --- a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart @@ -192,9 +192,16 @@ class _DesktopContactDetailsState extends ConsumerState<DesktopContactDetails> { ], ), ), - Text( - "Transaction history", - style: STextStyles.desktopTextExtraExtraSmall(context), + Padding( + padding: const EdgeInsets.only( + top: 20, + bottom: 12, + ), + child: Text( + "Transaction history", + style: + STextStyles.desktopTextExtraExtraSmall(context), + ), ), FutureBuilder( future: _filteredTransactionsByContact( From 318758f76850a471ce0dbfc0a1ecd6a190fb4e4a Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Thu, 17 Nov 2022 16:55:34 -0700 Subject: [PATCH 310/426] copy receive address has pointer finger cursor --- .../sub_widgets/desktop_receive.dart | 142 +++++++++--------- 1 file changed, 73 insertions(+), 69 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart index 3de4ed1e3..1dd2e607e 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart @@ -117,78 +117,82 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - GestureDetector( - onTap: () { - clipboard.setData( - ClipboardData(text: receivingAddress), - ); - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - iconAsset: Assets.svg.copy, - context: context, - ); - }, - child: Container( - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).extension<StackColors>()!.background, - width: 2, + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + clipboard.setData( + ClipboardData(text: receivingAddress), + ); + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ); + }, + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).extension<StackColors>()!.background, + width: 2, + ), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - child: RoundedWhiteContainer( - child: Column( - children: [ - Row( - children: [ - Text( - "Your ${coin.ticker} address", - style: STextStyles.itemSubtitle(context), - ), - const Spacer(), - Row( - children: [ - SvgPicture.asset( - Assets.svg.copy, - width: 15, - height: 15, - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - ), - const SizedBox( - width: 4, - ), - Text( - "Copy", - style: STextStyles.link2(context), - ), - ], - ), - ], - ), - const SizedBox( - height: 8, - ), - Row( - children: [ - Expanded( - child: Text( - receivingAddress, - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, + child: RoundedWhiteContainer( + child: Column( + children: [ + Row( + children: [ + Text( + "Your ${coin.ticker} address", + style: STextStyles.itemSubtitle(context), + ), + const Spacer(), + Row( + children: [ + SvgPicture.asset( + Assets.svg.copy, + width: 15, + height: 15, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + ), + const SizedBox( + width: 4, + ), + Text( + "Copy", + style: STextStyles.link2(context), + ), + ], + ), + ], + ), + const SizedBox( + height: 8, + ), + Row( + children: [ + Expanded( + child: Text( + receivingAddress, + style: + STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), ), ), - ), - ], - ), - ], + ], + ), + ], + ), ), ), ), From cd19d776ae04631571a8d64b008d3cf52746d331 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 16:47:50 -0600 Subject: [PATCH 311/426] desktop edit contact address entry --- .../subviews/edit_contact_address_view.dart | 433 +++++++++--------- .../subwidgets/desktop_address_card.dart | 62 ++- 2 files changed, 268 insertions(+), 227 deletions(-) diff --git a/lib/pages/address_book_views/subviews/edit_contact_address_view.dart b/lib/pages/address_book_views/subviews/edit_contact_address_view.dart index 618a41982..f0143d39d 100644 --- a/lib/pages/address_book_views/subviews/edit_contact_address_view.dart +++ b/lib/pages/address_book_views/subviews/edit_contact_address_view.dart @@ -12,7 +12,11 @@ import 'package:stackwallet/utilities/barcode_scanner_interface.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; class EditContactAddressView extends ConsumerStatefulWidget { const EditContactAddressView({ @@ -44,6 +48,42 @@ class _EditContactAddressViewState late final BarcodeScannerInterface barcodeScanner; late final ClipboardInterface clipboard; + Future<void> save(Contact contact) async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75), + ); + } + List<ContactAddressEntry> entries = contact.addresses.toList(); + + final entry = entries.firstWhere( + (e) => + e.label == addressEntry.label && + e.address == addressEntry.address && + e.coin == addressEntry.coin, + ); + + final index = entries.indexOf(entry); + entries.remove(entry); + + ContactAddressEntry editedEntry = + ref.read(addressEntryDataProvider(0)).buildAddressEntry(); + + entries.insert(index, editedEntry); + + Contact editedContact = contact.copyWith(addresses: entries); + + if (await ref.read(addressBookServiceProvider).editContact(editedContact)) { + if (mounted) { + Navigator.of(context).pop(); + } + // TODO show success notification + } else { + // TODO show error notification + } + } + @override void initState() { contactId = widget.contactId; @@ -59,236 +99,181 @@ class _EditContactAddressViewState final contact = ref.watch(addressBookServiceProvider .select((value) => value.getContactById(contactId))); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, + final bool isDesktop = Util.isDesktop; + + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Edit address", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Edit address", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (context, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - children: [ - Row( - children: [ - Container( - height: 48, - width: 48, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - color: Theme.of(context) - .extension<StackColors>()! - .textFieldActiveBG, - ), - child: Center( - child: contact.emojiChar == null - ? SvgPicture.asset( - Assets.svg.user, - height: 24, - width: 24, - ) - : Text( - contact.emojiChar!, - style: STextStyles.pageTitleH1(context), - ), - ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - contact.name, - style: STextStyles.pageTitleH2(context), - ), - ), - ), - ], - ), - const SizedBox( - height: 16, - ), - NewContactAddressEntryForm( - id: 0, - barcodeScanner: barcodeScanner, - clipboard: clipboard, - ), - const SizedBox( - height: 24, - ), - GestureDetector( - onTap: () async { - // delete address - final _addresses = contact.addresses; - final entry = _addresses.firstWhere( - (e) => - e.label == addressEntry.label && - e.address == addressEntry.address && - e.coin == addressEntry.coin, - ); - - _addresses.remove(entry); - Contact editedContact = - contact.copyWith(addresses: _addresses); - if (await ref - .read(addressBookServiceProvider) - .editContact(editedContact)) { - Navigator.of(context).pop(); - // TODO show success notification - } else { - // TODO show error notification - } - }, - child: Text( - "Delete address", - style: STextStyles.link(context), - ), - ), - const Spacer(), - const SizedBox( - height: 16, - ), - Row( - children: [ - Expanded( - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - ), - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: Builder( - builder: (context) { - bool shouldEnableSave = - ref.watch(validContactStateProvider([0])); - - return TextButton( - style: shouldEnableSave - ? Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor( - context) - : Theme.of(context) - .extension<StackColors>()! - .getPrimaryDisabledButtonColor( - context), - onPressed: shouldEnableSave - ? () async { - if (FocusScope.of(context) - .hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration( - milliseconds: 75), - ); - } - List<ContactAddressEntry> entries = - contact.addresses.toList(); - - final entry = entries.firstWhere( - (e) => - e.label == - addressEntry.label && - e.address == - addressEntry.address && - e.coin == addressEntry.coin, - ); - - final index = - entries.indexOf(entry); - entries.remove(entry); - - ContactAddressEntry editedEntry = ref - .read( - addressEntryDataProvider(0)) - .buildAddressEntry(); - - entries.insert(index, editedEntry); - - Contact editedContact = contact - .copyWith(addresses: entries); - - if (await ref - .read( - addressBookServiceProvider) - .editContact(editedContact)) { - if (mounted) { - Navigator.of(context).pop(); - } - // TODO show success notification - } else { - // TODO show error notification - } - } - : null, - child: Text( - "Save", - style: STextStyles.button(context), - ), - ); - }, - ), - ), - ], - ), - ], + body: LayoutBuilder( + builder: (context, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, ), ), ), ), + ); + }, + ), + ), + child: Column( + children: [ + Row( + children: [ + Container( + height: 48, + width: 48, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveBG, + ), + child: Center( + child: contact.emojiChar == null + ? SvgPicture.asset( + Assets.svg.user, + height: 24, + width: 24, + ) + : Text( + contact.emojiChar!, + style: STextStyles.pageTitleH1(context), + ), + ), + ), + const SizedBox( + width: 16, + ), + if (isDesktop) + Text( + contact.name, + style: STextStyles.pageTitleH2(context), + ), + if (!isDesktop) + Expanded( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + contact.name, + style: STextStyles.pageTitleH2(context), + ), + ), + ), + ], + ), + const SizedBox( + height: 16, + ), + NewContactAddressEntryForm( + id: 0, + barcodeScanner: barcodeScanner, + clipboard: clipboard, + ), + const SizedBox( + height: 24, + ), + ConditionalParent( + condition: isDesktop, + builder: (child) => MouseRegion( + cursor: SystemMouseCursors.click, + child: child, ), - ); - }, + child: GestureDetector( + onTap: () async { + // delete address + final _addresses = contact.addresses; + final entry = _addresses.firstWhere( + (e) => + e.label == addressEntry.label && + e.address == addressEntry.address && + e.coin == addressEntry.coin, + ); + + _addresses.remove(entry); + Contact editedContact = contact.copyWith(addresses: _addresses); + if (await ref + .read(addressBookServiceProvider) + .editContact(editedContact)) { + Navigator.of(context).pop(); + // TODO show success notification + } else { + // TODO show error notification + } + }, + child: Text( + "Delete address", + style: STextStyles.link(context), + ), + ), + ), + const Spacer(), + const SizedBox( + height: 16, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: () async { + if (!isDesktop && FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Save", + enabled: ref.watch(validContactStateProvider([0])), + onPressed: () => save(contact), + buttonHeight: isDesktop ? ButtonHeight.l : null, + ), + ), + ], + ), + ], ), ); } diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart index f00e0c137..713c9e965 100644 --- a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart @@ -1,15 +1,20 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/models/contact_address_entry.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/address_book_views/subviews/edit_contact_address_view.dart'; +import 'package:stackwallet/providers/ui/address_book_providers/address_entry_data_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/flush_bar_type.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/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; class DesktopAddressCard extends StatelessWidget { const DesktopAddressCard({ @@ -80,9 +85,60 @@ class DesktopAddressCard extends StatelessWidget { width: 16, ), if (contactId != "default") - BlueTextButton( - text: "Edit", - onTap: () {}, + Consumer( + builder: (context, ref, child) { + return BlueTextButton( + text: "Edit", + onTap: () async { + ref.read(addressEntryDataProvider(0)).address = + entry.address; + ref.read(addressEntryDataProvider(0)).addressLabel = + entry.label; + ref.read(addressEntryDataProvider(0)).coin = + entry.coin; + + await showDialog<void>( + context: context, + builder: (context) => DesktopDialog( + maxWidth: 580, + maxHeight: 566, + child: Column( + children: [ + Row( + children: [ + const SizedBox( + width: 8, + ), + const AppBarBackButton( + isCompact: true, + ), + Text( + "Edit address", + style: STextStyles.desktopH3(context), + ), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + top: 20, + left: 32, + right: 32, + bottom: 32, + ), + child: EditContactAddressView( + contactId: contactId, + addressEntry: entry, + ), + ), + ), + ], + ), + ), + ); + }, + ); + }, ), ], ) From df64e48e1ef2985cc58363bf844c288bd71ebd5a Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 17:00:06 -0600 Subject: [PATCH 312/426] desktop add new contact address entry --- .../add_new_contact_address_view.dart | 342 +++++++++--------- .../subwidgets/desktop_address_card.dart | 2 + .../subwidgets/desktop_contact_details.dart | 49 ++- 3 files changed, 213 insertions(+), 180 deletions(-) diff --git a/lib/pages/address_book_views/subviews/add_new_contact_address_view.dart b/lib/pages/address_book_views/subviews/add_new_contact_address_view.dart index e5dbaa7b9..dc25c3dc1 100644 --- a/lib/pages/address_book_views/subviews/add_new_contact_address_view.dart +++ b/lib/pages/address_book_views/subviews/add_new_contact_address_view.dart @@ -12,7 +12,11 @@ import 'package:stackwallet/utilities/barcode_scanner_interface.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; class AddNewContactAddressView extends ConsumerStatefulWidget { const AddNewContactAddressView({ @@ -55,190 +59,170 @@ class _AddNewContactAddressViewState final contact = ref.watch(addressBookServiceProvider .select((value) => value.getContactById(contactId))); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, + final isDesktop = Util.isDesktop; + + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Add new address", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Add new address", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (context, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - children: [ - Row( - children: [ - Container( - height: 48, - width: 48, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - color: Theme.of(context) - .extension<StackColors>()! - .textFieldActiveBG, - ), - child: Center( - child: contact.emojiChar == null - ? SvgPicture.asset( - Assets.svg.user, - height: 24, - width: 24, - ) - : Text( - contact.emojiChar!, - style: STextStyles.pageTitleH1(context), - ), - ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - contact.name, - style: STextStyles.pageTitleH2(context), - ), - ), - ), - ], - ), - const SizedBox( - height: 16, - ), - NewContactAddressEntryForm( - id: 0, - barcodeScanner: barcodeScanner, - clipboard: clipboard, - ), - const SizedBox( - height: 16, - ), - const Spacer(), - Row( - children: [ - Expanded( - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - ), - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: Builder( - builder: (context) { - bool shouldEnableSave = - ref.watch(validContactStateProvider([0])); - - return TextButton( - style: shouldEnableSave - ? Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor( - context) - : Theme.of(context) - .extension<StackColors>()! - .getPrimaryDisabledButtonColor( - context), - onPressed: shouldEnableSave - ? () async { - if (FocusScope.of(context) - .hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration( - milliseconds: 75), - ); - } - List<ContactAddressEntry> entries = - contact.addresses; - - entries.add(ref - .read( - addressEntryDataProvider(0)) - .buildAddressEntry()); - - Contact editedContact = contact - .copyWith(addresses: entries); - - if (await ref - .read( - addressBookServiceProvider) - .editContact(editedContact)) { - if (mounted) { - Navigator.of(context).pop(); - } - // TODO show success notification - } else { - // TODO show error notification - } - } - : null, - child: Text( - "Save", - style: STextStyles.button(context), - ), - ); - }, - ), - ), - ], - ) - ], + body: LayoutBuilder( + builder: (context, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, ), ), ), ), - ), - ); - }, + ); + }, + ), + ), + child: Column( + children: [ + Row( + children: [ + Container( + height: 48, + width: 48, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveBG, + ), + child: Center( + child: contact.emojiChar == null + ? SvgPicture.asset( + Assets.svg.user, + height: 24, + width: 24, + ) + : Text( + contact.emojiChar!, + style: STextStyles.pageTitleH1(context), + ), + ), + ), + const SizedBox( + width: 16, + ), + if (isDesktop) + Text( + contact.name, + style: STextStyles.pageTitleH2(context), + ), + if (!isDesktop) + Expanded( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + contact.name, + style: STextStyles.pageTitleH2(context), + ), + ), + ), + ], + ), + const SizedBox( + height: 16, + ), + NewContactAddressEntryForm( + id: 0, + barcodeScanner: barcodeScanner, + clipboard: clipboard, + ), + const SizedBox( + height: 16, + ), + const Spacer(), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: () async { + if (!isDesktop && FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Save", + enabled: ref.watch(validContactStateProvider([0])), + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75), + ); + } + List<ContactAddressEntry> entries = contact.addresses; + + entries.add(ref + .read(addressEntryDataProvider(0)) + .buildAddressEntry()); + + Contact editedContact = + contact.copyWith(addresses: entries); + + if (await ref + .read(addressBookServiceProvider) + .editContact(editedContact)) { + if (mounted) { + Navigator.of(context).pop(); + } + // TODO show success notification + } else { + // TODO show error notification + } + }, + ), + ), + ], + ) + ], ), ); } diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart index 713c9e965..4d58dc474 100644 --- a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart @@ -90,6 +90,8 @@ class DesktopAddressCard extends StatelessWidget { return BlueTextButton( text: "Edit", onTap: () async { + ref.refresh( + addressEntryDataProviderFamilyRefresher); ref.read(addressEntryDataProvider(0)).address = entry.address; ref.read(addressEntryDataProvider(0)).addressLabel = diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart index fb094d766..1c27b6836 100644 --- a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart @@ -3,14 +3,18 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/models/contact.dart'; import 'package:stackwallet/models/paymint/transactions_model.dart'; +import 'package:stackwallet/pages/address_book_views/subviews/add_new_contact_address_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart'; import 'package:stackwallet/providers/global/address_book_service_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/providers/ui/address_book_providers/address_entry_data_provider.dart'; import 'package:stackwallet/services/coins/manager.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/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -154,7 +158,50 @@ class _DesktopContactDetailsState extends ConsumerState<DesktopContactDetails> { ), BlueTextButton( text: "Add new", - onTap: () {}, + onTap: () async { + ref.refresh( + addressEntryDataProviderFamilyRefresher); + + await showDialog<void>( + context: context, + builder: (context) => DesktopDialog( + maxWidth: 580, + maxHeight: 566, + child: Column( + children: [ + Row( + children: [ + const SizedBox( + width: 8, + ), + const AppBarBackButton( + isCompact: true, + ), + Text( + "Add new address", + style: + STextStyles.desktopH3(context), + ), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + top: 20, + left: 32, + right: 32, + bottom: 32, + ), + child: AddNewContactAddressView( + contactId: widget.contactId, + ), + ), + ), + ], + ), + ), + ); + }, ), ], ), From e70f5b0709eea46f42cfd4ed8dd9f7ba2aa24b20 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 18:14:30 -0600 Subject: [PATCH 313/426] WIP desktop contact options context popup menu --- .../subwidgets/desktop_contact_details.dart | 262 +++++++++++++++++- 1 file changed, 260 insertions(+), 2 deletions(-) diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart index 1c27b6836..2cd20a839 100644 --- a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart @@ -131,9 +131,19 @@ class _DesktopContactDetailsState extends ConsumerState<DesktopContactDetails> { ), SecondaryButton( label: "Options", - width: 86, + width: 96, buttonHeight: ButtonHeight.xxs, - onPressed: () {}, + onPressed: () async { + await showDialog<void>( + context: context, + barrierColor: Colors.transparent, + builder: (context) { + return DesktopContactOptionsMenuPopup( + contactId: contact.id, + ); + }, + ); + }, ), ], ), @@ -330,3 +340,251 @@ class _DesktopContactDetailsState extends ConsumerState<DesktopContactDetails> { ); } } + +class DesktopContactOptionsMenuPopup extends ConsumerStatefulWidget { + const DesktopContactOptionsMenuPopup({Key? key, required this.contactId}) + : super(key: key); + + final String contactId; + + @override + ConsumerState<DesktopContactOptionsMenuPopup> createState() => + _DesktopContactOptionsMenuPopupState(); +} + +class _DesktopContactOptionsMenuPopupState + extends ConsumerState<DesktopContactOptionsMenuPopup> { + bool hoveredOnStar = false; + bool hoveredOnPencil = false; + bool hoveredOnTrash = false; + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Positioned( + top: 210, + left: MediaQuery.of(context).size.width - 280, + child: Container( + width: 270, + decoration: BoxDecoration( + color: Theme.of(context).extension<StackColors>()!.popupBG, + borderRadius: BorderRadius.circular( + 20, + ), + boxShadow: [ + Theme.of(context).extension<StackColors>()!.standardBoxShadow, + ], + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + MouseRegion( + onEnter: (_) { + setState(() { + hoveredOnStar = true; + }); + }, + onExit: (_) { + setState(() { + hoveredOnStar = false; + }); + }, + child: RawMaterialButton( + hoverColor: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 1000, + ), + ), + onPressed: () { + final contact = + ref.read(addressBookServiceProvider).getContactById( + widget.contactId, + ); + ref.read(addressBookServiceProvider).editContact( + contact.copyWith( + isFavorite: !contact.isFavorite, + ), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 25, + vertical: 16, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.star, + width: 24, + height: 22, + color: hoveredOnStar + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultSearchIconLeft, + ), + const SizedBox( + width: 12, + ), + Text( + ref.watch(addressBookServiceProvider.select( + (value) => value + .getContactById(widget.contactId) + .isFavorite)) + ? "Remove from favorites" + : "Add to favorites", + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ) + ], + ), + ), + ), + ), + const SizedBox( + height: 2, + ), + MouseRegion( + onEnter: (_) { + setState(() { + hoveredOnPencil = true; + }); + }, + onExit: (_) { + setState(() { + hoveredOnPencil = false; + }); + }, + child: RawMaterialButton( + hoverColor: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 1000, + ), + ), + onPressed: () { + print("should go to edit"); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 25, + vertical: 16, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.pencil, + width: 24, + height: 22, + color: hoveredOnPencil + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultSearchIconLeft, + ), + const SizedBox( + width: 12, + ), + Text( + "Edit contact", + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ) + ], + ), + ), + ), + ), + const SizedBox( + height: 2, + ), + MouseRegion( + onEnter: (_) { + setState(() { + hoveredOnTrash = true; + }); + }, + onExit: (_) { + setState(() { + hoveredOnTrash = false; + }); + }, + child: RawMaterialButton( + hoverColor: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 1000, + ), + ), + onPressed: () { + print("should delete contact"); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 25, + vertical: 16, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.trash, + width: 24, + height: 22, + color: hoveredOnTrash + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultSearchIconLeft, + ), + const SizedBox( + width: 12, + ), + Text( + "Delete contact", + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ) + ], + ), + ), + ), + ), + ], + ), + ), + ), + ), + ], + ); + } +} From 38251dc5edb0d5e2f6a771e53deb98aa0e96a68e Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 18:21:04 -0600 Subject: [PATCH 314/426] textStyles prep for ocean theme --- lib/utilities/text_styles.dart | 54 ++++++++++++++++++++++++++++ lib/utilities/theme/color_theme.dart | 1 + 2 files changed, 55 insertions(+) diff --git a/lib/utilities/text_styles.dart b/lib/utilities/text_styles.dart index 63aa19afb..db4764459 100644 --- a/lib/utilities/text_styles.dart +++ b/lib/utilities/text_styles.dart @@ -15,6 +15,7 @@ class STextStyles { fontWeight: FontWeight.w600, fontSize: 20, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -32,6 +33,7 @@ class STextStyles { fontWeight: FontWeight.w600, fontSize: 18, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -49,6 +51,7 @@ class STextStyles { fontWeight: FontWeight.w600, fontSize: 16, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -66,6 +69,7 @@ class STextStyles { fontWeight: FontWeight.w600, fontSize: 16, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -83,6 +87,7 @@ class STextStyles { fontWeight: FontWeight.w400, fontSize: 16, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -100,6 +105,7 @@ class STextStyles { fontWeight: FontWeight.w400, fontSize: 16, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -117,6 +123,7 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 16, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -134,6 +141,7 @@ class STextStyles { fontWeight: FontWeight.w600, fontSize: 16, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -151,6 +159,7 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 16, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextPrimary, @@ -168,6 +177,7 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 16, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -185,6 +195,7 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 16, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark3, @@ -202,6 +213,7 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 14, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark3, @@ -219,6 +231,7 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 12, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textSubtitle1, @@ -237,6 +250,7 @@ class STextStyles { fontSize: 14, height: 14 / 14, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textFieldActiveSearchIconRight, @@ -255,6 +269,7 @@ class STextStyles { fontWeight: FontWeight.w700, fontSize: 12, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textSubtitle1, @@ -272,6 +287,7 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 14, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).infoItemLabel, @@ -289,6 +305,7 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 14, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -306,6 +323,7 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 14, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -324,6 +342,7 @@ class STextStyles { fontSize: 14, height: 1.5, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textSubtitle2, @@ -343,6 +362,7 @@ class STextStyles { fontSize: 14, height: 1.5, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -361,6 +381,7 @@ class STextStyles { fontWeight: FontWeight.w400, fontSize: 14, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -378,6 +399,7 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 14, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).accentColorRed, @@ -395,6 +417,7 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 14, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).infoItemIcons, @@ -412,6 +435,7 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 12, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).accentColorBlue, @@ -429,6 +453,7 @@ class STextStyles { fontWeight: FontWeight.w600, fontSize: 12, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -446,6 +471,7 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 12, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -463,6 +489,7 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 12, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -480,6 +507,7 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 10, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textError, @@ -497,6 +525,7 @@ class STextStyles { fontWeight: FontWeight.w500, fontSize: 10, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textSubtitle1, @@ -517,6 +546,7 @@ class STextStyles { fontSize: 40, height: 40 / 40, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -536,6 +566,7 @@ class STextStyles { fontSize: 32, height: 32 / 32, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -555,6 +586,7 @@ class STextStyles { fontSize: 24, height: 24 / 24, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -574,6 +606,7 @@ class STextStyles { fontSize: 20, height: 30 / 20, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -593,6 +626,7 @@ class STextStyles { fontSize: 20, height: 30 / 20, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -612,6 +646,7 @@ class STextStyles { fontSize: 20, height: 28 / 20, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -631,6 +666,7 @@ class STextStyles { fontSize: 24, height: 33 / 24, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -650,6 +686,7 @@ class STextStyles { fontSize: 20, height: 26 / 20, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextPrimary, @@ -669,6 +706,7 @@ class STextStyles { fontSize: 20, height: 26 / 20, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextPrimaryDisabled, @@ -688,6 +726,7 @@ class STextStyles { fontSize: 20, height: 26 / 20, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextSecondary, @@ -707,6 +746,7 @@ class STextStyles { fontSize: 20, height: 26 / 20, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextSecondaryDisabled, @@ -726,6 +766,7 @@ class STextStyles { fontSize: 18, height: 27 / 18, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextPrimaryDisabled, @@ -745,6 +786,7 @@ class STextStyles { fontSize: 16, height: 24 / 16, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextPrimaryDisabled, @@ -764,6 +806,7 @@ class STextStyles { fontSize: 14, height: 21 / 14, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textSubtitle1, @@ -783,6 +826,7 @@ class STextStyles { fontSize: 14, height: 21 / 14, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -802,6 +846,7 @@ class STextStyles { fontSize: 16, height: 24 / 16, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextSecondary, @@ -821,6 +866,7 @@ class STextStyles { fontSize: 20, height: 30 / 20, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textSubtitle2, @@ -840,6 +886,7 @@ class STextStyles { fontSize: 16, height: 20.8 / 16, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark.withOpacity(0.8), @@ -859,6 +906,7 @@ class STextStyles { fontSize: 16, height: 20.8 / 16, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -878,6 +926,7 @@ class STextStyles { fontSize: 16, height: 20.8 / 16, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark.withOpacity(0.5), @@ -897,6 +946,7 @@ class STextStyles { fontSize: 16, height: 20.8 / 16, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -915,6 +965,7 @@ class STextStyles { fontWeight: FontWeight.w600, fontSize: 8, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.roboto( color: _theme(context).textDark, @@ -932,6 +983,7 @@ class STextStyles { fontWeight: FontWeight.w400, fontSize: 26, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.roboto( color: _theme(context).numberTextDefault, @@ -950,6 +1002,7 @@ class STextStyles { fontWeight: FontWeight.w400, fontSize: 12, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( letterSpacing: 0.5, @@ -969,6 +1022,7 @@ class STextStyles { fontWeight: FontWeight.w600, fontSize: 16, ); + case ThemeType.oceanBreeze: case ThemeType.dark: return GoogleFonts.inter( letterSpacing: 0.5, diff --git a/lib/utilities/theme/color_theme.dart b/lib/utilities/theme/color_theme.dart index 49fd41a6e..852e2f586 100644 --- a/lib/utilities/theme/color_theme.dart +++ b/lib/utilities/theme/color_theme.dart @@ -5,6 +5,7 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; enum ThemeType { light, dark, + oceanBreeze, } abstract class StackColorTheme { From 390c3f186ff8266479a38ce90684840b0f554042 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 18:49:56 -0600 Subject: [PATCH 315/426] temporarily disable in wallet exchange button --- .../wallet_view/desktop_wallet_view.dart | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index a7de8fdf4..d08864eee 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -467,36 +467,36 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { ); }, ), - if (coin == Coin.firo) const SizedBox(width: 16), - SecondaryButton( - width: 180, - buttonHeight: ButtonHeight.l, - onPressed: () { - _onExchangePressed(context); - }, - label: "Exchange", - icon: Container( - width: 24, - height: 24, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - color: Theme.of(context) - .extension<StackColors>()! - .buttonBackPrimary - .withOpacity(0.2), - ), - child: Center( - child: SvgPicture.asset( - Assets.svg.arrowRotate2, - width: 14, - height: 14, - color: Theme.of(context) - .extension<StackColors>()! - .buttonTextSecondary, - ), - ), - ), - ), + // if (coin == Coin.firo) const SizedBox(width: 16), + // SecondaryButton( + // width: 180, + // buttonHeight: ButtonHeight.l, + // onPressed: () { + // _onExchangePressed(context); + // }, + // label: "Exchange", + // icon: Container( + // width: 24, + // height: 24, + // decoration: BoxDecoration( + // borderRadius: BorderRadius.circular(24), + // color: Theme.of(context) + // .extension<StackColors>()! + // .buttonBackPrimary + // .withOpacity(0.2), + // ), + // child: Center( + // child: SvgPicture.asset( + // Assets.svg.arrowRotate2, + // width: 14, + // height: 14, + // color: Theme.of(context) + // .extension<StackColors>()! + // .buttonTextSecondary, + // ), + // ), + // ), + // ), ], ), ), From 4eed147f10f978125f7ebaf1b57b7410732fe287 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 17 Nov 2022 19:23:15 -0600 Subject: [PATCH 316/426] update main to check for ocean breeze theme --- lib/main.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/main.dart b/lib/main.dart index b1f917f58..42b4ee718 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -301,6 +301,9 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme> case "dark": themeType = ThemeType.dark; break; + case "oceanBreeze": + themeType = ThemeType.oceanBreeze; + break; case "light": default: themeType = ThemeType.light; From 2137cffd8459b750c9bd7b056eb46c93e0f60418 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Thu, 17 Nov 2022 20:04:02 -0700 Subject: [PATCH 317/426] ocean breeze theme colors added --- .../appearance_settings_view.dart | 5 +- lib/utilities/text_styles.dart | 296 +++++++++++++++++ lib/utilities/theme/ocean_breeze_colors.dart | 306 ++++++++++++++++++ 3 files changed, 606 insertions(+), 1 deletion(-) create mode 100644 lib/utilities/theme/ocean_breeze_colors.dart diff --git a/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart b/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart index b0cf35a84..3a1b842f6 100644 --- a/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart +++ b/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart @@ -141,7 +141,10 @@ class AppearanceSettingsView extends ConsumerWidget { key: "colorScheme", value: (newValue ? ThemeType.dark - : ThemeType.light) + : (newValue + ? ThemeType.light + : ThemeType + .oceanBreeze)) .name, ); ref diff --git a/lib/utilities/text_styles.dart b/lib/utilities/text_styles.dart index db4764459..c9dd15e1d 100644 --- a/lib/utilities/text_styles.dart +++ b/lib/utilities/text_styles.dart @@ -16,6 +16,11 @@ class STextStyles { fontSize: 20, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 20, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -34,6 +39,11 @@ class STextStyles { fontSize: 18, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 18, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -52,6 +62,11 @@ class STextStyles { fontSize: 16, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -70,6 +85,11 @@ class STextStyles { fontSize: 16, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -88,6 +108,11 @@ class STextStyles { fontSize: 16, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -106,6 +131,11 @@ class STextStyles { fontSize: 16, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -124,6 +154,11 @@ class STextStyles { fontSize: 16, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -142,6 +177,11 @@ class STextStyles { fontSize: 16, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -160,6 +200,11 @@ class STextStyles { fontSize: 16, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).buttonTextPrimary, + fontWeight: FontWeight.w500, + fontSize: 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextPrimary, @@ -178,6 +223,11 @@ class STextStyles { fontSize: 16, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -196,6 +246,11 @@ class STextStyles { fontSize: 16, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark3, + fontWeight: FontWeight.w500, + fontSize: 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark3, @@ -214,6 +269,11 @@ class STextStyles { fontSize: 14, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark3, + fontWeight: FontWeight.w500, + fontSize: 14, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark3, @@ -232,6 +292,11 @@ class STextStyles { fontSize: 12, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textSubtitle1, + fontWeight: FontWeight.w500, + fontSize: 12, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textSubtitle1, @@ -251,6 +316,12 @@ class STextStyles { height: 14 / 14, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textFieldActiveSearchIconRight, + fontWeight: FontWeight.w500, + fontSize: 14, + height: 14 / 14, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textFieldActiveSearchIconRight, @@ -270,6 +341,11 @@ class STextStyles { fontSize: 12, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textSubtitle1, + fontWeight: FontWeight.w700, + fontSize: 12, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textSubtitle1, @@ -288,6 +364,11 @@ class STextStyles { fontSize: 14, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).infoItemLabel, + fontWeight: FontWeight.w500, + fontSize: 14, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).infoItemLabel, @@ -306,6 +387,11 @@ class STextStyles { fontSize: 14, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 14, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -324,6 +410,11 @@ class STextStyles { fontSize: 14, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 14, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -343,6 +434,12 @@ class STextStyles { height: 1.5, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textSubtitle2, + fontWeight: FontWeight.w500, + fontSize: 14, + height: 1.5, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textSubtitle2, @@ -363,6 +460,12 @@ class STextStyles { height: 1.5, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 14, + height: 1.5, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -382,6 +485,11 @@ class STextStyles { fontSize: 14, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 14, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -400,6 +508,11 @@ class STextStyles { fontSize: 14, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).accentColorRed, + fontWeight: FontWeight.w500, + fontSize: 14, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).accentColorRed, @@ -418,6 +531,11 @@ class STextStyles { fontSize: 14, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).infoItemIcons, + fontWeight: FontWeight.w500, + fontSize: 14, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).infoItemIcons, @@ -436,6 +554,11 @@ class STextStyles { fontSize: 12, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).accentColorBlue, + fontWeight: FontWeight.w500, + fontSize: 12, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).accentColorBlue, @@ -454,6 +577,11 @@ class STextStyles { fontSize: 12, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 12, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -472,6 +600,11 @@ class STextStyles { fontSize: 12, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 12, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -490,6 +623,11 @@ class STextStyles { fontSize: 12, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 12, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -508,6 +646,11 @@ class STextStyles { fontSize: 10, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textError, + fontWeight: FontWeight.w500, + fontSize: 10, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textError, @@ -526,6 +669,11 @@ class STextStyles { fontSize: 10, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textSubtitle1, + fontWeight: FontWeight.w500, + fontSize: 10, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textSubtitle1, @@ -547,6 +695,12 @@ class STextStyles { height: 40 / 40, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 40, + height: 40 / 40, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -567,6 +721,12 @@ class STextStyles { height: 32 / 32, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 32, + height: 32 / 32, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -587,6 +747,12 @@ class STextStyles { height: 24 / 24, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 24, + height: 24 / 24, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -607,6 +773,12 @@ class STextStyles { height: 30 / 20, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 20, + height: 30 / 20, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -627,6 +799,12 @@ class STextStyles { height: 30 / 20, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 20, + height: 30 / 20, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -647,6 +825,12 @@ class STextStyles { height: 28 / 20, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 20, + height: 28 / 20, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -667,6 +851,12 @@ class STextStyles { height: 33 / 24, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 24, + height: 33 / 24, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -687,6 +877,12 @@ class STextStyles { height: 26 / 20, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).buttonTextPrimary, + fontWeight: FontWeight.w500, + fontSize: 20, + height: 26 / 20, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextPrimary, @@ -707,6 +903,12 @@ class STextStyles { height: 26 / 20, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).buttonTextPrimaryDisabled, + fontWeight: FontWeight.w500, + fontSize: 20, + height: 26 / 20, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextPrimaryDisabled, @@ -727,6 +929,12 @@ class STextStyles { height: 26 / 20, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).buttonTextSecondary, + fontWeight: FontWeight.w500, + fontSize: 20, + height: 26 / 20, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextSecondary, @@ -747,6 +955,12 @@ class STextStyles { height: 26 / 20, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).buttonTextSecondaryDisabled, + fontWeight: FontWeight.w500, + fontSize: 20, + height: 26 / 20, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextSecondaryDisabled, @@ -767,6 +981,12 @@ class STextStyles { height: 27 / 18, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 18, + height: 27 / 18, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextPrimaryDisabled, @@ -787,6 +1007,12 @@ class STextStyles { height: 24 / 16, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).buttonTextPrimaryDisabled, + fontWeight: FontWeight.w500, + fontSize: 16, + height: 24 / 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextPrimaryDisabled, @@ -807,6 +1033,12 @@ class STextStyles { height: 21 / 14, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textSubtitle1, + fontWeight: FontWeight.w500, + fontSize: 14, + height: 21 / 14, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textSubtitle1, @@ -827,6 +1059,12 @@ class STextStyles { height: 21 / 14, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 14, + height: 21 / 14, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -847,6 +1085,12 @@ class STextStyles { height: 24 / 16, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).buttonTextSecondary, + fontWeight: FontWeight.w500, + fontSize: 16, + height: 24 / 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).buttonTextSecondary, @@ -867,6 +1111,12 @@ class STextStyles { height: 30 / 20, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textSubtitle2, + fontWeight: FontWeight.w500, + fontSize: 20, + height: 30 / 20, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textSubtitle2, @@ -887,6 +1137,12 @@ class STextStyles { height: 20.8 / 16, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark.withOpacity(0.8), + fontWeight: FontWeight.w500, + fontSize: 16, + height: 20.8 / 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark.withOpacity(0.8), @@ -907,6 +1163,12 @@ class STextStyles { height: 20.8 / 16, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 16, + height: 20.8 / 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -927,6 +1189,12 @@ class STextStyles { height: 20.8 / 16, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark.withOpacity(0.5), + fontWeight: FontWeight.w500, + fontSize: 16, + height: 20.8 / 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark.withOpacity(0.5), @@ -947,6 +1215,12 @@ class STextStyles { height: 20.8 / 16, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 16, + height: 20.8 / 16, + ); case ThemeType.dark: return GoogleFonts.inter( color: _theme(context).textDark, @@ -966,6 +1240,11 @@ class STextStyles { fontSize: 8, ); case ThemeType.oceanBreeze: + return GoogleFonts.roboto( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 8, + ); case ThemeType.dark: return GoogleFonts.roboto( color: _theme(context).textDark, @@ -984,6 +1263,11 @@ class STextStyles { fontSize: 26, ); case ThemeType.oceanBreeze: + return GoogleFonts.roboto( + color: _theme(context).numberTextDefault, + fontWeight: FontWeight.w400, + fontSize: 26, + ); case ThemeType.dark: return GoogleFonts.roboto( color: _theme(context).numberTextDefault, @@ -1003,6 +1287,12 @@ class STextStyles { fontSize: 12, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + letterSpacing: 0.5, + color: _theme(context).accentColorDark, + fontWeight: FontWeight.w400, + fontSize: 12, + ); case ThemeType.dark: return GoogleFonts.inter( letterSpacing: 0.5, @@ -1023,6 +1313,12 @@ class STextStyles { fontSize: 16, ); case ThemeType.oceanBreeze: + return GoogleFonts.inter( + letterSpacing: 0.5, + color: _theme(context).accentColorDark, + fontWeight: FontWeight.w600, + fontSize: 16, + ); case ThemeType.dark: return GoogleFonts.inter( letterSpacing: 0.5, diff --git a/lib/utilities/theme/ocean_breeze_colors.dart b/lib/utilities/theme/ocean_breeze_colors.dart new file mode 100644 index 000000000..ff2f4e85e --- /dev/null +++ b/lib/utilities/theme/ocean_breeze_colors.dart @@ -0,0 +1,306 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/theme/color_theme.dart'; + +class OceanBreezeColors extends StackColorTheme { + @override + ThemeType get themeType => ThemeType.oceanBreeze; + + @override + Color get background => const Color(0xFFF3F7FA); + @override + Color get overlay => const Color(0xFF111215); + + @override + Color get accentColorBlue => const Color(0xFF077CBE); + @override + Color get accentColorGreen => const Color(0xFF00A591); + @override + Color get accentColorYellow => const Color(0xFFF4C517); + @override + Color get accentColorRed => const Color(0xFFD1382D); + @override + Color get accentColorOrange => const Color(0xFFFF985F); + @override + Color get accentColorDark => const Color(0xFF232323); + + @override + Color get shadow => const Color(0xFF388192); + + @override + Color get textDark => const Color(0xFF232323); + @override + Color get textDark2 => const Color(0xFF333333); + @override + Color get textDark3 => const Color(0xFF696B6C); + @override + Color get textSubtitle1 => const Color(0xFF7E8284); + @override + Color get textSubtitle2 => const Color(0xFF919393); + @override + Color get textSubtitle3 => const Color(0xFFB0B2B2); + @override + Color get textSubtitle4 => const Color(0xFFD1D3D3); + @override + Color get textSubtitle5 => const Color(0xFFDEDFE1); + @override + Color get textSubtitle6 => const Color(0xFFF1F1F1); + @override + Color get textWhite => const Color(0xFFFFFFFF); + @override + Color get textFavoriteCard => const Color(0xFF232323); + @override + Color get textError => const Color(0xFF8D0006); + + // button background + @override + Color get buttonBackPrimary => const Color(0xFF227386); + @override + Color get buttonBackSecondary => const Color(0xFFC2DAE2); + @override + Color get buttonBackPrimaryDisabled => const Color(0xFFBDD5DB); + @override + Color get buttonBackSecondaryDisabled => const Color(0xFFBDBDBD); + @override + Color get buttonBackBorder => const Color(0xFF227386); + @override + Color get buttonBackBorderDisabled => const Color(0xFFBDD5DB); + + @override + Color get numberBackDefault => const Color(0xFFFFFFFF); + @override + Color get numpadBackDefault => const Color(0xFF227386); + @override + Color get bottomNavBack => const Color(0xFFFFFFFF); + + // button text/element + @override + Color get buttonTextPrimary => const Color(0xFFFFFFFF); + @override + Color get buttonTextSecondary => const Color(0xFF232323); + @override + Color get buttonTextPrimaryDisabled => const Color(0xFFFFFFFF); + @override + Color get buttonTextSecondaryDisabled => const Color(0xFFBDD5DB); + @override + Color get buttonTextBorder => const Color(0xFF227386); + @override + Color get buttonTextDisabled => const Color(0xFFFFFFFF); + @override + Color get buttonTextBorderless => const Color(0xFF056EC6); + @override + Color get buttonTextBorderlessDisabled => const Color(0xFFB6B6B6); + @override + Color get numberTextDefault => const Color(0xFF232323); + @override + Color get numpadTextDefault => const Color(0xFFFFFFFF); + @override + Color get bottomNavText => const Color(0xFF232323); + + // switch + @override + Color get switchBGOn => const Color(0xFF056EC6); + @override + Color get switchBGOff => const Color(0xFFCCDBF9); + @override + Color get switchBGDisabled => const Color(0xFFC5C6C9); + @override + Color get switchCircleOn => const Color(0xFFDAE2FF); + @override + Color get switchCircleOff => const Color(0xFFFBFCFF); + @override + Color get switchCircleDisabled => const Color(0xFFFBFCFF); + + // step indicator background + @override + Color get stepIndicatorBGCheck => const Color(0xFFCDD9FF); + @override + Color get stepIndicatorBGNumber => const Color(0xFFCDD9FF); + @override + Color get stepIndicatorBGInactive => const Color(0xFFA6C7D1); + @override + Color get stepIndicatorBGLines => const Color(0xFF90B8DC); + @override + Color get stepIndicatorBGLinesInactive => const Color(0xFFBCD4EA); + @override + Color get stepIndicatorIconText => const Color(0xFF005BAF); + @override + Color get stepIndicatorIconNumber => const Color(0xFF005BAF); + @override + Color get stepIndicatorIconInactive => const Color(0xFFD4DFFF); + + // checkbox + @override + Color get checkboxBGChecked => const Color(0xFF056EC6); + @override + Color get checkboxBorderEmpty => const Color(0xFF8C8F90); + @override + Color get checkboxBGDisabled => const Color(0xFFB0C9ED); + @override + Color get checkboxIconChecked => const Color(0xFFFFFFFF); + @override + Color get checkboxIconDisabled => const Color(0xFFFFFFFF); + @override + Color get checkboxTextLabel => const Color(0xFF232323); + + // snack bar + @override + Color get snackBarBackSuccess => const Color(0xFFADD6D2); + @override + Color get snackBarBackError => const Color(0xFFF6C7C3); + @override + Color get snackBarBackInfo => const Color(0xFFCCD7FF); + @override + Color get snackBarTextSuccess => const Color(0xFF075547); + @override + Color get snackBarTextError => const Color(0xFF8D0006); + @override + Color get snackBarTextInfo => const Color(0xFF002569); + + // icons + @override + Color get bottomNavIconBack => const Color(0xFFA7C7CF); + @override + Color get bottomNavIconIcon => const Color(0xFF227386); + + @override + Color get topNavIconPrimary => const Color(0xFF227386); + @override + Color get topNavIconGreen => const Color(0xFF00A591); + @override + Color get topNavIconYellow => const Color(0xFFFDD33A); + @override + Color get topNavIconRed => const Color(0xFFEA4649); + + @override + Color get settingsIconBack => const Color(0xFFE0E3E3); + @override + Color get settingsIconIcon => const Color(0xFF232323); + @override + Color get settingsIconBack2 => const Color(0xFF80D2C8); + @override + Color get settingsIconElement => const Color(0xFF00A591); + + // text field + @override + Color get textFieldActiveBG => const Color(0xFFD3E3E7); + @override + Color get textFieldDefaultBG => const Color(0xFFD8E7EB); + @override + Color get textFieldErrorBG => const Color(0xFFF6C7C3); + @override + Color get textFieldSuccessBG => const Color(0xFFADD6D2); + + @override + Color get textFieldActiveSearchIconLeft => const Color(0xFF86898C); + @override + Color get textFieldDefaultSearchIconLeft => const Color(0xFF86898C); + @override + Color get textFieldErrorSearchIconLeft => const Color(0xFF8D0006); + @override + Color get textFieldSuccessSearchIconLeft => const Color(0xFF006C4D); + + @override + Color get textFieldActiveText => const Color(0xFF232323); + @override + Color get textFieldDefaultText => const Color(0xFF86898C); + @override + Color get textFieldErrorText => const Color(0xFF000000); + @override + Color get textFieldSuccessText => const Color(0xFF000000); + + @override + Color get textFieldActiveLabel => const Color(0xFF86898C); + @override + Color get textFieldErrorLabel => const Color(0xFF8D0006); + @override + Color get textFieldSuccessLabel => const Color(0xFF077C6E); + + @override + Color get textFieldActiveSearchIconRight => const Color(0xFF388192); + @override + Color get textFieldDefaultSearchIconRight => const Color(0xFF388192); + @override + Color get textFieldErrorSearchIconRight => const Color(0xFF8D0006); + @override + Color get textFieldSuccessSearchIconRight => const Color(0xFF077C6E); + + // settings item level2 + @override + Color get settingsItem2ActiveBG => const Color(0xFFFFFFFF); + @override + Color get settingsItem2ActiveText => const Color(0xFF232323); + @override + Color get settingsItem2ActiveSub => const Color(0xFF8C8F90); + + // radio buttons + @override + Color get radioButtonIconBorder => const Color(0xFF056EC6); + @override + Color get radioButtonIconBorderDisabled => const Color(0xFF8C8D97); + @override + Color get radioButtonBorderEnabled => const Color(0xFF056EC6); + @override + Color get radioButtonBorderDisabled => const Color(0xFF8C8D97); + @override + Color get radioButtonIconCircle => const Color(0xFF056EC6); + @override + Color get radioButtonIconEnabled => const Color(0xFF056EC6); + @override + Color get radioButtonTextEnabled => const Color(0xFF42444B); + @override + Color get radioButtonTextDisabled => const Color(0xFF42444B); + @override + Color get radioButtonLabelEnabled => const Color(0xFF8C8F90); + @override + Color get radioButtonLabelDisabled => const Color(0xFF8C8F90); + + // info text + @override + Color get infoItemBG => const Color(0xFFFFFFFF); + @override + Color get infoItemLabel => const Color(0xFF838788); + @override + Color get infoItemText => const Color(0xFF232323); + @override + Color get infoItemIcons => const Color(0xFF056EC6); + + // popup + @override + Color get popupBG => const Color(0xFFFFFFFF); + + // currency list + @override + Color get currencyListItemBG => const Color(0xFFF0F5F7); + + // bottom nav + @override + Color get stackWalletBG => const Color(0xFFFFFFFF); + @override + Color get stackWalletMid => const Color(0xFFFFFFFF); + @override + Color get stackWalletBottom => const Color(0xFF232323); + @override + Color get bottomNavShadow => const Color(0xFF388192); + + @override + Color get favoriteStarActive => const Color(0xFFF4C517); + @override + Color get favoriteStarInactive => const Color(0xFFB0B2B2); + + @override + Color get splash => const Color(0xFF8E9192); + @override + Color get highlight => const Color(0xFFA9ACAC); + @override + Color get warningForeground => const Color(0xFF232323); + @override + Color get warningBackground => const Color(0xFFF6C7C3); + @override + Color get loadingOverlayTextColor => const Color(0xFFF7F7F7); + @override + Color get myStackContactIconBG => const Color(0xFFD8E7EB); + @override + Color get textConfirmTotalAmount => const Color(0xFF232323); + @override + Color get textSelectedWordTableItem => const Color(0xFF232323); +} From 02dadd432ca70d30197ca510474b8ee4bb29ba1b Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Thu, 17 Nov 2022 20:05:29 -0700 Subject: [PATCH 318/426] ocean breeze added --- lib/main.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 42b4ee718..2d1bab160 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -57,6 +57,7 @@ import 'package:stackwallet/utilities/stack_file_system.dart'; import 'package:stackwallet/utilities/theme/color_theme.dart'; import 'package:stackwallet/utilities/theme/dark_colors.dart'; import 'package:stackwallet/utilities/theme/light_colors.dart'; +import 'package:stackwallet/utilities/theme/ocean_breeze_colors.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:window_size/window_size.dart'; @@ -317,8 +318,11 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme> WidgetsBinding.instance.addPostFrameCallback((_) async { ref.read(colorThemeProvider.state).state = - StackColors.fromStackColorTheme( - themeType == ThemeType.dark ? DarkColors() : LightColors()); + StackColors.fromStackColorTheme(themeType == ThemeType.dark + ? DarkColors() + : (themeType == ThemeType.light + ? LightColors() + : OceanBreezeColors())); if (Platform.isAndroid) { // fetch open file if it exists From 17e4976a899b3a0f5ceecd20edf10d1495a0568c Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 18 Nov 2022 09:07:11 -0600 Subject: [PATCH 319/426] include flushbartype in flushbar import --- lib/notifications/show_flush_bar.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/notifications/show_flush_bar.dart b/lib/notifications/show_flush_bar.dart index 5320c8a9d..47cea682a 100644 --- a/lib/notifications/show_flush_bar.dart +++ b/lib/notifications/show_flush_bar.dart @@ -6,6 +6,8 @@ import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +export 'package:stackwallet/utilities/enums/flush_bar_type.dart'; + Future<dynamic> showFloatingFlushBar({ required FlushBarType type, required String message, From aa966a106dd3c603da01bf3d24b3f6358441399c Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 18 Nov 2022 09:07:31 -0600 Subject: [PATCH 320/426] add delete contact functionality for desktop --- .../subwidgets/desktop_contact_details.dart | 382 ++++++++++++------ lib/widgets/address_book_card.dart | 13 +- 2 files changed, 260 insertions(+), 135 deletions(-) diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart index 2cd20a839..96eed4835 100644 --- a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/models/contact.dart'; import 'package:stackwallet/models/paymint/transactions_model.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/address_book_views/subviews/add_new_contact_address_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart'; import 'package:stackwallet/providers/global/address_book_service_provider.dart'; @@ -15,6 +16,8 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.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/loading_indicator.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -74,8 +77,16 @@ class _DesktopContactDetailsState extends ConsumerState<DesktopContactDetails> { @override Widget build(BuildContext context) { - final contact = ref.watch(addressBookServiceProvider - .select((value) => value.getContactById(widget.contactId))); + // provider hack to prevent trying to update widget with deleted contact + Contact? _contact; + try { + _contact = ref.watch(addressBookServiceProvider + .select((value) => value.getContactById(widget.contactId))); + } catch (_) { + return Container(); + } + + final contact = _contact!; return Row( crossAxisAlignment: CrossAxisAlignment.start, @@ -129,22 +140,23 @@ class _DesktopContactDetailsState extends ConsumerState<DesktopContactDetails> { ), ], ), - SecondaryButton( - label: "Options", - width: 96, - buttonHeight: ButtonHeight.xxs, - onPressed: () async { - await showDialog<void>( - context: context, - barrierColor: Colors.transparent, - builder: (context) { - return DesktopContactOptionsMenuPopup( - contactId: contact.id, - ); - }, - ); - }, - ), + if (widget.contactId != "default") + SecondaryButton( + label: "Options", + width: 96, + buttonHeight: ButtonHeight.xxs, + onPressed: () async { + await showDialog<void>( + context: context, + barrierColor: Colors.transparent, + builder: (context) { + return DesktopContactOptionsMenuPopup( + contactId: contact.id, + ); + }, + ); + }, + ), ], ), const SizedBox( @@ -453,132 +465,238 @@ class _DesktopContactOptionsMenuPopupState ), ), ), - const SizedBox( - height: 2, - ), - MouseRegion( - onEnter: (_) { - setState(() { - hoveredOnPencil = true; - }); - }, - onExit: (_) { - setState(() { - hoveredOnPencil = false; - }); - }, - child: RawMaterialButton( - hoverColor: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - 1000, - ), - ), - onPressed: () { - print("should go to edit"); + if (widget.contactId != "default") + const SizedBox( + height: 2, + ), + if (widget.contactId != "default") + MouseRegion( + onEnter: (_) { + setState(() { + hoveredOnPencil = true; + }); }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 25, - vertical: 16, + onExit: (_) { + setState(() { + hoveredOnPencil = false; + }); + }, + child: RawMaterialButton( + hoverColor: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 1000, + ), ), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.pencil, - width: 24, - height: 22, - color: hoveredOnPencil - ? Theme.of(context) - .extension<StackColors>()! - .textDark - : Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultSearchIconLeft, - ), - const SizedBox( - width: 12, - ), - Text( - "Edit contact", - style: STextStyles.desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, + onPressed: () { + print("should go to edit"); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 25, + vertical: 16, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.pencil, + width: 24, + height: 22, + color: hoveredOnPencil + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultSearchIconLeft, ), - ) - ], + const SizedBox( + width: 12, + ), + Text( + "Edit contact", + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ) + ], + ), ), ), ), - ), - const SizedBox( - height: 2, - ), - MouseRegion( - onEnter: (_) { - setState(() { - hoveredOnTrash = true; - }); - }, - onExit: (_) { - setState(() { - hoveredOnTrash = false; - }); - }, - child: RawMaterialButton( - hoverColor: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - 1000, - ), - ), - onPressed: () { - print("should delete contact"); + if (widget.contactId != "default") + const SizedBox( + height: 2, + ), + if (widget.contactId != "default") + MouseRegion( + onEnter: (_) { + setState(() { + hoveredOnTrash = true; + }); }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 25, - vertical: 16, + onExit: (_) { + setState(() { + hoveredOnTrash = false; + }); + }, + child: RawMaterialButton( + hoverColor: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 1000, + ), ), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.trash, - width: 24, - height: 22, - color: hoveredOnTrash - ? Theme.of(context) - .extension<StackColors>()! - .textDark - : Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultSearchIconLeft, - ), - const SizedBox( - width: 12, - ), - Text( - "Delete contact", - style: STextStyles.desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, + onPressed: () { + final contact = ref + .read(addressBookServiceProvider) + .getContactById(widget.contactId); + + // pop context menu + Navigator.of(context).pop(); + + showDialog<dynamic>( + context: context, + useSafeArea: true, + barrierDismissible: true, + builder: (_) => DesktopDialog( + maxWidth: 500, + maxHeight: 300, + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Delete ${contact.name}?", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Spacer( + flex: 1, + ), + Text( + "Contact will be deleted permanently!", + style: STextStyles.desktopTextSmall( + context), + ), + const Spacer( + flex: 2, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + onPressed: + Navigator.of(context).pop, + buttonHeight: ButtonHeight.l, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: Consumer( + builder: (context, ref, __) => + PrimaryButton( + label: "Delete", + buttonHeight: + ButtonHeight.l, + onPressed: () { + ref + .read( + addressBookServiceProvider) + .removeContact( + contact.id); + Navigator.of(context) + .pop(); + showFloatingFlushBar( + type: FlushBarType + .success, + message: + "${contact.name} deleted", + context: context, + ); + }, + ), + ), + ), + ], + ) + ], + ), + ), + ), + ], ), - ) - ], + ), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 25, + vertical: 16, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.trash, + width: 24, + height: 22, + color: hoveredOnTrash + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultSearchIconLeft, + ), + const SizedBox( + width: 12, + ), + Text( + "Delete contact", + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ) + ], + ), ), ), ), - ), ], ), ), diff --git a/lib/widgets/address_book_card.dart b/lib/widgets/address_book_card.dart index b79f89662..dfa655f86 100644 --- a/lib/widgets/address_book_card.dart +++ b/lib/widgets/address_book_card.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/models/contact.dart'; import 'package:stackwallet/pages/address_book_views/subviews/contact_popup.dart'; import 'package:stackwallet/providers/global/address_book_service_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -44,10 +45,16 @@ class _AddressBookCardState extends ConsumerState<AddressBookCard> { @override Widget build(BuildContext context) { - // final isTiny = SizingUtilities.isTinyWidth(context); + // provider hack to prevent trying to update widget with deleted contact + Contact? _contact; + try { + _contact = ref.watch(addressBookServiceProvider + .select((value) => value.getContactById(contactId))); + } catch (_) { + return Container(); + } - final contact = ref.watch(addressBookServiceProvider - .select((value) => value.getContactById(contactId))); + final contact = _contact!; final List<Coin> coins = []; for (var element in contact.addresses) { From 07f229f2a0089a8a566ea846fa1e1d00b0fcc181 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 18 Nov 2022 09:19:48 -0600 Subject: [PATCH 321/426] refactor popup --- .../subwidgets/desktop_contact_details.dart | 358 +---------------- .../desktop_contact_options_menu_popup.dart | 366 ++++++++++++++++++ 2 files changed, 367 insertions(+), 357 deletions(-) create mode 100644 lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_options_menu_popup.dart diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart index 96eed4835..62cd993af 100644 --- a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_details.dart @@ -3,9 +3,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/models/contact.dart'; import 'package:stackwallet/models/paymint/transactions_model.dart'; -import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/address_book_views/subviews/add_new_contact_address_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/address_book_view/subwidgets/desktop_address_card.dart'; +import 'package:stackwallet/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_options_menu_popup.dart'; import 'package:stackwallet/providers/global/address_book_service_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/providers/ui/address_book_providers/address_entry_data_provider.dart'; @@ -16,8 +16,6 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.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/loading_indicator.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -352,357 +350,3 @@ class _DesktopContactDetailsState extends ConsumerState<DesktopContactDetails> { ); } } - -class DesktopContactOptionsMenuPopup extends ConsumerStatefulWidget { - const DesktopContactOptionsMenuPopup({Key? key, required this.contactId}) - : super(key: key); - - final String contactId; - - @override - ConsumerState<DesktopContactOptionsMenuPopup> createState() => - _DesktopContactOptionsMenuPopupState(); -} - -class _DesktopContactOptionsMenuPopupState - extends ConsumerState<DesktopContactOptionsMenuPopup> { - bool hoveredOnStar = false; - bool hoveredOnPencil = false; - bool hoveredOnTrash = false; - - @override - Widget build(BuildContext context) { - return Stack( - children: [ - Positioned( - top: 210, - left: MediaQuery.of(context).size.width - 280, - child: Container( - width: 270, - decoration: BoxDecoration( - color: Theme.of(context).extension<StackColors>()!.popupBG, - borderRadius: BorderRadius.circular( - 20, - ), - boxShadow: [ - Theme.of(context).extension<StackColors>()!.standardBoxShadow, - ], - ), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - MouseRegion( - onEnter: (_) { - setState(() { - hoveredOnStar = true; - }); - }, - onExit: (_) { - setState(() { - hoveredOnStar = false; - }); - }, - child: RawMaterialButton( - hoverColor: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - 1000, - ), - ), - onPressed: () { - final contact = - ref.read(addressBookServiceProvider).getContactById( - widget.contactId, - ); - ref.read(addressBookServiceProvider).editContact( - contact.copyWith( - isFavorite: !contact.isFavorite, - ), - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 25, - vertical: 16, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.star, - width: 24, - height: 22, - color: hoveredOnStar - ? Theme.of(context) - .extension<StackColors>()! - .textDark - : Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultSearchIconLeft, - ), - const SizedBox( - width: 12, - ), - Text( - ref.watch(addressBookServiceProvider.select( - (value) => value - .getContactById(widget.contactId) - .isFavorite)) - ? "Remove from favorites" - : "Add to favorites", - style: STextStyles.desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ), - ) - ], - ), - ), - ), - ), - if (widget.contactId != "default") - const SizedBox( - height: 2, - ), - if (widget.contactId != "default") - MouseRegion( - onEnter: (_) { - setState(() { - hoveredOnPencil = true; - }); - }, - onExit: (_) { - setState(() { - hoveredOnPencil = false; - }); - }, - child: RawMaterialButton( - hoverColor: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - 1000, - ), - ), - onPressed: () { - print("should go to edit"); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 25, - vertical: 16, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.pencil, - width: 24, - height: 22, - color: hoveredOnPencil - ? Theme.of(context) - .extension<StackColors>()! - .textDark - : Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultSearchIconLeft, - ), - const SizedBox( - width: 12, - ), - Text( - "Edit contact", - style: STextStyles.desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ), - ) - ], - ), - ), - ), - ), - if (widget.contactId != "default") - const SizedBox( - height: 2, - ), - if (widget.contactId != "default") - MouseRegion( - onEnter: (_) { - setState(() { - hoveredOnTrash = true; - }); - }, - onExit: (_) { - setState(() { - hoveredOnTrash = false; - }); - }, - child: RawMaterialButton( - hoverColor: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - 1000, - ), - ), - onPressed: () { - final contact = ref - .read(addressBookServiceProvider) - .getContactById(widget.contactId); - - // pop context menu - Navigator.of(context).pop(); - - showDialog<dynamic>( - context: context, - useSafeArea: true, - barrierDismissible: true, - builder: (_) => DesktopDialog( - maxWidth: 500, - maxHeight: 300, - child: Column( - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, - ), - child: Text( - "Delete ${contact.name}?", - style: STextStyles.desktopH3(context), - ), - ), - const DesktopDialogCloseButton(), - ], - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - const Spacer( - flex: 1, - ), - Text( - "Contact will be deleted permanently!", - style: STextStyles.desktopTextSmall( - context), - ), - const Spacer( - flex: 2, - ), - Row( - children: [ - Expanded( - child: SecondaryButton( - label: "Cancel", - onPressed: - Navigator.of(context).pop, - buttonHeight: ButtonHeight.l, - ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: Consumer( - builder: (context, ref, __) => - PrimaryButton( - label: "Delete", - buttonHeight: - ButtonHeight.l, - onPressed: () { - ref - .read( - addressBookServiceProvider) - .removeContact( - contact.id); - Navigator.of(context) - .pop(); - showFloatingFlushBar( - type: FlushBarType - .success, - message: - "${contact.name} deleted", - context: context, - ); - }, - ), - ), - ), - ], - ) - ], - ), - ), - ), - ], - ), - ), - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 25, - vertical: 16, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.trash, - width: 24, - height: 22, - color: hoveredOnTrash - ? Theme.of(context) - .extension<StackColors>()! - .textDark - : Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultSearchIconLeft, - ), - const SizedBox( - width: 12, - ), - Text( - "Delete contact", - style: STextStyles.desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ), - ) - ], - ), - ), - ), - ), - ], - ), - ), - ), - ), - ], - ); - } -} diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_options_menu_popup.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_options_menu_popup.dart new file mode 100644 index 000000000..88bd6abf0 --- /dev/null +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_options_menu_popup.dart @@ -0,0 +1,366 @@ +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/providers/global/address_book_service_provider.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/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'; + +class DesktopContactOptionsMenuPopup extends ConsumerStatefulWidget { + const DesktopContactOptionsMenuPopup({Key? key, required this.contactId}) + : super(key: key); + + final String contactId; + + @override + ConsumerState<DesktopContactOptionsMenuPopup> createState() => + _DesktopContactOptionsMenuPopupState(); +} + +class _DesktopContactOptionsMenuPopupState + extends ConsumerState<DesktopContactOptionsMenuPopup> { + bool hoveredOnStar = false; + bool hoveredOnPencil = false; + bool hoveredOnTrash = false; + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Positioned( + top: 210, + left: MediaQuery.of(context).size.width - 280, + child: Container( + width: 270, + decoration: BoxDecoration( + color: Theme.of(context).extension<StackColors>()!.popupBG, + borderRadius: BorderRadius.circular( + 20, + ), + boxShadow: [ + Theme.of(context).extension<StackColors>()!.standardBoxShadow, + ], + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + MouseRegion( + onEnter: (_) { + setState(() { + hoveredOnStar = true; + }); + }, + onExit: (_) { + setState(() { + hoveredOnStar = false; + }); + }, + child: RawMaterialButton( + hoverColor: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 1000, + ), + ), + onPressed: () { + final contact = + ref.read(addressBookServiceProvider).getContactById( + widget.contactId, + ); + ref.read(addressBookServiceProvider).editContact( + contact.copyWith( + isFavorite: !contact.isFavorite, + ), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 25, + vertical: 16, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.star, + width: 24, + height: 22, + color: hoveredOnStar + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultSearchIconLeft, + ), + const SizedBox( + width: 12, + ), + Text( + ref.watch(addressBookServiceProvider.select( + (value) => value + .getContactById(widget.contactId) + .isFavorite)) + ? "Remove from favorites" + : "Add to favorites", + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ) + ], + ), + ), + ), + ), + if (widget.contactId != "default") + const SizedBox( + height: 2, + ), + if (widget.contactId != "default") + MouseRegion( + onEnter: (_) { + setState(() { + hoveredOnPencil = true; + }); + }, + onExit: (_) { + setState(() { + hoveredOnPencil = false; + }); + }, + child: RawMaterialButton( + hoverColor: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 1000, + ), + ), + onPressed: () { + print("should go to edit"); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 25, + vertical: 16, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.pencil, + width: 24, + height: 22, + color: hoveredOnPencil + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultSearchIconLeft, + ), + const SizedBox( + width: 12, + ), + Text( + "Edit contact", + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ) + ], + ), + ), + ), + ), + if (widget.contactId != "default") + const SizedBox( + height: 2, + ), + if (widget.contactId != "default") + MouseRegion( + onEnter: (_) { + setState(() { + hoveredOnTrash = true; + }); + }, + onExit: (_) { + setState(() { + hoveredOnTrash = false; + }); + }, + child: RawMaterialButton( + hoverColor: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 1000, + ), + ), + onPressed: () { + final contact = ref + .read(addressBookServiceProvider) + .getContactById(widget.contactId); + + // pop context menu + Navigator.of(context).pop(); + + showDialog<dynamic>( + context: context, + useSafeArea: true, + barrierDismissible: true, + builder: (_) => DesktopDialog( + maxWidth: 500, + maxHeight: 300, + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Delete ${contact.name}?", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Spacer( + flex: 1, + ), + Text( + "Contact will be deleted permanently!", + style: STextStyles.desktopTextSmall( + context), + ), + const Spacer( + flex: 2, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + onPressed: + Navigator.of(context).pop, + buttonHeight: ButtonHeight.l, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: Consumer( + builder: (context, ref, __) => + PrimaryButton( + label: "Delete", + buttonHeight: + ButtonHeight.l, + onPressed: () { + ref + .read( + addressBookServiceProvider) + .removeContact( + contact.id); + Navigator.of(context) + .pop(); + showFloatingFlushBar( + type: FlushBarType + .success, + message: + "${contact.name} deleted", + context: context, + ); + }, + ), + ), + ), + ], + ) + ], + ), + ), + ), + ], + ), + ), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 25, + vertical: 16, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.trash, + width: 24, + height: 22, + color: hoveredOnTrash + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultSearchIconLeft, + ), + const SizedBox( + width: 12, + ), + Text( + "Delete contact", + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ) + ], + ), + ), + ), + ), + ], + ), + ), + ), + ), + ], + ); + } +} From d4b7ec0f174101e406796f4a2bc6162a0c9f0f58 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 18 Nov 2022 09:54:26 -0600 Subject: [PATCH 322/426] desktop edit contact --- .../edit_contact_name_emoji_view.dart | 576 ++++++++++-------- .../desktop_contact_options_menu_popup.dart | 252 ++++---- 2 files changed, 462 insertions(+), 366 deletions(-) diff --git a/lib/pages/address_book_views/subviews/edit_contact_name_emoji_view.dart b/lib/pages/address_book_views/subviews/edit_contact_name_emoji_view.dart index fff01eee3..a9b264b3c 100644 --- a/lib/pages/address_book_views/subviews/edit_contact_name_emoji_view.dart +++ b/lib/pages/address_book_views/subviews/edit_contact_name_emoji_view.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:emojis/emoji.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -7,14 +9,17 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.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/emoji_select_sheet.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; -import 'package:stackwallet/utilities/util.dart'; - class EditContactNameEmojiView extends ConsumerStatefulWidget { const EditContactNameEmojiView({ Key? key, @@ -69,268 +74,323 @@ class _EditContactNameEmojiViewState final contact = ref.watch(addressBookServiceProvider .select((value) => value.getContactById(contactId))); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "Edit contact", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (context, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - children: [ - GestureDetector( - onTap: () { - if (_selectedEmoji != null) { - setState(() { - _selectedEmoji = null; - }); - return; - } - showModalBottomSheet<dynamic>( - backgroundColor: Colors.transparent, - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - builder: (_) => const EmojiSelectSheet(), - ).then((value) { - if (value is Emoji) { - setState(() { - _selectedEmoji = value; - }); - } - }); - }, - child: SizedBox( - height: 48, - width: 48, - child: Stack( - children: [ - Container( - height: 48, - width: 48, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - color: Theme.of(context) - .extension<StackColors>()! - .textFieldActiveBG, - ), - child: Center( - child: _selectedEmoji == null - ? SvgPicture.asset( - Assets.svg.user, - height: 24, - width: 24, - ) - : Text( - _selectedEmoji!.char, - style: STextStyles.pageTitleH1( - context), - ), - ), - ), - Align( - alignment: Alignment.bottomRight, - child: Container( - height: 14, - width: 14, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(14), - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - child: Center( - child: _selectedEmoji == null - ? SvgPicture.asset( - Assets.svg.plus, - color: Theme.of(context) - .extension<StackColors>()! - .textWhite, - width: 12, - height: 12, - ) - : SvgPicture.asset( - Assets.svg.thickX, - color: Theme.of(context) - .extension<StackColors>()! - .textWhite, - width: 8, - height: 8, - ), - ), - ), - ) - ], - ), - ), - ), - const SizedBox( - height: 8, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: nameController, - focusNode: nameFocusNode, - style: STextStyles.field(context), - onChanged: (_) => setState(() {}), - decoration: standardInputDecoration( - "Enter contact name", - nameFocusNode, - context, - ).copyWith( - suffixIcon: nameController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - nameController.text = ""; - }); - }, - ), - ], - ), - ), - ) - : null, - ), - ), - ), - const Spacer(), - const SizedBox( - height: 16, - ), - Row( - children: [ - Expanded( - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - ), - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: Builder( - builder: (context) { - bool shouldEnableSave = - nameController.text.isNotEmpty; + final isDesktop = Util.isDesktop; + final double emojiSize = isDesktop ? 56 : 48; - return TextButton( - style: shouldEnableSave - ? Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor( - context) - : Theme.of(context) - .extension<StackColors>()! - .getPrimaryDisabledButtonColor( - context), - onPressed: shouldEnableSave - ? () async { - if (FocusScope.of(context) - .hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration( - milliseconds: 75), - ); - } - final editedContact = - contact.copyWith( - shouldCopyEmojiWithNull: true, - name: nameController.text, - emojiChar: _selectedEmoji == null - ? null - : _selectedEmoji!.char, - ); - ref - .read( - addressBookServiceProvider) - .editContact( - editedContact, - ); - if (mounted) { - Navigator.of(context).pop(); - } - } - : null, - child: Text( - "Save", - style: STextStyles.button(context), - ), - ); - }, - ), - ), - ], - ) - ], + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Edit contact", + style: STextStyles.navBarTitle(context), + ), + ), + body: LayoutBuilder( + builder: (context, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, ), ), ), ), + ); + }, + ), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + onTap: () { + if (_selectedEmoji != null) { + setState(() { + _selectedEmoji = null; + }); + return; + } + if (isDesktop) { + showDialog<dynamic>( + barrierColor: Colors.transparent, + context: context, + builder: (context) { + return const DesktopDialog( + maxHeight: 700, + maxWidth: 600, + child: Padding( + padding: EdgeInsets.only( + left: 32, + right: 20, + top: 32, + bottom: 32, + ), + child: EmojiSelectSheet(), + ), + ); + }).then((value) { + if (value is Emoji) { + setState(() { + _selectedEmoji = value; + }); + } + }); + } else { + showModalBottomSheet<dynamic>( + backgroundColor: Colors.transparent, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + builder: (_) => const EmojiSelectSheet(), + ).then((value) { + if (value is Emoji) { + setState(() { + _selectedEmoji = value; + }); + } + }); + } + }, + child: SizedBox( + height: emojiSize, + width: emojiSize, + child: Stack( + children: [ + Container( + height: emojiSize, + width: emojiSize, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(emojiSize / 2), + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveBG, + ), + child: Center( + child: _selectedEmoji == null + ? SvgPicture.asset( + Assets.svg.user, + height: emojiSize / 2, + width: emojiSize / 2, + ) + : Text( + _selectedEmoji!.char, + style: isDesktop + ? STextStyles.desktopH3(context) + : STextStyles.pageTitleH1(context), + ), + ), + ), + Align( + alignment: Alignment.bottomRight, + child: Container( + height: 14, + width: 14, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + child: Center( + child: _selectedEmoji == null + ? SvgPicture.asset( + Assets.svg.plus, + color: Theme.of(context) + .extension<StackColors>()! + .textWhite, + width: 12, + height: 12, + ) + : SvgPicture.asset( + Assets.svg.thickX, + color: Theme.of(context) + .extension<StackColors>()! + .textWhite, + width: 8, + height: 8, + ), + ), + ), + ) + ], + ), + ), + ), + if (isDesktop) + const SizedBox( + width: 8, + ), + if (isDesktop) + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: nameController, + focusNode: nameFocusNode, + style: STextStyles.field(context), + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Enter contact name", + nameFocusNode, + context, + ).copyWith( + suffixIcon: nameController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + nameController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + ), + ], + ), + if (!isDesktop) + const SizedBox( + height: 8, ), - ); - }, + if (!isDesktop) + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: nameController, + focusNode: nameFocusNode, + style: STextStyles.field(context), + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Enter contact name", + nameFocusNode, + context, + ).copyWith( + suffixIcon: nameController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + nameController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + const Spacer(), + const SizedBox( + height: 16, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: () async { + if (!isDesktop && FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Save", + enabled: nameController.text.isNotEmpty, + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: () async { + if (!isDesktop && FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75), + ); + } + final editedContact = contact.copyWith( + shouldCopyEmojiWithNull: true, + name: nameController.text, + emojiChar: + _selectedEmoji == null ? null : _selectedEmoji!.char, + ); + unawaited( + ref.read(addressBookServiceProvider).editContact( + editedContact, + ), + ); + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + ), + ], + ) + ], ), ); } diff --git a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_options_menu_popup.dart b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_options_menu_popup.dart index 88bd6abf0..690d1be98 100644 --- a/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_options_menu_popup.dart +++ b/lib/pages_desktop_specific/home/address_book_view/subwidgets/desktop_contact_options_menu_popup.dart @@ -2,6 +2,7 @@ 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/address_book_views/subviews/edit_contact_name_emoji_view.dart'; import 'package:stackwallet/providers/global/address_book_service_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -28,6 +29,147 @@ class _DesktopContactOptionsMenuPopupState bool hoveredOnPencil = false; bool hoveredOnTrash = false; + void editContact() { + // pop context menu + Navigator.of(context).pop(); + + showDialog<dynamic>( + context: context, + useSafeArea: true, + barrierDismissible: true, + builder: (_) => DesktopDialog( + maxWidth: 580, + maxHeight: 400, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Edit contact", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + top: 16, + left: 32, + right: 32, + bottom: 32, + ), + child: EditContactNameEmojiView( + contactId: widget.contactId, + ), + ), + ), + ], + ), + ), + ); + } + + void attemptDeleteContact() { + final contact = + ref.read(addressBookServiceProvider).getContactById(widget.contactId); + + // pop context menu + Navigator.of(context).pop(); + + showDialog<dynamic>( + context: context, + useSafeArea: true, + barrierDismissible: true, + builder: (_) => DesktopDialog( + maxWidth: 500, + maxHeight: 400, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Delete ${contact.name}?", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Spacer( + flex: 1, + ), + Text( + "Contact will be deleted permanently!", + style: STextStyles.desktopTextSmall(context), + ), + const Spacer( + flex: 2, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + onPressed: Navigator.of(context).pop, + buttonHeight: ButtonHeight.l, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: Consumer( + builder: (context, ref, __) => PrimaryButton( + label: "Delete", + buttonHeight: ButtonHeight.l, + onPressed: () { + ref + .read(addressBookServiceProvider) + .removeContact(contact.id); + Navigator.of(context).pop(); + showFloatingFlushBar( + type: FlushBarType.success, + message: "${contact.name} deleted", + context: context, + ); + }, + ), + ), + ), + ], + ) + ], + ), + ), + ), + ], + ), + ), + ); + } + @override Widget build(BuildContext context) { return Stack( @@ -148,9 +290,7 @@ class _DesktopContactOptionsMenuPopupState 1000, ), ), - onPressed: () { - print("should go to edit"); - }, + onPressed: editContact, child: Padding( padding: const EdgeInsets.symmetric( horizontal: 25, @@ -213,111 +353,7 @@ class _DesktopContactOptionsMenuPopupState 1000, ), ), - onPressed: () { - final contact = ref - .read(addressBookServiceProvider) - .getContactById(widget.contactId); - - // pop context menu - Navigator.of(context).pop(); - - showDialog<dynamic>( - context: context, - useSafeArea: true, - barrierDismissible: true, - builder: (_) => DesktopDialog( - maxWidth: 500, - maxHeight: 300, - child: Column( - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, - ), - child: Text( - "Delete ${contact.name}?", - style: STextStyles.desktopH3(context), - ), - ), - const DesktopDialogCloseButton(), - ], - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - const Spacer( - flex: 1, - ), - Text( - "Contact will be deleted permanently!", - style: STextStyles.desktopTextSmall( - context), - ), - const Spacer( - flex: 2, - ), - Row( - children: [ - Expanded( - child: SecondaryButton( - label: "Cancel", - onPressed: - Navigator.of(context).pop, - buttonHeight: ButtonHeight.l, - ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: Consumer( - builder: (context, ref, __) => - PrimaryButton( - label: "Delete", - buttonHeight: - ButtonHeight.l, - onPressed: () { - ref - .read( - addressBookServiceProvider) - .removeContact( - contact.id); - Navigator.of(context) - .pop(); - showFloatingFlushBar( - type: FlushBarType - .success, - message: - "${contact.name} deleted", - context: context, - ); - }, - ), - ), - ), - ], - ) - ], - ), - ), - ), - ], - ), - ), - ); - }, + onPressed: attemptDeleteContact, child: Padding( padding: const EdgeInsets.symmetric( horizontal: 25, From 4d17c1db5f732acb00b1726a530125318889f706 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 18 Nov 2022 10:22:34 -0600 Subject: [PATCH 323/426] addressbook filter coins list fix --- lib/pages/address_book_views/address_book_view.dart | 8 ++++---- .../subviews/address_book_filter_view.dart | 2 +- .../home/address_book_view/desktop_address_book.dart | 7 ++++--- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/pages/address_book_views/address_book_view.dart b/lib/pages/address_book_views/address_book_view.dart index cdc9fb5b7..c87906870 100644 --- a/lib/pages/address_book_views/address_book_view.dart +++ b/lib/pages/address_book_views/address_book_view.dart @@ -51,8 +51,7 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> { ref.refresh(addressBookFilterProvider); if (widget.coin == null) { - List<Coin> coins = - Coin.values.where((e) => !(e == Coin.epicCash)).toList(); + List<Coin> coins = Coin.values.toList(); coins.remove(Coin.firoTestNet); bool showTestNet = ref.read(prefsChangeNotifierProvider).showTestNetCoins; @@ -60,8 +59,9 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> { if (showTestNet) { ref.read(addressBookFilterProvider).addAll(coins, false); } else { - ref.read(addressBookFilterProvider).addAll( - coins.getRange(0, coins.length - kTestNetCoinCount + 1), false); + ref + .read(addressBookFilterProvider) + .addAll(coins.getRange(0, coins.length - kTestNetCoinCount), false); } } else { ref.read(addressBookFilterProvider).add(widget.coin!, false); diff --git a/lib/pages/address_book_views/subviews/address_book_filter_view.dart b/lib/pages/address_book_views/subviews/address_book_filter_view.dart index c129251d5..55c3d47ac 100644 --- a/lib/pages/address_book_views/subviews/address_book_filter_view.dart +++ b/lib/pages/address_book_views/subviews/address_book_filter_view.dart @@ -38,7 +38,7 @@ class _AddressBookFilterViewState extends ConsumerState<AddressBookFilterView> { } else { _coins = coins .toList(growable: false) - .getRange(0, coins.length - kTestNetCoinCount + 1) + .getRange(0, coins.length - kTestNetCoinCount) .toList(growable: false); } super.initState(); diff --git a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart index f028a3424..e5432081c 100644 --- a/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart +++ b/lib/pages_desktop_specific/home/address_book_view/desktop_address_book.dart @@ -84,7 +84,7 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { ref.refresh(addressBookFilterProvider); // if (widget.coin == null) { - List<Coin> coins = Coin.values.where((e) => !(e == Coin.epicCash)).toList(); + List<Coin> coins = Coin.values.toList(); coins.remove(Coin.firoTestNet); bool showTestNet = ref.read(prefsChangeNotifierProvider).showTestNetCoins; @@ -92,8 +92,9 @@ class _DesktopAddressBook extends ConsumerState<DesktopAddressBook> { if (showTestNet) { ref.read(addressBookFilterProvider).addAll(coins, false); } else { - ref.read(addressBookFilterProvider).addAll( - coins.getRange(0, coins.length - kTestNetCoinCount + 1), false); + ref + .read(addressBookFilterProvider) + .addAll(coins.getRange(0, coins.length - kTestNetCoinCount), false); } // } else { // ref.read(addressBookFilterProvider).add(widget.coin!, false); From 8207474d0973387b9fa7d32d4eb510132b34cc72 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Fri, 18 Nov 2022 09:30:21 -0700 Subject: [PATCH 324/426] ocean breeze selector + functionality added --- assets/svg/ocean-breeze-theme.svg | 28 +++++ lib/main.dart | 2 +- .../settings_menu/appearance_settings.dart | 105 +++++++++++++++++- lib/utilities/assets.dart | 1 + pubspec.yaml | 1 + 5 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 assets/svg/ocean-breeze-theme.svg diff --git a/assets/svg/ocean-breeze-theme.svg b/assets/svg/ocean-breeze-theme.svg new file mode 100644 index 000000000..0deb96ec8 --- /dev/null +++ b/assets/svg/ocean-breeze-theme.svg @@ -0,0 +1,28 @@ +<svg width="200" height="162" viewBox="0 0 200 162" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0_518_22068)"> +<rect width="200" height="162" rx="8" fill="url(#paint0_linear_518_22068)"/> +<rect x="10" y="10" width="180" height="20" rx="2" fill="#C2DAE2"/> +<rect x="16" y="16" width="106" height="8" rx="1" fill="#227386"/> +<rect x="10" y="40" width="180" height="20" rx="2" fill="#FEFEFE"/> +<rect x="16" y="46" width="106" height="8" rx="1" fill="#BDD5DB"/> +<rect x="10" y="62" width="180" height="20" rx="2" fill="#FEFEFE"/> +<rect x="16" y="68" width="106" height="8" rx="1" fill="#BDD5DB"/> +<rect x="10" y="84" width="180" height="20" rx="2" fill="#FEFEFE"/> +<rect x="16" y="90" width="106" height="8" rx="1" fill="#BDD5DB"/> +<rect x="10" y="106" width="180" height="20" rx="2" fill="#FEFEFE"/> +<rect x="16" y="112" width="106" height="8" rx="1" fill="#BDD5DB"/> +<rect x="10" y="128" width="180" height="20" rx="2" fill="#FEFEFE"/> +<rect x="16" y="134" width="106" height="8" rx="1" fill="#BDD5DB"/> +<rect x="10" y="150" width="180" height="20" rx="2" fill="#FEFEFE"/> +<rect x="16" y="156" width="106" height="8" rx="1" fill="#BDD5DB"/> +</g> +<defs> +<linearGradient id="paint0_linear_518_22068" x1="100" y1="0" x2="100" y2="162" gradientUnits="userSpaceOnUse"> +<stop stop-color="#F3F7FA"/> +<stop offset="1" stop-color="#E8F2F9"/> +</linearGradient> +<clipPath id="clip0_518_22068"> +<rect width="200" height="162" rx="8" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/lib/main.dart b/lib/main.dart index 2d1bab160..66b3bb974 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -77,7 +77,7 @@ void main() async { if (Util.isDesktop) { setWindowTitle('Stack Wallet'); - setWindowMinSize(const Size(1200, 1100)); + setWindowMinSize(const Size(1220, 1100)); setWindowMaxSize(Size.infinite); } diff --git a/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart b/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart index 7a9ed557f..bfd5f3b61 100644 --- a/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/appearance_settings.dart @@ -10,6 +10,7 @@ import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/color_theme.dart'; import 'package:stackwallet/utilities/theme/dark_colors.dart'; import 'package:stackwallet/utilities/theme/light_colors.dart'; +import 'package:stackwallet/utilities/theme/ocean_breeze_colors.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -291,7 +292,109 @@ class _ThemeToggle extends ConsumerState<ThemeToggle> { ), ), const SizedBox( - width: 20, + width: 10, + ), + MaterialButton( + splashColor: Colors.transparent, + hoverColor: Colors.transparent, + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.oceanBreeze.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + OceanBreezeColors(), + ); + + setState(() { + _selectedTheme = "oceanBreeze"; + }); + }, + child: SizedBox( + width: 200, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + border: Border.all( + width: 2.5, + color: _selectedTheme == "oceanBreeze" + ? Theme.of(context) + .extension<StackColors>()! + .infoItemIcons + : Theme.of(context).extension<StackColors>()!.popupBG, + ), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: SvgPicture.asset( + Assets.svg.themeOcean, + ), + ), + const SizedBox( + height: 12, + ), + Row( + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: "oceanBreeze", + groupValue: _selectedTheme, + onChanged: (newValue) { + if (newValue is String && newValue == "oceanBreeze") { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.oceanBreeze.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + OceanBreezeColors(), + ); + + setState(() { + _selectedTheme = "oceanBreeze"; + }); + } + }, + ), + ), + const SizedBox( + width: 14, + ), + Text( + "Ocean Breeze", + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + ], + ), + ], + ), + ), + ), + const SizedBox( + width: 10, ), MaterialButton( splashColor: Colors.transparent, diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 38d720969..d7d2ebc6f 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -59,6 +59,7 @@ class _SVG { String txExchangeFailed(BuildContext context) => "assets/svg/${Theme.of(context).extension<StackColors>()!.themeType.name}/tx-exchange-icon-failed.svg"; + String get themeOcean => "assets/svg/ocean-breeze-theme.svg"; String get circleSliders => "assets/svg/configuration.svg"; String get circlePlus => "assets/svg/plus-circle.svg"; String get framedGear => "assets/svg/framed-gear.svg"; diff --git a/pubspec.yaml b/pubspec.yaml index bba5f6ed2..94670af1a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -316,6 +316,7 @@ flutter: - assets/svg/arrow-down.svg - assets/svg/plus-circle.svg - assets/svg/configuration.svg + - assets/svg/ocean-breeze-theme.svg # coin icons - assets/svg/coin_icons/Bitcoin.svg - assets/svg/coin_icons/Litecoin.svg From 9508afbd5b84823a264e3c091cab9768292e9e01 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 18 Nov 2022 12:26:27 -0600 Subject: [PATCH 325/426] add ocean breeze specific assets --- assets/svg/{dark => }/dark-theme.svg | 0 assets/svg/{light => }/light-mode.svg | 0 assets/svg/oceanBreeze/bell-new.svg | 5 ++ assets/svg/oceanBreeze/buy-coins-icon.svg | 18 +++++ assets/svg/oceanBreeze/exchange-2.svg | 4 + assets/svg/oceanBreeze/stack-icon1.svg | 5 ++ .../oceanBreeze/tx-exchange-icon-failed.svg | 7 ++ .../oceanBreeze/tx-exchange-icon-pending.svg | 6 ++ assets/svg/oceanBreeze/tx-exchange-icon.svg | 4 + .../oceanBreeze/tx-icon-receive-failed.svg | 7 ++ .../oceanBreeze/tx-icon-receive-pending.svg | 5 ++ assets/svg/oceanBreeze/tx-icon-receive.svg | 4 + .../svg/oceanBreeze/tx-icon-send-failed.svg | 7 ++ .../svg/oceanBreeze/tx-icon-send-pending.svg | 6 ++ assets/svg/oceanBreeze/tx-icon-send.svg | 4 + lib/utilities/assets.dart | 5 +- pubspec.yaml | 77 ++++++++++++------- 17 files changed, 135 insertions(+), 29 deletions(-) rename assets/svg/{dark => }/dark-theme.svg (100%) rename assets/svg/{light => }/light-mode.svg (100%) create mode 100644 assets/svg/oceanBreeze/bell-new.svg create mode 100644 assets/svg/oceanBreeze/buy-coins-icon.svg create mode 100644 assets/svg/oceanBreeze/exchange-2.svg create mode 100644 assets/svg/oceanBreeze/stack-icon1.svg create mode 100644 assets/svg/oceanBreeze/tx-exchange-icon-failed.svg create mode 100644 assets/svg/oceanBreeze/tx-exchange-icon-pending.svg create mode 100644 assets/svg/oceanBreeze/tx-exchange-icon.svg create mode 100644 assets/svg/oceanBreeze/tx-icon-receive-failed.svg create mode 100644 assets/svg/oceanBreeze/tx-icon-receive-pending.svg create mode 100644 assets/svg/oceanBreeze/tx-icon-receive.svg create mode 100644 assets/svg/oceanBreeze/tx-icon-send-failed.svg create mode 100644 assets/svg/oceanBreeze/tx-icon-send-pending.svg create mode 100644 assets/svg/oceanBreeze/tx-icon-send.svg diff --git a/assets/svg/dark/dark-theme.svg b/assets/svg/dark-theme.svg similarity index 100% rename from assets/svg/dark/dark-theme.svg rename to assets/svg/dark-theme.svg diff --git a/assets/svg/light/light-mode.svg b/assets/svg/light-mode.svg similarity index 100% rename from assets/svg/light/light-mode.svg rename to assets/svg/light-mode.svg diff --git a/assets/svg/oceanBreeze/bell-new.svg b/assets/svg/oceanBreeze/bell-new.svg new file mode 100644 index 000000000..8cef32715 --- /dev/null +++ b/assets/svg/oceanBreeze/bell-new.svg @@ -0,0 +1,5 @@ +<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M12.5 17.5C12.5 17.9193 12.2383 18.3672 11.7695 18.6797C11.3008 18.9922 10.6289 19.1667 10 19.1667C9.33594 19.1667 8.69922 18.9922 8.23047 18.6797C7.76172 18.3672 7.5 17.9193 7.5 17.5H12.5Z" fill="#227386"/> +<path d="M11.1903 1.98716V2.67947C13.9059 3.2142 15.9519 5.54245 15.9519 8.33331V9.0112C15.9519 10.7095 16.5955 12.3429 17.7561 13.6122L18.0314 13.9114C18.3439 14.254 18.422 14.7372 18.2286 15.1518C18.0351 15.5665 17.611 15.8333 17.1423 15.8333H2.85739C2.38867 15.8333 1.96351 15.5665 1.77148 15.1518C1.57945 14.7372 1.65626 14.254 1.96771 13.9114L2.24359 13.6122C3.40573 12.3429 4.0478 10.7095 4.0478 9.0112V8.33331C4.0478 5.54245 6.06034 3.2142 8.80945 2.67947V1.98716C8.80945 1.35002 9.34141 0.833313 9.99986 0.833313C10.6583 0.833313 11.1903 1.35002 11.1903 1.98716Z" fill="#227386"/> +<ellipse cx="17.0833" cy="2.91665" rx="2.08333" ry="2.08333" fill="#D34E50"/> +</svg> diff --git a/assets/svg/oceanBreeze/buy-coins-icon.svg b/assets/svg/oceanBreeze/buy-coins-icon.svg new file mode 100644 index 000000000..d9613bccb --- /dev/null +++ b/assets/svg/oceanBreeze/buy-coins-icon.svg @@ -0,0 +1,18 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0_519_18707)"> +<g opacity="0.4"> +<path d="M22.2 6C23.3297 5.37187 24 4.59422 24 3.75C24 1.67906 19.9688 0 15 0C9.98906 0 6 1.67906 6 3.75C6 4.59422 6.67031 5.37187 7.8 6C7.80937 6.00469 7.81758 6.00937 7.82578 6.01406C7.83398 6.01875 7.84219 6.02344 7.85156 6.02813C8.23125 6.00938 8.61094 6 9 6C11.6344 6 14.0906 6.44062 15.9422 7.21406C16.1203 7.28906 16.2984 7.36875 16.4672 7.44844C18.8062 7.28906 20.8359 6.75469 22.2 6Z" fill="#227386"/> +<path d="M19.9435 12.9151C19.7958 12.9551 19.6477 12.9951 19.5 13.0359V13.5C20.7602 13.5 21.9296 13.8885 22.8951 14.5522C23.5995 14.0172 24 13.4028 24 12.75V11.0906C23.4141 11.5734 22.7063 11.9672 21.9422 12.2859C21.3382 12.5376 20.6447 12.7253 19.9435 12.9151Z" fill="#227386"/> +<path d="M18.3703 8.74688C19.0031 9.37969 19.5 10.2234 19.5 11.25V11.4984C20.4328 11.2734 21.2625 10.9781 21.9469 10.6359C21.9739 10.6209 22.0009 10.6021 22.0279 10.5833C22.0852 10.5432 22.1426 10.5032 22.2 10.5C23.3297 9.87187 24 9.09375 24 8.25V6.59063C23.4141 7.07344 22.7063 7.46719 21.9422 7.78594C20.9109 8.2125 19.6969 8.54063 18.3703 8.74688Z" fill="#227386"/> +</g> +<path d="M16.2 13.5C17.3297 12.8719 18 12.0938 18 11.25C18 9.17813 13.9688 7.5 9 7.5C4.02938 7.5 0 9.17813 0 11.25C0 12.0938 0.669375 12.8719 1.79953 13.5C1.85443 13.5031 1.91057 13.5415 1.96782 13.5807C1.9966 13.6004 2.02567 13.6203 2.055 13.6359C3.70594 14.4703 6.20625 15 9 15C11.9438 15 14.5594 14.4094 16.2 13.5Z" fill="#227386"/> +<path d="M14.8788 15.6729C13.1948 16.2046 11.1571 16.5 9 16.5C6.36562 16.5 3.91125 16.0594 2.05922 15.2859C1.29469 14.9672 0.583594 14.5734 0 14.0906V15.75C0 16.5938 0.669375 17.3719 1.79953 18C3.44109 18.9094 6.05625 19.5 9 19.5C10.6471 19.5 12.1916 19.3159 13.5211 18.9937C13.6261 17.7367 14.1186 16.5898 14.8788 15.6729Z" fill="#227386"/> +<path d="M13.5862 20.5191C13.7529 21.4936 14.1547 22.3879 14.731 23.1415C13.1742 23.6778 11.1771 24 9 24C4.02938 24 0 22.3219 0 20.25V18.5906C0.583594 19.0734 1.29469 19.4672 2.05922 19.7859C3.91125 20.5594 6.36562 21 9 21C10.6307 21 12.1932 20.8312 13.5862 20.5191Z" fill="#227386"/> +<path d="M24 19.5C24 21.9844 21.9844 24 19.5 24C17.0156 24 15 21.9844 15 19.5C15 17.0156 17.0156 15 19.5 15C21.9844 15 24 17.0156 24 19.5ZM19 17.4719V18.9719H17.5C17.225 18.9719 17 19.225 17 19.4719C17 19.775 17.225 19.9719 17.5 19.9719H19V21.4719C19 21.775 19.225 21.9719 19.5 21.9719C19.775 21.9719 20 21.775 20 21.4719V19.9719H21.5C21.775 19.9719 22 19.775 22 19.4719C22 19.225 21.775 18.9719 21.5 18.9719H20V17.4719C20 17.225 19.775 16.9719 19.5 16.9719C19.225 16.9719 19 17.225 19 17.4719Z" fill="#227386"/> +</g> +<defs> +<clipPath id="clip0_519_18707"> +<rect width="24" height="24" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/assets/svg/oceanBreeze/exchange-2.svg b/assets/svg/oceanBreeze/exchange-2.svg new file mode 100644 index 000000000..7baeaf87f --- /dev/null +++ b/assets/svg/oceanBreeze/exchange-2.svg @@ -0,0 +1,4 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M19.5 6.5L20.4343 7.33045C20.8552 6.85685 20.8552 6.14315 20.4343 5.66955L19.5 6.5ZM16.4343 1.16955C15.9756 0.653567 15.1855 0.607091 14.6695 1.06574C14.1536 1.52439 14.1071 2.31448 14.5657 2.83045L16.4343 1.16955ZM14.5657 10.1695C14.1071 10.6855 14.1536 11.4756 14.6695 11.9343C15.1855 12.3929 15.9756 12.3464 16.4343 11.8305L14.5657 10.1695ZM0.75 10.5C0.75 11.1904 1.30964 11.75 2 11.75C2.69036 11.75 3.25 11.1904 3.25 10.5H0.75ZM6 7.75H19.5V5.25H6V7.75ZM14.5657 2.83045L18.5657 7.33045L20.4343 5.66955L16.4343 1.16955L14.5657 2.83045ZM16.4343 11.8305L20.4343 7.33045L18.5657 5.66955L14.5657 10.1695L16.4343 11.8305ZM3.25 10.5C3.25 8.98122 4.48122 7.75 6 7.75V5.25C3.10051 5.25 0.75 7.60051 0.75 10.5H3.25Z" fill="#227386"/> +<path opacity="0.4" d="M4.5 18L3.56574 17.1695C3.14475 17.6432 3.14475 18.3568 3.56574 18.8305L4.5 18ZM7.56574 23.3305C8.02439 23.8464 8.81448 23.8929 9.33045 23.4343C9.84643 22.9756 9.89291 22.1855 9.43426 21.6695L7.56574 23.3305ZM9.43426 14.3305C9.89291 13.8145 9.84643 13.0244 9.33046 12.5657C8.81448 12.1071 8.02439 12.1536 7.56574 12.6695L9.43426 14.3305ZM23.25 14C23.25 13.3096 22.6904 12.75 22 12.75C21.3096 12.75 20.75 13.3096 20.75 14L23.25 14ZM18 16.75L4.5 16.75L4.5 19.25L18 19.25L18 16.75ZM9.43426 21.6695L5.43426 17.1695L3.56574 18.8305L7.56574 23.3305L9.43426 21.6695ZM7.56574 12.6695L3.56574 17.1695L5.43426 18.8305L9.43426 14.3305L7.56574 12.6695ZM20.75 14C20.75 15.5188 19.5188 16.75 18 16.75L18 19.25C20.8995 19.25 23.25 16.8995 23.25 14L20.75 14Z" fill="#227386"/> +</svg> diff --git a/assets/svg/oceanBreeze/stack-icon1.svg b/assets/svg/oceanBreeze/stack-icon1.svg new file mode 100644 index 000000000..f316012d7 --- /dev/null +++ b/assets/svg/oceanBreeze/stack-icon1.svg @@ -0,0 +1,5 @@ +<svg width="70" height="70" viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M41.3715 9.57675C37.2965 7.22564 32.2041 10.1695 32.2041 14.8717C32.2041 19.5739 34.4762 23.6489 38.2004 26.163L53.9717 35.3057L54.0112 35.2908L69.9948 26.0543L41.3715 9.57675Z" fill="#B3B3B3"/> +<path d="M38.2014 26.163C34.4771 23.6489 32.205 19.4159 32.205 14.8717C32.205 12.6342 33.3757 10.7671 35.0402 9.7101C34.9612 9.75455 35.1192 9.66564 35.0402 9.7101L10.0917 23.7279L6.08593 26.1481L3.35449 27.7188C5.07337 26.8446 7.22692 26.7754 9.14831 27.8917L16.0189 31.8037L22.0399 35.2859L38.0236 44.5076L53.9677 35.2958L38.1964 26.1531L38.2014 26.163Z" fill="#666666"/> +<path d="M70 44.5187L38.0278 62.9917L31.992 59.5095L31.9673 59.4848L6.06054 44.5187C4.28733 43.3629 2.84505 41.7872 1.82755 40.014C0.642111 37.9691 0 35.618 0 33.1829C0 30.9899 1.10147 29.1771 2.70181 28.1004C2.91914 27.967 3.13153 27.8435 3.35874 27.725C5.07762 26.8507 7.23116 26.7816 9.15256 27.8979L15.9836 31.8394L22.0047 35.3068L22.0442 35.292L38.0278 44.5137L53.9719 35.3019L70 44.5137V44.5187Z" fill="#232323"/> +</svg> diff --git a/assets/svg/oceanBreeze/tx-exchange-icon-failed.svg b/assets/svg/oceanBreeze/tx-exchange-icon-failed.svg new file mode 100644 index 000000000..a54836bba --- /dev/null +++ b/assets/svg/oceanBreeze/tx-exchange-icon-failed.svg @@ -0,0 +1,7 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path opacity="0.4" d="M23.0154 16.7681C23.6489 15.3066 24 13.6943 24 12C24 5.37258 18.6274 0 12 0C5.37258 0 0 5.37258 0 12C0 18.6274 5.37258 24 12 24C13.6943 24 15.3066 23.6489 16.7681 23.0154C16.2832 22.2973 16 21.4317 16 20.5C16 18.0147 18.0147 16 20.5 16C21.4317 16 22.2973 16.2832 23.0154 16.7681Z" fill="#0056D2"/> +<path d="M5.30071 12.4C4.91018 12.7905 4.91018 13.4236 5.30071 13.8142C5.69123 14.2047 6.32439 14.2047 6.71492 13.8142L5.30071 12.4ZM13.0789 6.03599L14.0787 6.05567C14.0839 5.78863 13.9821 5.53058 13.796 5.33904C13.6098 5.1475 13.3548 5.03839 13.0877 5.03603L13.0789 6.03599ZM9.00968 5.00004C8.45741 4.99516 8.00576 5.43891 8.00089 5.99117C7.99601 6.54344 8.43976 6.99509 8.99202 6.99996L9.00968 5.00004ZM12.001 9.98032C11.9902 10.5325 12.429 10.9889 12.9812 10.9998C13.5333 11.0107 13.9898 10.5719 14.0007 10.0197L12.001 9.98032ZM18.6429 11.6C19.0334 11.2095 19.0334 10.5764 18.6429 10.1858C18.2524 9.79531 17.6192 9.79531 17.2287 10.1858L18.6429 11.6ZM10.8647 17.964L9.8653 17.9297C9.85604 18.1992 9.95602 18.461 10.1426 18.6557C10.3291 18.8505 10.5864 18.9616 10.856 18.964L10.8647 17.964ZM14.9922 19C15.5444 19.0048 15.996 18.561 16.0008 18.0087C16.0056 17.4564 15.5618 17.0048 15.0096 17L14.9922 19ZM12.0003 14.0343C12.0192 13.4824 11.5871 13.0195 11.0352 13.0006C10.4832 12.9816 10.0204 13.4137 10.0014 13.9657L12.0003 14.0343ZM6.71492 13.8142L13.786 6.7431L12.3718 5.32889L5.30071 12.4L6.71492 13.8142ZM8.99202 6.99996L13.0701 7.03595L13.0877 5.03603L9.00968 5.00004L8.99202 6.99996ZM12.0791 6.01631L12.001 9.98032L14.0007 10.0197L14.0787 6.05567L12.0791 6.01631ZM17.2287 10.1858L10.1576 17.2569L11.5718 18.6711L18.6429 11.6L17.2287 10.1858ZM15.0096 17L10.8734 16.964L10.856 18.964L14.9922 19L15.0096 17ZM11.8641 17.9983L12.0003 14.0343L10.0014 13.9657L9.8653 17.9297L11.8641 17.9983Z" fill="#0056D2"/> +<circle cx="20.5" cy="20.5" r="3.5" fill="#C00205"/> +<path d="M19.4395 19.4395L20.5001 20.5001L21.5608 21.5608" stroke="white" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M19.5 21.5605L20.5607 20.4999L21.6213 19.4392" stroke="white" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/assets/svg/oceanBreeze/tx-exchange-icon-pending.svg b/assets/svg/oceanBreeze/tx-exchange-icon-pending.svg new file mode 100644 index 000000000..5f9aa4256 --- /dev/null +++ b/assets/svg/oceanBreeze/tx-exchange-icon-pending.svg @@ -0,0 +1,6 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path opacity="0.4" d="M23.0154 16.7681C23.6489 15.3066 24 13.6943 24 12C24 5.37258 18.6274 0 12 0C5.37258 0 0 5.37258 0 12C0 18.6274 5.37258 24 12 24C13.6943 24 15.3066 23.6489 16.7681 23.0154C16.2832 22.2973 16 21.4317 16 20.5C16 18.0147 18.0147 16 20.5 16C21.4317 16 22.2973 16.2832 23.0154 16.7681Z" fill="#0056D2"/> +<circle cx="20.5" cy="20.5" r="3.5" fill="#F4C517"/> +<path d="M20.5 19V20.5H21.5" stroke="white" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M5.30071 12.4C4.91018 12.7905 4.91018 13.4236 5.30071 13.8142C5.69123 14.2047 6.32439 14.2047 6.71492 13.8142L5.30071 12.4ZM13.0789 6.03599L14.0787 6.05567C14.0839 5.78863 13.9821 5.53058 13.796 5.33904C13.6098 5.1475 13.3548 5.03839 13.0877 5.03603L13.0789 6.03599ZM9.00968 5.00004C8.45741 4.99516 8.00576 5.43891 8.00089 5.99117C7.99601 6.54344 8.43976 6.99509 8.99202 6.99996L9.00968 5.00004ZM12.001 9.98032C11.9902 10.5325 12.429 10.9889 12.9812 10.9998C13.5333 11.0107 13.9898 10.5719 14.0007 10.0197L12.001 9.98032ZM18.6429 11.6C19.0334 11.2095 19.0334 10.5764 18.6429 10.1858C18.2524 9.79531 17.6192 9.79531 17.2287 10.1858L18.6429 11.6ZM10.8647 17.964L9.8653 17.9297C9.85605 18.1992 9.95602 18.461 10.1426 18.6557C10.3291 18.8505 10.5864 18.9616 10.856 18.964L10.8647 17.964ZM14.9922 19C15.5444 19.0048 15.996 18.561 16.0008 18.0087C16.0056 17.4564 15.5618 17.0048 15.0096 17L14.9922 19ZM12.0003 14.0343C12.0192 13.4824 11.5871 13.0195 11.0352 13.0006C10.4832 12.9816 10.0204 13.4137 10.0014 13.9657L12.0003 14.0343ZM6.71492 13.8142L13.786 6.7431L12.3718 5.32889L5.30071 12.4L6.71492 13.8142ZM8.99202 6.99996L13.0701 7.03595L13.0877 5.03603L9.00968 5.00004L8.99202 6.99996ZM12.0791 6.01631L12.001 9.98032L14.0007 10.0197L14.0787 6.05567L12.0791 6.01631ZM17.2287 10.1858L10.1576 17.2569L11.5718 18.6711L18.6429 11.6L17.2287 10.1858ZM15.0096 17L10.8734 16.964L10.856 18.964L14.9922 19L15.0096 17ZM11.8641 17.9983L12.0003 14.0343L10.0014 13.9657L9.8653 17.9297L11.8641 17.9983Z" fill="#0056D2"/> +</svg> diff --git a/assets/svg/oceanBreeze/tx-exchange-icon.svg b/assets/svg/oceanBreeze/tx-exchange-icon.svg new file mode 100644 index 000000000..fcd3ef9dc --- /dev/null +++ b/assets/svg/oceanBreeze/tx-exchange-icon.svg @@ -0,0 +1,4 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<circle opacity="0.4" cx="12" cy="12" r="12" fill="#0056D2"/> +<path d="M5.30071 12.4C4.91018 12.7905 4.91018 13.4236 5.30071 13.8142C5.69123 14.2047 6.32439 14.2047 6.71492 13.8142L5.30071 12.4ZM13.0789 6.03599L14.0787 6.05567C14.0839 5.78863 13.9821 5.53058 13.796 5.33904C13.6098 5.1475 13.3548 5.03839 13.0877 5.03603L13.0789 6.03599ZM9.00968 5.00004C8.45741 4.99516 8.00576 5.43891 8.00089 5.99117C7.99601 6.54344 8.43976 6.99509 8.99202 6.99996L9.00968 5.00004ZM12.001 9.98032C11.9902 10.5325 12.429 10.9889 12.9812 10.9998C13.5333 11.0107 13.9898 10.5719 14.0007 10.0197L12.001 9.98032ZM18.6429 11.6C19.0334 11.2095 19.0334 10.5764 18.6429 10.1858C18.2524 9.79531 17.6192 9.79531 17.2287 10.1858L18.6429 11.6ZM10.8647 17.964L9.8653 17.9297C9.85604 18.1992 9.95602 18.461 10.1426 18.6557C10.3291 18.8505 10.5864 18.9616 10.856 18.964L10.8647 17.964ZM14.9922 19C15.5444 19.0048 15.996 18.561 16.0008 18.0087C16.0056 17.4564 15.5618 17.0048 15.0096 17L14.9922 19ZM12.0003 14.0343C12.0192 13.4824 11.5871 13.0195 11.0352 13.0006C10.4832 12.9816 10.0204 13.4137 10.0014 13.9657L12.0003 14.0343ZM6.71492 13.8142L13.786 6.7431L12.3718 5.32889L5.30071 12.4L6.71492 13.8142ZM8.99202 6.99996L13.0701 7.03595L13.0877 5.03603L9.00968 5.00004L8.99202 6.99996ZM12.0791 6.01631L12.001 9.98032L14.0007 10.0197L14.0787 6.05567L12.0791 6.01631ZM17.2287 10.1858L10.1576 17.2569L11.5718 18.6711L18.6429 11.6L17.2287 10.1858ZM15.0096 17L10.8734 16.964L10.856 18.964L14.9922 19L15.0096 17ZM11.8641 17.9983L12.0003 14.0343L10.0014 13.9657L9.8653 17.9297L11.8641 17.9983Z" fill="#0056D2"/> +</svg> diff --git a/assets/svg/oceanBreeze/tx-icon-receive-failed.svg b/assets/svg/oceanBreeze/tx-icon-receive-failed.svg new file mode 100644 index 000000000..189bd15c9 --- /dev/null +++ b/assets/svg/oceanBreeze/tx-icon-receive-failed.svg @@ -0,0 +1,7 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path opacity="0.4" fill-rule="evenodd" clip-rule="evenodd" d="M23.0154 16.7681C23.6489 15.3066 24 13.6943 24 12C24 5.37258 18.6274 0 12 0C5.37258 0 0 5.37258 0 12C0 18.6274 5.37258 24 12 24C13.6943 24 15.3066 23.6489 16.7681 23.0154C16.2832 22.2973 16 21.4317 16 20.5C16 18.0147 18.0147 16 20.5 16C21.4317 16 22.2973 16.2832 23.0154 16.7681Z" fill="#00A578"/> +<circle cx="20.5" cy="20.5" r="3.5" fill="#C00205"/> +<path d="M16 8L8 16M8 16H14M8 16V10" stroke="#00A578" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M19.4395 19.4395L20.5001 20.5001L21.5608 21.5608" stroke="white" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M19.5 21.5605L20.5607 20.4999L21.6213 19.4392" stroke="white" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/assets/svg/oceanBreeze/tx-icon-receive-pending.svg b/assets/svg/oceanBreeze/tx-icon-receive-pending.svg new file mode 100644 index 000000000..64ea8da3d --- /dev/null +++ b/assets/svg/oceanBreeze/tx-icon-receive-pending.svg @@ -0,0 +1,5 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path opacity="0.4" d="M23.0154 16.7681C23.6489 15.3066 24 13.6943 24 12C24 5.37258 18.6274 0 12 0C5.37258 0 0 5.37258 0 12C0 18.6274 5.37258 24 12 24C13.6943 24 15.3066 23.6489 16.7681 23.0154C16.2832 22.2973 16 21.4317 16 20.5C16 18.0147 18.0147 16 20.5 16C21.4317 16 22.2973 16.2832 23.0154 16.7681Z" fill="#00A578"/> +<path d="M16 8L8 16M8 16H14M8 16V10" stroke="#00A578" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M20.5 24C22.433 24 24 22.433 24 20.5C24 18.567 22.433 17 20.5 17C18.567 17 17 18.567 17 20.5C17 22.433 18.567 24 20.5 24ZM21 19C21 18.7239 20.7761 18.5 20.5 18.5C20.2239 18.5 20 18.7239 20 19V20.5C20 20.7761 20.2239 21 20.5 21H21.5C21.7761 21 22 20.7761 22 20.5C22 20.2239 21.7761 20 21.5 20H21V19Z" fill="#F4C517"/> +</svg> diff --git a/assets/svg/oceanBreeze/tx-icon-receive.svg b/assets/svg/oceanBreeze/tx-icon-receive.svg new file mode 100644 index 000000000..1076d8d57 --- /dev/null +++ b/assets/svg/oceanBreeze/tx-icon-receive.svg @@ -0,0 +1,4 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<circle opacity="0.4" cx="12" cy="12" r="12" fill="#00A578"/> +<path d="M16 8L8 16M8 16H14M8 16V10" stroke="#00A578" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/assets/svg/oceanBreeze/tx-icon-send-failed.svg b/assets/svg/oceanBreeze/tx-icon-send-failed.svg new file mode 100644 index 000000000..9751b61e8 --- /dev/null +++ b/assets/svg/oceanBreeze/tx-icon-send-failed.svg @@ -0,0 +1,7 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path opacity="0.4" d="M23.0154 16.7681C23.6489 15.3066 24 13.6943 24 12C24 5.37258 18.6274 0 12 0C5.37258 0 0 5.37258 0 12C0 18.6274 5.37258 24 12 24C13.6943 24 15.3066 23.6489 16.7681 23.0154C16.2832 22.2973 16 21.4317 16 20.5C16 18.0147 18.0147 16 20.5 16C21.4317 16 22.2973 16.2832 23.0154 16.7681Z" fill="#FE805C"/> +<path d="M8 16L16 8M16 8L10 8M16 8L16 14" stroke="#FE805C" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/> +<circle cx="20.5" cy="20.5" r="3.5" fill="#C00205"/> +<path d="M19.4395 19.4395L20.5001 20.5001L21.5608 21.5608" stroke="white" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M19.5 21.5605L20.5607 20.4999L21.6213 19.4392" stroke="white" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/assets/svg/oceanBreeze/tx-icon-send-pending.svg b/assets/svg/oceanBreeze/tx-icon-send-pending.svg new file mode 100644 index 000000000..e4ec777e3 --- /dev/null +++ b/assets/svg/oceanBreeze/tx-icon-send-pending.svg @@ -0,0 +1,6 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path opacity="0.4" d="M23.0154 16.7681C23.6489 15.3066 24 13.6943 24 12C24 5.37258 18.6274 0 12 0C5.37258 0 0 5.37258 0 12C0 18.6274 5.37258 24 12 24C13.6943 24 15.3066 23.6489 16.7681 23.0154C16.2832 22.2973 16 21.4317 16 20.5C16 18.0147 18.0147 16 20.5 16C21.4317 16 22.2973 16.2832 23.0154 16.7681Z" fill="#FE805C"/> +<path d="M8 16L16 8M16 8L10 8M16 8L16 14" stroke="#FE805C" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/> +<circle cx="20.5" cy="20.5" r="3.5" fill="#F4C517"/> +<path d="M20.5 19V20.5H21.5" stroke="white" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/assets/svg/oceanBreeze/tx-icon-send.svg b/assets/svg/oceanBreeze/tx-icon-send.svg new file mode 100644 index 000000000..ee32aa6b4 --- /dev/null +++ b/assets/svg/oceanBreeze/tx-icon-send.svg @@ -0,0 +1,4 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<circle opacity="0.4" cx="12" cy="12" r="12" fill="#FE805C"/> +<path d="M8 16L16 8M16 8L10 8M16 8L16 14" stroke="#FE805C" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index d7d2ebc6f..c423ec491 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -60,12 +60,13 @@ class _SVG { "assets/svg/${Theme.of(context).extension<StackColors>()!.themeType.name}/tx-exchange-icon-failed.svg"; String get themeOcean => "assets/svg/ocean-breeze-theme.svg"; + String get themeLight => "assets/svg/light-mode.svg"; + String get themeDark => "assets/svg/dark-theme.svg"; + String get circleSliders => "assets/svg/configuration.svg"; String get circlePlus => "assets/svg/plus-circle.svg"; String get framedGear => "assets/svg/framed-gear.svg"; String get framedAddressBook => "assets/svg/framed-address-book.svg"; - String get themeLight => "assets/svg/light/light-mode.svg"; - String get themeDark => "assets/svg/dark/dark-theme.svg"; String get circleNode => "assets/svg/node-circle.svg"; String get circleSun => "assets/svg/sun-circle.svg"; String get circleArrowRotate => "assets/svg/rotate-circle.svg"; diff --git a/pubspec.yaml b/pubspec.yaml index 94670af1a..6a721c5c1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -205,9 +205,6 @@ flutter: - assets/svg/plus.svg - assets/svg/gear.svg - assets/svg/bell.svg - - assets/svg/light/bell-new.svg - - assets/svg/dark/bell-new.svg - - assets/svg/stack-icon1.svg - assets/svg/arrow-left-fa.svg - assets/svg/copy-fa.svg - assets/svg/star.svg @@ -220,10 +217,7 @@ flutter: - assets/svg/bars.svg - assets/svg/filter.svg - assets/svg/pending.svg - - assets/svg/dark/exchange-2.svg - - assets/svg/light/exchange-2.svg - assets/svg/signal-stream.svg - - assets/svg/buy-coins-icon.svg - assets/svg/Ellipse-43.svg - assets/svg/Ellipse-42.svg - assets/svg/arrow-rotate.svg @@ -265,25 +259,7 @@ flutter: - assets/svg/ellipsis-vertical1.svg - assets/svg/dice-alt.svg - assets/svg/circle-arrow-up-right2.svg - - assets/svg/dark/tx-exchange-icon.svg - - assets/svg/light/tx-exchange-icon.svg - - assets/svg/dark/tx-exchange-icon-pending.svg - - assets/svg/light/tx-exchange-icon-pending.svg - - assets/svg/dark/tx-exchange-icon-failed.svg - - assets/svg/light/tx-exchange-icon-failed.svg - assets/svg/loader.svg - - assets/svg/dark/tx-icon-send.svg - - assets/svg/light/tx-icon-send.svg - - assets/svg/dark/tx-icon-send-pending.svg - - assets/svg/light/tx-icon-send-pending.svg - - assets/svg/dark/tx-icon-send-failed.svg - - assets/svg/light/tx-icon-send-failed.svg - - assets/svg/dark/tx-icon-receive.svg - - assets/svg/light/tx-icon-receive.svg - - assets/svg/dark/tx-icon-receive-pending.svg - - assets/svg/light/tx-icon-receive-pending.svg - - assets/svg/dark/tx-icon-receive-failed.svg - - assets/svg/light/tx-icon-receive-failed.svg - assets/svg/add-backup.svg - assets/svg/auto-backup.svg - assets/svg/restore-backup.svg @@ -305,8 +281,6 @@ flutter: - assets/svg/rotate-circle.svg - assets/svg/sun-circle.svg - assets/svg/node-circle.svg - - assets/svg/dark/dark-theme.svg - - assets/svg/light/light-mode.svg - assets/svg/address-book-desktop.svg - assets/svg/about-desktop.svg - assets/svg/exchange-desktop.svg @@ -316,7 +290,6 @@ flutter: - assets/svg/arrow-down.svg - assets/svg/plus-circle.svg - assets/svg/configuration.svg - - assets/svg/ocean-breeze-theme.svg # coin icons - assets/svg/coin_icons/Bitcoin.svg - assets/svg/coin_icons/Litecoin.svg @@ -348,6 +321,56 @@ flutter: - assets/svg/exchange_icons/change_now_logo_1.svg - assets/svg/exchange_icons/simpleswap-icon.svg + # theme selectors + - assets/svg/dark-theme.svg + - assets/svg/light-mode.svg + - assets/svg/ocean-breeze-theme.svg + + # light theme specific + - assets/svg/light/tx-exchange-icon.svg + - assets/svg/light/tx-exchange-icon-pending.svg + - assets/svg/light/tx-exchange-icon-failed.svg + - assets/svg/light/tx-icon-send.svg + - assets/svg/light/tx-icon-send-pending.svg + - assets/svg/light/tx-icon-send-failed.svg + - assets/svg/light/tx-icon-receive.svg + - assets/svg/light/tx-icon-receive-pending.svg + - assets/svg/light/tx-icon-receive-failed.svg + - assets/svg/light/exchange-2.svg + - assets/svg/light/bell-new.svg + - assets/svg/light/stack-icon1.svg + - assets/svg/light/buy-coins-icon.svg + + # dark theme specific + - assets/svg/dark/tx-exchange-icon.svg + - assets/svg/dark/tx-exchange-icon-pending.svg + - assets/svg/dark/tx-exchange-icon-failed.svg + - assets/svg/dark/tx-icon-send.svg + - assets/svg/dark/tx-icon-send-pending.svg + - assets/svg/dark/tx-icon-send-failed.svg + - assets/svg/dark/tx-icon-receive.svg + - assets/svg/dark/tx-icon-receive-pending.svg + - assets/svg/dark/tx-icon-receive-failed.svg + - assets/svg/dark/exchange-2.svg + - assets/svg/dark/bell-new.svg + - assets/svg/dark/stack-icon1.svg + - assets/svg/dark/buy-coins-icon.svg + + # light theme specific + - assets/svg/oceanBreeze/tx-exchange-icon.svg + - assets/svg/oceanBreeze/tx-exchange-icon-pending.svg + - assets/svg/oceanBreeze/tx-exchange-icon-failed.svg + - assets/svg/oceanBreeze/tx-icon-send.svg + - assets/svg/oceanBreeze/tx-icon-send-pending.svg + - assets/svg/oceanBreeze/tx-icon-send-failed.svg + - assets/svg/oceanBreeze/tx-icon-receive.svg + - assets/svg/oceanBreeze/tx-icon-receive-pending.svg + - assets/svg/oceanBreeze/tx-icon-receive-failed.svg + - assets/svg/oceanBreeze/exchange-2.svg + - assets/svg/oceanBreeze/bell-new.svg + - assets/svg/oceanBreeze/stack-icon1.svg + - assets/svg/oceanBreeze/buy-coins-icon.svg + # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. # For details regarding adding assets from package dependencies, see From 792b91b7c4d27c7212b251150ad4fafef39b96a8 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Fri, 18 Nov 2022 11:26:27 -0700 Subject: [PATCH 326/426] syncing pref options show on button press + shows card w current syncing prefs --- .../syncing_preferences_settings.dart | 116 ++++++++++++++---- 1 file changed, 91 insertions(+), 25 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart b/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart index 815e506db..720d77b8b 100644 --- a/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/syncing_preferences_settings.dart @@ -3,9 +3,13 @@ import 'package:flutter/src/widgets/framework.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart'; +import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/enums/sync_type_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class SyncingPreferencesSettings extends ConsumerStatefulWidget { @@ -20,6 +24,19 @@ class SyncingPreferencesSettings extends ConsumerStatefulWidget { class _SyncingPreferencesSettings extends ConsumerState<SyncingPreferencesSettings> { + String _currentTypeDescription(SyncingType type) { + switch (type) { + case SyncingType.currentWalletOnly: + return "Sync only currently open wallet"; + case SyncingType.selectedWalletsAtStartup: + return "Sync only selected wallets at startup"; + case SyncingType.allWalletsOnStartup: + return "Sync all wallets at startup"; + } + } + + late bool changePrefs = false; + @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); @@ -34,13 +51,40 @@ class _SyncingPreferencesSettings child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: SvgPicture.asset( - Assets.svg.circleArrowRotate, - width: 48, - height: 48, - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + Assets.svg.circleArrowRotate, + width: 48, + height: 48, + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: RoundedContainer( + color: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondaryDisabled, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + _currentTypeDescription(ref.watch( + prefsChangeNotifierProvider + .select((value) => value.syncType))), + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark2), + textAlign: TextAlign.left, + ), + ), + ), + ), + ], ), Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -67,28 +111,50 @@ class _SyncingPreferencesSettings ), ], ), - - ///TODO: ONLY SHOW SYNC OPTIONS ON BUTTON PRESS - Column( - children: const [ - SyncingOptionsView(), - ], - ), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: const EdgeInsets.all( - 10, - ), - child: PrimaryButton( - width: 210, - buttonHeight: ButtonHeight.m, - enabled: true, - label: "Change preferences", - onPressed: () {}, - ), - ), + padding: const EdgeInsets.all( + 10, + ), + child: changePrefs + ? SizedBox( + width: 512, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SyncingOptionsView(), + PrimaryButton( + width: 200, + buttonHeight: ButtonHeight.m, + enabled: true, + label: "Save", + onPressed: () { + setState(() { + changePrefs = false; + }); + }, + ), + ], + ), + ) + : Column( + children: [ + const SizedBox(height: 10), + PrimaryButton( + width: 200, + buttonHeight: ButtonHeight.m, + enabled: true, + label: "Change preferences", + onPressed: () { + setState(() { + changePrefs = true; + }); + }, + ), + ], + )), ], ), ], From 7ef31cbf87a80598478c2ef68a4ff98cbe3c130a Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 18 Nov 2022 12:34:25 -0600 Subject: [PATCH 327/426] add back exchange menu option and adjust icon color --- .../home/desktop_menu.dart | 76 ++++++++++--------- lib/utilities/theme/ocean_breeze_colors.dart | 2 +- 2 files changed, 41 insertions(+), 37 deletions(-) diff --git a/lib/pages_desktop_specific/home/desktop_menu.dart b/lib/pages_desktop_specific/home/desktop_menu.dart index bdaa1d6ce..60a424a06 100644 --- a/lib/pages_desktop_specific/home/desktop_menu.dart +++ b/lib/pages_desktop_specific/home/desktop_menu.dart @@ -104,10 +104,10 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { .state ? Theme.of(context) .extension<StackColors>()! - .textDark + .accentColorDark : Theme.of(context) .extension<StackColors>()! - .textDark + .accentColorDark .withOpacity(0.8), ), label: "My Stack", @@ -120,29 +120,33 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { const SizedBox( height: 2, ), - // DesktopMenuItem( - // icon: SvgPicture.asset( - // Assets.svg.exchangeDesktop, - // width: 20, - // height: 20, - // color: DesktopMenuItemId.exchange == ref.watch(currentDesktopMenuItemProvider.state).state - // ? Theme.of(context) - // .extension<StackColors>()! - // .textDark - // : Theme.of(context) - // .extension<StackColors>()! - // .textDark - // .withOpacity(0.8), - // ), - // label: "Exchange", - // value: DesktopMenuItemId.exchange, - // group: ref.watch(currentDesktopMenuItemProvider.state).state, - // onChanged: updateSelectedMenuItem, - // iconOnly: _width == minimizedWidth, - // ), - // const SizedBox( - // height: 2, - // ), + DesktopMenuItem( + icon: SvgPicture.asset( + Assets.svg.exchangeDesktop, + width: 20, + height: 20, + color: DesktopMenuItemId.exchange == + ref + .watch(currentDesktopMenuItemProvider.state) + .state + ? Theme.of(context) + .extension<StackColors>()! + .accentColorDark + : Theme.of(context) + .extension<StackColors>()! + .accentColorDark + .withOpacity(0.8), + ), + label: "Exchange", + value: DesktopMenuItemId.exchange, + group: + ref.watch(currentDesktopMenuItemProvider.state).state, + onChanged: updateSelectedMenuItem, + iconOnly: _width == minimizedWidth, + ), + const SizedBox( + height: 2, + ), DesktopMenuItem( icon: SvgPicture.asset( Assets.svg.bell, @@ -154,10 +158,10 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { .state ? Theme.of(context) .extension<StackColors>()! - .textDark + .accentColorDark : Theme.of(context) .extension<StackColors>()! - .textDark + .accentColorDark .withOpacity(0.8), ), label: "Notifications", @@ -181,10 +185,10 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { .state ? Theme.of(context) .extension<StackColors>()! - .textDark + .accentColorDark : Theme.of(context) .extension<StackColors>()! - .textDark + .accentColorDark .withOpacity(0.8), ), label: "Address Book", @@ -208,10 +212,10 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { .state ? Theme.of(context) .extension<StackColors>()! - .textDark + .accentColorDark : Theme.of(context) .extension<StackColors>()! - .textDark + .accentColorDark .withOpacity(0.8), ), label: "Settings", @@ -235,10 +239,10 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { .state ? Theme.of(context) .extension<StackColors>()! - .textDark + .accentColorDark : Theme.of(context) .extension<StackColors>()! - .textDark + .accentColorDark .withOpacity(0.8), ), label: "Support", @@ -262,10 +266,10 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { .state ? Theme.of(context) .extension<StackColors>()! - .textDark + .accentColorDark : Theme.of(context) .extension<StackColors>()! - .textDark + .accentColorDark .withOpacity(0.8), ), label: "About", @@ -283,7 +287,7 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { height: 20, color: Theme.of(context) .extension<StackColors>()! - .textDark + .accentColorDark .withOpacity(0.8), ), label: "Exit", diff --git a/lib/utilities/theme/ocean_breeze_colors.dart b/lib/utilities/theme/ocean_breeze_colors.dart index ff2f4e85e..1eb06e068 100644 --- a/lib/utilities/theme/ocean_breeze_colors.dart +++ b/lib/utilities/theme/ocean_breeze_colors.dart @@ -21,7 +21,7 @@ class OceanBreezeColors extends StackColorTheme { @override Color get accentColorOrange => const Color(0xFFFF985F); @override - Color get accentColorDark => const Color(0xFF232323); + Color get accentColorDark => const Color(0xFF227386); @override Color get shadow => const Color(0xFF388192); From ea143d9ffa901d30cf9f2631b0362fab80cfee71 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 18 Nov 2022 12:45:42 -0600 Subject: [PATCH 328/426] basic desktop exchange layout --- .../desktop_exchange_view.dart | 89 +++++++++++++++ .../subwidgets/desktop_trade_history.dart | 103 ++++++++++++++++++ .../home/desktop_home_view.dart | 10 +- lib/route_generator.dart | 7 ++ 4 files changed, 205 insertions(+), 4 deletions(-) create mode 100644 lib/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart create mode 100644 lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart diff --git a/lib/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart b/lib/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart new file mode 100644 index 000000000..0f44eb59b --- /dev/null +++ b/lib/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/pages/exchange_view/exchange_form.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class DesktopExchangeView extends StatefulWidget { + const DesktopExchangeView({Key? key}) : super(key: key); + + static const String routeName = "/desktopExchange"; + + @override + State<DesktopExchangeView> createState() => _DesktopExchangeViewState(); +} + +class _DesktopExchangeViewState extends State<DesktopExchangeView> { + @override + Widget build(BuildContext context) { + return DesktopScaffold( + appBar: DesktopAppBar( + isCompactHeight: true, + leading: Padding( + padding: const EdgeInsets.only( + left: 24, + ), + child: Text( + "Exchange", + style: STextStyles.desktopH3(context), + ), + ), + ), + body: Padding( + padding: const EdgeInsets.only( + left: 24, + right: 24, + bottom: 24, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Exchange details", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + const SizedBox( + height: 16, + ), + const RoundedWhiteContainer( + padding: EdgeInsets.all(24), + child: ExchangeForm(), + ), + ], + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Exchange details", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + const SizedBox( + height: 16, + ), + const RoundedWhiteContainer( + padding: EdgeInsets.all(0), + child: DesktopTradeHistory(), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart new file mode 100644 index 000000000..40eeb8c1b --- /dev/null +++ b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart @@ -0,0 +1,103 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages/exchange_view/trade_details_view.dart'; +import 'package:stackwallet/providers/exchange/trade_sent_from_stack_lookup_provider.dart'; +import 'package:stackwallet/providers/global/trades_service_provider.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/trade_card.dart'; +import 'package:tuple/tuple.dart'; + +class DesktopTradeHistory extends ConsumerStatefulWidget { + const DesktopTradeHistory({Key? key}) : super(key: key); + + @override + ConsumerState<DesktopTradeHistory> createState() => + _DesktopTradeHistoryState(); +} + +class _DesktopTradeHistoryState extends ConsumerState<DesktopTradeHistory> { + @override + Widget build(BuildContext context) { + final trades = + ref.watch(tradesServiceProvider.select((value) => value.trades)); + + final tradeCount = trades.length; + final hasHistory = tradeCount > 0; + + if (hasHistory) { + return ListView.separated( + itemBuilder: (context, index) { + return TradeCard( + key: Key("tradeCard_${trades[index].uuid}"), + trade: trades[index], + onTap: () async { + final String tradeId = trades[index].tradeId; + + final lookup = ref.read(tradeSentFromStackLookupProvider).all; + + debugPrint("ALL: $lookup"); + + final String? txid = ref + .read(tradeSentFromStackLookupProvider) + .getTxidForTradeId(tradeId); + final List<String>? walletIds = ref + .read(tradeSentFromStackLookupProvider) + .getWalletIdsForTradeId(tradeId); + + if (txid != null && walletIds != null && walletIds.isNotEmpty) { + final manager = ref + .read(walletsChangeNotifierProvider) + .getManager(walletIds.first); + + debugPrint("name: ${manager.walletName}"); + + // TODO store tx data completely locally in isar so we don't lock up ui here when querying txData + final txData = await manager.transactionData; + + final tx = txData.getAllTransactions()[txid]; + + if (mounted) { + unawaited( + Navigator.of(context).pushNamed( + TradeDetailsView.routeName, + arguments: Tuple4( + tradeId, tx, walletIds.first, manager.walletName), + ), + ); + } + } else { + unawaited( + Navigator.of(context).pushNamed( + TradeDetailsView.routeName, + arguments: Tuple4(tradeId, null, walletIds?.first, null), + ), + ); + } + }, + ); + }, + separatorBuilder: (context, index) { + return Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ); + }, + itemCount: tradeCount, + ); + } else { + return RoundedWhiteContainer( + child: Center( + child: Text( + "Trades will appear here", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + ); + } + } +} diff --git a/lib/pages_desktop_specific/home/desktop_home_view.dart b/lib/pages_desktop_specific/home/desktop_home_view.dart index b1c35f00b..9791cd867 100644 --- a/lib/pages_desktop_specific/home/desktop_home_view.dart +++ b/lib/pages_desktop_specific/home/desktop_home_view.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/address_book_view/desktop_address_book.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_menu.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_settings_view.dart'; @@ -29,10 +30,11 @@ class _DesktopHomeViewState extends ConsumerState<DesktopHomeView> { onGenerateRoute: RouteGenerator.generateRoute, initialRoute: MyStackView.routeName, ), - // Container( - // // todo: exchange - // color: Colors.green, - // ), + DesktopMenuItemId.exchange: const Navigator( + key: Key("desktopExchangeHomeKey"), + onGenerateRoute: RouteGenerator.generateRoute, + initialRoute: DesktopExchangeView.routeName, + ), DesktopMenuItemId.notifications: const Navigator( key: Key("desktopNotificationsHomeKey"), onGenerateRoute: RouteGenerator.generateRoute, diff --git a/lib/route_generator.dart b/lib/route_generator.dart index d7865d013..8ccc923bc 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -85,6 +85,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/create_password/create_password_view.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart'; import 'package:stackwallet/pages_desktop_specific/forgot_password_desktop_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/address_book_view/desktop_address_book.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; @@ -1019,6 +1020,12 @@ class RouteGenerator { builder: (_) => const DesktopNotificationsView(), settings: RouteSettings(name: settings.name)); + case DesktopExchangeView.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const DesktopExchangeView(), + settings: RouteSettings(name: settings.name)); + case DesktopSettingsView.routeName: return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, From 83e2554b545e62a6cf4af323093a150dfe054812 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Fri, 18 Nov 2022 12:42:12 -0700 Subject: [PATCH 329/426] mobile theme radio buttons --- .../appearance_settings_view.dart | 364 +++++++++++++++--- 1 file changed, 309 insertions(+), 55 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart b/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart index 3a1b842f6..d1e893802 100644 --- a/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart +++ b/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart @@ -8,6 +8,7 @@ import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/color_theme.dart'; import 'package:stackwallet/utilities/theme/dark_colors.dart'; import 'package:stackwallet/utilities/theme/light_colors.dart'; +import 'package:stackwallet/utilities/theme/ocean_breeze_colors.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart'; @@ -18,6 +19,17 @@ class AppearanceSettingsView extends ConsumerWidget { static const String routeName = "/appearanceSettings"; + String chooseThemeType(ThemeType type) { + switch (type) { + case ThemeType.light: + return "Light theme"; + case ThemeType.oceanBreeze: + return "Ocean theme"; + case ThemeType.dark: + return "Dark theme"; + } + } + @override Widget build(BuildContext context, WidgetRef ref) { return Scaffold( @@ -100,68 +112,39 @@ class AppearanceSettingsView extends ConsumerWidget { height: 10, ), RoundedWhiteContainer( - child: Consumer( - builder: (_, ref, __) { - return RawMaterialButton( - splashColor: Theme.of(context) - .extension<StackColors>()! - .highlight, - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: null, - child: Padding( - padding: - const EdgeInsets.symmetric(vertical: 8), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + padding: const EdgeInsets.all(0), + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: null, + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Enable dark mode", + "Choose Theme", style: STextStyles.titleBold12(context), textAlign: TextAlign.left, ), - SizedBox( - height: 20, - width: 40, - child: DraggableSwitchButton( - isOn: (DB.instance.get<dynamic>( - boxName: DB.boxNameTheme, - key: "colorScheme") - as String?) == - "dark", - onValueChanged: (newValue) { - DB.instance.put<dynamic>( - boxName: DB.boxNameTheme, - key: "colorScheme", - value: (newValue - ? ThemeType.dark - : (newValue - ? ThemeType.light - : ThemeType - .oceanBreeze)) - .name, - ); - ref - .read(colorThemeProvider.state) - .state = - StackColors.fromStackColorTheme( - newValue - ? DarkColors() - : LightColors()); - }, - ), - ) + const Padding( + padding: EdgeInsets.all(10), + child: ThemeOptionsView(), + ), ], ), - ), - ); - }, + ], + ), + ), ), ), ], @@ -175,3 +158,274 @@ class AppearanceSettingsView extends ConsumerWidget { ); } } + +class ThemeOptionsView extends ConsumerStatefulWidget { + const ThemeOptionsView({ + Key? key, + }) : super(key: key); + + @override + ConsumerState<ThemeOptionsView> createState() => _ThemeOptionsView(); +} + +class _ThemeOptionsView extends ConsumerState<ThemeOptionsView> { + late String _selectedTheme; + + @override + void initState() { + _selectedTheme = + DB.instance.get<dynamic>(boxName: DB.boxNameTheme, key: "colorScheme") + as String? ?? + "light"; + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + MaterialButton( + splashColor: Colors.transparent, + hoverColor: Colors.transparent, + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.light.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + LightColors(), + ); + + setState(() { + _selectedTheme = "light"; + }); + }, + child: SizedBox( + width: 200, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + SizedBox( + width: 10, + height: 10, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: "light", + groupValue: _selectedTheme, + onChanged: (newValue) { + if (newValue is String && newValue == "light") { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.light.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + LightColors(), + ); + + setState(() { + _selectedTheme = "light"; + }); + } + }, + ), + ), + const SizedBox( + width: 14, + ), + Text( + "Light", + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark2, + ), + ), + ], + ), + ], + ), + ), + ), + const SizedBox( + height: 10, + ), + MaterialButton( + splashColor: Colors.transparent, + hoverColor: Colors.transparent, + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.oceanBreeze.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + OceanBreezeColors(), + ); + + setState(() { + _selectedTheme = "oceanBreeze"; + }); + }, + child: SizedBox( + width: 200, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + SizedBox( + width: 10, + height: 10, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: "oceanBreeze", + groupValue: _selectedTheme, + onChanged: (newValue) { + if (newValue is String && newValue == "oceanBreeze") { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.oceanBreeze.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + OceanBreezeColors(), + ); + + setState(() { + _selectedTheme = "oceanBreeze"; + }); + } + }, + ), + ), + const SizedBox( + width: 14, + ), + Text( + "Ocean Breeze", + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark2, + ), + ), + ], + ), + ], + ), + ), + ), + const SizedBox( + height: 10, + ), + MaterialButton( + splashColor: Colors.transparent, + hoverColor: Colors.transparent, + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.dark.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + DarkColors(), + ); + + setState(() { + _selectedTheme = "dark"; + }); + }, + child: SizedBox( + width: 200, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + SizedBox( + width: 10, + height: 10, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: "dark", + groupValue: _selectedTheme, + onChanged: (newValue) { + if (newValue is String && newValue == "dark") { + DB.instance.put<dynamic>( + boxName: DB.boxNameTheme, + key: "colorScheme", + value: ThemeType.dark.name, + ); + ref.read(colorThemeProvider.state).state = + StackColors.fromStackColorTheme( + DarkColors(), + ); + + setState(() { + _selectedTheme = "dark"; + }); + } + }, + ), + ), + const SizedBox( + width: 14, + ), + Text( + "Dark", + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark2, + ), + ), + ], + ), + ], + ), + ), + ), + ], + ); + } +} From 9956a497df081a54a67a90ae68f93ee5d6e32cc3 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Fri, 18 Nov 2022 13:26:17 -0700 Subject: [PATCH 330/426] ocean breeze shadow color fix --- lib/utilities/theme/ocean_breeze_colors.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utilities/theme/ocean_breeze_colors.dart b/lib/utilities/theme/ocean_breeze_colors.dart index 1eb06e068..665eaa0c3 100644 --- a/lib/utilities/theme/ocean_breeze_colors.dart +++ b/lib/utilities/theme/ocean_breeze_colors.dart @@ -24,7 +24,7 @@ class OceanBreezeColors extends StackColorTheme { Color get accentColorDark => const Color(0xFF227386); @override - Color get shadow => const Color(0xFF388192); + Color get shadow => const Color(0x0F2D3132); @override Color get textDark => const Color(0xFF232323); From e665926b1bc229e6abee16bdda38debe242191e4 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 18 Nov 2022 14:59:53 -0600 Subject: [PATCH 331/426] firo anonymize navigation fix --- .../wallet_view/desktop_wallet_view.dart | 47 +++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index d08864eee..85dde4aba 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -193,6 +193,7 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { if (publicBalance <= Decimal.zero) { shouldPop = true; if (mounted) { + Navigator.of(context, rootNavigator: true).pop(); Navigator.of(context).popUntil( ModalRoute.withName(DesktopWalletView.routeName), ); @@ -211,6 +212,7 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { await firoWallet.anonymizeAllPublicFunds(); shouldPop = true; if (mounted) { + Navigator.of(context, rootNavigator: true).pop(); Navigator.of(context).popUntil( ModalRoute.withName(DesktopWalletView.routeName), ); @@ -225,14 +227,53 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { } catch (e) { shouldPop = true; if (mounted) { + Navigator.of(context, rootNavigator: true).pop(); Navigator.of(context).popUntil( ModalRoute.withName(DesktopWalletView.routeName), ); await showDialog<dynamic>( context: context, - builder: (_) => StackOkDialog( - title: "Anonymize all failed", - message: "Reason: $e", + 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, + ), + ), + ], + ) + ], + ), + ), ), ); } From 9ba83f36eb480ff0dab6af8ca4a2ebed5d37642d Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 18 Nov 2022 16:05:15 -0600 Subject: [PATCH 332/426] desktop exchange rate toggle style --- assets/svg/lock-open.svg | 3 + .../sub_widgets/rate_type_toggle.dart | 115 +++++++++++++----- pubspec.yaml | 1 + 3 files changed, 88 insertions(+), 31 deletions(-) create mode 100644 assets/svg/lock-open.svg diff --git a/assets/svg/lock-open.svg b/assets/svg/lock-open.svg new file mode 100644 index 000000000..f2b00f341 --- /dev/null +++ b/assets/svg/lock-open.svg @@ -0,0 +1,3 @@ +<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M7 1.75C5.79141 1.75 4.8125 2.72945 4.8125 3.9375V5.25H11.375C12.3402 5.25 13.125 6.03477 13.125 7V12.25C13.125 13.2152 12.3402 14 11.375 14H2.625C1.6584 14 0.875 13.2152 0.875 12.25V7C0.875 6.03477 1.6584 5.25 2.625 5.25H3.0625V3.9375C3.0625 1.76285 4.82617 0 7 0C8.57227 0 9.92578 0.921211 10.5574 2.24957C10.7652 2.68598 10.5793 3.20742 10.1199 3.41523C9.68242 3.62305 9.18476 3.43711 8.97695 2.99961C8.62422 2.25941 7.87227 1.75 7 1.75ZM7.875 10.5C8.35898 10.5 8.75 10.109 8.75 9.625C8.75 9.14102 8.35898 8.75 7.875 8.75H6.125C5.64102 8.75 5.25 9.14102 5.25 9.625C5.25 10.109 5.64102 10.5 6.125 10.5H7.875Z" fill="#0056D2"/> +</svg> diff --git a/lib/pages/exchange_view/sub_widgets/rate_type_toggle.dart b/lib/pages/exchange_view/sub_widgets/rate_type_toggle.dart index 31460c75f..9697710e8 100644 --- a/lib/pages/exchange_view/sub_widgets/rate_type_toggle.dart +++ b/lib/pages/exchange_view/sub_widgets/rate_type_toggle.dart @@ -7,8 +7,8 @@ import 'package:stackwallet/providers/providers.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/utilities/util.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; -import 'package:stackwallet/widgets/rounded_white_container.dart'; class RateTypeToggle extends ConsumerWidget { const RateTypeToggle({ @@ -21,12 +21,17 @@ class RateTypeToggle extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { debugPrint("BUILD: $runtimeType"); + final isDesktop = Util.isDesktop; + final estimated = ref.watch(prefsChangeNotifierProvider .select((value) => value.exchangeRateType)) == ExchangeRateType.estimated; - return RoundedWhiteContainer( + return RoundedContainer( padding: const EdgeInsets.all(0), + color: isDesktop + ? Theme.of(context).extension<StackColors>()!.buttonBackSecondary + : Theme.of(context).extension<StackColors>()!.popupBG, child: Row( children: [ Expanded( @@ -39,6 +44,9 @@ class RateTypeToggle extends ConsumerWidget { } }, child: RoundedContainer( + padding: isDesktop + ? const EdgeInsets.all(17) + : const EdgeInsets.all(0), color: estimated ? Theme.of(context) .extension<StackColors>()! @@ -48,29 +56,50 @@ class RateTypeToggle extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ SvgPicture.asset( - Assets.svg.lock, + Assets.svg.lockOpen, width: 12, height: 14, - color: estimated - ? Theme.of(context).extension<StackColors>()!.textDark - : Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, + color: isDesktop + ? estimated + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary + : estimated + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, ), const SizedBox( width: 5, ), Text( "Estimate rate", - style: STextStyles.smallMed12(context).copyWith( - color: estimated - ? Theme.of(context) - .extension<StackColors>()! - .textDark - : Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: estimated + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, + ) + : STextStyles.smallMed12(context).copyWith( + color: estimated + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), ), ], ), @@ -87,6 +116,9 @@ class RateTypeToggle extends ConsumerWidget { } }, child: RoundedContainer( + padding: isDesktop + ? const EdgeInsets.all(17) + : const EdgeInsets.all(0), color: !estimated ? Theme.of(context) .extension<StackColors>()! @@ -99,26 +131,47 @@ class RateTypeToggle extends ConsumerWidget { Assets.svg.lock, width: 12, height: 14, - color: !estimated - ? Theme.of(context).extension<StackColors>()!.textDark - : Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, + color: isDesktop + ? !estimated + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary + : !estimated + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, ), const SizedBox( width: 5, ), Text( "Fixed rate", - style: STextStyles.smallMed12(context).copyWith( - color: !estimated - ? Theme.of(context) - .extension<StackColors>()! - .textDark - : Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: !estimated + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, + ) + : STextStyles.smallMed12(context).copyWith( + color: !estimated + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), ), ], ), diff --git a/pubspec.yaml b/pubspec.yaml index 6a721c5c1..e8f417586 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -228,6 +228,7 @@ flutter: - assets/svg/chevron-down.svg - assets/svg/chevron-up.svg - assets/svg/lock-keyhole.svg + - assets/svg/lock-open.svg - assets/svg/rotate-exclamation.svg - assets/svg/folder-down.svg - assets/svg/network-wired.svg From 16113fd1d52319b855cb6e38044da84f810feb7a Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 18 Nov 2022 16:05:46 -0600 Subject: [PATCH 333/426] desktop exchange provider options dropdown style --- .../exchange_provider_options.dart | 706 ++++++++++-------- 1 file changed, 379 insertions(+), 327 deletions(-) diff --git a/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart b/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart index 2113e199c..4dd768403 100644 --- a/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart +++ b/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart @@ -15,7 +15,9 @@ import 'package:stackwallet/utilities/format.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/utilities/util.dart'; import 'package:stackwallet/widgets/animated_text.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class ExchangeProviderOptions extends ConsumerWidget { @@ -38,353 +40,403 @@ class ExchangeProviderOptions extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final isDesktop = Util.isDesktop; return RoundedWhiteContainer( + padding: isDesktop ? const EdgeInsets.all(0) : const EdgeInsets.all(12), + borderColor: isDesktop + ? Theme.of(context).extension<StackColors>()!.background + : null, child: Column( children: [ - GestureDetector( - onTap: () { - if (ref.read(currentExchangeNameStateProvider.state).state != - ChangeNowExchange.exchangeName) { - ref.read(currentExchangeNameStateProvider.state).state = - ChangeNowExchange.exchangeName; - ref.read(exchangeFormStateProvider).exchange = - Exchange.fromName( - ref.read(currentExchangeNameStateProvider.state).state); - } - }, - child: Container( - color: Colors.transparent, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 20, - child: Radio( - activeColor: Theme.of(context) - .extension<StackColors>()! - .radioButtonIconEnabled, - value: ChangeNowExchange.exchangeName, - groupValue: ref - .watch(currentExchangeNameStateProvider.state) - .state, - onChanged: (value) { - if (value is String) { - ref - .read(currentExchangeNameStateProvider.state) - .state = value; - ref.read(exchangeFormStateProvider).exchange = - Exchange.fromName(ref + ConditionalParent( + condition: isDesktop, + builder: (child) => MouseRegion( + cursor: SystemMouseCursors.click, + child: child, + ), + child: GestureDetector( + onTap: () { + if (ref.read(currentExchangeNameStateProvider.state).state != + ChangeNowExchange.exchangeName) { + ref.read(currentExchangeNameStateProvider.state).state = + ChangeNowExchange.exchangeName; + ref.read(exchangeFormStateProvider).exchange = + Exchange.fromName(ref + .read(currentExchangeNameStateProvider.state) + .state); + } + }, + child: Container( + color: Colors.transparent, + child: Padding( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: ChangeNowExchange.exchangeName, + groupValue: ref + .watch(currentExchangeNameStateProvider.state) + .state, + onChanged: (value) { + if (value is String) { + ref .read(currentExchangeNameStateProvider.state) - .state); - } - }, - ), - ), - const SizedBox( - width: 14, - ), - SvgPicture.asset( - Assets.exchange.changeNow, - width: 24, - height: 24, - ), - const SizedBox( - width: 10, - ), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - ChangeNowExchange.exchangeName, - style: STextStyles.titleBold12(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark2, - ), + .state = value; + ref.read(exchangeFormStateProvider).exchange = + Exchange.fromName(ref + .read(currentExchangeNameStateProvider + .state) + .state); + } + }, ), - if (from != null && - to != null && - toAmount != null && - toAmount! > Decimal.zero && - fromAmount != null && - fromAmount! > Decimal.zero) - FutureBuilder( - future: ChangeNowExchange().getEstimate( - from!, - to!, - reversed ? toAmount! : fromAmount!, - fixedRate, - reversed, + ), + const SizedBox( + width: 14, + ), + SvgPicture.asset( + Assets.exchange.changeNow, + width: isDesktop ? 32 : 24, + height: isDesktop ? 32 : 24, + ), + const SizedBox( + width: 10, + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + ChangeNowExchange.exchangeName, + style: STextStyles.titleBold12(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark2, + ), ), - builder: (context, - AsyncSnapshot<ExchangeResponse<Estimate>> - snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - final estimate = snapshot.data?.value; - if (estimate != null) { - Decimal rate; - if (estimate.reversed) { - rate = - (toAmount! / estimate.estimatedAmount) + if (from != null && + to != null && + toAmount != null && + toAmount! > Decimal.zero && + fromAmount != null && + fromAmount! > Decimal.zero) + FutureBuilder( + future: ChangeNowExchange().getEstimate( + from!, + to!, + reversed ? toAmount! : fromAmount!, + fixedRate, + reversed, + ), + builder: (context, + AsyncSnapshot<ExchangeResponse<Estimate>> + snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + final estimate = snapshot.data?.value; + if (estimate != null) { + Decimal rate; + if (estimate.reversed) { + rate = (toAmount! / + estimate.estimatedAmount) .toDecimal( scaleOnInfinitePrecision: 12); + } else { + rate = (estimate.estimatedAmount / + fromAmount!) + .toDecimal( + scaleOnInfinitePrecision: 12); + } + return Text( + "1 ${from!.toUpperCase()} ~ ${Format.localizedStringAsFixed( + value: rate, + locale: ref.watch( + localeServiceChangeNotifierProvider + .select( + (value) => value.locale), + ), + decimalPlaces: to!.toUpperCase() == + Coin.monero.ticker + .toUpperCase() + ? Constants.decimalPlacesMonero + : Constants.decimalPlaces, + )} ${to!.toUpperCase()}", + style: + STextStyles.itemSubtitle12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ); + } else { + Logging.instance.log( + "$runtimeType failed to fetch rate for ChangeNOW: ${snapshot.data}", + level: LogLevel.Warning, + ); + return Text( + "Failed to fetch rate", + style: + STextStyles.itemSubtitle12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ); + } } else { - rate = - (estimate.estimatedAmount / fromAmount!) - .toDecimal( - scaleOnInfinitePrecision: 12); - } - return Text( - "1 ${from!.toUpperCase()} ~ ${Format.localizedStringAsFixed( - value: rate, - locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), + return AnimatedText( + stringsToLoopThrough: const [ + "Loading", + "Loading.", + "Loading..", + "Loading...", + ], + style: STextStyles.itemSubtitle12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, ), - decimalPlaces: to!.toUpperCase() == - Coin.monero.ticker.toUpperCase() - ? Constants.decimalPlacesMonero - : Constants.decimalPlaces, - )} ${to!.toUpperCase()}", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ); - } else { - Logging.instance.log( - "$runtimeType failed to fetch rate for ChangeNOW: ${snapshot.data}", - level: LogLevel.Warning, - ); - return Text( - "Failed to fetch rate", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ); - } - } else { - return AnimatedText( - stringsToLoopThrough: const [ - "Loading", - "Loading.", - "Loading..", - "Loading...", - ], - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ); - } - }, - ), - if (!(from != null && - to != null && - toAmount != null && - toAmount! > Decimal.zero && - fromAmount != null && - fromAmount! > Decimal.zero)) - Text( - "n/a", - style: STextStyles.itemSubtitle12(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ), - ], - ), + ); + } + }, + ), + if (!(from != null && + to != null && + toAmount != null && + toAmount! > Decimal.zero && + fromAmount != null && + fromAmount! > Decimal.zero)) + Text( + "n/a", + style: STextStyles.itemSubtitle12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ), + ], + ), + ), + ], ), - ], + ), ), ), ), - const SizedBox( - height: 16, - ), - GestureDetector( - onTap: () { - if (ref.read(currentExchangeNameStateProvider.state).state != - SimpleSwapExchange.exchangeName) { - ref.read(currentExchangeNameStateProvider.state).state = - SimpleSwapExchange.exchangeName; - ref.read(exchangeFormStateProvider).exchange = - Exchange.fromName( - ref.read(currentExchangeNameStateProvider.state).state); - } - }, - child: Container( - color: Colors.transparent, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 20, - child: Radio( - activeColor: Theme.of(context) - .extension<StackColors>()! - .radioButtonIconEnabled, - value: SimpleSwapExchange.exchangeName, - groupValue: ref - .watch(currentExchangeNameStateProvider.state) - .state, - onChanged: (value) { - if (value is String) { - ref - .read(currentExchangeNameStateProvider.state) - .state = value; - ref.read(exchangeFormStateProvider).exchange = - Exchange.fromName(ref + if (isDesktop) + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + if (!isDesktop) + const SizedBox( + height: 16, + ), + ConditionalParent( + condition: isDesktop, + builder: (child) => MouseRegion( + cursor: SystemMouseCursors.click, + child: child, + ), + child: GestureDetector( + onTap: () { + if (ref.read(currentExchangeNameStateProvider.state).state != + SimpleSwapExchange.exchangeName) { + ref.read(currentExchangeNameStateProvider.state).state = + SimpleSwapExchange.exchangeName; + ref.read(exchangeFormStateProvider).exchange = + Exchange.fromName(ref + .read(currentExchangeNameStateProvider.state) + .state); + } + }, + child: Container( + color: Colors.transparent, + child: Padding( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: SimpleSwapExchange.exchangeName, + groupValue: ref + .watch(currentExchangeNameStateProvider.state) + .state, + onChanged: (value) { + if (value is String) { + ref .read(currentExchangeNameStateProvider.state) - .state); - } - }, - ), - ), - const SizedBox( - width: 14, - ), - SvgPicture.asset( - Assets.exchange.simpleSwap, - width: 24, - height: 24, - ), - const SizedBox( - width: 10, - ), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - SimpleSwapExchange.exchangeName, - style: STextStyles.titleBold12(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark2, - ), + .state = value; + ref.read(exchangeFormStateProvider).exchange = + Exchange.fromName(ref + .read(currentExchangeNameStateProvider + .state) + .state); + } + }, ), - if (from != null && - to != null && - toAmount != null && - toAmount! > Decimal.zero && - fromAmount != null && - fromAmount! > Decimal.zero) - FutureBuilder( - future: SimpleSwapExchange().getEstimate( - from!, - to!, - // reversed ? toAmount! : fromAmount!, - fromAmount!, - fixedRate, - // reversed, - false, + ), + const SizedBox( + width: 14, + ), + SvgPicture.asset( + Assets.exchange.simpleSwap, + width: isDesktop ? 32 : 24, + height: isDesktop ? 32 : 24, + ), + const SizedBox( + width: 10, + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + SimpleSwapExchange.exchangeName, + style: STextStyles.titleBold12(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark2, + ), ), - builder: (context, - AsyncSnapshot<ExchangeResponse<Estimate>> - snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - final estimate = snapshot.data?.value; - if (estimate != null) { - Decimal rate = (estimate.estimatedAmount / - fromAmount!) - .toDecimal(scaleOnInfinitePrecision: 12); + if (from != null && + to != null && + toAmount != null && + toAmount! > Decimal.zero && + fromAmount != null && + fromAmount! > Decimal.zero) + FutureBuilder( + future: SimpleSwapExchange().getEstimate( + from!, + to!, + // reversed ? toAmount! : fromAmount!, + fromAmount!, + fixedRate, + // reversed, + false, + ), + builder: (context, + AsyncSnapshot<ExchangeResponse<Estimate>> + snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + final estimate = snapshot.data?.value; + if (estimate != null) { + Decimal rate = (estimate.estimatedAmount / + fromAmount!) + .toDecimal( + scaleOnInfinitePrecision: 12); - return Text( - "1 ${from!.toUpperCase()} ~ ${Format.localizedStringAsFixed( - value: rate, - locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), + return Text( + "1 ${from!.toUpperCase()} ~ ${Format.localizedStringAsFixed( + value: rate, + locale: ref.watch( + localeServiceChangeNotifierProvider + .select( + (value) => value.locale), + ), + decimalPlaces: to!.toUpperCase() == + Coin.monero.ticker + .toUpperCase() + ? Constants.decimalPlacesMonero + : Constants.decimalPlaces, + )} ${to!.toUpperCase()}", + style: + STextStyles.itemSubtitle12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ); + } else { + Logging.instance.log( + "$runtimeType failed to fetch rate for SimpleSwap: ${snapshot.data}", + level: LogLevel.Warning, + ); + return Text( + "Failed to fetch rate", + style: + STextStyles.itemSubtitle12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ); + } + } else { + return AnimatedText( + stringsToLoopThrough: const [ + "Loading", + "Loading.", + "Loading..", + "Loading...", + ], + style: STextStyles.itemSubtitle12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, ), - decimalPlaces: to!.toUpperCase() == - Coin.monero.ticker.toUpperCase() - ? Constants.decimalPlacesMonero - : Constants.decimalPlaces, - )} ${to!.toUpperCase()}", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ); - } else { - Logging.instance.log( - "$runtimeType failed to fetch rate for SimpleSwap: ${snapshot.data}", - level: LogLevel.Warning, - ); - return Text( - "Failed to fetch rate", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ); - } - } else { - return AnimatedText( - stringsToLoopThrough: const [ - "Loading", - "Loading.", - "Loading..", - "Loading...", - ], - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ); - } - }, - ), - // if (!(from != null && - // to != null && - // (reversed - // ? toAmount != null && toAmount! > Decimal.zero - // : fromAmount != null && - // fromAmount! > Decimal.zero))) - if (!(from != null && - to != null && - toAmount != null && - toAmount! > Decimal.zero && - fromAmount != null && - fromAmount! > Decimal.zero)) - Text( - "n/a", - style: STextStyles.itemSubtitle12(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ), - ], - ), + ); + } + }, + ), + // if (!(from != null && + // to != null && + // (reversed + // ? toAmount != null && toAmount! > Decimal.zero + // : fromAmount != null && + // fromAmount! > Decimal.zero))) + if (!(from != null && + to != null && + toAmount != null && + toAmount! > Decimal.zero && + fromAmount != null && + fromAmount! > Decimal.zero)) + Text( + "n/a", + style: STextStyles.itemSubtitle12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ), + ], + ), + ), + ], ), - ], + ), ), ), ), From 96453e90541ce78a9c7a2f6e57f81554fc1839c0 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 18 Nov 2022 16:06:03 -0600 Subject: [PATCH 334/426] missing asset declaration --- lib/utilities/assets.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index c423ec491..6fbe61005 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -107,6 +107,7 @@ class _SVG { String get swap => "assets/svg/swap.svg"; String get downloadFolder => "assets/svg/folder-down.svg"; String get lock => "assets/svg/lock-keyhole.svg"; + String get lockOpen => "assets/svg/lock-open.svg"; String get network => "assets/svg/network-wired.svg"; String get networkWired => "assets/svg/network-wired-2.svg"; String get addressBook => "assets/svg/address-book.svg"; From 3ae38c582bdcd70c9ab68560ef832a6a7aad3fdf Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 18 Nov 2022 16:06:29 -0600 Subject: [PATCH 335/426] desktop exchange form layout --- lib/pages/exchange_view/exchange_form.dart | 194 ++++++++++++++------- 1 file changed, 134 insertions(+), 60 deletions(-) diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart index 7b04f90b3..5ece5aba8 100644 --- a/lib/pages/exchange_view/exchange_form.dart +++ b/lib/pages/exchange_view/exchange_form.dart @@ -27,9 +27,12 @@ import 'package:stackwallet/utilities/enums/flush_bar_type.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/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_loading_overlay.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:tuple/tuple.dart'; @@ -54,6 +57,7 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { late final TextEditingController _sendController; late final TextEditingController _receiveController; + final isDesktop = Util.isDesktop; final FocusNode _sendFocusNode = FocusNode(); final FocusNode _receiveFocusNode = FocusNode(); @@ -960,8 +964,8 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { color: Theme.of(context).extension<StackColors>()!.textDark3, ), ), - const SizedBox( - height: 4, + SizedBox( + height: isDesktop ? 10 : 4, ), TextFormField( style: STextStyles.smallMed14(context).copyWith( @@ -970,6 +974,8 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { focusNode: _sendFocusNode, controller: _sendController, textAlign: TextAlign.right, + enableSuggestions: false, + autocorrect: false, onTap: () { if (_sendController.text == "-") { _sendController.text = ""; @@ -1100,68 +1106,122 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { ), ), ), - const SizedBox( - height: 4, + + SizedBox( + height: isDesktop ? 10 : 4, ), - Stack( + if (ref + .watch( + exchangeFormStateProvider.select((value) => value.warning)) + .isNotEmpty && + !ref.watch( + exchangeFormStateProvider.select((value) => value.reversed))) + Text( + ref.watch( + exchangeFormStateProvider.select((value) => value.warning)), + style: STextStyles.errorSmall(context), + ), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Positioned.fill( - child: Align( - alignment: Alignment.bottomLeft, - child: Text( - "You will receive", - style: STextStyles.itemSubtitle(context).copyWith( - color: - Theme.of(context).extension<StackColors>()!.textDark3, - ), + Text( + "You will receive", + style: STextStyles.itemSubtitle(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark3, + ), + ), + ConditionalParent( + condition: isDesktop, + builder: (child) => MouseRegion( + cursor: SystemMouseCursors.click, + child: RoundedContainer( + padding: const EdgeInsets.all(6), + color: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + radiusMultiplier: 0.75, + child: child, ), ), - ), - Center( - child: Column( - children: [ - const SizedBox( - height: 6, + child: GestureDetector( + onTap: () async { + await _swap(); + }, + child: Padding( + padding: const EdgeInsets.all(4), + child: SvgPicture.asset( + Assets.svg.swap, + width: 20, + height: 20, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, ), - GestureDetector( - onTap: () async { - await _swap(); - }, - child: Padding( - padding: const EdgeInsets.all(4), - child: SvgPicture.asset( - Assets.svg.swap, - width: 20, - height: 20, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - ), - ), - ), - const SizedBox( - height: 6, - ), - ], - ), - ), - Positioned.fill( - child: Align( - alignment: ref.watch(exchangeFormStateProvider - .select((value) => value.reversed)) - ? Alignment.bottomRight - : Alignment.topRight, - child: Text( - ref.watch(exchangeFormStateProvider - .select((value) => value.warning)), - style: STextStyles.errorSmall(context), ), ), ), ], ), - const SizedBox( - height: 4, + // Stack( + // children: [ + // Positioned.fill( + // child: Align( + // alignment: Alignment.bottomLeft, + // child: Text( + // "You will receive", + // style: STextStyles.itemSubtitle(context).copyWith( + // color: + // Theme.of(context).extension<StackColors>()!.textDark3, + // ), + // ), + // ), + // ), + // Center( + // child: Column( + // children: [ + // const SizedBox( + // height: 6, + // ), + // GestureDetector( + // onTap: () async { + // await _swap(); + // }, + // child: Padding( + // padding: const EdgeInsets.all(4), + // child: SvgPicture.asset( + // Assets.svg.swap, + // width: 20, + // height: 20, + // color: Theme.of(context) + // .extension<StackColors>()! + // .accentColorDark, + // ), + // ), + // ), + // const SizedBox( + // height: 6, + // ), + // ], + // ), + // ), + // Positioned.fill( + // child: Align( + // alignment: ref.watch(exchangeFormStateProvider + // .select((value) => value.reversed)) + // ? Alignment.bottomRight + // : Alignment.topRight, + // child: Text( + // ref.watch(exchangeFormStateProvider + // .select((value) => value.warning)), + // style: STextStyles.errorSmall(context), + // ), + // ), + // ), + // ], + // ), + SizedBox( + height: isDesktop ? 10 : 4, ), TextFormField( style: STextStyles.smallMed14(context).copyWith( @@ -1169,6 +1229,8 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { ), focusNode: _receiveFocusNode, controller: _receiveController, + enableSuggestions: false, + autocorrect: false, readOnly: ref.watch(prefsChangeNotifierProvider .select((value) => value.exchangeRateType)) == ExchangeRateType.estimated || @@ -1304,16 +1366,27 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { ), ), ), - const SizedBox( - height: 12, + if (ref + .watch( + exchangeFormStateProvider.select((value) => value.warning)) + .isNotEmpty && + ref.watch( + exchangeFormStateProvider.select((value) => value.reversed))) + Text( + ref.watch( + exchangeFormStateProvider.select((value) => value.warning)), + style: STextStyles.errorSmall(context), + ), + SizedBox( + height: isDesktop ? 20 : 12, ), RateTypeToggle( onChanged: onRateTypeChanged, ), if (ref.read(exchangeFormStateProvider).fromAmount != null && ref.read(exchangeFormStateProvider).fromAmount != Decimal.zero) - const SizedBox( - height: 8, + SizedBox( + height: isDesktop ? 20 : 12, ), if (ref.read(exchangeFormStateProvider).fromAmount != null && ref.read(exchangeFormStateProvider).fromAmount != Decimal.zero) @@ -1328,10 +1401,11 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { reversed: ref.watch( exchangeFormStateProvider.select((value) => value.reversed)), ), - const SizedBox( - height: 12, + SizedBox( + height: isDesktop ? 20 : 12, ), PrimaryButton( + buttonHeight: isDesktop ? ButtonHeight.l : null, enabled: ref.watch( exchangeFormStateProvider.select((value) => value.canExchange)), onPressed: ref.watch(exchangeFormStateProvider From 51cfc3f4dfce57fd22832173bdd0f2a3daddbdf0 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 18 Nov 2022 16:14:27 -0600 Subject: [PATCH 336/426] light colors accent blue fix? --- lib/utilities/theme/light_colors.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utilities/theme/light_colors.dart b/lib/utilities/theme/light_colors.dart index ea3a7cb92..896ae4e5e 100644 --- a/lib/utilities/theme/light_colors.dart +++ b/lib/utilities/theme/light_colors.dart @@ -11,7 +11,7 @@ class LightColors extends StackColorTheme { Color get overlay => const Color(0xFF111215); @override - Color get accentColorBlue => const Color(0xFF4C86E9); + Color get accentColorBlue => const Color(0xFF0052DF); @override Color get accentColorGreen => const Color(0xFF4CC0A0); @override From 80802727381722405be4bee6a06941b076d9510d Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Fri, 18 Nov 2022 16:19:38 -0700 Subject: [PATCH 337/426] WIP: delete wallet --- .../wallet_view/desktop_wallet_view.dart | 5 + .../sub_widgets/delete_wallet_button.dart | 238 ++++++++++++++++++ 2 files changed, 243 insertions(+) create mode 100644 lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index 85dde4aba..739dbe1a1 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -8,6 +8,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart'; import 'package:stackwallet/pages/exchange_view/wallet_initiated_exchange_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/my_wallet.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/network_info_button.dart'; @@ -412,6 +413,10 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { WalletKeysButton( walletId: walletId, ), + const SizedBox( + width: 2, + ), + DeleteWalletButton(), const SizedBox( width: 12, ), diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart new file mode 100644 index 000000000..f45367d0b --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart @@ -0,0 +1,238 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.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/stack_text_field.dart'; + +class DeleteWalletButton extends ConsumerStatefulWidget { + const DeleteWalletButton({ + Key? key, + }) : super(key: key); + + @override + ConsumerState<DeleteWalletButton> createState() => _DeleteWalletButton(); +} + +class _DeleteWalletButton extends ConsumerState<DeleteWalletButton> { + late final TextEditingController passwordController; + late final FocusNode passwordFocusNode; + + bool hidePassword = true; + bool _continueEnabled = false; + + Future<void> attentionDelete() async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) => DesktopDialog( + maxWidth: 580, + maxHeight: 400, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: const [ + DesktopDialogCloseButton(), + ], + ), + Column( + children: [ + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 32, vertical: 26), + child: Text( + "Attention!", + style: STextStyles.desktopH2(context), + ), + ), + ], + ), + ], + ), + ), + ); + } + + @override + void initState() { + passwordController = TextEditingController(); + passwordFocusNode = FocusNode(); + + super.initState(); + } + + @override + void dispose() { + passwordController.dispose(); + passwordFocusNode.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return RawMaterialButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(1000), + ), + onPressed: () { + showDialog( + barrierDismissible: true, + context: context, + builder: (context) => DesktopDialog( + maxHeight: 475, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: const [ + DesktopDialogCloseButton(), + ], + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 32, vertical: 26), + child: Column( + children: [ + const SizedBox(height: 16), + Text( + "Delete wallet", + style: STextStyles.desktopH2(context), + ), + const SizedBox(height: 16), + Text( + "Enter your password", + style: STextStyles.desktopTextMedium(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + ), + ), + const SizedBox(height: 24), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("desktopDeleteWalletPasswordFieldKey"), + focusNode: passwordFocusNode, + controller: passwordController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Enter password", + passwordFocusNode, + context, + ).copyWith( + labelStyle: STextStyles.fieldLabel(context), + suffixIcon: UnconstrainedBox( + child: SizedBox( + height: 70, + child: Row( + children: [ + const SizedBox( + width: 24, + ), + GestureDetector( + key: const Key( + "desktopDeleteWalletShowPasswordButtonKey"), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 24, + height: 24, + ), + ), + ), + const SizedBox( + width: 12, + ), + ], + ), + ), + ), + ), + onChanged: (newValue) { + setState(() { + _continueEnabled = + passwordController.text.isNotEmpty; + }); + }, + ), + ), + const SizedBox(height: 50), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SecondaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + label: "Cancel", + onPressed: () { + Navigator.of(context).pop(); + }, + ), + const SizedBox(width: 16), + PrimaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + enabled: _continueEnabled, + label: "Continue", + onPressed: () { + Navigator.of(context).pop(); + + attentionDelete(); + }, + ), + ], + ) + ], + ), + ), + ], + ), + ), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 19, + horizontal: 32, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.ellipsis, + width: 20, + height: 20, + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, + ), + ], + ), + ), + ); + } +} From 92da601fb80350706b412bc72bc2f952120446a0 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Fri, 18 Nov 2022 17:43:35 -0700 Subject: [PATCH 338/426] WIP: delete wallete Attention dialog --- .../sub_widgets/delete_wallet_button.dart | 66 ++++++++++++++++--- 1 file changed, 56 insertions(+), 10 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart index f45367d0b..96930c044 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart @@ -9,6 +9,7 @@ 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'; import 'package:stackwallet/widgets/stack_text_field.dart'; class DeleteWalletButton extends ConsumerStatefulWidget { @@ -34,7 +35,7 @@ class _DeleteWalletButton extends ConsumerState<DeleteWalletButton> { barrierDismissible: true, builder: (context) => DesktopDialog( maxWidth: 580, - maxHeight: 400, + maxHeight: 530, child: Column( children: [ Row( @@ -43,17 +44,62 @@ class _DeleteWalletButton extends ConsumerState<DeleteWalletButton> { DesktopDialogCloseButton(), ], ), - Column( - children: [ - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 32, vertical: 26), - child: Text( + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 26), + child: Column( + children: [ + Text( "Attention!", style: STextStyles.desktopH2(context), ), - ), - ], + const SizedBox( + height: 16, + ), + RoundedContainer( + color: Theme.of(context) + .extension<StackColors>()! + .snackBarBackError, + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Text( + "You are going to permanently delete you wallet.\n\nIf you delete your wallet, " + "the only way you can have access to your funds is by using your backup key." + "\n\nStack Wallet does not keep nor is able to restore your backup key or your wallet." + "\n\nPLEASE SAVE YOUR BACKUP KEY.", + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + ), + ), + ), + ), + const SizedBox(height: 30), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SecondaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + label: "Cancel", + onPressed: () { + Navigator.of(context).pop(); + }, + ), + const SizedBox(width: 16), + PrimaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + label: "View Backup Key", + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ) + ], + ), ), ], ), @@ -84,7 +130,7 @@ class _DeleteWalletButton extends ConsumerState<DeleteWalletButton> { borderRadius: BorderRadius.circular(1000), ), onPressed: () { - showDialog( + showDialog<dynamic>( barrierDismissible: true, context: context, builder: (context) => DesktopDialog( From 5f1a485ed5b4a7f2db44bee6a06172774fafe5a6 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Sat, 19 Nov 2022 11:00:15 -0700 Subject: [PATCH 339/426] WIP: delete wallete stateful widget + attention warning dialog --- .../wallet_view/desktop_wallet_view.dart | 4 +- .../sub_widgets/delete_wallet_button.dart | 253 ++------------- .../desktop_delete_wallet_dialog.dart | 299 ++++++++++++++++++ lib/route_generator.dart | 23 ++ 4 files changed, 350 insertions(+), 229 deletions(-) create mode 100644 lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index 739dbe1a1..5996597b5 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -416,7 +416,9 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { const SizedBox( width: 2, ), - DeleteWalletButton(), + DeleteWalletButton( + walletId: walletId, + ), const SizedBox( width: 12, ), diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart index 96930c044..54f991c37 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart @@ -1,128 +1,37 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/assets.dart'; -import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.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'; -import 'package:stackwallet/widgets/stack_text_field.dart'; + +import 'desktop_delete_wallet_dialog.dart'; class DeleteWalletButton extends ConsumerStatefulWidget { const DeleteWalletButton({ Key? key, + required this.walletId, }) : super(key: key); + final String walletId; + @override ConsumerState<DeleteWalletButton> createState() => _DeleteWalletButton(); } class _DeleteWalletButton extends ConsumerState<DeleteWalletButton> { - late final TextEditingController passwordController; - late final FocusNode passwordFocusNode; - - bool hidePassword = true; - bool _continueEnabled = false; - - Future<void> attentionDelete() async { - await showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) => DesktopDialog( - maxWidth: 580, - maxHeight: 530, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: const [ - DesktopDialogCloseButton(), - ], - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 26), - child: Column( - children: [ - Text( - "Attention!", - style: STextStyles.desktopH2(context), - ), - const SizedBox( - height: 16, - ), - RoundedContainer( - color: Theme.of(context) - .extension<StackColors>()! - .snackBarBackError, - child: Padding( - padding: const EdgeInsets.all(10.0), - child: Text( - "You are going to permanently delete you wallet.\n\nIf you delete your wallet, " - "the only way you can have access to your funds is by using your backup key." - "\n\nStack Wallet does not keep nor is able to restore your backup key or your wallet." - "\n\nPLEASE SAVE YOUR BACKUP KEY.", - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, - ), - ), - ), - ), - const SizedBox(height: 30), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SecondaryButton( - width: 250, - buttonHeight: ButtonHeight.xl, - label: "Cancel", - onPressed: () { - Navigator.of(context).pop(); - }, - ), - const SizedBox(width: 16), - PrimaryButton( - width: 250, - buttonHeight: ButtonHeight.xl, - label: "View Backup Key", - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ], - ) - ], - ), - ), - ], - ), - ), - ); - } + late final String walletId; @override void initState() { - passwordController = TextEditingController(); - passwordFocusNode = FocusNode(); + walletId = widget.walletId; + final managerProvider = + ref.read(walletsChangeNotifierProvider).getManagerProvider(walletId); super.initState(); } - @override - void dispose() { - passwordController.dispose(); - passwordFocusNode.dispose(); - - super.dispose(); - } - @override Widget build(BuildContext context) { return RawMaterialButton( @@ -130,134 +39,22 @@ class _DeleteWalletButton extends ConsumerState<DeleteWalletButton> { borderRadius: BorderRadius.circular(1000), ), onPressed: () { - showDialog<dynamic>( - barrierDismissible: true, + showDialog<void>( context: context, - builder: (context) => DesktopDialog( - maxHeight: 475, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: const [ - DesktopDialogCloseButton(), - ], - ), - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 32, vertical: 26), - child: Column( - children: [ - const SizedBox(height: 16), - Text( - "Delete wallet", - style: STextStyles.desktopH2(context), - ), - const SizedBox(height: 16), - Text( - "Enter your password", - style: STextStyles.desktopTextMedium(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, - ), - ), - const SizedBox(height: 24), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("desktopDeleteWalletPasswordFieldKey"), - focusNode: passwordFocusNode, - controller: passwordController, - style: STextStyles.field(context), - obscureText: hidePassword, - enableSuggestions: false, - autocorrect: false, - decoration: standardInputDecoration( - "Enter password", - passwordFocusNode, - context, - ).copyWith( - labelStyle: STextStyles.fieldLabel(context), - suffixIcon: UnconstrainedBox( - child: SizedBox( - height: 70, - child: Row( - children: [ - const SizedBox( - width: 24, - ), - GestureDetector( - key: const Key( - "desktopDeleteWalletShowPasswordButtonKey"), - onTap: () async { - setState(() { - hidePassword = !hidePassword; - }); - }, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, - width: 24, - height: 24, - ), - ), - ), - const SizedBox( - width: 12, - ), - ], - ), - ), - ), - ), - onChanged: (newValue) { - setState(() { - _continueEnabled = - passwordController.text.isNotEmpty; - }); - }, - ), - ), - const SizedBox(height: 50), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SecondaryButton( - width: 250, - buttonHeight: ButtonHeight.xl, - label: "Cancel", - onPressed: () { - Navigator.of(context).pop(); - }, - ), - const SizedBox(width: 16), - PrimaryButton( - width: 250, - buttonHeight: ButtonHeight.xl, - enabled: _continueEnabled, - label: "Continue", - onPressed: () { - Navigator.of(context).pop(); - - attentionDelete(); - }, - ), - ], - ) - ], + barrierDismissible: false, + builder: (context) => Navigator( + initialRoute: DesktopDeleteWalletDialog.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + RouteGenerator.generateRoute( + RouteSettings( + name: DesktopDeleteWalletDialog.routeName, + arguments: walletId, ), - ), - ], - ), + ) + ]; + }, ), ); }, diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart new file mode 100644 index 000000000..e2ab4fa86 --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart @@ -0,0 +1,299 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.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'; +import 'package:stackwallet/widgets/stack_text_field.dart'; + +import '../../../../../providers/desktop/storage_crypto_handler_provider.dart'; +import '../../../../../providers/global/wallets_provider.dart'; + +class DesktopDeleteWalletDialog extends ConsumerStatefulWidget { + const DesktopDeleteWalletDialog({ + Key? key, + required this.walletId, + }) : super(key: key); + + final String walletId; + + static const String routeName = "/desktopDeleteWalletDialog"; + + @override + ConsumerState<DesktopDeleteWalletDialog> createState() => + _DesktopDeleteWalletDialog(); +} + +class _DesktopDeleteWalletDialog + extends ConsumerState<DesktopDeleteWalletDialog> { + late final TextEditingController passwordController; + late final FocusNode passwordFocusNode; + + bool hidePassword = true; + bool _continueEnabled = false; + + Future<void> attentionDelete() async { + await showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) => DesktopDialog( + maxWidth: 610, + maxHeight: 530, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + DesktopDialogCloseButton( + onPressedOverride: () { + int count = 0; + Navigator.of(context).popUntil((_) => count++ >= 2); + }, + ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 26), + child: Column( + children: [ + Text( + "Attention!", + style: STextStyles.desktopH2(context), + ), + const SizedBox( + height: 16, + ), + RoundedContainer( + color: Theme.of(context) + .extension<StackColors>()! + .snackBarBackError, + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Text( + "You are going to permanently delete you wallet.\n\nIf you delete your wallet, " + "the only way you can have access to your funds is by using your backup key." + "\n\nStack Wallet does not keep nor is able to restore your backup key or your wallet." + "\n\nPLEASE SAVE YOUR BACKUP KEY.", + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + ), + ), + ), + ), + const SizedBox(height: 30), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SecondaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + label: "Cancel", + onPressed: () { + int count = 0; + Navigator.of(context).popUntil((_) => count++ >= 2); + }, + ), + const SizedBox(width: 16), + PrimaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + label: "View Backup Key", + onPressed: () {}, + ), + ], + ) + ], + ), + ), + ], + ), + ), + ); + } + + @override + void initState() { + passwordController = TextEditingController(); + passwordFocusNode = FocusNode(); + + super.initState(); + } + + @override + void dispose() { + passwordController.dispose(); + passwordFocusNode.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + DesktopDialogCloseButton( + onPressedOverride: Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 26), + child: Column( + children: [ + const SizedBox(height: 16), + Text( + "Delete wallet", + style: STextStyles.desktopH2(context), + ), + const SizedBox(height: 16), + Text( + "Enter your password", + style: STextStyles.desktopTextMedium(context).copyWith( + color: + Theme.of(context).extension<StackColors>()!.textDark3, + ), + ), + const SizedBox(height: 24), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("desktopDeleteWalletPasswordFieldKey"), + focusNode: passwordFocusNode, + controller: passwordController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Enter password", + passwordFocusNode, + context, + ).copyWith( + labelStyle: STextStyles.fieldLabel(context), + suffixIcon: UnconstrainedBox( + child: SizedBox( + height: 70, + child: Row( + children: [ + const SizedBox( + width: 24, + ), + GestureDetector( + key: const Key( + "desktopDeleteWalletShowPasswordButtonKey"), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 24, + height: 24, + ), + ), + ), + const SizedBox( + width: 12, + ), + ], + ), + ), + ), + ), + onChanged: (newValue) { + setState(() { + _continueEnabled = passwordController.text.isNotEmpty; + }); + }, + ), + ), + const SizedBox(height: 50), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SecondaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + label: "Cancel", + onPressed: Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + const SizedBox(width: 16), + PrimaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + enabled: _continueEnabled, + label: "Continue", + onPressed: _continueEnabled + ? () async { + final verified = await ref + .read(storageCryptoHandlerProvider) + .verifyPassphrase(passwordController.text); + + if (verified) { + final words = await ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .mnemonic; + + if (mounted) { + Navigator.of(context).pop(); + + attentionDelete(); + } + } else { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Invalid passphrase!", + context: context, + ), + ); + } + } + : null, + ), + ], + ) + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 8ccc923bc..77b5e7d11 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -92,6 +92,7 @@ import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_settings_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/qr_code_desktop_popup_content.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart'; import 'package:stackwallet/pages_desktop_specific/home/notifications/desktop_notifications_view.dart'; @@ -1170,6 +1171,28 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case DesktopDeleteWalletDialog.routeName: + if (args is String) { + return FadePageRoute( + DesktopDeleteWalletDialog( + walletId: args, + ), + RouteSettings( + name: settings.name, + ), + ); + // return getRoute( + // shouldUseMaterialRoute: useMaterialPageRoute, + // builder: (_) => WalletKeysDesktopPopup( + // words: args, + // ), + // settings: RouteSettings( + // name: settings.name, + // ), + // ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case QRCodeDesktopPopupContent.routeName: if (args is String) { return FadePageRoute( From a8faa7b8e7b851c0e919b0678f9d066fc237120a Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Sat, 19 Nov 2022 09:20:43 -0600 Subject: [PATCH 340/426] exchange form desktop routing and dialogs --- lib/pages/exchange_view/exchange_form.dart | 223 +++++++++++++----- .../desktop/simple_desktop_dialog.dart | 65 +++++ 2 files changed, 229 insertions(+), 59 deletions(-) create mode 100644 lib/widgets/desktop/simple_desktop_dialog.dart diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart index 5ece5aba8..a6d736228 100644 --- a/lib/pages/exchange_view/exchange_form.dart +++ b/lib/pages/exchange_view/exchange_form.dart @@ -30,7 +30,11 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/conditional_parent.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/desktop/simple_desktop_dialog.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; @@ -139,14 +143,27 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { .read(exchangeFormStateProvider) .updateMarket(market, true); } catch (e) { - unawaited(showDialog<dynamic>( - context: context, - builder: (_) => const StackDialog( - title: "Fixed rate market error", - message: - "Could not find the specified fixed rate trade pair", + unawaited( + showDialog<dynamic>( + context: context, + builder: (_) { + if (isDesktop) { + return const SimpleDesktopDialog( + title: "Fixed rate market error", + message: + "Could not find the specified fixed rate trade pair", + ); + } else { + return const StackDialog( + title: "Fixed rate market error", + message: + "Could not find the specified fixed rate trade pair", + ); + } + }, ), - )); + ); + return; } }, @@ -229,14 +246,26 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { .read(exchangeFormStateProvider) .updateMarket(market, true); } catch (e) { - unawaited(showDialog<dynamic>( - context: context, - builder: (_) => const StackDialog( - title: "Fixed rate market error", - message: - "Could not find the specified fixed rate trade pair", + unawaited( + showDialog<dynamic>( + context: context, + builder: (_) { + if (isDesktop) { + return const SimpleDesktopDialog( + title: "Fixed rate market error", + message: + "Could not find the specified fixed rate trade pair", + ); + } else { + return const StackDialog( + title: "Fixed rate market error", + message: + "Could not find the specified fixed rate trade pair", + ); + } + }, ), - )); + ); return; } }, @@ -324,7 +353,7 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { await ref.read(exchangeFormStateProvider).swap(); } if (mounted) { - Navigator.of(context).pop(); + Navigator.of(context, rootNavigator: isDesktop).pop(); } _swapLock = false; } @@ -567,14 +596,14 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { ? "-" : ref.read(exchangeFormStateProvider).toAmountString; if (mounted) { - Navigator.of(context).pop(); + Navigator.of(context, rootNavigator: isDesktop).pop(); } return; } } } if (mounted) { - Navigator.of(context).pop(); + Navigator.of(context, rootNavigator: isDesktop).pop(); } if (!(fromTicker == "-" || toTicker == "-")) { unawaited( @@ -620,7 +649,7 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { true, ); if (mounted) { - Navigator.of(context).pop(); + Navigator.of(context, rootNavigator: isDesktop).pop(); } return; case SimpleSwapExchange.exchangeName: @@ -657,7 +686,7 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { ? "-" : ref.read(exchangeFormStateProvider).toAmountString; if (mounted) { - Navigator.of(context).pop(); + Navigator.of(context, rootNavigator: isDesktop).pop(); } return; } @@ -669,7 +698,7 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { } } if (mounted) { - Navigator.of(context).pop(); + Navigator.of(context, rootNavigator: isDesktop).pop(); } unawaited( showFloatingFlushBar( @@ -722,15 +751,27 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { } if (!isAvailable) { - unawaited(showDialog<dynamic>( - context: context, - barrierDismissible: true, - builder: (_) => StackDialog( - title: "Selected trade pair unavailable", - message: - "The $fromTicker - $toTicker market is currently disabled for estimated/floating rate trades", + unawaited( + showDialog<dynamic>( + context: context, + barrierDismissible: true, + builder: (_) { + if (isDesktop) { + return SimpleDesktopDialog( + title: "Selected trade pair unavailable", + message: + "The $fromTicker - $toTicker market is currently disabled for estimated/floating rate trades", + ); + } else { + return StackDialog( + title: "Selected trade pair unavailable", + message: + "The $fromTicker - $toTicker market is currently disabled for estimated/floating rate trades", + ); + } + }, ), - )); + ); return; } rate = @@ -744,37 +785,101 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { shouldCancel = await showDialog<bool?>( context: context, barrierDismissible: true, - builder: (_) => StackDialog( - title: "Failed to update trade estimate", - message: - "${estimate.warningMessage!}\n\nDo you want to attempt trade anyways?", - leftButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Cancel", - style: STextStyles.itemSubtitle12(context), - ), - onPressed: () { - // notify return to cancel - Navigator.of(context).pop(true); - }, - ), - rightButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - child: Text( - "Attempt", - style: STextStyles.button(context), - ), - onPressed: () { - // continue and try to attempt trade - Navigator.of(context).pop(false); - }, - ), - ), + builder: (_) { + if (isDesktop) { + return DesktopDialog( + maxWidth: 500, + maxHeight: 300, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Failed to update trade estimate", + style: STextStyles.desktopH3(context), + ), + const DesktopDialogCloseButton(), + ], + ), + const Spacer(), + Text( + estimate.warningMessage!, + style: STextStyles.desktopTextSmall(context), + ), + const Spacer(), + Text( + "Do you want to attempt trade anyways?", + style: STextStyles.desktopTextSmall(context), + ), + const Spacer( + flex: 2, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: ButtonHeight.l, + onPressed: () => Navigator.of( + context, + rootNavigator: true, + ).pop(true), + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Attempt", + buttonHeight: ButtonHeight.l, + onPressed: () => Navigator.of( + context, + rootNavigator: true, + ).pop(false), + ), + ), + ], + ) + ], + ), + ); + } else { + return StackDialog( + title: "Failed to update trade estimate", + message: + "${estimate.warningMessage!}\n\nDo you want to attempt trade anyways?", + leftButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Cancel", + style: STextStyles.itemSubtitle12(context), + ), + onPressed: () { + // notify return to cancel + Navigator.of(context).pop(true); + }, + ), + rightButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + child: Text( + "Attempt", + style: STextStyles.button(context), + ), + onPressed: () { + // continue and try to attempt trade + Navigator.of(context).pop(false); + }, + ), + ); + } + }, ); } diff --git a/lib/widgets/desktop/simple_desktop_dialog.dart b/lib/widgets/desktop/simple_desktop_dialog.dart new file mode 100644 index 000000000..cd066c221 --- /dev/null +++ b/lib/widgets/desktop/simple_desktop_dialog.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/text_styles.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'; + +class SimpleDesktopDialog extends StatelessWidget { + const SimpleDesktopDialog({ + Key? key, + required this.title, + required this.message, + }) : super(key: key); + + final String title; + final String message; + + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxWidth: 500, + maxHeight: 300, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: STextStyles.desktopH3(context), + ), + const DesktopDialogCloseButton(), + ], + ), + const Spacer(), + Text( + message, + 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, + ), + ), + ], + ) + ], + ), + ); + } +} From b2ff99be19400374c71b9e2fbe6be4eb08bc8147 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Sat, 19 Nov 2022 09:20:58 -0600 Subject: [PATCH 341/426] login loading indicator size --- lib/pages_desktop_specific/desktop_login_view.dart | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/pages_desktop_specific/desktop_login_view.dart b/lib/pages_desktop_specific/desktop_login_view.dart index f60ce2240..f865fad47 100644 --- a/lib/pages_desktop_specific/desktop_login_view.dart +++ b/lib/pages_desktop_specific/desktop_login_view.dart @@ -49,8 +49,15 @@ class _DesktopLoginViewState extends ConsumerState<DesktopLoginView> { unawaited( showDialog( context: context, - builder: (context) => const LoadingIndicator( - width: 200, + builder: (context) => Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: const [ + LoadingIndicator( + width: 200, + height: 200, + ), + ], ), ), ); From cc4dc9e3c71d67745fd0294e38dd74ddd709f413 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Sat, 19 Nov 2022 09:24:32 -0600 Subject: [PATCH 342/426] exchange rate type toggle mouse regions --- .../sub_widgets/rate_type_toggle.dart | 287 ++++++++++-------- 1 file changed, 153 insertions(+), 134 deletions(-) diff --git a/lib/pages/exchange_view/sub_widgets/rate_type_toggle.dart b/lib/pages/exchange_view/sub_widgets/rate_type_toggle.dart index 9697710e8..31ee01ce2 100644 --- a/lib/pages/exchange_view/sub_widgets/rate_type_toggle.dart +++ b/lib/pages/exchange_view/sub_widgets/rate_type_toggle.dart @@ -8,6 +8,7 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; class RateTypeToggle extends ConsumerWidget { @@ -35,145 +36,163 @@ class RateTypeToggle extends ConsumerWidget { child: Row( children: [ Expanded( - child: GestureDetector( - onTap: () { - if (!estimated) { - ref.read(prefsChangeNotifierProvider).exchangeRateType = - ExchangeRateType.estimated; - onChanged?.call(ExchangeRateType.estimated); - } - }, - child: RoundedContainer( - padding: isDesktop - ? const EdgeInsets.all(17) - : const EdgeInsets.all(0), - color: estimated - ? Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG - : Colors.transparent, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SvgPicture.asset( - Assets.svg.lockOpen, - width: 12, - height: 14, - color: isDesktop - ? estimated - ? Theme.of(context) - .extension<StackColors>()! - .accentColorBlue - : Theme.of(context) - .extension<StackColors>()! - .buttonTextSecondary - : estimated - ? Theme.of(context) - .extension<StackColors>()! - .textDark - : Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - const SizedBox( - width: 5, - ), - Text( - "Estimate rate", - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: estimated - ? Theme.of(context) - .extension<StackColors>()! - .accentColorBlue - : Theme.of(context) - .extension<StackColors>()! - .buttonTextSecondary, - ) - : STextStyles.smallMed12(context).copyWith( - color: estimated - ? Theme.of(context) - .extension<StackColors>()! - .textDark - : Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ), - ], + child: ConditionalParent( + condition: isDesktop, + builder: (child) => MouseRegion( + cursor: estimated + ? SystemMouseCursors.basic + : SystemMouseCursors.click, + child: child, + ), + child: GestureDetector( + onTap: () { + if (!estimated) { + ref.read(prefsChangeNotifierProvider).exchangeRateType = + ExchangeRateType.estimated; + onChanged?.call(ExchangeRateType.estimated); + } + }, + child: RoundedContainer( + padding: isDesktop + ? const EdgeInsets.all(17) + : const EdgeInsets.all(0), + color: estimated + ? Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG + : Colors.transparent, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + Assets.svg.lockOpen, + width: 12, + height: 14, + color: isDesktop + ? estimated + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary + : estimated + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + const SizedBox( + width: 5, + ), + Text( + "Estimate rate", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: estimated + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, + ) + : STextStyles.smallMed12(context).copyWith( + color: estimated + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ), + ], + ), ), ), ), ), Expanded( - child: GestureDetector( - onTap: () { - if (estimated) { - ref.read(prefsChangeNotifierProvider).exchangeRateType = - ExchangeRateType.fixed; - onChanged?.call(ExchangeRateType.fixed); - } - }, - child: RoundedContainer( - padding: isDesktop - ? const EdgeInsets.all(17) - : const EdgeInsets.all(0), - color: !estimated - ? Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG - : Colors.transparent, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SvgPicture.asset( - Assets.svg.lock, - width: 12, - height: 14, - color: isDesktop - ? !estimated - ? Theme.of(context) - .extension<StackColors>()! - .accentColorBlue - : Theme.of(context) - .extension<StackColors>()! - .buttonTextSecondary - : !estimated - ? Theme.of(context) - .extension<StackColors>()! - .textDark - : Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - const SizedBox( - width: 5, - ), - Text( - "Fixed rate", - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: !estimated - ? Theme.of(context) - .extension<StackColors>()! - .accentColorBlue - : Theme.of(context) - .extension<StackColors>()! - .buttonTextSecondary, - ) - : STextStyles.smallMed12(context).copyWith( - color: !estimated - ? Theme.of(context) - .extension<StackColors>()! - .textDark - : Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ), - ], + child: ConditionalParent( + condition: isDesktop, + builder: (child) => MouseRegion( + cursor: !estimated + ? SystemMouseCursors.basic + : SystemMouseCursors.click, + child: child, + ), + child: GestureDetector( + onTap: () { + if (estimated) { + ref.read(prefsChangeNotifierProvider).exchangeRateType = + ExchangeRateType.fixed; + onChanged?.call(ExchangeRateType.fixed); + } + }, + child: RoundedContainer( + padding: isDesktop + ? const EdgeInsets.all(17) + : const EdgeInsets.all(0), + color: !estimated + ? Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG + : Colors.transparent, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + Assets.svg.lock, + width: 12, + height: 14, + color: isDesktop + ? !estimated + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary + : !estimated + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + const SizedBox( + width: 5, + ), + Text( + "Fixed rate", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: !estimated + ? Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + : Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, + ) + : STextStyles.smallMed12(context).copyWith( + color: !estimated + ? Theme.of(context) + .extension<StackColors>()! + .textDark + : Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ), + ], + ), ), ), ), From 601001f96df83e182959782e1de05e90e782ce9a Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Sat, 19 Nov 2022 10:01:09 -0600 Subject: [PATCH 343/426] WIP: desktop exchange steps flow ui --- lib/pages/exchange_view/exchange_form.dart | 54 ++++++-- .../exchange_steps/step_scaffold.dart | 55 ++++++++ .../desktop_exchange_steps_indicator.dart | 121 ++++++++++++++++++ 3 files changed, 218 insertions(+), 12 deletions(-) create mode 100644 lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart create mode 100644 lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_exchange_steps_indicator.dart diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart index a6d736228..2d89c7660 100644 --- a/lib/pages/exchange_view/exchange_form.dart +++ b/lib/pages/exchange_view/exchange_form.dart @@ -18,6 +18,7 @@ import 'package:stackwallet/pages/exchange_view/exchange_step_views/step_2_view. import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_provider_options.dart'; import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart'; import 'package:stackwallet/pages/exchange_view/sub_widgets/rate_type_toggle.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart'; import 'package:stackwallet/services/exchange/simpleswap/simpleswap_exchange.dart'; @@ -908,20 +909,49 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { if (walletInitiated) { ref.read(exchangeSendFromWalletIdStateProvider.state).state = Tuple2(walletId!, coin!); - unawaited( - Navigator.of(context).pushNamed( - Step2View.routeName, - arguments: model, - ), - ); + if (isDesktop) { + await showDialog<void>( + context: context, + builder: (context) { + return const DesktopDialog( + maxWidth: 700, + child: StepScaffold( + step: 1, + ), + ); + }, + ); + } else { + unawaited( + Navigator.of(context).pushNamed( + Step2View.routeName, + arguments: model, + ), + ); + } } else { ref.read(exchangeSendFromWalletIdStateProvider.state).state = null; - unawaited( - Navigator.of(context).pushNamed( - Step1View.routeName, - arguments: model, - ), - ); + + if (isDesktop) { + await showDialog<void>( + context: context, + builder: (context) { + return const DesktopDialog( + maxWidth: 700, + child: StepScaffold( + step: 0, + ), + ); + }, + ); + } else { + unawaited( + Navigator.of(context).pushNamed( + Step1View.routeName, + arguments: model, + ), + ); + } } } } 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 new file mode 100644 index 000000000..09aea9dbf --- /dev/null +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/subwidgets/desktop_exchange_steps_indicator.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; + +class StepScaffold extends StatefulWidget { + const StepScaffold({Key? key, required this.step}) : super(key: key); + + final int step; + + @override + State<StepScaffold> createState() => _StepScaffoldState(); +} + +class _StepScaffoldState extends State<StepScaffold> { + int currentStep = 0; + + @override + void initState() { + currentStep = widget.step; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Row( + children: [ + const AppBarBackButton( + isCompact: true, + ), + Text( + "Exchange XXX to XXX", + style: STextStyles.desktopH3(context), + ), + ], + ), + const SizedBox( + height: 32, + ), + DesktopExchangeStepsIndicator( + currentStep: currentStep, + ), + const SizedBox( + height: 32, + ), + Container( + height: 200, + color: Colors.red, + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_exchange_steps_indicator.dart b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_exchange_steps_indicator.dart new file mode 100644 index 000000000..44831bb4b --- /dev/null +++ b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_exchange_steps_indicator.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; + +class DesktopExchangeStepsIndicator extends StatelessWidget { + const DesktopExchangeStepsIndicator({Key? key, required this.currentStep}) + : super(key: key); + + final int currentStep; + + Color getColor(BuildContext context, int step) { + if (currentStep > step) { + return Theme.of(context) + .extension<StackColors>()! + .accentColorBlue + .withOpacity(0.5); + } else if (currentStep < step) { + return Theme.of(context).extension<StackColors>()!.textSubtitle3; + } else { + return Theme.of(context).extension<StackColors>()!.accentColorBlue; + } + } + + static const double verticalSpacing = 4; + static const double horizontalSpacing = 16; + static const double barHeight = 6; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: Column( + children: [ + Text( + "Confirm amount", + style: STextStyles.desktopTextSmall(context).copyWith( + color: getColor(context, 0), + ), + ), + const SizedBox( + height: verticalSpacing, + ), + RoundedContainer( + color: getColor(context, 0), + height: barHeight, + ), + ], + ), + ), + const SizedBox( + width: horizontalSpacing, + ), + Expanded( + child: Column( + children: [ + Text( + "Enter details", + style: STextStyles.desktopTextSmall(context).copyWith( + color: getColor(context, 1), + ), + ), + const SizedBox( + height: verticalSpacing, + ), + RoundedContainer( + color: getColor(context, 1), + height: barHeight, + ), + ], + ), + ), + const SizedBox( + width: horizontalSpacing, + ), + Expanded( + child: Column( + children: [ + Text( + "Confirm details", + style: STextStyles.desktopTextSmall(context).copyWith( + color: getColor(context, 2), + ), + ), + const SizedBox( + height: verticalSpacing, + ), + RoundedContainer( + color: getColor(context, 2), + height: barHeight, + ), + ], + ), + ), + const SizedBox( + width: horizontalSpacing, + ), + Expanded( + child: Column( + children: [ + Text( + "Complete exchange", + style: STextStyles.desktopTextSmall(context).copyWith( + color: getColor(context, 3), + ), + ), + const SizedBox( + height: verticalSpacing, + ), + RoundedContainer( + color: getColor(context, 3), + height: barHeight, + ), + ], + ), + ), + ], + ); + } +} From 90dc9e3116747ca80f0aa541bfce550bbee1113c Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Sat, 19 Nov 2022 11:32:18 -0600 Subject: [PATCH 344/426] mobile button height fix --- .../manage_nodes_views/node_details_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart index 3d49ae6f7..71d764135 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart @@ -349,7 +349,7 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> { Expanded( child: SecondaryButton( label: "Test connection", - buttonHeight: ButtonHeight.l, + buttonHeight: isDesktop ? ButtonHeight.l : null, onPressed: () async { await _testConnection(ref, context); }, From d4d85259e1c89d6f418c044a48e432704c83501b Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Sat, 19 Nov 2022 12:52:32 -0600 Subject: [PATCH 345/426] logging fix --- lib/services/coins/wownero/wownero_wallet.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart index e39d13005..3d5ae3ad6 100644 --- a/lib/services/coins/wownero/wownero_wallet.dart +++ b/lib/services/coins/wownero/wownero_wallet.dart @@ -863,7 +863,8 @@ class WowneroWallet extends CoinServiceAPI { await DB.instance.get<dynamic>(boxName: walletId, key: indexKey) as int; // Use new index to derive a new receiving address final newReceivingAddress = await _generateAddressForChain(0, curIndex); - Logging.instance.log("xmr address in init existing: $newReceivingAddress", + Logging.instance.log( + "wownero address in init existing: $newReceivingAddress", level: LogLevel.Info); _currentReceivingAddress = Future(() => newReceivingAddress); } From 719c7abd49906cc621612aabd19e4e1fe1776f43 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Sat, 19 Nov 2022 13:44:36 -0600 Subject: [PATCH 346/426] clean up logs --- lib/services/coins/monero/monero_wallet.dart | 4 ++-- lib/services/coins/wownero/wownero_wallet.dart | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/services/coins/monero/monero_wallet.dart b/lib/services/coins/monero/monero_wallet.dart index c35323d53..f94f0cd2a 100644 --- a/lib/services/coins/monero/monero_wallet.dart +++ b/lib/services/coins/monero/monero_wallet.dart @@ -185,8 +185,8 @@ class MoneroWallet extends CoinServiceAPI { try { if (walletBase!.syncStatus! is SyncedSyncStatus && walletBase!.syncStatus!.progress() == 1.0) { - Logging.instance - .log("currentSyncingHeight lol", level: LogLevel.Warning); + // Logging.instance + // .log("currentSyncingHeight lol", level: LogLevel.Warning); return getSyncingHeight(); } } catch (e, s) {} diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart index 3d5ae3ad6..e6a531b78 100644 --- a/lib/services/coins/wownero/wownero_wallet.dart +++ b/lib/services/coins/wownero/wownero_wallet.dart @@ -153,7 +153,7 @@ class WowneroWallet extends CoinServiceAPI { try { _height = (walletBase!.syncStatus as SyncingSyncStatus).height; } catch (e, s) { - Logging.instance.log("$e $s", level: LogLevel.Warning); + // Logging.instance.log("$e $s", level: LogLevel.Warning); } int blocksRemaining = -1; @@ -162,7 +162,7 @@ class WowneroWallet extends CoinServiceAPI { blocksRemaining = (walletBase!.syncStatus as SyncingSyncStatus).blocksLeft; } catch (e, s) { - Logging.instance.log("$e $s", level: LogLevel.Warning); + // Logging.instance.log("$e $s", level: LogLevel.Warning); } int currentHeight = _height + blocksRemaining; if (_height == -1 || blocksRemaining == -1) { @@ -186,8 +186,8 @@ class WowneroWallet extends CoinServiceAPI { try { if (walletBase!.syncStatus! is SyncedSyncStatus && walletBase!.syncStatus!.progress() == 1.0) { - Logging.instance - .log("currentSyncingHeight lol", level: LogLevel.Warning); + // Logging.instance + // .log("currentSyncingHeight lol", level: LogLevel.Warning); return getSyncingHeight(); } } catch (e, s) {} @@ -195,7 +195,7 @@ class WowneroWallet extends CoinServiceAPI { try { syncingHeight = (walletBase!.syncStatus as SyncingSyncStatus).height; } catch (e, s) { - Logging.instance.log("$e $s", level: LogLevel.Warning); + // Logging.instance.log("$e $s", level: LogLevel.Warning); } final cachedHeight = DB.instance.get<dynamic>(boxName: walletId, key: "storedSyncingHeight") @@ -418,7 +418,7 @@ class WowneroWallet extends CoinServiceAPI { try { progress = (walletBase!.syncStatus!).progress(); } catch (e, s) { - Logging.instance.log("$e $s", level: LogLevel.Warning); + // Logging.instance.log("$e $s", level: LogLevel.Warning); } await _fetchTransactionData(); From b333253287c44beb6bc86428cdfe3f55eb938df9 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Sat, 19 Nov 2022 15:12:08 -0600 Subject: [PATCH 347/426] reduce minimum window height --- lib/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/main.dart b/lib/main.dart index 66b3bb974..6879b69c5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -77,7 +77,7 @@ void main() async { if (Util.isDesktop) { setWindowTitle('Stack Wallet'); - setWindowMinSize(const Size(1220, 1100)); + setWindowMinSize(const Size(1220, 1000)); setWindowMaxSize(Size.infinite); } From e2a172f7477550da845224003325da7bfbce35c5 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Sat, 19 Nov 2022 18:04:53 -0600 Subject: [PATCH 348/426] firo private/public balance desktop toggle --- assets/images/glasses-hidden.png | Bin 0 -> 1060 bytes assets/images/glasses.png | Bin 0 -> 1955 bytes .../desktop_balance_toggle_button.dart | 60 +++ .../sub_widgets/desktop_wallet_summary.dart | 375 +++++++----------- lib/utilities/assets.dart | 3 + pubspec.yaml | 2 + 6 files changed, 215 insertions(+), 225 deletions(-) create mode 100644 assets/images/glasses-hidden.png create mode 100644 assets/images/glasses.png create mode 100644 lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_balance_toggle_button.dart diff --git a/assets/images/glasses-hidden.png b/assets/images/glasses-hidden.png new file mode 100644 index 0000000000000000000000000000000000000000..9176cc69b914fb6b435fe881fd4987835a0fd5dc GIT binary patch literal 1060 zcmeAS@N?(olHy`uVBq!ia0vp^HXzKw1|+Ti+$;i8Ea{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xg+b<JR45HFasD-dK5TavfC3&Vd9T(EcfWS|Ip ziKnkC`!gmfA#Qn#&!RvBm^D0I978hhy}fZZJ0?`(_(y+ZwT&*^T_Kt*8^v4}zBn|s zgS%Ys5A&U0;vGA}1Y9mG^**?Cg@Rs}NMN9W%SzsBZrvT7x2A`z{&seW=Pb{4-NkuD z#ygE?s+2G3e$L%)U7Y^zukHTNpXP`+DR3a69ZPS9*IE4fA^i0FUx~xAKX=J1OT4Qn z6V;A5CM2evDrp{g;LFLA`<hM~PATk3FnK-w>9?AEiM4&lZ%393nj6eNt<+`IRx3Jj z`A3<)2MX`bC>`Fx>nzLUYIgVBvFV#G?BCJk{ew5%pnLy@3IC%*dU&6A?PceGFtNtA zV|S5E^34|>eM>`g*PMxR*SY@DJne&>-9-6m@!y2z-3+yVc{a<e$~Jy_Q9+SvUz#)f zlDMn4THW;L9crF&z$vrB_Z46Bf~jX^Brg2^ooHDd^M09j1+)6U<qTZXcdIVUIrREc z#I;=}<>&UcAA4wHcRbO?MB!t0g?Ro$<;*$q(~6EN{17>x+%?6<db7#O{qlzoGjlu; z-J{l<9rUPDF#p!9mT#wY=1raS?3d5Rd6nA1$9eWiHQdY&S{5_c&NA|qXpr_FV}`x! zKL)pN*A4bP$q&@cKWFXEdPgZg-{ND(asu*q1PR-FUv{5-=0VZfzpEqHd)~^5D(`;C zwRzV2w9BuS?+cxi7x7xFZsqTVvmbWfED7H7QFnIso==rtn{8i}&$-?Cu(IZQ0>ks| zmG?{dHfipg9#vU=cxu%A2~o>9)vl<onPgdWbHcT#YpY{#@O-WKz*3j1(7?K9x%bo1 zWy|@Otjt~?b0FpE-CAk8?#H|=1w8R3+zSq|*yT99H;xqw3kRB1?|$pi+i9A6v~s1l zKVG)w+21t&D)EoodZ*qI+m(0p)30Ctnh#};-mUuHG0DlR>i9>dx-}t5x>LXYNnL4u zue~ty^$q^)OCPEw<-cEgy|ZrZoTi?~o;Gd2H~+lYb%Qtavc0;lf!}kZ{c~?h*<Skh z<6=zTg9#-P25PbrB9~0cTxZq!d7geKIrVa{tn@n}pVQBjr#X3fyv^B{-)wzBX4B6n zOKox^Pwwi?WOEc?furp<{~34v{Bv>IuQX?1Hc~BdjVMV;EJ?LWE=mPb3`Pb<M!E(@ zx<+Oph89+ahE_)A+6D$z1_t}Z-)}?Fkei>9nO2Eg!#S^TP<g=M>FVdQ&MBb@01M61 A`2YX_ literal 0 HcmV?d00001 diff --git a/assets/images/glasses.png b/assets/images/glasses.png new file mode 100644 index 0000000000000000000000000000000000000000..8c9e7dc276660d6b8284916943c78137a0d6ad2f GIT binary patch literal 1955 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 Fz~8{u`yl`T literal 0 HcmV?d00001 diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_balance_toggle_button.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_balance_toggle_button.dart new file mode 100644 index 000000000..9c890b223 --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_balance_toggle_button.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/providers/wallet/wallet_balance_toggle_state_provider.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; + +class DesktopBalanceToggleButton extends ConsumerWidget { + const DesktopBalanceToggleButton({ + Key? key, + this.onPressed, + }) : super(key: key); + + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SizedBox( + height: 22, + width: 22, + child: MaterialButton( + color: Theme.of(context).extension<StackColors>()!.buttonBackSecondary, + splashColor: Theme.of(context).extension<StackColors>()!.highlight, + onPressed: () { + if (ref.read(walletBalanceToggleStateProvider.state).state == + WalletBalanceToggleState.available) { + ref.read(walletBalanceToggleStateProvider.state).state = + WalletBalanceToggleState.full; + } else { + ref.read(walletBalanceToggleStateProvider.state).state = + WalletBalanceToggleState.available; + } + onPressed?.call(); + }, + elevation: 0, + highlightElevation: 0, + hoverElevation: 0, + padding: EdgeInsets.zero, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: Center( + child: Image( + image: AssetImage( + ref.watch(walletBalanceToggleStateProvider.state).state == + WalletBalanceToggleState.available + ? Assets.png.glassesHidden + : Assets.png.glasses, + ), + width: 16, + ), + ), + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart index f4bfed976..d6e99ce70 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart @@ -1,13 +1,15 @@ import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_refresh_button.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_balance_toggle_button.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/providers/wallet/wallet_balance_toggle_state_provider.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -33,19 +35,6 @@ class _WDesktopWalletSummaryState extends State<DesktopWalletSummary> { late final String walletId; late final ChangeNotifierProvider<Manager> managerProvider; - void showSheet() { - showModalBottomSheet<dynamic>( - backgroundColor: Colors.transparent, - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - builder: (_) => WalletBalanceToggleSheet(walletId: walletId), - ); - } - Decimal? _balanceTotalCached; Decimal? _balanceCached; @@ -59,225 +48,161 @@ class _WDesktopWalletSummaryState extends State<DesktopWalletSummary> { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( + return Consumer( + builder: (context, ref, __) { + final Coin coin = + ref.watch(managerProvider.select((value) => value.coin)); + return Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Consumer( - builder: (_, ref, __) { - final Coin coin = - ref.watch(managerProvider.select((value) => value.coin)); - final externalCalls = ref.watch(prefsChangeNotifierProvider - .select((value) => value.externalCalls)); + Column( + children: [ + Consumer( + builder: (_, ref, __) { + final externalCalls = ref.watch(prefsChangeNotifierProvider + .select((value) => value.externalCalls)); - Future<Decimal>? totalBalanceFuture; - Future<Decimal>? availableBalanceFuture; - if (coin == Coin.firo || coin == Coin.firoTestNet) { - final firoWallet = - ref.watch(managerProvider.select((value) => value.wallet)) + Future<Decimal>? totalBalanceFuture; + Future<Decimal>? availableBalanceFuture; + if (coin == Coin.firo || coin == Coin.firoTestNet) { + final firoWallet = ref.watch( + managerProvider.select((value) => value.wallet)) as FiroWallet; - totalBalanceFuture = firoWallet.availablePublicBalance(); - availableBalanceFuture = firoWallet.availablePrivateBalance(); - } else { - totalBalanceFuture = ref.watch( - managerProvider.select((value) => value.totalBalance)); - - availableBalanceFuture = ref.watch(managerProvider - .select((value) => value.availableBalance)); - } - - final locale = ref.watch(localeServiceChangeNotifierProvider - .select((value) => value.locale)); - - final baseCurrency = ref.watch(prefsChangeNotifierProvider - .select((value) => value.currency)); - - final priceTuple = ref.watch(priceAnd24hChangeNotifierProvider - .select((value) => value.getPrice(coin))); - - final _showAvailable = false; - // ref.watch(walletBalanceToggleStateProvider.state).state == - // WalletBalanceToggleState.available; - - return FutureBuilder( - future: _showAvailable - ? availableBalanceFuture - : totalBalanceFuture, - builder: (fbContext, AsyncSnapshot<Decimal> snapshot) { - if (snapshot.connectionState == ConnectionState.done && - snapshot.hasData && - snapshot.data != null) { - if (_showAvailable) { - _balanceCached = snapshot.data!; - } else { - _balanceTotalCached = snapshot.data!; - } - } - Decimal? balanceToShow = - _showAvailable ? _balanceCached : _balanceTotalCached; - - if (balanceToShow != null) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // GestureDetector( - // onTap: showSheet, - // child: Row( - // children: [ - // if (coin == Coin.firo || - // coin == Coin.firoTestNet) - // Text( - // "${_showAvailable ? "Private" : "Public"} Balance", - // style: STextStyles.subtitle500(context) - // .copyWith( - // color: Theme.of(context) - // .extension<StackColors>()! - // .textFavoriteCard, - // ), - // ), - // if (coin != Coin.firo && - // coin != Coin.firoTestNet) - // Text( - // "${_showAvailable ? "Available" : "Full"} Balance", - // style: STextStyles.subtitle500(context) - // .copyWith( - // color: Theme.of(context) - // .extension<StackColors>()! - // .textFavoriteCard, - // ), - // ), - // const SizedBox( - // width: 4, - // ), - // SvgPicture.asset( - // Assets.svg.chevronDown, - // color: Theme.of(context) - // .extension<StackColors>()! - // .textFavoriteCard, - // width: 8, - // height: 4, - // ), - // ], - // ), - // ), - FittedBox( - fit: BoxFit.scaleDown, - child: Text( - "${Format.localizedStringAsFixed( - value: balanceToShow, - locale: locale, - decimalPlaces: 8, - )} ${coin.ticker}", - style: STextStyles.desktopH3(context), - ), - ), - if (externalCalls) - Text( - "${Format.localizedStringAsFixed( - value: priceTuple.item1 * balanceToShow, - locale: locale, - decimalPlaces: 2, - )} $baseCurrency", - style: STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ), - ], - ); + totalBalanceFuture = firoWallet.availablePublicBalance(); + availableBalanceFuture = + firoWallet.availablePrivateBalance(); } else { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // GestureDetector( - // onTap: showSheet, - // child: Row( - // children: [ - // if (coin == Coin.firo || - // coin == Coin.firoTestNet) - // Text( - // "${_showAvailable ? "Private" : "Public"} Balance", - // style: STextStyles.subtitle500(context) - // .copyWith( - // color: Theme.of(context) - // .extension<StackColors>()! - // .textFavoriteCard, - // ), - // ), - // if (coin != Coin.firo && - // coin != Coin.firoTestNet) - // Text( - // "${_showAvailable ? "Available" : "Full"} Balance", - // style: STextStyles.subtitle500(context) - // .copyWith( - // color: Theme.of(context) - // .extension<StackColors>()! - // .textFavoriteCard, - // ), - // ), - // const SizedBox( - // width: 4, - // ), - // SvgPicture.asset( - // Assets.svg.chevronDown, - // width: 8, - // height: 4, - // color: Theme.of(context) - // .extension<StackColors>()! - // .textFavoriteCard, - // ), - // ], - // ), - // ), - AnimatedText( - stringsToLoopThrough: const [ - "Loading balance ", - "Loading balance. ", - "Loading balance.. ", - "Loading balance..." - ], - style: STextStyles.desktopH3(context).copyWith( - fontSize: 24, - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ), - ), - if (externalCalls) - AnimatedText( - stringsToLoopThrough: const [ - "Loading balance ", - "Loading balance. ", - "Loading balance.. ", - "Loading balance..." - ], - style: STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ), - ], - ); + totalBalanceFuture = ref.watch(managerProvider + .select((value) => value.totalBalance)); + + availableBalanceFuture = ref.watch(managerProvider + .select((value) => value.availableBalance)); } + + final locale = ref.watch(localeServiceChangeNotifierProvider + .select((value) => value.locale)); + + final baseCurrency = ref.watch(prefsChangeNotifierProvider + .select((value) => value.currency)); + + final priceTuple = ref.watch( + priceAnd24hChangeNotifierProvider + .select((value) => value.getPrice(coin))); + + final _showAvailable = ref + .watch(walletBalanceToggleStateProvider.state) + .state == + WalletBalanceToggleState.available; + + return FutureBuilder( + future: _showAvailable + ? availableBalanceFuture + : totalBalanceFuture, + builder: (fbContext, AsyncSnapshot<Decimal> snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData && + snapshot.data != null) { + if (_showAvailable) { + _balanceCached = snapshot.data!; + } else { + _balanceTotalCached = snapshot.data!; + } + } + Decimal? balanceToShow = _showAvailable + ? _balanceCached + : _balanceTotalCached; + + if (balanceToShow != null) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FittedBox( + fit: BoxFit.scaleDown, + child: Text( + "${Format.localizedStringAsFixed( + value: balanceToShow, + locale: locale, + decimalPlaces: 8, + )} ${coin.ticker}", + style: STextStyles.desktopH3(context), + ), + ), + if (externalCalls) + Text( + "${Format.localizedStringAsFixed( + value: priceTuple.item1 * balanceToShow, + locale: locale, + decimalPlaces: 2, + )} $baseCurrency", + style: + STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ), + ], + ); + } else { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AnimatedText( + stringsToLoopThrough: const [ + "Loading balance ", + "Loading balance. ", + "Loading balance.. ", + "Loading balance..." + ], + style: STextStyles.desktopH3(context).copyWith( + fontSize: 24, + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + if (externalCalls) + AnimatedText( + stringsToLoopThrough: const [ + "Loading balance ", + "Loading balance. ", + "Loading balance.. ", + "Loading balance..." + ], + style: + STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ), + ], + ); + } + }, + ); }, - ); - }, + ), + ], ), + if (coin == Coin.firo || coin == Coin.firoTestNet) + const SizedBox( + width: 8, + ), + if (coin == Coin.firo || coin == Coin.firoTestNet) + const DesktopBalanceToggleButton(), + const SizedBox( + width: 8, + ), + WalletRefreshButton( + walletId: walletId, + initialSyncStatus: widget.initialSyncStatus, + ) ], - ), - const SizedBox( - width: 8, - ), - WalletRefreshButton( - walletId: walletId, - initialSyncStatus: widget.initialSyncStatus, - ) - ], + ); + }, ); } } diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 6fbe61005..149d46b3c 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -231,6 +231,9 @@ class _PNG { String get bitcoincash => "assets/images/bitcoincash.png"; String get namecoin => "assets/images/namecoin.png"; + String get glasses => "assets/images/glasses.png"; + String get glassesHidden => "assets/images/glasses-hidden.png"; + String imageFor({required Coin coin}) { switch (coin) { case Coin.bitcoin: diff --git a/pubspec.yaml b/pubspec.yaml index e8f417586..af4370d99 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -202,6 +202,8 @@ flutter: - assets/images/epic-cash.png - assets/images/bitcoincash.png - assets/images/namecoin.png + - assets/images/glasses.png + - assets/images/glasses-hidden.png - assets/svg/plus.svg - assets/svg/gear.svg - assets/svg/bell.svg From 345ed958e080999ac2696ca24850d0d5944bbc39 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 21 Nov 2022 07:44:45 -0600 Subject: [PATCH 349/426] initial window size linux --- lib/main.dart | 2 +- linux/my_application.cc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 6879b69c5..728152951 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -77,7 +77,7 @@ void main() async { if (Util.isDesktop) { setWindowTitle('Stack Wallet'); - setWindowMinSize(const Size(1220, 1000)); + setWindowMinSize(const Size(1220, 900)); setWindowMaxSize(Size.infinite); } diff --git a/linux/my_application.cc b/linux/my_application.cc index 280895e03..9cb3acebd 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -47,7 +47,7 @@ static void my_application_activate(GApplication* application) { gtk_window_set_title(window, "Stack Wallet"); } - gtk_window_set_default_size(window, 720, 1280); + gtk_window_set_default_size(window, 1220, 900); gtk_widget_show(GTK_WIDGET(window)); g_autoptr(FlDartProject) project = fl_dart_project_new(); From b22b4195d6bf3cd4f7bec097a58c0cef7fb737d9 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 21 Nov 2022 09:15:13 -0600 Subject: [PATCH 350/426] desktop exchange steps scaffolding --- lib/pages/exchange_view/exchange_form.dart | 23 ++-- .../exchange_steps/step_scaffold.dart | 28 +++-- .../subwidgets/desktop_step_1.dart | 104 ++++++++++++++++++ .../subwidgets/desktop_step_2.dart | 66 +++++++++++ .../subwidgets/desktop_step_3.dart | 91 +++++++++++++++ .../subwidgets/desktop_step_4.dart | 98 +++++++++++++++++ .../subwidgets/step_one_item.dart | 38 +++++++ .../desktop_exchange_steps_indicator.dart | 32 +++--- 8 files changed, 452 insertions(+), 28 deletions(-) create mode 100644 lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart create mode 100644 lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart create mode 100644 lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart create mode 100644 lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart create mode 100644 lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/step_one_item.dart diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart index 2d89c7660..148c74920 100644 --- a/lib/pages/exchange_view/exchange_form.dart +++ b/lib/pages/exchange_view/exchange_form.dart @@ -19,12 +19,13 @@ import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_provider_op import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart'; import 'package:stackwallet/pages/exchange_view/sub_widgets/rate_type_toggle.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart'; import 'package:stackwallet/services/exchange/simpleswap/simpleswap_exchange.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -913,10 +914,14 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { await showDialog<void>( context: context, builder: (context) { - return const DesktopDialog( - maxWidth: 700, + return DesktopDialog( + maxWidth: 720, + maxHeight: double.infinity, child: StepScaffold( - step: 1, + step: 2, + body: DesktopStep2( + model: model, + ), ), ); }, @@ -936,10 +941,14 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { await showDialog<void>( context: context, builder: (context) { - return const DesktopDialog( - maxWidth: 700, + return DesktopDialog( + maxWidth: 720, + maxHeight: double.infinity, child: StepScaffold( - step: 0, + step: 1, + body: DesktopStep1( + model: model, + ), ), ); }, 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 09aea9dbf..62a293c27 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 @@ -4,8 +4,13 @@ import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; class StepScaffold extends StatefulWidget { - const StepScaffold({Key? key, required this.step}) : super(key: key); + const StepScaffold({ + Key? key, + required this.body, + required this.step, + }) : super(key: key); + final Widget body; final int step; @override @@ -24,11 +29,13 @@ class _StepScaffoldState extends State<StepScaffold> { @override Widget build(BuildContext context) { return Column( + mainAxisAlignment: MainAxisAlignment.start, children: [ Row( children: [ const AppBarBackButton( isCompact: true, + iconSize: 23, ), Text( "Exchange XXX to XXX", @@ -37,17 +44,24 @@ class _StepScaffoldState extends State<StepScaffold> { ], ), const SizedBox( - height: 32, + height: 12, ), - DesktopExchangeStepsIndicator( - currentStep: currentStep, + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: DesktopExchangeStepsIndicator( + currentStep: currentStep, + ), ), const SizedBox( height: 32, ), - Container( - height: 200, - color: Colors.red, + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: widget.body, ), ], ); diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart new file mode 100644 index 000000000..7334cae05 --- /dev/null +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/step_one_item.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class DesktopStep1 extends StatelessWidget { + const DesktopStep1({ + Key? key, + required this.model, + }) : super(key: key); + + final IncompleteExchangeModel model; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text( + "Confirm amount", + style: STextStyles.desktopTextMedium(context), + ), + const SizedBox( + height: 8, + ), + Text( + "Network fees and other exchange charges are included in the rate.", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + const SizedBox( + height: 20, + ), + RoundedWhiteContainer( + borderColor: Theme.of(context).extension<StackColors>()!.background, + padding: const EdgeInsets.all(0), + child: Column( + children: [ + const StepOneItem( + label: "Exchange", + value: "lol", + ), + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + const StepOneItem( + label: "You send", + value: "lol", + ), + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + const StepOneItem( + label: "You receive", + value: "lol", + ), + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + const StepOneItem( + label: "Rate", + value: "lol", + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only( + top: 20, + bottom: 32, + ), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Back", + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of(context).pop, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Next", + buttonHeight: ButtonHeight.l, + onPressed: () { + // todo + }, + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart new file mode 100644 index 000000000..c9072cb76 --- /dev/null +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; + +class DesktopStep2 extends StatelessWidget { + const DesktopStep2({ + Key? key, + required this.model, + }) : super(key: key); + + final IncompleteExchangeModel model; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text( + "Enter exchange details", + style: STextStyles.desktopTextMedium(context), + ), + const SizedBox( + height: 8, + ), + Text( + "Enter your recipient and refund addresses", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + const SizedBox( + height: 20, + ), + // + Padding( + padding: const EdgeInsets.only( + top: 20, + bottom: 32, + ), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Back", + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of(context).pop, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Next", + buttonHeight: ButtonHeight.l, + onPressed: () { + // todo + }, + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart new file mode 100644 index 000000000..1e2743ef5 --- /dev/null +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/step_one_item.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class DesktopStep3 extends StatelessWidget { + const DesktopStep3({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text( + "Confirm exchange details", + style: STextStyles.desktopTextMedium(context), + ), + const SizedBox( + height: 20, + ), + RoundedWhiteContainer( + borderColor: Theme.of(context).extension<StackColors>()!.background, + padding: const EdgeInsets.all(0), + child: Column( + children: [ + const StepOneItem( + label: "Exchange", + value: "lol", + ), + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + const StepOneItem( + label: "You send", + value: "lol", + ), + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + const StepOneItem( + label: "You receive", + value: "lol", + ), + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + const StepOneItem( + label: "Rate", + value: "lol", + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only( + top: 20, + bottom: 32, + ), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Back", + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of(context).pop, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Confirm", + buttonHeight: ButtonHeight.l, + onPressed: () { + // todo + }, + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart new file mode 100644 index 000000000..8604e7c23 --- /dev/null +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/step_one_item.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class DesktopStep4 extends StatelessWidget { + const DesktopStep4({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text( + "Confirm amount", + style: STextStyles.desktopTextMedium(context), + ), + const SizedBox( + height: 8, + ), + Text( + "Network fees and other exchange charges are included in the rate.", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + const SizedBox( + height: 20, + ), + RoundedWhiteContainer( + borderColor: Theme.of(context).extension<StackColors>()!.background, + padding: const EdgeInsets.all(0), + child: Column( + children: [ + const StepOneItem( + label: "Exchange", + value: "lol", + ), + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + const StepOneItem( + label: "You send", + value: "lol", + ), + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + const StepOneItem( + label: "You receive", + value: "lol", + ), + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + const StepOneItem( + label: "Rate", + value: "lol", + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only( + top: 20, + bottom: 32, + ), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Send from Stack Wallet", + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of(context).pop, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Show QR code", + buttonHeight: ButtonHeight.l, + onPressed: () { + // todo + }, + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/step_one_item.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/step_one_item.dart new file mode 100644 index 000000000..001383a17 --- /dev/null +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/step_one_item.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; + +class StepOneItem extends StatelessWidget { + const StepOneItem({ + Key? key, + required this.label, + required this.value, + this.padding = const EdgeInsets.all(16), + }) : super(key: key); + + final String label; + final String value; + final EdgeInsets padding; + + @override + Widget build(BuildContext context) { + return Padding( + padding: padding, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + Text( + value, + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_exchange_steps_indicator.dart b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_exchange_steps_indicator.dart index 44831bb4b..ddcd2e6c4 100644 --- a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_exchange_steps_indicator.dart +++ b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_exchange_steps_indicator.dart @@ -22,9 +22,9 @@ class DesktopExchangeStepsIndicator extends StatelessWidget { } } - static const double verticalSpacing = 4; + static const double verticalSpacing = 6; static const double horizontalSpacing = 16; - static const double barHeight = 6; + static const double barHeight = 4; @override Widget build(BuildContext context) { @@ -35,16 +35,17 @@ class DesktopExchangeStepsIndicator extends StatelessWidget { children: [ Text( "Confirm amount", - style: STextStyles.desktopTextSmall(context).copyWith( - color: getColor(context, 0), + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: getColor(context, 1), ), ), const SizedBox( height: verticalSpacing, ), RoundedContainer( - color: getColor(context, 0), + color: getColor(context, 1), height: barHeight, + width: double.infinity, ), ], ), @@ -57,16 +58,17 @@ class DesktopExchangeStepsIndicator extends StatelessWidget { children: [ Text( "Enter details", - style: STextStyles.desktopTextSmall(context).copyWith( - color: getColor(context, 1), + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: getColor(context, 2), ), ), const SizedBox( height: verticalSpacing, ), RoundedContainer( - color: getColor(context, 1), + color: getColor(context, 2), height: barHeight, + width: double.infinity, ), ], ), @@ -79,16 +81,17 @@ class DesktopExchangeStepsIndicator extends StatelessWidget { children: [ Text( "Confirm details", - style: STextStyles.desktopTextSmall(context).copyWith( - color: getColor(context, 2), + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: getColor(context, 3), ), ), const SizedBox( height: verticalSpacing, ), RoundedContainer( - color: getColor(context, 2), + color: getColor(context, 3), height: barHeight, + width: double.infinity, ), ], ), @@ -101,16 +104,17 @@ class DesktopExchangeStepsIndicator extends StatelessWidget { children: [ Text( "Complete exchange", - style: STextStyles.desktopTextSmall(context).copyWith( - color: getColor(context, 3), + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: getColor(context, 4), ), ), const SizedBox( height: verticalSpacing, ), RoundedContainer( - color: getColor(context, 3), + color: getColor(context, 4), height: barHeight, + width: double.infinity, ), ], ), From 11845b8b05b712cc6a9a931f44c5fb0ab7577de7 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 21 Nov 2022 09:23:11 -0600 Subject: [PATCH 351/426] populate desktop step one trade info --- .../subwidgets/desktop_step_1.dart | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart index 7334cae05..1f892dd52 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart @@ -1,13 +1,16 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; +import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/step_one_item.dart'; +import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; -class DesktopStep1 extends StatelessWidget { +class DesktopStep1 extends ConsumerWidget { const DesktopStep1({ Key? key, required this.model, @@ -16,7 +19,7 @@ class DesktopStep1 extends StatelessWidget { final IncompleteExchangeModel model; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return Column( children: [ Text( @@ -38,33 +41,37 @@ class DesktopStep1 extends StatelessWidget { padding: const EdgeInsets.all(0), child: Column( children: [ - const StepOneItem( + StepOneItem( label: "Exchange", - value: "lol", + value: ref.watch(currentExchangeNameStateProvider.state).state, ), Container( height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - const StepOneItem( + StepOneItem( label: "You send", - value: "lol", + value: + "${model.sendAmount.toStringAsFixed(8)} ${model.sendTicker.toUpperCase()}", ), Container( height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - const StepOneItem( + StepOneItem( label: "You receive", - value: "lol", + value: + "~${model.receiveAmount.toStringAsFixed(8)} ${model.receiveTicker.toUpperCase()}", ), Container( height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - const StepOneItem( - label: "Rate", - value: "lol", + StepOneItem( + label: model.rateType == ExchangeRateType.estimated + ? "Estimated rate" + : "Fixed rate", + value: model.rateInfo, ), ], ), From 2654d50e407b74916ad5ef95b4f27327518247ca Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 21 Nov 2022 09:41:16 -0600 Subject: [PATCH 352/426] populate desktop step two trade info --- .../subwidgets/desktop_step_2.dart | 423 +++++++++++++++++- 1 file changed, 421 insertions(+), 2 deletions(-) diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart index c9072cb76..e1c5a5620 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart @@ -1,16 +1,199 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; +import 'package:stackwallet/pages/address_book_views/address_book_view.dart'; +import 'package:stackwallet/pages/address_book_views/subviews/contact_popup.dart'; +import 'package:stackwallet/pages/exchange_view/choose_from_stack_view.dart'; +import 'package:stackwallet/providers/exchange/exchange_flow_is_active_state_provider.dart'; +import 'package:stackwallet/providers/exchange/exchange_send_from_wallet_id_provider.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/utilities/clipboard_interface.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_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_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/icon_widgets/addressbook_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; -class DesktopStep2 extends StatelessWidget { +class DesktopStep2 extends ConsumerStatefulWidget { const DesktopStep2({ Key? key, required this.model, + this.clipboard = const ClipboardWrapper(), }) : super(key: key); final IncompleteExchangeModel model; + final ClipboardInterface clipboard; + + @override + ConsumerState<DesktopStep2> createState() => _DesktopStep2State(); +} + +class _DesktopStep2State extends ConsumerState<DesktopStep2> { + late final IncompleteExchangeModel model; + late final ClipboardInterface clipboard; + + late final TextEditingController _toController; + late final TextEditingController _refundController; + + late final FocusNode _toFocusNode; + late final FocusNode _refundFocusNode; + + bool isStackCoin(String ticker) { + try { + coinFromTickerCaseInsensitive(ticker); + return true; + } on ArgumentError catch (_) { + return false; + } + } + + void selectRecipientAddressFromStack() { + try { + final coin = coinFromTickerCaseInsensitive( + model.receiveTicker, + ); + Navigator.of(context) + .pushNamed( + ChooseFromStackView.routeName, + arguments: coin, + ) + .then((value) async { + if (value is String) { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(value); + + _toController.text = manager.walletName; + model.recipientAddress = await manager.currentReceivingAddress; + } + }); + } catch (e, s) { + Logging.instance.log("$e\n$s", level: LogLevel.Info); + } + } + + void selectRefundAddressFromStack() { + try { + final coin = coinFromTickerCaseInsensitive( + model.sendTicker, + ); + Navigator.of(context) + .pushNamed( + ChooseFromStackView.routeName, + arguments: coin, + ) + .then((value) async { + if (value is String) { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(value); + + _refundController.text = manager.walletName; + model.refundAddress = await manager.currentReceivingAddress; + } + }); + } catch (e, s) { + Logging.instance.log("$e\n$s", level: LogLevel.Info); + } + } + + void selectRecipientFromAddressBook() { + ref.read(exchangeFlowIsActiveStateProvider.state).state = true; + Navigator.of(context) + .pushNamed( + AddressBookView.routeName, + ) + .then((_) { + ref.read(exchangeFlowIsActiveStateProvider.state).state = false; + + final address = + ref.read(exchangeFromAddressBookAddressStateProvider.state).state; + if (address.isNotEmpty) { + _toController.text = address; + model.recipientAddress = _toController.text; + ref.read(exchangeFromAddressBookAddressStateProvider.state).state = ""; + } + }); + } + + void selectRefundFromAddressBook() { + ref.read(exchangeFlowIsActiveStateProvider.state).state = true; + Navigator.of(context) + .pushNamed( + AddressBookView.routeName, + ) + .then( + (_) { + ref.read(exchangeFlowIsActiveStateProvider.state).state = false; + final address = + ref.read(exchangeFromAddressBookAddressStateProvider.state).state; + if (address.isNotEmpty) { + _refundController.text = address; + model.refundAddress = _refundController.text; + } + }, + ); + } + + @override + void initState() { + model = widget.model; + clipboard = widget.clipboard; + + _toController = TextEditingController(); + _refundController = TextEditingController(); + + _toFocusNode = FocusNode(); + _refundFocusNode = FocusNode(); + + final tuple = ref.read(exchangeSendFromWalletIdStateProvider.state).state; + if (tuple != null) { + if (model.receiveTicker.toLowerCase() == + tuple.item2.ticker.toLowerCase()) { + ref + .read(walletsChangeNotifierProvider) + .getManager(tuple.item1) + .currentReceivingAddress + .then((value) { + _toController.text = value; + model.recipientAddress = _toController.text; + }); + } else { + if (model.sendTicker.toUpperCase() == + tuple.item2.ticker.toUpperCase()) { + ref + .read(walletsChangeNotifierProvider) + .getManager(tuple.item1) + .currentReceivingAddress + .then((value) { + _refundController.text = value; + model.refundAddress = _refundController.text; + }); + } + } + } + + super.initState(); + } + + @override + void dispose() { + _toController.dispose(); + _refundController.dispose(); + + _toFocusNode.dispose(); + _refundFocusNode.dispose(); + + super.dispose(); + } @override Widget build(BuildContext context) { @@ -30,7 +213,243 @@ class DesktopStep2 extends StatelessWidget { const SizedBox( height: 20, ), - // + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Recipient Wallet", + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveSearchIconRight), + ), + if (isStackCoin(model.receiveTicker)) + BlueTextButton( + text: "Choose from stack", + onTap: selectRecipientAddressFromStack, + ), + ], + ), + const SizedBox( + height: 4, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + onTap: () {}, + key: const Key("recipientExchangeStep2ViewAddressFieldKey"), + controller: _toController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + // inputFormatters: <TextInputFormatter>[ + // FilteringTextInputFormatter.allow(RegExp("[a-zA-Z0-9]{34}")), + // ], + toolbarOptions: const ToolbarOptions( + copy: false, + cut: false, + paste: true, + selectAll: false, + ), + focusNode: _toFocusNode, + style: STextStyles.field(context), + onChanged: (value) { + setState(() {}); + }, + decoration: standardInputDecoration( + "Enter the ${model.receiveTicker.toUpperCase()} payout address", + _toFocusNode, + context, + desktopMed: true, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _toController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _toController.text.isNotEmpty + ? TextFieldIconButton( + key: const Key( + "sendViewClearAddressFieldButtonKey"), + onTap: () { + _toController.text = ""; + model.recipientAddress = _toController.text; + setState(() {}); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "sendViewPasteAddressFieldButtonKey"), + onTap: () async { + final ClipboardData? data = await clipboard + .getData(Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + final content = data.text!.trim(); + _toController.text = content; + model.recipientAddress = _toController.text; + setState(() {}); + } + }, + child: _toController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_toController.text.isEmpty) + TextFieldIconButton( + key: const Key("sendViewAddressBookButtonKey"), + onTap: selectRecipientFromAddressBook, + child: const AddressBookIcon(), + ), + ], + ), + ), + ), + ), + ), + ), + const SizedBox( + height: 6, + ), + RoundedWhiteContainer( + child: Text( + "This is the wallet where your ${model.receiveTicker.toUpperCase()} will be sent to.", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + const SizedBox( + height: 24, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Refund Wallet (required)", + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveSearchIconRight), + ), + if (isStackCoin(model.sendTicker)) + BlueTextButton( + text: "Choose from stack", + onTap: selectRefundAddressFromStack, + ), + ], + ), + const SizedBox( + height: 4, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("refundExchangeStep2ViewAddressFieldKey"), + controller: _refundController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + // inputFormatters: <TextInputFormatter>[ + // FilteringTextInputFormatter.allow(RegExp("[a-zA-Z0-9]{34}")), + // ], + toolbarOptions: const ToolbarOptions( + copy: false, + cut: false, + paste: true, + selectAll: false, + ), + focusNode: _refundFocusNode, + style: STextStyles.field(context), + onChanged: (value) { + setState(() {}); + }, + decoration: standardInputDecoration( + "Enter ${model.sendTicker.toUpperCase()} refund address", + _refundFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _refundController.text.isEmpty + ? const EdgeInsets.only(right: 16) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _refundController.text.isNotEmpty + ? TextFieldIconButton( + key: const Key( + "sendViewClearAddressFieldButtonKey"), + onTap: () { + _refundController.text = ""; + model.refundAddress = _refundController.text; + + setState(() {}); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "sendViewPasteAddressFieldButtonKey"), + onTap: () async { + final ClipboardData? data = await clipboard + .getData(Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + final content = data.text!.trim(); + + _refundController.text = content; + model.refundAddress = _refundController.text; + + setState(() {}); + } + }, + child: _refundController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_refundController.text.isEmpty) + TextFieldIconButton( + key: const Key("sendViewAddressBookButtonKey"), + onTap: selectRefundFromAddressBook, + child: const AddressBookIcon(), + ), + ], + ), + ), + ), + ), + ), + ), + const SizedBox( + height: 6, + ), + RoundedWhiteContainer( + borderColor: Theme.of(context).extension<StackColors>()!.background, + child: Text( + "In case something goes wrong during the exchange, we might need a refund address so we can return your coins back to you.", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ), Padding( padding: const EdgeInsets.only( top: 20, From 648c896b9e9a8a38e00faf63762c1bba5c37280d Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 21 Nov 2022 09:49:27 -0600 Subject: [PATCH 353/426] refactor desktop step item --- .../subwidgets/desktop_step_1.dart | 10 ++-- .../subwidgets/desktop_step_3.dart | 10 ++-- .../subwidgets/desktop_step_4.dart | 10 ++-- .../subwidgets/desktop_step_item.dart | 59 +++++++++++++++++++ .../subwidgets/step_one_item.dart | 38 ------------ 5 files changed, 74 insertions(+), 53 deletions(-) create mode 100644 lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart delete mode 100644 lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/step_one_item.dart diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart index 1f892dd52..942747ea2 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart'; -import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/step_one_item.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -41,7 +41,7 @@ class DesktopStep1 extends ConsumerWidget { padding: const EdgeInsets.all(0), child: Column( children: [ - StepOneItem( + DesktopStepItem( label: "Exchange", value: ref.watch(currentExchangeNameStateProvider.state).state, ), @@ -49,7 +49,7 @@ class DesktopStep1 extends ConsumerWidget { height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - StepOneItem( + DesktopStepItem( label: "You send", value: "${model.sendAmount.toStringAsFixed(8)} ${model.sendTicker.toUpperCase()}", @@ -58,7 +58,7 @@ class DesktopStep1 extends ConsumerWidget { height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - StepOneItem( + DesktopStepItem( label: "You receive", value: "~${model.receiveAmount.toStringAsFixed(8)} ${model.receiveTicker.toUpperCase()}", @@ -67,7 +67,7 @@ class DesktopStep1 extends ConsumerWidget { height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - StepOneItem( + DesktopStepItem( label: model.rateType == ExchangeRateType.estimated ? "Estimated rate" : "Fixed rate", diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart index 1e2743ef5..655c3518e 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/step_one_item.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; @@ -25,7 +25,7 @@ class DesktopStep3 extends StatelessWidget { padding: const EdgeInsets.all(0), child: Column( children: [ - const StepOneItem( + const DesktopStepItem( label: "Exchange", value: "lol", ), @@ -33,7 +33,7 @@ class DesktopStep3 extends StatelessWidget { height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - const StepOneItem( + const DesktopStepItem( label: "You send", value: "lol", ), @@ -41,7 +41,7 @@ class DesktopStep3 extends StatelessWidget { height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - const StepOneItem( + const DesktopStepItem( label: "You receive", value: "lol", ), @@ -49,7 +49,7 @@ class DesktopStep3 extends StatelessWidget { height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - const StepOneItem( + const DesktopStepItem( label: "Rate", value: "lol", ), diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart index 8604e7c23..3b3853efb 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/step_one_item.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; @@ -32,7 +32,7 @@ class DesktopStep4 extends StatelessWidget { padding: const EdgeInsets.all(0), child: Column( children: [ - const StepOneItem( + const DesktopStepItem( label: "Exchange", value: "lol", ), @@ -40,7 +40,7 @@ class DesktopStep4 extends StatelessWidget { height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - const StepOneItem( + const DesktopStepItem( label: "You send", value: "lol", ), @@ -48,7 +48,7 @@ class DesktopStep4 extends StatelessWidget { height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - const StepOneItem( + const DesktopStepItem( label: "You receive", value: "lol", ), @@ -56,7 +56,7 @@ class DesktopStep4 extends StatelessWidget { height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - const StepOneItem( + const DesktopStepItem( label: "Rate", value: "lol", ), diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart new file mode 100644 index 000000000..7c777c2dd --- /dev/null +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; + +class DesktopStepItem extends StatelessWidget { + const DesktopStepItem( + {Key? key, + required this.label, + required this.value, + this.padding = const EdgeInsets.all(16), + this.vertical = false}) + : super(key: key); + + final String label; + final String value; + final EdgeInsets padding; + final bool vertical; + + @override + Widget build(BuildContext context) { + return Padding( + padding: padding, + child: ConditionalParent( + condition: vertical, + builder: (child) => Column( + children: [ + child, + const SizedBox( + height: 2, + ), + Text( + value, + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark, + ), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + if (!vertical) + Text( + value, + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/step_one_item.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/step_one_item.dart deleted file mode 100644 index 001383a17..000000000 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/step_one_item.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/utilities/theme/stack_colors.dart'; - -class StepOneItem extends StatelessWidget { - const StepOneItem({ - Key? key, - required this.label, - required this.value, - this.padding = const EdgeInsets.all(16), - }) : super(key: key); - - final String label; - final String value; - final EdgeInsets padding; - - @override - Widget build(BuildContext context) { - return Padding( - padding: padding, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - label, - style: STextStyles.desktopTextExtraExtraSmall(context), - ), - Text( - value, - style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: Theme.of(context).extension<StackColors>()!.textDark, - ), - ), - ], - ), - ); - } -} From c9e2c4abb7c3fe41f9ea4c7b8152d5e00f597b76 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 21 Nov 2022 10:14:27 -0600 Subject: [PATCH 354/426] desktop trade steps 3 and 4 mostly laid out --- .../exchange_step_views/step_4_view.dart | 1 - .../subwidgets/desktop_step_3.dart | 170 ++++++++++++++++-- .../subwidgets/desktop_step_4.dart | 155 ++++++++++++++-- 3 files changed, 295 insertions(+), 31 deletions(-) diff --git a/lib/pages/exchange_view/exchange_step_views/step_4_view.dart b/lib/pages/exchange_view/exchange_step_views/step_4_view.dart index 0921f68e0..a8b403dcf 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 @@ -18,7 +18,6 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; -import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart index 655c3518e..65b6ed2b3 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart @@ -1,13 +1,135 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; +import 'package:stackwallet/models/exchange/response_objects/trade.dart'; +import 'package:stackwallet/pages/exchange_view/exchange_step_views/step_4_view.dart'; +import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart'; +import 'package:stackwallet/providers/exchange/current_exchange_name_state_provider.dart'; +import 'package:stackwallet/providers/exchange/exchange_provider.dart'; +import 'package:stackwallet/providers/global/trades_service_provider.dart'; +import 'package:stackwallet/services/exchange/exchange_response.dart'; +import 'package:stackwallet/services/notifications_api.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_loading_overlay.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; -class DesktopStep3 extends StatelessWidget { - const DesktopStep3({Key? key}) : super(key: key); +class DesktopStep3 extends ConsumerStatefulWidget { + const DesktopStep3({ + Key? key, + required this.model, + }) : super(key: key); + + final IncompleteExchangeModel model; + + @override + ConsumerState<DesktopStep3> createState() => _DesktopStep3State(); +} + +class _DesktopStep3State extends ConsumerState<DesktopStep3> { + late final IncompleteExchangeModel model; + + Future<void> createTrade() async { + unawaited( + showDialog<void>( + context: context, + barrierDismissible: false, + builder: (_) => WillPopScope( + onWillPop: () async => false, + child: Container( + color: Theme.of(context) + .extension<StackColors>()! + .overlay + .withOpacity(0.6), + child: const CustomLoadingOverlay( + message: "Creating a trade", + eventBus: null, + ), + ), + ), + ), + ); + + final ExchangeResponse<Trade> response = + await ref.read(exchangeProvider).createTrade( + from: model.sendTicker, + to: model.receiveTicker, + fixedRate: model.rateType != ExchangeRateType.estimated, + amount: model.reversed ? model.receiveAmount : model.sendAmount, + addressTo: model.recipientAddress!, + extraId: null, + addressRefund: model.refundAddress!, + refundExtraId: "", + rateId: model.rateId, + reversed: model.reversed, + ); + + if (response.value == null) { + if (mounted) { + Navigator.of(context).pop(); + } + + unawaited(showDialog<void>( + context: context, + barrierDismissible: true, + builder: (_) => StackDialog( + title: "Failed to create trade", + message: response.exception?.toString(), + ), + )); + return; + } + + // save trade to hive + await ref.read(tradesServiceProvider).add( + trade: response.value!, + shouldNotifyListeners: true, + ); + + String status = response.value!.status; + + model.trade = response.value!; + + // extra info if status is waiting + if (status == "Waiting") { + status += " for deposit"; + } + + if (mounted) { + Navigator.of(context).pop(); + } + + unawaited(NotificationApi.showNotification( + changeNowId: model.trade!.tradeId, + title: status, + body: "Trade ID ${model.trade!.tradeId}", + walletId: "", + iconAssetName: Assets.svg.arrowRotate, + date: model.trade!.timestamp, + shouldWatchForUpdates: true, + coinName: "coinName", + )); + + if (mounted) { + unawaited(Navigator.of(context).pushNamed( + Step4View.routeName, + arguments: model, + )); + } + } + + @override + void initState() { + model = widget.model; + super.initState(); + } @override Widget build(BuildContext context) { @@ -25,33 +147,55 @@ class DesktopStep3 extends StatelessWidget { padding: const EdgeInsets.all(0), child: Column( children: [ - const DesktopStepItem( + DesktopStepItem( label: "Exchange", - value: "lol", + value: ref.watch(currentExchangeNameStateProvider.state).state, ), Container( height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - const DesktopStepItem( + DesktopStepItem( label: "You send", - value: "lol", + value: + "${model.sendAmount.toStringAsFixed(8)} ${model.sendTicker.toUpperCase()}", ), Container( height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - const DesktopStepItem( + DesktopStepItem( label: "You receive", - value: "lol", + value: + "~${model.receiveAmount.toStringAsFixed(8)} ${model.receiveTicker.toUpperCase()}", ), Container( height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - const DesktopStepItem( - label: "Rate", - value: "lol", + DesktopStepItem( + label: model.rateType == ExchangeRateType.estimated + ? "Estimated rate" + : "Fixed rate", + value: model.rateInfo, + ), + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + DesktopStepItem( + vertical: true, + label: "Recipient ${model.receiveTicker.toUpperCase()} address", + value: model.recipientAddress!, + ), + Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + DesktopStepItem( + vertical: true, + label: "Refund ${model.sendTicker.toUpperCase()} address", + value: model.refundAddress!, ), ], ), @@ -77,9 +221,7 @@ class DesktopStep3 extends StatelessWidget { child: PrimaryButton( label: "Confirm", buttonHeight: ButtonHeight.l, - onPressed: () { - // todo - }, + onPressed: createTrade, ), ), ], diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart index 3b3853efb..ba9838086 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart @@ -1,64 +1,187 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart'; +import 'package:stackwallet/providers/providers.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/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; -class DesktopStep4 extends StatelessWidget { - const DesktopStep4({Key? key}) : super(key: key); +class DesktopStep4 extends ConsumerStatefulWidget { + const DesktopStep4({ + Key? key, + required this.model, + }) : super(key: key); + + final IncompleteExchangeModel model; + + @override + ConsumerState<DesktopStep4> createState() => _DesktopStep4State(); +} + +class _DesktopStep4State extends ConsumerState<DesktopStep4> { + late final IncompleteExchangeModel model; + + String _statusString = "New"; + + Timer? _statusTimer; + + bool _isWalletCoinAndHasWallet(String ticker) { + try { + final coin = coinFromTickerCaseInsensitive(ticker); + return ref + .read(walletsChangeNotifierProvider) + .managers + .where((element) => element.coin == coin) + .isNotEmpty; + } catch (_) { + return false; + } + } + + Future<void> _updateStatus() async { + final statusResponse = + await ref.read(exchangeProvider).updateTrade(model.trade!); + String status = "Waiting"; + if (statusResponse.value != null) { + status = statusResponse.value!.status; + } + + // extra info if status is waiting + if (status == "Waiting") { + status += " for deposit"; + } + + if (mounted) { + setState(() { + _statusString = status; + }); + } + } + + @override + void initState() { + model = widget.model; + + _statusTimer = Timer.periodic(const Duration(seconds: 60), (_) { + _updateStatus(); + }); + + super.initState(); + } + + @override + void dispose() { + _statusTimer?.cancel(); + _statusTimer = null; + super.dispose(); + } @override Widget build(BuildContext context) { return Column( children: [ Text( - "Confirm amount", + "Send ${model.sendTicker.toUpperCase()} to the address below", style: STextStyles.desktopTextMedium(context), ), const SizedBox( height: 8, ), Text( - "Network fees and other exchange charges are included in the rate.", + "Send ${model.sendTicker.toUpperCase()} to the address below. Once it is received, ${model.trade!.exchangeName} will send the ${model.receiveTicker.toUpperCase()} to the recipient address you provided. You can find this trade details and check its status in the list of trades.", style: STextStyles.desktopTextExtraExtraSmall(context), ), const SizedBox( height: 20, ), + RoundedContainer( + color: Theme.of(context).extension<StackColors>()!.warningBackground, + child: RichText( + text: TextSpan( + text: + "You must send at least ${model.sendAmount.toString()} ${model.sendTicker}. ", + style: STextStyles.label700(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .warningForeground, + fontSize: 14, + ), + children: [ + TextSpan( + text: + "If you send less than ${model.sendAmount.toString()} ${model.sendTicker}, your transaction may not be converted and it may not be refunded.", + style: STextStyles.label(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .warningForeground, + fontSize: 14, + ), + ), + ], + ), + ), + ), + const SizedBox( + height: 20, + ), RoundedWhiteContainer( borderColor: Theme.of(context).extension<StackColors>()!.background, padding: const EdgeInsets.all(0), child: Column( children: [ - const DesktopStepItem( - label: "Exchange", - value: "lol", + DesktopStepItem( + vertical: true, + label: "Send ${model.sendTicker.toUpperCase()} to this address", + value: model.trade!.payInAddress, ), Container( height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - const DesktopStepItem( - label: "You send", - value: "lol", + DesktopStepItem( + label: "Amount", + value: + "${model.sendAmount.toString()} ${model.sendTicker.toUpperCase()}", ), Container( height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - const DesktopStepItem( - label: "You receive", - value: "lol", + DesktopStepItem( + label: "Trade ID", + value: model.trade!.tradeId, ), Container( height: 1, color: Theme.of(context).extension<StackColors>()!.background, ), - const DesktopStepItem( - label: "Rate", - value: "lol", + Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Status", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + Text( + _statusString, + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .colorForStatus(_statusString), + ), + ), + ], + ), ), ], ), From 78186358b9de2fc28b989575e1d6bee1c62d1f38 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 21 Nov 2022 10:50:53 -0700 Subject: [PATCH 355/426] WIP: wallet will be deleted dialog --- .../sub_widgets/delete_wallet_keys_popup.dart | 195 ++++++++++++++++++ .../desktop_attention_delete_wallet.dart | 122 +++++++++++ .../desktop_delete_wallet_dialog.dart | 90 +------- lib/route_generator.dart | 47 +++++ 4 files changed, 369 insertions(+), 85 deletions(-) create mode 100644 lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart create mode 100644 lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart new file mode 100644 index 000000000..5f46e0f2f --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart @@ -0,0 +1,195 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.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'; + +class DeleteWalletKeysPopup extends ConsumerStatefulWidget { + const DeleteWalletKeysPopup({ + Key? key, + required this.walletId, + required this.words, + }) : super(key: key); + + final String walletId; + final List<String> words; + + static const String routeName = "/desktopDeleteWalletKeysPopup"; + + @override + ConsumerState<DeleteWalletKeysPopup> createState() => + _DeleteWalletKeysPopup(); +} + +class _DeleteWalletKeysPopup extends ConsumerState<DeleteWalletKeysPopup> { + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxWidth: 614, + maxHeight: double.infinity, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Wallet keys", + style: STextStyles.desktopH3(context), + ), + ), + DesktopDialogCloseButton( + onPressedOverride: () { + int count = 0; + Navigator.of(context).popUntil((_) => count++ >= 2); + }, + ), + ], + ), + const SizedBox( + height: 28, + ), + Text( + "Recovery phrase", + style: STextStyles.desktopTextMedium(context), + ), + const SizedBox( + height: 8, + ), + Center( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: Text( + "Please write down your recovery phrase in the correct order and save it to keep your funds secure. You will also be asked to verify the words on the next screen.", + style: STextStyles.desktopTextExtraExtraSmall(context), + textAlign: TextAlign.center, + ), + ), + ), + const SizedBox( + height: 24, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: MnemonicTable( + words: widget.words, + isDesktop: true, + itemBorderColor: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + ), + ), + const SizedBox( + height: 24, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: Row( + children: [ + Expanded( + child: PrimaryButton( + label: "Continue", + onPressed: () async { + int count = 0; + Navigator.of(context).popUntil((_) => count++ >= 2); + + unawaited( + showDialog( + context: context, + builder: (context) { + return DesktopDialog( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + DesktopDialogCloseButton( + onPressedOverride: () { + int count = 0; + Navigator.of(context) + .popUntil((_) => count++ >= 2); + }, + ), + ], + ), + Column( + children: [ + Text( + "Thanks! " + "\n\nYour wallet will be deleted.", + style: STextStyles.desktopH2(context), + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + SecondaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + label: "Cancel", + onPressed: () { + int count = 0; + Navigator.of(context) + .popUntil( + (_) => count++ >= 2); + }), + const SizedBox(width: 16), + PrimaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + label: "Continue", + onPressed: () async { + // final walletsInstance = + // ref.read(walletsChangeNotifierProvider); + // await ref + // .read(walletsServiceChangeNotifierProvider) + // .deleteWallet(walletId, true); + // + // if (mounted) { + // Navigator.of(context).popUntil( + // ModalRoute.withName(HomeView.routeName)); + // } + + // // wait for widget tree to dispose of any widgets watching the manager + // await Future<void>.delayed(const Duration(seconds: 1)); + // walletsInstance.removeWallet(walletId: walletId); + }), + ], + ) + ], + ), + ], + ), + ); + }), + ); + }, + ), + ), + ], + ), + ), + const SizedBox( + height: 32, + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart new file mode 100644 index 000000000..30546f60b --- /dev/null +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.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'; +import 'package:tuple/tuple.dart'; + +import 'delete_wallet_keys_popup.dart'; + +class DesktopAttentionDeleteWallet extends ConsumerStatefulWidget { + const DesktopAttentionDeleteWallet({ + Key? key, + required this.walletId, + }) : super(key: key); + + final String walletId; + + static const String routeName = "/desktopAttentionDeleteWallet"; + + @override + ConsumerState<DesktopAttentionDeleteWallet> createState() => + _DesktopAttentionDeleteWallet(); +} + +class _DesktopAttentionDeleteWallet + extends ConsumerState<DesktopAttentionDeleteWallet> { + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxWidth: 610, + maxHeight: 530, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + DesktopDialogCloseButton( + onPressedOverride: () { + int count = 0; + Navigator.of(context).popUntil((_) => count++ >= 2); + }, + ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 26), + child: Column( + children: [ + Text( + "Attention!", + style: STextStyles.desktopH2(context), + ), + const SizedBox( + height: 16, + ), + RoundedContainer( + color: Theme.of(context) + .extension<StackColors>()! + .snackBarBackError, + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Text( + "You are going to permanently delete you wallet.\n\nIf you delete your wallet, " + "the only way you can have access to your funds is by using your backup key." + "\n\nStack Wallet does not keep nor is able to restore your backup key or your wallet." + "\n\nPLEASE SAVE YOUR BACKUP KEY.", + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + ), + ), + ), + ), + const SizedBox(height: 30), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SecondaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + label: "Cancel", + onPressed: () { + int count = 0; + Navigator.of(context).popUntil((_) => count++ >= 2); + }, + ), + const SizedBox(width: 16), + PrimaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + label: "View Backup Key", + onPressed: () async { + final words = await ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .mnemonic; + + await Navigator.of(context) + .pushNamed(DeleteWalletKeysPopup.routeName, + arguments: Tuple2( + widget.walletId, + words, + )); + }, + ), + ], + ) + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart index e2ab4fa86..087629673 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -12,7 +13,6 @@ 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'; import 'package:stackwallet/widgets/stack_text_field.dart'; import '../../../../../providers/desktop/storage_crypto_handler_provider.dart'; @@ -41,89 +41,6 @@ class _DesktopDeleteWalletDialog bool hidePassword = true; bool _continueEnabled = false; - Future<void> attentionDelete() async { - await showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) => DesktopDialog( - maxWidth: 610, - maxHeight: 530, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - DesktopDialogCloseButton( - onPressedOverride: () { - int count = 0; - Navigator.of(context).popUntil((_) => count++ >= 2); - }, - ), - ], - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 26), - child: Column( - children: [ - Text( - "Attention!", - style: STextStyles.desktopH2(context), - ), - const SizedBox( - height: 16, - ), - RoundedContainer( - color: Theme.of(context) - .extension<StackColors>()! - .snackBarBackError, - child: Padding( - padding: const EdgeInsets.all(10.0), - child: Text( - "You are going to permanently delete you wallet.\n\nIf you delete your wallet, " - "the only way you can have access to your funds is by using your backup key." - "\n\nStack Wallet does not keep nor is able to restore your backup key or your wallet." - "\n\nPLEASE SAVE YOUR BACKUP KEY.", - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, - ), - ), - ), - ), - const SizedBox(height: 30), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SecondaryButton( - width: 250, - buttonHeight: ButtonHeight.xl, - label: "Cancel", - onPressed: () { - int count = 0; - Navigator.of(context).popUntil((_) => count++ >= 2); - }, - ), - const SizedBox(width: 16), - PrimaryButton( - width: 250, - buttonHeight: ButtonHeight.xl, - label: "View Backup Key", - onPressed: () {}, - ), - ], - ) - ], - ), - ), - ], - ), - ), - ); - } - @override void initState() { passwordController = TextEditingController(); @@ -273,7 +190,10 @@ class _DesktopDeleteWalletDialog if (mounted) { Navigator.of(context).pop(); - attentionDelete(); + await Navigator.of(context).pushNamed( + DesktopAttentionDeleteWallet.routeName, + arguments: widget.walletId, + ); } } else { unawaited( diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 77b5e7d11..cbc4cb343 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -92,6 +92,8 @@ import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_settings_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/qr_code_desktop_popup_content.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart'; @@ -1193,6 +1195,51 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case DesktopAttentionDeleteWallet.routeName: + if (args is String) { + return FadePageRoute( + DesktopAttentionDeleteWallet( + walletId: args, + ), + RouteSettings( + name: settings.name, + ), + ); + // return getRoute( + // shouldUseMaterialRoute: useMaterialPageRoute, + // builder: (_) => WalletKeysDesktopPopup( + // words: args, + // ), + // settings: RouteSettings( + // name: settings.name, + // ), + // ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case DeleteWalletKeysPopup.routeName: + if (args is Tuple2<String, List<String>>) { + return FadePageRoute( + DeleteWalletKeysPopup( + walletId: args.item1, + words: args.item2, + ), + RouteSettings( + name: settings.name, + ), + ); + // return getRoute( + // shouldUseMaterialRoute: useMaterialPageRoute, + // builder: (_) => WalletKeysDesktopPopup( + // words: args, + // ), + // settings: RouteSettings( + // name: settings.name, + // ), + // ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case QRCodeDesktopPopupContent.routeName: if (args is String) { return FadePageRoute( From 5c7cb8a3c5d033dab7f0614ce090f5d783d53b77 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 21 Nov 2022 12:18:35 -0700 Subject: [PATCH 356/426] WIP: unmounted widget --- .../sub_widgets/delete_wallet_keys_popup.dart | 67 +++++++++++++++---- 1 file changed, 53 insertions(+), 14 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart index 5f46e0f2f..f70c2eadf 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart @@ -3,6 +3,9 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_view.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/providers/global/wallets_service_provider.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; @@ -28,6 +31,15 @@ class DeleteWalletKeysPopup extends ConsumerStatefulWidget { } class _DeleteWalletKeysPopup extends ConsumerState<DeleteWalletKeysPopup> { + late final String _walletId; + + @override + void initState() { + _walletId = widget.walletId; + + super.initState(); + } + @override Widget build(BuildContext context) { return DesktopDialog( @@ -113,6 +125,7 @@ class _DeleteWalletKeysPopup extends ConsumerState<DeleteWalletKeysPopup> { context: context, builder: (context) { return DesktopDialog( + maxHeight: 350, child: Column( children: [ Row( @@ -128,13 +141,16 @@ class _DeleteWalletKeysPopup extends ConsumerState<DeleteWalletKeysPopup> { ], ), Column( + crossAxisAlignment: + CrossAxisAlignment.center, children: [ Text( "Thanks! " "\n\nYour wallet will be deleted.", style: STextStyles.desktopH2(context), + textAlign: TextAlign.center, ), - const SizedBox(height: 20), + const SizedBox(height: 50), Row( mainAxisAlignment: MainAxisAlignment.center, @@ -155,20 +171,43 @@ class _DeleteWalletKeysPopup extends ConsumerState<DeleteWalletKeysPopup> { buttonHeight: ButtonHeight.xl, label: "Continue", onPressed: () async { - // final walletsInstance = - // ref.read(walletsChangeNotifierProvider); - // await ref - // .read(walletsServiceChangeNotifierProvider) - // .deleteWallet(walletId, true); - // - // if (mounted) { - // Navigator.of(context).popUntil( - // ModalRoute.withName(HomeView.routeName)); - // } + // int count = 0; + // Navigator.of(context) + // .popUntil( + // (_) => count++ >= 2); - // // wait for widget tree to dispose of any widgets watching the manager - // await Future<void>.delayed(const Duration(seconds: 1)); - // walletsInstance.removeWallet(walletId: walletId); + final walletsInstance = ref.read( + walletsChangeNotifierProvider); + final manager = ref + .read( + walletsChangeNotifierProvider) + .getManager(_walletId); + + final _managerWalletId = + manager.walletId; + + await ref + .read( + walletsServiceChangeNotifierProvider) + .deleteWallet( + manager.walletName, + true); + + if (mounted) { + Navigator.of(context) + .popUntil( + ModalRoute.withName( + MyStackView + .routeName)); + } + + // wait for widget tree to dispose of any widgets watching the manager + await Future<void>.delayed( + const Duration( + seconds: 1)); + walletsInstance.removeWallet( + walletId: + _managerWalletId); }), ], ) From d06c4862b1685b19f48686b8c3c27a7542fedd16 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 21 Nov 2022 11:21:44 -0600 Subject: [PATCH 357/426] desktop exchange coin selection ui --- .../fixed_rate_pair_coin_selection_view.dart | 319 +++++++++--------- ...floating_rate_currency_selection_view.dart | 317 ++++++++--------- lib/pages/exchange_view/exchange_form.dart | 143 +++++++- 3 files changed, 459 insertions(+), 320 deletions(-) diff --git a/lib/pages/exchange_view/exchange_coin_selection/fixed_rate_pair_coin_selection_view.dart b/lib/pages/exchange_view/exchange_coin_selection/fixed_rate_pair_coin_selection_view.dart index 80bdcda62..779d99306 100644 --- a/lib/pages/exchange_view/exchange_coin_selection/fixed_rate_pair_coin_selection_view.dart +++ b/lib/pages/exchange_view/exchange_coin_selection/fixed_rate_pair_coin_selection_view.dart @@ -8,6 +8,8 @@ import 'package:stackwallet/utilities/constants.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/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; @@ -16,8 +18,6 @@ import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; import 'package:tuple/tuple.dart'; -import 'package:stackwallet/utilities/util.dart'; - class FixedRateMarketPairCoinSelectionView extends ConsumerStatefulWidget { const FixedRateMarketPairCoinSelectionView({ Key? key, @@ -120,95 +120,106 @@ class _FixedRateMarketPairCoinSelectionViewState @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 50)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "Choose a coin to exchange", - style: STextStyles.pageTitleH2(context), - ), - ), - body: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ + final isDesktop = Util.isDesktop; + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 50)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Choose a coin to exchange", + style: STextStyles.pageTitleH2(context), + ), + ), + body: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: child, + ), + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isDesktop) const SizedBox( height: 16, ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: _searchController, - focusNode: _searchFocusNode, - onChanged: filter, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - _searchFocusNode, - context, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, - ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: filter, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, ), - suffixIcon: _searchController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchController.text = ""; - }); - }, - ), - ], - ), - ), - ) - : null, ), + suffixIcon: _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, ), ), - const SizedBox( - height: 10, - ), - Text( - "Popular coins", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 12, - ), - Builder(builder: (context) { + ), + const SizedBox( + height: 10, + ), + Text( + "Popular coins", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 12, + ), + Flexible( + child: Builder(builder: (context) { final items = _markets .where((e) => Coin.values .where((coin) => @@ -221,6 +232,7 @@ class _FixedRateMarketPairCoinSelectionViewState padding: const EdgeInsets.all(0), child: ListView.builder( shrinkWrap: true, + primary: isDesktop ? false : null, itemCount: items.length, itemBuilder: (builderContext, index) { final String ticker = @@ -282,84 +294,85 @@ class _FixedRateMarketPairCoinSelectionViewState ), ); }), - const SizedBox( - height: 20, - ), - Text( - "All coins", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 12, - ), - Flexible( - child: RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: ListView.builder( - shrinkWrap: true, - itemCount: _markets.length, - itemBuilder: (builderContext, index) { - final String ticker = - isFrom ? _markets[index].from : _markets[index].to; + ), + const SizedBox( + height: 20, + ), + Text( + "All coins", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 12, + ), + Flexible( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: ListView.builder( + shrinkWrap: true, + primary: isDesktop ? false : null, + itemCount: _markets.length, + itemBuilder: (builderContext, index) { + final String ticker = + isFrom ? _markets[index].from : _markets[index].to; - final tuple = _imageUrlAndNameFor(ticker); - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: GestureDetector( - onTap: () { - Navigator.of(context).pop(ticker); - }, - child: RoundedWhiteContainer( - child: Row( - children: [ - SizedBox( + final tuple = _imageUrlAndNameFor(ticker); + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: GestureDetector( + onTap: () { + Navigator.of(context).pop(ticker); + }, + child: RoundedWhiteContainer( + child: Row( + children: [ + SizedBox( + width: 24, + height: 24, + child: SvgPicture.network( + tuple.item1, width: 24, height: 24, - child: SvgPicture.network( - tuple.item1, - width: 24, - height: 24, - placeholderBuilder: (_) => - const LoadingIndicator(), - ), + placeholderBuilder: (_) => + const LoadingIndicator(), ), - const SizedBox( - width: 10, + ), + const SizedBox( + width: 10, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + tuple.item2, + style: STextStyles.largeMedium14(context), + ), + const SizedBox( + height: 2, + ), + Text( + ticker.toUpperCase(), + style: STextStyles.smallMed12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ), + ], ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - tuple.item2, - style: STextStyles.largeMedium14(context), - ), - const SizedBox( - height: 2, - ), - Text( - ticker.toUpperCase(), - style: STextStyles.smallMed12(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ), - ], - ), - ), - ], - ), + ), + ], ), ), - ); - }, - ), + ), + ); + }, ), ), - ], - ), + ), + ], ), ); } diff --git a/lib/pages/exchange_view/exchange_coin_selection/floating_rate_currency_selection_view.dart b/lib/pages/exchange_view/exchange_coin_selection/floating_rate_currency_selection_view.dart index e1c1addd2..eb7a99299 100644 --- a/lib/pages/exchange_view/exchange_coin_selection/floating_rate_currency_selection_view.dart +++ b/lib/pages/exchange_view/exchange_coin_selection/floating_rate_currency_selection_view.dart @@ -6,6 +6,8 @@ import 'package:stackwallet/utilities/constants.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/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; @@ -13,8 +15,6 @@ import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; -import 'package:stackwallet/utilities/util.dart'; - class FloatingRateCurrencySelectionView extends StatefulWidget { const FloatingRateCurrencySelectionView({ Key? key, @@ -76,96 +76,109 @@ class _FloatingRateCurrencySelectionViewState @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 50)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "Choose a coin to exchange", - style: STextStyles.pageTitleH2(context), - ), - ), - body: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ + final isDesktop = Util.isDesktop; + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 50)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Choose a coin to exchange", + style: STextStyles.pageTitleH2(context), + ), + ), + body: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: child, + ), + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max, + children: [ + if (!isDesktop) const SizedBox( height: 16, ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: _searchController, - focusNode: _searchFocusNode, - onChanged: filter, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - _searchFocusNode, - context, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, - ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: filter, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, ), - suffixIcon: _searchController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchController.text = ""; - }); - filter(""); - }, - ), - ], - ), - ), - ) - : null, ), + suffixIcon: _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + }); + filter(""); + }, + ), + ], + ), + ), + ) + : null, ), ), - const SizedBox( - height: 10, - ), - Text( - "Popular coins", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 12, - ), - Builder(builder: (context) { + ), + const SizedBox( + height: 10, + ), + Text( + "Popular coins", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 12, + ), + Flexible( + child: Builder(builder: (context) { final items = _currencies .where((e) => Coin.values .where((coin) => @@ -177,6 +190,7 @@ class _FloatingRateCurrencySelectionViewState padding: const EdgeInsets.all(0), child: ListView.builder( shrinkWrap: true, + primary: isDesktop ? false : null, itemCount: items.length, itemBuilder: (builderContext, index) { return Padding( @@ -234,80 +248,81 @@ class _FloatingRateCurrencySelectionViewState ), ); }), - const SizedBox( - height: 20, - ), - Text( - "All coins", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 12, - ), - Flexible( - child: RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: ListView.builder( - shrinkWrap: true, - itemCount: _currencies.length, - itemBuilder: (builderContext, index) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: GestureDetector( - onTap: () { - Navigator.of(context).pop(_currencies[index]); - }, - child: RoundedWhiteContainer( - child: Row( - children: [ - SizedBox( + ), + const SizedBox( + height: 20, + ), + Text( + "All coins", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 12, + ), + Flexible( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: ListView.builder( + shrinkWrap: true, + primary: isDesktop ? false : null, + itemCount: _currencies.length, + itemBuilder: (builderContext, index) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: GestureDetector( + onTap: () { + Navigator.of(context).pop(_currencies[index]); + }, + child: RoundedWhiteContainer( + child: Row( + children: [ + SizedBox( + width: 24, + height: 24, + child: SvgPicture.network( + _currencies[index].image, width: 24, height: 24, - child: SvgPicture.network( - _currencies[index].image, - width: 24, - height: 24, - placeholderBuilder: (_) => - const LoadingIndicator(), - ), + placeholderBuilder: (_) => + const LoadingIndicator(), ), - const SizedBox( - width: 10, + ), + const SizedBox( + width: 10, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _currencies[index].name, + style: STextStyles.largeMedium14(context), + ), + const SizedBox( + height: 2, + ), + Text( + _currencies[index].ticker.toUpperCase(), + style: STextStyles.smallMed12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + ), + ], ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - _currencies[index].name, - style: STextStyles.largeMedium14(context), - ), - const SizedBox( - height: 2, - ), - Text( - _currencies[index].ticker.toUpperCase(), - style: STextStyles.smallMed12(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ), - ), - ], - ), - ), - ], - ), + ), + ], ), ), - ); - }, - ), + ), + ); + }, ), ), - ], - ), + ), + ], ), ); } diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart index 148c74920..cdc6f16b9 100644 --- a/lib/pages/exchange_view/exchange_form.dart +++ b/lib/pages/exchange_view/exchange_form.dart @@ -39,6 +39,7 @@ import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/desktop/simple_desktop_dialog.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:tuple/tuple.dart'; @@ -410,13 +411,65 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { } }).toList(growable: false); - final result = await Navigator.of(context).push( - MaterialPageRoute<dynamic>( - builder: (_) => FloatingRateCurrencySelectionView( - currencies: tickers, - ), - ), - ); + final result = isDesktop + ? await showDialog<Currency?>( + context: context, + builder: (context) { + return DesktopDialog( + maxHeight: 700, + maxWidth: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Choose a coin to exchange", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(16), + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: FloatingRateCurrencySelectionView( + currencies: tickers, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + }) + : await Navigator.of(context).push( + MaterialPageRoute<dynamic>( + builder: (_) => FloatingRateCurrencySelectionView( + currencies: tickers, + ), + ), + ); if (mounted && result is Currency) { onSelected(result); @@ -490,15 +543,73 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { .toList(growable: false); } - final result = await Navigator.of(context).push( - MaterialPageRoute<dynamic>( - builder: (_) => FixedRateMarketPairCoinSelectionView( - markets: marketsThatPairWithExcludedTicker, - currencies: ref.read(availableChangeNowCurrenciesProvider).currencies, - isFrom: excludedTicker != fromTicker, - ), - ), - ); + final result = isDesktop + ? await showDialog<String?>( + context: context, + builder: (context) { + return DesktopDialog( + maxHeight: 700, + maxWidth: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Choose a coin to exchange", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(16), + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: FixedRateMarketPairCoinSelectionView( + markets: marketsThatPairWithExcludedTicker, + currencies: ref + .read( + availableChangeNowCurrenciesProvider) + .currencies, + isFrom: excludedTicker != fromTicker, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + }) + : await Navigator.of(context).push( + MaterialPageRoute<dynamic>( + builder: (_) => FixedRateMarketPairCoinSelectionView( + markets: marketsThatPairWithExcludedTicker, + currencies: + ref.read(availableChangeNowCurrenciesProvider).currencies, + isFrom: excludedTicker != fromTicker, + ), + ), + ); if (mounted && result is String) { onSelected(result); From 3e9039ac90b724cb8886e17e6907c677fcdc36d2 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 21 Nov 2022 11:26:34 -0600 Subject: [PATCH 358/426] show to and from tickers in exchange steps flow --- lib/pages/exchange_view/exchange_form.dart | 2 ++ .../desktop_exchange/exchange_steps/step_scaffold.dart | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart index cdc6f16b9..7faa83a35 100644 --- a/lib/pages/exchange_view/exchange_form.dart +++ b/lib/pages/exchange_view/exchange_form.dart @@ -1030,6 +1030,7 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { maxHeight: double.infinity, child: StepScaffold( step: 2, + model: model, body: DesktopStep2( model: model, ), @@ -1057,6 +1058,7 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { maxHeight: double.infinity, child: StepScaffold( step: 1, + model: model, body: DesktopStep1( model: model, ), 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 62a293c27..8dbaf9580 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 @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_exchange/subwidgets/desktop_exchange_steps_indicator.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; @@ -8,10 +9,12 @@ class StepScaffold extends StatefulWidget { Key? key, required this.body, required this.step, + required this.model, }) : super(key: key); final Widget body; final int step; + final IncompleteExchangeModel model; @override State<StepScaffold> createState() => _StepScaffoldState(); @@ -19,10 +22,12 @@ class StepScaffold extends StatefulWidget { class _StepScaffoldState extends State<StepScaffold> { int currentStep = 0; + late final IncompleteExchangeModel model; @override void initState() { currentStep = widget.step; + model = widget.model; super.initState(); } @@ -38,7 +43,7 @@ class _StepScaffoldState extends State<StepScaffold> { iconSize: 23, ), Text( - "Exchange XXX to XXX", + "Exchange ${model.sendTicker.toUpperCase()} to ${model.receiveTicker.toUpperCase()}", style: STextStyles.desktopH3(context), ), ], From 3fca3d8b1e9d7ce15a5610c60342f94363e93cf5 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 21 Nov 2022 12:03:15 -0600 Subject: [PATCH 359/426] desktop exchange flow styling and choose addresses from addressbook functionality --- .../subwidgets/desktop_step_1.dart | 23 ++- .../subwidgets/desktop_step_2.dart | 139 +++++++++++++----- 2 files changed, 121 insertions(+), 41 deletions(-) diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart index 942747ea2..a97b722ef 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart @@ -2,10 +2,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.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/rounded_white_container.dart'; @@ -97,8 +100,24 @@ class DesktopStep1 extends ConsumerWidget { child: PrimaryButton( label: "Next", buttonHeight: ButtonHeight.l, - onPressed: () { - // todo + onPressed: () async { + await showDialog<void>( + context: context, + barrierColor: Colors.transparent, + builder: (context) { + return DesktopDialog( + maxWidth: 720, + maxHeight: double.infinity, + child: StepScaffold( + step: 2, + model: model, + body: DesktopStep2( + model: model, + ), + ), + ); + }, + ); }, ), ), diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart index e1c5a5620..38c01822e 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart @@ -2,10 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; -import 'package:stackwallet/pages/address_book_views/address_book_view.dart'; -import 'package:stackwallet/pages/address_book_views/subviews/contact_popup.dart'; import 'package:stackwallet/pages/exchange_view/choose_from_stack_view.dart'; -import 'package:stackwallet/providers/exchange/exchange_flow_is_active_state_provider.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart'; import 'package:stackwallet/providers/exchange/exchange_send_from_wallet_id_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; @@ -24,6 +22,10 @@ import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; +import '../../../../models/contact_address_entry.dart'; +import '../../../../widgets/desktop/desktop_dialog.dart'; +import '../../../../widgets/desktop/desktop_dialog_close_button.dart'; + class DesktopStep2 extends ConsumerStatefulWidget { const DesktopStep2({ Key? key, @@ -105,42 +107,96 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { } } - void selectRecipientFromAddressBook() { - ref.read(exchangeFlowIsActiveStateProvider.state).state = true; - Navigator.of(context) - .pushNamed( - AddressBookView.routeName, - ) - .then((_) { - ref.read(exchangeFlowIsActiveStateProvider.state).state = false; + void selectRecipientFromAddressBook() async { + final coin = coinFromTickerCaseInsensitive( + model.receiveTicker, + ); - final address = - ref.read(exchangeFromAddressBookAddressStateProvider.state).state; - if (address.isNotEmpty) { - _toController.text = address; - model.recipientAddress = _toController.text; - ref.read(exchangeFromAddressBookAddressStateProvider.state).state = ""; - } - }); + final entry = await showDialog<ContactAddressEntry?>( + context: context, + barrierColor: Colors.transparent, + builder: (context) => DesktopDialog( + maxWidth: 720, + maxHeight: 670, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Address book", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: AddressBookAddressChooser( + coin: coin, + ), + ), + ], + ), + ), + ); + + if (entry != null) { + _toController.text = entry.address; + model.recipientAddress = entry.address; + setState(() {}); + } } - void selectRefundFromAddressBook() { - ref.read(exchangeFlowIsActiveStateProvider.state).state = true; - Navigator.of(context) - .pushNamed( - AddressBookView.routeName, - ) - .then( - (_) { - ref.read(exchangeFlowIsActiveStateProvider.state).state = false; - final address = - ref.read(exchangeFromAddressBookAddressStateProvider.state).state; - if (address.isNotEmpty) { - _refundController.text = address; - model.refundAddress = _refundController.text; - } - }, + void selectRefundFromAddressBook() async { + final coin = coinFromTickerCaseInsensitive( + model.sendTicker, ); + + final entry = await showDialog<ContactAddressEntry?>( + context: context, + barrierColor: Colors.transparent, + builder: (context) => DesktopDialog( + maxWidth: 720, + maxHeight: 670, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Address book", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: AddressBookAddressChooser( + coin: coin, + ), + ), + ], + ), + ), + ); + + if (entry != null) { + _refundController.text = entry.address; + model.refundAddress = entry.address; + setState(() {}); + } } @override @@ -198,10 +254,12 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { @override Widget build(BuildContext context) { return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( "Enter exchange details", style: STextStyles.desktopTextMedium(context), + textAlign: TextAlign.center, ), const SizedBox( height: 8, @@ -209,6 +267,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { Text( "Enter your recipient and refund addresses", style: STextStyles.desktopTextExtraExtraSmall(context), + textAlign: TextAlign.center, ), const SizedBox( height: 20, @@ -231,7 +290,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { ], ), const SizedBox( - height: 4, + height: 10, ), ClipRRect( borderRadius: BorderRadius.circular( @@ -321,9 +380,10 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { ), ), const SizedBox( - height: 6, + height: 10, ), RoundedWhiteContainer( + borderColor: Theme.of(context).extension<StackColors>()!.background, child: Text( "This is the wallet where your ${model.receiveTicker.toUpperCase()} will be sent to.", style: STextStyles.desktopTextExtraExtraSmall(context), @@ -350,7 +410,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { ], ), const SizedBox( - height: 4, + height: 10, ), ClipRRect( borderRadius: BorderRadius.circular( @@ -380,6 +440,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { "Enter ${model.sendTicker.toUpperCase()} refund address", _refundFocusNode, context, + desktopMed: true, ).copyWith( contentPadding: const EdgeInsets.only( left: 16, @@ -441,7 +502,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { ), ), const SizedBox( - height: 6, + height: 10, ), RoundedWhiteContainer( borderColor: Theme.of(context).extension<StackColors>()!.background, From 7e8f0db96726f509bfb9f20ef18de7b1a5021ed8 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 21 Nov 2022 12:07:15 -0600 Subject: [PATCH 360/426] long address layout fix --- .../sub_widgets/contact_list_item.dart | 68 +++++++++++-------- 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/sub_widgets/contact_list_item.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/sub_widgets/contact_list_item.dart index 7acfaae9e..d7bfefb1f 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/sub_widgets/contact_list_item.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/sub_widgets/contact_list_item.dart @@ -87,35 +87,47 @@ class _ContactListItemState extends ConsumerState<ContactListItem> { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - children: [ - WalletInfoCoinIcon(coin: e.coin), - const SizedBox( - width: 12, - ), - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "${contactId == "default" ? e.other! : e.label} (${e.coin.ticker})", - style: STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ), + Flexible( + child: Row( + children: [ + WalletInfoCoinIcon(coin: e.coin), + const SizedBox( + width: 12, + ), + Flexible( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "${contactId == "default" ? e.other! : e.label} (${e.coin.ticker})", + style: STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + Row( + children: [ + Flexible( + child: Text( + e.address, + style: STextStyles + .desktopTextExtraExtraSmall( + context), + ), + ), + ], + ), + ], ), - Text( - e.address, - style: STextStyles - .desktopTextExtraExtraSmall(context), - ), - ], - ), - ], + ), + ], + ), ), BlueTextButton( text: "Select wallet", From 04b982fb250156d3d96e3b44e955a2bd890dfd02 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 21 Nov 2022 13:21:52 -0600 Subject: [PATCH 361/426] desktop exchange choose from stack address ui --- .../subwidgets/desktop_step_2.dart | 50 ++- .../subwidgets/desktop_choose_from_stack.dart | 329 ++++++++++++++++++ 2 files changed, 364 insertions(+), 15 deletions(-) create mode 100644 lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart index 38c01822e..270b98fc7 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; -import 'package:stackwallet/pages/exchange_view/choose_from_stack_view.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart'; import 'package:stackwallet/providers/exchange/exchange_send_from_wallet_id_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; @@ -64,12 +64,21 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { final coin = coinFromTickerCaseInsensitive( model.receiveTicker, ); - Navigator.of(context) - .pushNamed( - ChooseFromStackView.routeName, - arguments: coin, - ) - .then((value) async { + + showDialog<String?>( + context: context, + barrierColor: Colors.transparent, + builder: (context) => DesktopDialog( + maxWidth: 720, + maxHeight: 670, + child: Padding( + padding: const EdgeInsets.all(32), + child: DesktopChooseFromStack( + coin: coin, + ), + ), + ), + ).then((value) async { if (value is String) { final manager = ref.read(walletsChangeNotifierProvider).getManager(value); @@ -88,12 +97,21 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { final coin = coinFromTickerCaseInsensitive( model.sendTicker, ); - Navigator.of(context) - .pushNamed( - ChooseFromStackView.routeName, - arguments: coin, - ) - .then((value) async { + + showDialog<String?>( + context: context, + barrierColor: Colors.transparent, + builder: (context) => DesktopDialog( + maxWidth: 720, + maxHeight: 670, + child: Padding( + padding: const EdgeInsets.all(32), + child: DesktopChooseFromStack( + coin: coin, + ), + ), + ), + ).then((value) async { if (value is String) { final manager = ref.read(walletsChangeNotifierProvider).getManager(value); @@ -366,7 +384,8 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { ? const ClipboardIcon() : const XIcon(), ), - if (_toController.text.isEmpty) + if (_toController.text.isEmpty && + isStackCoin(model.receiveTicker)) TextFieldIconButton( key: const Key("sendViewAddressBookButtonKey"), onTap: selectRecipientFromAddressBook, @@ -488,7 +507,8 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { ? const ClipboardIcon() : const XIcon(), ), - if (_refundController.text.isEmpty) + if (_refundController.text.isEmpty && + isStackCoin(model.sendTicker)) TextFieldIconButton( key: const Key("sendViewAddressBookButtonKey"), onTap: selectRefundFromAddressBook, diff --git a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart new file mode 100644 index 000000000..a3fb91f61 --- /dev/null +++ b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart @@ -0,0 +1,329 @@ +import 'package:decimal/decimal.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/providers/providers.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/format.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/animated_text.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; +import 'package:stackwallet/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart'; + +class DesktopChooseFromStack extends ConsumerStatefulWidget { + const DesktopChooseFromStack({ + Key? key, + required this.coin, + }) : super(key: key); + + final Coin coin; + + @override + ConsumerState<DesktopChooseFromStack> createState() => + _DesktopChooseFromStackState(); +} + +class _DesktopChooseFromStackState + extends ConsumerState<DesktopChooseFromStack> { + late final TextEditingController _searchController; + late final FocusNode searchFieldFocusNode; + + String _searchTerm = ""; + + List<String> filter(List<String> walletIds, String searchTerm) { + if (searchTerm.isEmpty) { + return walletIds; + } + + final List<String> result = []; + for (final walletId in walletIds) { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(walletId); + + if (manager.walletName.toLowerCase().contains(searchTerm.toLowerCase())) { + result.add(walletId); + } + } + + return result; + } + + @override + void initState() { + searchFieldFocusNode = FocusNode(); + _searchController = TextEditingController(); + super.initState(); + } + + @override + void dispose() { + _searchController.dispose(); + searchFieldFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Choose from Stack", + style: STextStyles.desktopH3(context), + ), + const SizedBox( + height: 28, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: false, + enableSuggestions: false, + controller: _searchController, + focusNode: searchFieldFocusNode, + onChanged: (value) { + setState(() { + _searchTerm = value; + }); + }, + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveText, + height: 1.8, + ), + decoration: standardInputDecoration( + "Search", + searchFieldFocusNode, + context, + desktopMed: true, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 18, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 20, + height: 20, + ), + ), + suffixIcon: _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + _searchTerm = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + const SizedBox( + height: 16, + ), + Flexible( + child: Builder( + builder: (context) { + List<String> walletIds = ref.watch( + walletsChangeNotifierProvider.select( + (value) => value.getWalletIdsFor(coin: widget.coin), + ), + ); + + if (walletIds.isEmpty) { + return Column( + children: [ + RoundedWhiteContainer( + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: Center( + child: Text( + "No ${widget.coin.ticker.toUpperCase()} wallets", + style: + STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + ), + ], + ); + } + + walletIds = filter(walletIds, _searchTerm); + + return ListView.separated( + primary: false, + itemCount: walletIds.length, + separatorBuilder: (_, __) => const SizedBox( + height: 5, + ), + itemBuilder: (context, index) { + final manager = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletIds[index]))); + + return RoundedWhiteContainer( + borderColor: + Theme.of(context).extension<StackColors>()!.background, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 14, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Row( + children: [ + WalletInfoCoinIcon(coin: widget.coin), + const SizedBox( + width: 12, + ), + Text( + manager.walletName, + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + ], + ), + const Spacer(), + BalanceDisplay( + walletId: walletIds[index], + ), + const SizedBox( + width: 80, + ), + BlueTextButton( + text: "Select wallet", + onTap: () { + Navigator.of(context).pop(manager.walletId); + }, + ), + ], + ), + ); + }, + ); + }, + ), + ), + const SizedBox( + height: 20, + ), + Row( + children: [ + const Spacer(), + const SizedBox( + width: 16, + ), + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of(context).pop, + ), + ), + ], + ) + ], + ); + } +} + +class BalanceDisplay extends ConsumerStatefulWidget { + const BalanceDisplay({ + Key? key, + required this.walletId, + }) : super(key: key); + + final String walletId; + + @override + ConsumerState<BalanceDisplay> createState() => _BalanceDisplayState(); +} + +class _BalanceDisplayState extends ConsumerState<BalanceDisplay> { + late final String walletId; + + Decimal? _cachedBalance; + + static const loopedText = [ + "Loading balance ", + "Loading balance. ", + "Loading balance.. ", + "Loading balance..." + ]; + + @override + void initState() { + walletId = widget.walletId; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final manager = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletId))); + final locale = ref.watch( + localeServiceChangeNotifierProvider.select((value) => value.locale)); + + return FutureBuilder( + future: manager.availableBalance, + builder: (context, AsyncSnapshot<Decimal> snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData && + snapshot.data != null) { + _cachedBalance = snapshot.data; + } + + if (_cachedBalance == null) { + return AnimatedText( + stringsToLoopThrough: loopedText, + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textSubtitle1, + ), + ); + } else { + return Text( + "${Format.localizedStringAsFixed( + value: _cachedBalance!, + locale: locale, + decimalPlaces: 8, + )} ${manager.coin.ticker}", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textSubtitle1, + ), + textAlign: TextAlign.right, + ); + } + }, + ); + } +} From f75e4ea2faf8e71c385aa05732ace062ee90a324 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 21 Nov 2022 14:52:41 -0600 Subject: [PATCH 362/426] desktop delete routing fixes --- .../sub_widgets/delete_wallet_button.dart | 113 ++++++++-- .../sub_widgets/delete_wallet_keys_popup.dart | 204 +++++++++--------- .../desktop_attention_delete_wallet.dart | 27 ++- 3 files changed, 207 insertions(+), 137 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart index 54f991c37..fd401d613 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; -import 'desktop_delete_wallet_dialog.dart'; - class DeleteWalletButton extends ConsumerStatefulWidget { const DeleteWalletButton({ Key? key, @@ -26,8 +26,6 @@ class _DeleteWalletButton extends ConsumerState<DeleteWalletButton> { @override void initState() { walletId = widget.walletId; - final managerProvider = - ref.read(walletsChangeNotifierProvider).getManagerProvider(walletId); super.initState(); } @@ -38,25 +36,45 @@ class _DeleteWalletButton extends ConsumerState<DeleteWalletButton> { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(1000), ), - onPressed: () { - showDialog<void>( + onPressed: () async { + final shouldOpenDeleteDialog = await showDialog<bool?>( context: context, - barrierDismissible: false, - builder: (context) => Navigator( - initialRoute: DesktopDeleteWalletDialog.routeName, - onGenerateRoute: RouteGenerator.generateRoute, - onGenerateInitialRoutes: (_, __) { - return [ - RouteGenerator.generateRoute( - RouteSettings( - name: DesktopDeleteWalletDialog.routeName, - arguments: walletId, - ), - ) - ]; - }, - ), + barrierColor: Colors.transparent, + builder: (context) { + return DeletePopupButton( + onTap: () async { + Navigator.of(context).pop(true); + }, + ); + }, ); + + if (shouldOpenDeleteDialog == true) { + final result = await showDialog<bool?>( + context: context, + barrierDismissible: false, + builder: (context) => Navigator( + initialRoute: DesktopDeleteWalletDialog.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + RouteGenerator.generateRoute( + RouteSettings( + name: DesktopDeleteWalletDialog.routeName, + arguments: walletId, + ), + ), + ]; + }, + ), + ); + + if (result == true) { + if (mounted) { + Navigator.of(context).pop(); + } + } + } }, child: Padding( padding: const EdgeInsets.symmetric( @@ -79,3 +97,54 @@ class _DeleteWalletButton extends ConsumerState<DeleteWalletButton> { ); } } + +class DeletePopupButton extends StatefulWidget { + const DeletePopupButton({ + Key? key, + this.onTap, + }) : super(key: key); + + final VoidCallback? onTap; + + @override + State<DeletePopupButton> createState() => _DeletePopupButtonState(); +} + +class _DeletePopupButtonState extends State<DeletePopupButton> { + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Positioned( + top: 24, + left: MediaQuery.of(context).size.width - 234, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: widget.onTap, + child: Container( + width: 210, + height: 70, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + color: Colors.red, + boxShadow: [ + Theme.of(context) + .extension<StackColors>()! + .standardBoxShadow, + ], + ), + child: Text( + "Delete", + style: STextStyles.desktopButtonSecondaryEnabled(context), + ), + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart index f70c2eadf..6b638bb75 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart @@ -1,11 +1,9 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart'; -import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_stack_view.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/providers/global/wallets_service_provider.dart'; +import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; @@ -61,8 +59,10 @@ class _DeleteWalletKeysPopup extends ConsumerState<DeleteWalletKeysPopup> { ), DesktopDialogCloseButton( onPressedOverride: () { - int count = 0; - Navigator.of(context).popUntil((_) => count++ >= 2); + Navigator.of( + context, + rootNavigator: true, + ).pop(); }, ), ], @@ -117,106 +117,17 @@ class _DeleteWalletKeysPopup extends ConsumerState<DeleteWalletKeysPopup> { child: PrimaryButton( label: "Continue", onPressed: () async { - int count = 0; - Navigator.of(context).popUntil((_) => count++ >= 2); - - unawaited( - showDialog( - context: context, - builder: (context) { - return DesktopDialog( - maxHeight: 350, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - DesktopDialogCloseButton( - onPressedOverride: () { - int count = 0; - Navigator.of(context) - .popUntil((_) => count++ >= 2); - }, - ), - ], - ), - Column( - crossAxisAlignment: - CrossAxisAlignment.center, - children: [ - Text( - "Thanks! " - "\n\nYour wallet will be deleted.", - style: STextStyles.desktopH2(context), - textAlign: TextAlign.center, - ), - const SizedBox(height: 50), - Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - SecondaryButton( - width: 250, - buttonHeight: ButtonHeight.xl, - label: "Cancel", - onPressed: () { - int count = 0; - Navigator.of(context) - .popUntil( - (_) => count++ >= 2); - }), - const SizedBox(width: 16), - PrimaryButton( - width: 250, - buttonHeight: ButtonHeight.xl, - label: "Continue", - onPressed: () async { - // int count = 0; - // Navigator.of(context) - // .popUntil( - // (_) => count++ >= 2); - - final walletsInstance = ref.read( - walletsChangeNotifierProvider); - final manager = ref - .read( - walletsChangeNotifierProvider) - .getManager(_walletId); - - final _managerWalletId = - manager.walletId; - - await ref - .read( - walletsServiceChangeNotifierProvider) - .deleteWallet( - manager.walletName, - true); - - if (mounted) { - Navigator.of(context) - .popUntil( - ModalRoute.withName( - MyStackView - .routeName)); - } - - // wait for widget tree to dispose of any widgets watching the manager - await Future<void>.delayed( - const Duration( - seconds: 1)); - walletsInstance.removeWallet( - walletId: - _managerWalletId); - }), - ], - ) - ], - ), - ], - ), - ); - }), + await Navigator.of(context).push( + RouteGenerator.getRoute( + builder: (context) { + return ConfirmDelete( + walletId: _walletId, + ); + }, + settings: const RouteSettings( + name: "/desktopConfirmDelete", + ), + ), ); }, ), @@ -232,3 +143,86 @@ class _DeleteWalletKeysPopup extends ConsumerState<DeleteWalletKeysPopup> { ); } } + +class ConfirmDelete extends ConsumerStatefulWidget { + const ConfirmDelete({ + Key? key, + required this.walletId, + }) : super(key: key); + + final String walletId; + + @override + ConsumerState<ConfirmDelete> createState() => _ConfirmDeleteState(); +} + +class _ConfirmDeleteState extends ConsumerState<ConfirmDelete> { + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxHeight: 350, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: const [ + DesktopDialogCloseButton(), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "Thanks! " + "\n\nYour wallet will be deleted.", + style: STextStyles.desktopH2(context), + textAlign: TextAlign.center, + ), + const SizedBox(height: 50), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SecondaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + label: "Cancel", + onPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + }, + ), + const SizedBox(width: 16), + PrimaryButton( + width: 250, + buttonHeight: ButtonHeight.xl, + label: "Continue", + onPressed: () async { + final walletsInstance = + ref.read(walletsChangeNotifierProvider); + final manager = ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId); + + final _managerWalletId = manager.walletId; + // + await ref + .read(walletsServiceChangeNotifierProvider) + .deleteWallet(manager.walletName, true); + + if (mounted) { + Navigator.of(context, rootNavigator: true).pop(true); + } + + // wait for widget tree to dispose of any widgets watching the manager + await Future<void>.delayed(const Duration(seconds: 1)); + walletsInstance.removeWallet(walletId: _managerWalletId); + }, + ), + ], + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart index 30546f60b..6eb04502e 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart @@ -41,8 +41,10 @@ class _DesktopAttentionDeleteWallet children: [ DesktopDialogCloseButton( onPressedOverride: () { - int count = 0; - Navigator.of(context).popUntil((_) => count++ >= 2); + Navigator.of( + context, + rootNavigator: true, + ).pop(); }, ), ], @@ -87,8 +89,10 @@ class _DesktopAttentionDeleteWallet buttonHeight: ButtonHeight.xl, label: "Cancel", onPressed: () { - int count = 0; - Navigator.of(context).popUntil((_) => count++ >= 2); + Navigator.of( + context, + rootNavigator: true, + ).pop(); }, ), const SizedBox(width: 16), @@ -102,12 +106,15 @@ class _DesktopAttentionDeleteWallet .getManager(widget.walletId) .mnemonic; - await Navigator.of(context) - .pushNamed(DeleteWalletKeysPopup.routeName, - arguments: Tuple2( - widget.walletId, - words, - )); + if (mounted) { + await Navigator.of(context).pushNamed( + DeleteWalletKeysPopup.routeName, + arguments: Tuple2( + widget.walletId, + words, + ), + ); + } }, ), ], From 8a6025db4b308756eaf5cf91d50cdbd1e545a801 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 21 Nov 2022 15:29:09 -0600 Subject: [PATCH 363/426] place node url and port on their own line --- .../add_edit_node_view.dart | 195 +++++++++--------- 1 file changed, 93 insertions(+), 102 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index 606c4481f..9062314f0 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -494,6 +494,7 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> { condition: isDesktop, builder: (child) => DesktopDialog( maxWidth: 580, + maxHeight: 500, child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -830,110 +831,100 @@ class _NodeFormState extends ConsumerState<NodeForm> { const SizedBox( height: 8, ), - Row( - children: [ - Expanded( - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - key: const Key("addCustomNodeNodeAddressFieldKey"), - readOnly: widget.readOnly, - enabled: enableField(_hostController), - controller: _hostController, - focusNode: _hostFocusNode, - style: STextStyles.field(context), - decoration: standardInputDecoration( - (widget.coin != Coin.monero && - widget.coin != Coin.wownero && - widget.coin != Coin.epicCash) - ? "IP address" - : "Url", - _hostFocusNode, - context, - ).copyWith( - suffixIcon: - !widget.readOnly && _hostController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - _hostController.text = ""; - _updateState(); - }, - ), - ], - ), - ), - ) - : null, - ), - onChanged: (newValue) { - _updateState(); - setState(() {}); - }, - ), - ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + key: const Key("addCustomNodeNodeAddressFieldKey"), + readOnly: widget.readOnly, + enabled: enableField(_hostController), + controller: _hostController, + focusNode: _hostFocusNode, + style: STextStyles.field(context), + decoration: standardInputDecoration( + (widget.coin != Coin.monero && + widget.coin != Coin.wownero && + widget.coin != Coin.epicCash) + ? "IP address" + : "Url", + _hostFocusNode, + context, + ).copyWith( + suffixIcon: !widget.readOnly && _hostController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + _hostController.text = ""; + _updateState(); + }, + ), + ], + ), + ), + ) + : null, ), - const SizedBox( - width: 12, + onChanged: (newValue) { + _updateState(); + setState(() {}); + }, + ), + ), + const SizedBox( + height: 8, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + key: const Key("addCustomNodeNodePortFieldKey"), + readOnly: widget.readOnly, + enabled: enableField(_portController), + controller: _portController, + focusNode: _portFocusNode, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + keyboardType: TextInputType.number, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Port", + _portFocusNode, + context, + ).copyWith( + suffixIcon: !widget.readOnly && _portController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + _portController.text = ""; + _updateState(); + }, + ), + ], + ), + ), + ) + : null, ), - Expanded( - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - key: const Key("addCustomNodeNodePortFieldKey"), - readOnly: widget.readOnly, - enabled: enableField(_portController), - controller: _portController, - focusNode: _portFocusNode, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - keyboardType: TextInputType.number, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Port", - _portFocusNode, - context, - ).copyWith( - suffixIcon: - !widget.readOnly && _portController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - _portController.text = ""; - _updateState(); - }, - ), - ], - ), - ), - ) - : null, - ), - onChanged: (newValue) { - _updateState(); - setState(() {}); - }, - ), - ), - ), - ], + onChanged: (newValue) { + _updateState(); + setState(() {}); + }, + ), ), const SizedBox( height: 8, From 66950ccc5059bbf04187180fb7c60665cd892c41 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 21 Nov 2022 14:40:11 -0700 Subject: [PATCH 364/426] delete popup styled --- .../sub_widgets/delete_wallet_button.dart | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart index fd401d613..f2553c2da 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart @@ -129,16 +129,30 @@ class _DeletePopupButtonState extends State<DeletePopupButton> { borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), - color: Colors.red, + color: Theme.of(context).extension<StackColors>()!.popupBG, boxShadow: [ Theme.of(context) .extension<StackColors>()! .standardBoxShadow, ], ), - child: Text( - "Delete", - style: STextStyles.desktopButtonSecondaryEnabled(context), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const SizedBox(width: 24), + SvgPicture.asset( + Assets.svg.trash, + ), + const SizedBox(width: 14), + Text( + "Delete wallet", + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark), + ), + ], ), ), ), From e099089d04a3154fbb1cd61fc331d0435e6aeba4 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 21 Nov 2022 14:58:03 -0700 Subject: [PATCH 365/426] loading indicator and delay --- .../desktop_delete_wallet_dialog.dart | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart index 087629673..6729d33b9 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart @@ -13,6 +13,7 @@ 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/loading_indicator.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import '../../../../../providers/desktop/storage_crypto_handler_provider.dart'; @@ -177,11 +178,35 @@ class _DesktopDeleteWalletDialog label: "Continue", onPressed: _continueEnabled ? () async { + // add loading indicator + unawaited( + showDialog( + context: context, + builder: (context) => Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: + CrossAxisAlignment.center, + children: const [ + LoadingIndicator( + width: 200, + height: 200, + ), + ], + ), + ), + ); + + await Future<void>.delayed( + const Duration(seconds: 1)); + final verified = await ref .read(storageCryptoHandlerProvider) .verifyPassphrase(passwordController.text); if (verified) { + Navigator.of(context, rootNavigator: true) + .pop(); + final words = await ref .read(walletsChangeNotifierProvider) .getManager(widget.walletId) @@ -190,12 +215,20 @@ class _DesktopDeleteWalletDialog if (mounted) { Navigator.of(context).pop(); - await Navigator.of(context).pushNamed( - DesktopAttentionDeleteWallet.routeName, - arguments: widget.walletId, + unawaited( + Navigator.of(context).pushNamed( + DesktopAttentionDeleteWallet.routeName, + arguments: widget.walletId, + ), ); } } else { + Navigator.of(context, rootNavigator: true) + .pop(); + + await Future<void>.delayed( + const Duration(milliseconds: 300)); + unawaited( showFloatingFlushBar( type: FlushBarType.warning, From c935c590c7c5635487503e4587f34d94e8f0ae77 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 21 Nov 2022 16:00:00 -0600 Subject: [PATCH 366/426] desktop exchange flow tweaks and show QR code --- lib/pages/exchange_view/exchange_form.dart | 2 + .../subwidgets/desktop_step_1.dart | 1 + .../subwidgets/desktop_step_2.dart | 30 +++++++-- .../subwidgets/desktop_step_3.dart | 67 ++++++++++++------- .../subwidgets/desktop_step_4.dart | 49 +++++++++++++- .../subwidgets/desktop_step_item.dart | 1 + .../subwidgets/desktop_trade_history.dart | 2 + 7 files changed, 121 insertions(+), 31 deletions(-) diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart index 7faa83a35..7f0d9b5d2 100644 --- a/lib/pages/exchange_view/exchange_form.dart +++ b/lib/pages/exchange_view/exchange_form.dart @@ -1024,6 +1024,7 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { if (isDesktop) { await showDialog<void>( context: context, + barrierDismissible: false, builder: (context) { return DesktopDialog( maxWidth: 720, @@ -1052,6 +1053,7 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { if (isDesktop) { await showDialog<void>( context: context, + barrierDismissible: false, builder: (context) { return DesktopDialog( maxWidth: 720, diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart index a97b722ef..031dc0649 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_1.dart @@ -104,6 +104,7 @@ class DesktopStep1 extends ConsumerWidget { await showDialog<void>( context: context, barrierColor: Colors.transparent, + barrierDismissible: false, builder: (context) { return DesktopDialog( maxWidth: 720, diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart index 270b98fc7..525d561fc 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/contact_address_entry.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart'; import 'package:stackwallet/providers/exchange/exchange_send_from_wallet_id_provider.dart'; @@ -13,6 +16,8 @@ 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_buttons/blue_text_button.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/icon_widgets/addressbook_icon.dart'; @@ -22,10 +27,6 @@ import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; -import '../../../../models/contact_address_entry.dart'; -import '../../../../widgets/desktop/desktop_dialog.dart'; -import '../../../../widgets/desktop/desktop_dialog_close_button.dart'; - class DesktopStep2 extends ConsumerStatefulWidget { const DesktopStep2({ Key? key, @@ -552,8 +553,25 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { child: PrimaryButton( label: "Next", buttonHeight: ButtonHeight.l, - onPressed: () { - // todo + onPressed: () async { + await showDialog<void>( + context: context, + barrierColor: Colors.transparent, + barrierDismissible: false, + builder: (context) { + return DesktopDialog( + maxWidth: 720, + maxHeight: double.infinity, + child: StepScaffold( + step: 3, + model: model, + body: DesktopStep3( + model: model, + ), + ), + ); + }, + ); }, ), ), diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart index 65b6ed2b3..284416545 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_3.dart @@ -4,8 +4,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; import 'package:stackwallet/models/exchange/response_objects/trade.dart'; -import 'package:stackwallet/pages/exchange_view/exchange_step_views/step_4_view.dart'; import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart'; import 'package:stackwallet/providers/exchange/current_exchange_name_state_provider.dart'; import 'package:stackwallet/providers/exchange/exchange_provider.dart'; @@ -16,10 +17,11 @@ 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_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/desktop/simple_desktop_dialog.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; -import 'package:stackwallet/widgets/stack_dialog.dart'; class DesktopStep3 extends ConsumerStatefulWidget { const DesktopStep3({ @@ -76,14 +78,15 @@ class _DesktopStep3State extends ConsumerState<DesktopStep3> { Navigator.of(context).pop(); } - unawaited(showDialog<void>( - context: context, - barrierDismissible: true, - builder: (_) => StackDialog( - title: "Failed to create trade", - message: response.exception?.toString(), + unawaited( + showDialog<void>( + context: context, + barrierDismissible: true, + builder: (_) => SimpleDesktopDialog( + title: "Failed to create trade", + message: response.exception?.toString() ?? ""), ), - )); + ); return; } @@ -106,22 +109,40 @@ class _DesktopStep3State extends ConsumerState<DesktopStep3> { Navigator.of(context).pop(); } - unawaited(NotificationApi.showNotification( - changeNowId: model.trade!.tradeId, - title: status, - body: "Trade ID ${model.trade!.tradeId}", - walletId: "", - iconAssetName: Assets.svg.arrowRotate, - date: model.trade!.timestamp, - shouldWatchForUpdates: true, - coinName: "coinName", - )); + unawaited( + NotificationApi.showNotification( + changeNowId: model.trade!.tradeId, + title: status, + body: "Trade ID ${model.trade!.tradeId}", + walletId: "", + iconAssetName: Assets.svg.arrowRotate, + date: model.trade!.timestamp, + shouldWatchForUpdates: true, + coinName: "coinName", + ), + ); if (mounted) { - unawaited(Navigator.of(context).pushNamed( - Step4View.routeName, - arguments: model, - )); + unawaited( + showDialog<void>( + context: context, + barrierColor: Colors.transparent, + barrierDismissible: false, + builder: (context) { + return DesktopDialog( + maxWidth: 720, + maxHeight: double.infinity, + child: StepScaffold( + step: 4, + model: model, + body: DesktopStep4( + model: model, + ), + ), + ); + }, + ), + ); } } diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart index ba9838086..7747f570f 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart @@ -2,12 +2,14 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:qr_flutter/qr_flutter.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart'; import 'package:stackwallet/providers/providers.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/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; @@ -148,7 +150,7 @@ class _DesktopStep4State extends ConsumerState<DesktopStep4> { DesktopStepItem( label: "Amount", value: - "${model.sendAmount.toString()} ${model.sendTicker.toUpperCase()}", + "${model.sendAmount.toStringAsFixed(8)} ${model.sendTicker.toUpperCase()}", ), Container( height: 1, @@ -208,7 +210,50 @@ class _DesktopStep4State extends ConsumerState<DesktopStep4> { label: "Show QR code", buttonHeight: ButtonHeight.l, onPressed: () { - // todo + showDialog<dynamic>( + context: context, + barrierColor: Colors.transparent, + barrierDismissible: true, + builder: (_) { + return DesktopDialog( + maxHeight: 720, + maxWidth: 720, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "Send ${model.sendAmount.toStringAsFixed(8)} ${model.sendTicker} to this address", + style: STextStyles.desktopH3(context), + ), + const SizedBox( + height: 48, + ), + Center( + child: QrImage( + // TODO: grab coin uri scheme from somewhere + // data: "${coin.uriScheme}:$receivingAddress", + data: model.trade!.payInAddress, + size: 290, + foregroundColor: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + ), + const SizedBox( + height: 48, + ), + SecondaryButton( + label: "Cancel", + width: 310, + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of(context).pop, + ), + ], + ), + ); + }, + ); }, ), ), diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart index 7c777c2dd..323517e13 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart @@ -24,6 +24,7 @@ class DesktopStepItem extends StatelessWidget { child: ConditionalParent( condition: vertical, builder: (child) => Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ child, const SizedBox( diff --git a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart index 40eeb8c1b..41bf4246a 100644 --- a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart +++ b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart @@ -31,6 +31,8 @@ class _DesktopTradeHistoryState extends ConsumerState<DesktopTradeHistory> { if (hasHistory) { return ListView.separated( + shrinkWrap: true, + primary: false, itemBuilder: (context, index) { return TradeCard( key: Key("tradeCard_${trades[index].uuid}"), From a10958b12d02f0073354cb4402504f46ec75efa9 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 21 Nov 2022 15:08:31 -0700 Subject: [PATCH 367/426] delete popup rounded corner fix --- .../wallet_view/sub_widgets/delete_wallet_button.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart index f2553c2da..a2071e7d6 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart @@ -127,7 +127,7 @@ class _DeletePopupButtonState extends State<DeletePopupButton> { height: 70, decoration: BoxDecoration( borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + Constants.size.circularBorderRadius * 2, ), color: Theme.of(context).extension<StackColors>()!.popupBG, boxShadow: [ From 7a650c78d39006620e6ba4f96069feb42f6e1153 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 21 Nov 2022 15:17:26 -0700 Subject: [PATCH 368/426] loading indicator and delay for wallet keys --- .../unlock_wallet_keys_desktop.dart | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart index 23360c98c..739a3ebc4 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart @@ -16,6 +16,7 @@ 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/loading_indicator.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; class UnlockWalletKeysDesktop extends ConsumerStatefulWidget { @@ -201,11 +202,32 @@ class _UnlockWalletKeysDesktopState enabled: continueEnabled, onPressed: continueEnabled ? () async { + unawaited( + showDialog( + context: context, + builder: (context) => Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: const [ + LoadingIndicator( + width: 200, + height: 200, + ), + ], + ), + ), + ); + + await Future<void>.delayed( + const Duration(seconds: 1)); + final verified = await ref .read(storageCryptoHandlerProvider) .verifyPassphrase(passwordController.text); if (verified) { + Navigator.of(context, rootNavigator: true).pop(); + final words = await ref .read(walletsChangeNotifierProvider) .getManager(widget.walletId) @@ -219,6 +241,11 @@ class _UnlockWalletKeysDesktopState ); } } else { + Navigator.of(context, rootNavigator: true).pop(); + + await Future<void>.delayed( + const Duration(milliseconds: 300)); + unawaited( showFloatingFlushBar( type: FlushBarType.warning, From 675977c787d9d987819852199fefe32882e7bfe6 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 21 Nov 2022 20:33:38 -0700 Subject: [PATCH 369/426] copy to clipboard added to wallet keys dialog --- .../sub_widgets/delete_wallet_keys_popup.dart | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart index 6b638bb75..70f4a3e13 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart @@ -1,9 +1,15 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/providers/global/wallets_service_provider.dart'; import 'package:stackwallet/route_generator.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; @@ -16,10 +22,12 @@ class DeleteWalletKeysPopup extends ConsumerStatefulWidget { Key? key, required this.walletId, required this.words, + this.clipboardInterface = const ClipboardWrapper(), }) : super(key: key); final String walletId; final List<String> words; + final ClipboardInterface clipboardInterface; static const String routeName = "/desktopDeleteWalletKeysPopup"; @@ -30,10 +38,14 @@ class DeleteWalletKeysPopup extends ConsumerStatefulWidget { class _DeleteWalletKeysPopup extends ConsumerState<DeleteWalletKeysPopup> { late final String _walletId; + late final List<String> _words; + late final ClipboardInterface _clipboardInterface; @override void initState() { _walletId = widget.walletId; + _words = widget.words; + _clipboardInterface = widget.clipboardInterface; super.initState(); } @@ -96,12 +108,28 @@ class _DeleteWalletKeysPopup extends ConsumerState<DeleteWalletKeysPopup> { padding: const EdgeInsets.symmetric( horizontal: 32, ), - child: MnemonicTable( - words: widget.words, - isDesktop: true, - itemBorderColor: Theme.of(context) - .extension<StackColors>()! - .buttonBackSecondary, + child: RawMaterialButton( + hoverColor: Colors.transparent, + onPressed: () async { + await _clipboardInterface.setData( + ClipboardData(text: _words.join(" ")), + ); + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ), + ); + }, + child: MnemonicTable( + words: widget.words, + isDesktop: true, + itemBorderColor: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + ), ), ), const SizedBox( From 6384d66308066645500f516eca9784163a8285d0 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 21 Nov 2022 20:34:36 -0700 Subject: [PATCH 370/426] added delays for floatingFlushBar in settings change password --- .../home/settings_menu/security_settings.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/pages_desktop_specific/home/settings_menu/security_settings.dart b/lib/pages_desktop_specific/home/settings_menu/security_settings.dart index f6762afa1..ff7537126 100644 --- a/lib/pages_desktop_specific/home/settings_menu/security_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/security_settings.dart @@ -61,6 +61,8 @@ class _SecuritySettings extends ConsumerState<SecuritySettings> { if (verified) { if (pwNew != pwNewRepeat) { + await Future<void>.delayed(const Duration(seconds: 1)); + unawaited( showFloatingFlushBar( type: FlushBarType.warning, @@ -77,6 +79,8 @@ class _SecuritySettings extends ConsumerState<SecuritySettings> { ); if (success) { + await Future<void>.delayed(const Duration(seconds: 1)); + unawaited( showFloatingFlushBar( type: FlushBarType.success, @@ -86,6 +90,8 @@ class _SecuritySettings extends ConsumerState<SecuritySettings> { ); return true; } else { + await Future<void>.delayed(const Duration(seconds: 1)); + unawaited( showFloatingFlushBar( type: FlushBarType.warning, @@ -97,6 +103,8 @@ class _SecuritySettings extends ConsumerState<SecuritySettings> { } } } else { + await Future<void>.delayed(const Duration(seconds: 1)); + unawaited( showFloatingFlushBar( type: FlushBarType.warning, From b32e15a3ea1176eb4f77920fa97b46f1d95c3828 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 22 Nov 2022 07:13:03 -0600 Subject: [PATCH 371/426] desktop login on enter pressed --- lib/pages_desktop_specific/desktop_login_view.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/pages_desktop_specific/desktop_login_view.dart b/lib/pages_desktop_specific/desktop_login_view.dart index f865fad47..eb5dec18a 100644 --- a/lib/pages_desktop_specific/desktop_login_view.dart +++ b/lib/pages_desktop_specific/desktop_login_view.dart @@ -165,6 +165,12 @@ class _DesktopLoginViewState extends ConsumerState<DesktopLoginView> { obscureText: hidePassword, enableSuggestions: false, autocorrect: false, + autofocus: true, + onSubmitted: (_) { + if (_continueEnabled) { + login(); + } + }, decoration: standardInputDecoration( "Enter password", passwordFocusNode, From b512b2cefb4663a0075b749c2cc2d4126060717b Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 22 Nov 2022 07:15:08 -0600 Subject: [PATCH 372/426] consistent decimal places on firo balance selection sheet --- .../firo_balance_selection_sheet.dart | 4 ++-- .../wallet_balance_toggle_sheet.dart | 11 ++++----- lib/utilities/constants.dart | 24 +++++++++++++++++++ 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart b/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart index e639a8cf8..d6de3c6ee 100644 --- a/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart +++ b/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart @@ -161,7 +161,7 @@ class _FiroBalanceSelectionSheetState ConnectionState.done && snapshot.hasData) { return Text( - "${snapshot.data!} ${manager.coin.ticker}", + "${snapshot.data!.toStringAsFixed(8)} ${manager.coin.ticker}", style: STextStyles.itemSubtitle(context), textAlign: TextAlign.left, ); @@ -251,7 +251,7 @@ class _FiroBalanceSelectionSheetState ConnectionState.done && snapshot.hasData) { return Text( - "${snapshot.data!} ${manager.coin.ticker}", + "${snapshot.data!.toStringAsFixed(8)} ${manager.coin.ticker}", style: STextStyles.itemSubtitle(context), textAlign: TextAlign.left, ); diff --git a/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart b/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart index c9ff64393..74308f2e8 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart @@ -3,14 +3,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/wallet/wallet_balance_toggle_state_provider.dart'; +import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; -import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; - class WalletBalanceToggleSheet extends ConsumerWidget { const WalletBalanceToggleSheet({ Key? key, @@ -153,7 +152,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget { snapshot.hasData && snapshot.data != null) { return Text( - "${snapshot.data!}", + "${snapshot.data!.toStringAsFixed(Constants.decimalPlacesForCoin(coin))} ${coin.ticker}", style: STextStyles.itemSubtitle12(context) .copyWith( color: Theme.of(context) @@ -195,7 +194,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget { snapshot.hasData && snapshot.data != null) { return Text( - "${snapshot.data!}", + "${snapshot.data!.toStringAsFixed(Constants.decimalPlacesForCoin(coin))} ${coin.ticker}", style: STextStyles.itemSubtitle12(context) .copyWith( color: Theme.of(context) @@ -287,7 +286,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget { snapshot.hasData && snapshot.data != null) { return Text( - "${snapshot.data!}", + "${snapshot.data!.toStringAsFixed(Constants.decimalPlacesForCoin(coin))} ${coin.ticker}", style: STextStyles.itemSubtitle12(context) .copyWith( color: Theme.of(context) @@ -329,7 +328,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget { snapshot.hasData && snapshot.data != null) { return Text( - "${snapshot.data!}", + "${snapshot.data!.toStringAsFixed(Constants.decimalPlacesForCoin(coin))} ${coin.ticker}", style: STextStyles.itemSubtitle12(context) .copyWith( color: Theme.of(context) diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index e27fbaa3d..0a062de67 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -40,6 +40,30 @@ abstract class Constants { static const int currentHiveDbVersion = 3; + static int decimalPlacesForCoin(Coin coin) { + switch (coin) { + case Coin.bitcoin: + case Coin.litecoin: + case Coin.litecoinTestNet: + case Coin.bitcoincash: + case Coin.bitcoincashTestnet: + case Coin.dogecoin: + case Coin.firo: + case Coin.bitcoinTestNet: + case Coin.dogecoinTestNet: + case Coin.firoTestNet: + case Coin.epicCash: + case Coin.namecoin: + return decimalPlaces; + + case Coin.wownero: + return decimalPlacesWownero; + + case Coin.monero: + return decimalPlacesMonero; + } + } + static List<int> possibleLengthsForCoin(Coin coin) { final List<int> values = []; switch (coin) { From 7afe6940f9dc5f20f08eb59e606d8e4cbb452cd4 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 22 Nov 2022 08:07:22 -0600 Subject: [PATCH 373/426] desktop trade history details updated --- .../exchange_view/trade_details_view.dart | 186 ++++++++++++++---- .../subwidgets/desktop_trade_history.dart | 124 +++++++++++- lib/widgets/trade_card.dart | 134 +++++++------ 3 files changed, 335 insertions(+), 109 deletions(-) diff --git a/lib/pages/exchange_view/trade_details_view.dart b/lib/pages/exchange_view/trade_details_view.dart index 602d588da..f28d1d617 100644 --- a/lib/pages/exchange_view/trade_details_view.dart +++ b/lib/pages/exchange_view/trade_details_view.dart @@ -206,16 +206,57 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> { padding: const EdgeInsets.only( right: 12, ), - child: RoundedWhiteContainer( - borderColor: isDesktop - ? Theme.of(context).extension<StackColors>()!.background - : null, - padding: const EdgeInsets.all(0), - child: ListView( - primary: false, - shrinkWrap: true, - children: children, - ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + RoundedWhiteContainer( + borderColor: + Theme.of(context).extension<StackColors>()!.background, + padding: const EdgeInsets.all(0), + child: ListView( + primary: false, + shrinkWrap: true, + children: children, + ), + ), + if (isStackCoin(trade.payInCurrency) && + (trade.status == "New" || + trade.status == "new" || + trade.status == "waiting" || + trade.status == "Waiting")) + const SizedBox( + height: 32, + ), + if (isStackCoin(trade.payInCurrency) && + (trade.status == "New" || + trade.status == "new" || + trade.status == "waiting" || + trade.status == "Waiting")) + SecondaryButton( + label: "Send from Stack", + buttonHeight: ButtonHeight.l, + onPressed: () { + final amount = sendAmount; + final address = trade.payInAddress; + + final coin = + coinFromTickerCaseInsensitive(trade.payInCurrency); + + Navigator.of(context).pushNamed( + SendFromView.routeName, + arguments: Tuple4( + coin, + amount, + address, + trade, + ), + ); + }, + ), + const SizedBox( + height: 32, + ), + ], ), ), ), @@ -350,33 +391,94 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> { padding: isDesktop ? const EdgeInsets.all(16) : const EdgeInsets.all(12), - color: Theme.of(context) - .extension<StackColors>()! - .warningBackground, - child: RichText( - text: TextSpan( - text: - "You must send at least ${sendAmount.toStringAsFixed( - trade.payInCurrency.toLowerCase() == "xmr" ? 12 : 8, - )} ${trade.payInCurrency.toUpperCase()}. ", - style: STextStyles.label700(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .warningForeground, - ), - children: [ - TextSpan( - text: - "If you send less than ${sendAmount.toStringAsFixed( - trade.payInCurrency.toLowerCase() == "xmr" ? 12 : 8, - )} ${trade.payInCurrency.toUpperCase()}, your transaction may not be converted and it may not be refunded.", - style: STextStyles.label(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .warningForeground, + color: isDesktop + ? Theme.of(context).extension<StackColors>()!.popupBG + : Theme.of(context) + .extension<StackColors>()! + .warningBackground, + child: ConditionalParent( + condition: isDesktop, + builder: (child) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Amount", + style: STextStyles.desktopTextExtraExtraSmall( + context), + ), + const SizedBox( + height: 2, + ), + Text( + "${trade.payInAmount} ${trade.payInCurrency.toUpperCase()}", + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + ], ), - ), - ]), + IconCopyButton( + data: trade.payInAmount, + ), + ], + ), + const SizedBox( + height: 6, + ), + child, + ], + ), + child: RichText( + text: TextSpan( + text: + "You must send at least ${sendAmount.toStringAsFixed( + trade.payInCurrency.toLowerCase() == "xmr" ? 12 : 8, + )} ${trade.payInCurrency.toUpperCase()}. ", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorRed) + : STextStyles.label(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .warningForeground, + ), + children: [ + TextSpan( + text: + "If you send less than ${sendAmount.toStringAsFixed( + trade.payInCurrency.toLowerCase() == "xmr" + ? 12 + : 8, + )} ${trade.payInCurrency.toUpperCase()}, your transaction may not be converted and it may not be refunded.", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorRed) + : STextStyles.label(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .warningForeground, + ), + ), + ]), + ), ), ), if (sentFromStack) @@ -1035,12 +1137,12 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> { ], ), ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - if (isStackCoin(trade.payInCurrency) && + if (!isDesktop) + const SizedBox( + height: 12, + ), + if (!isDesktop && + isStackCoin(trade.payInCurrency) && (trade.status == "New" || trade.status == "new" || trade.status == "waiting" || diff --git a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart index 41bf4246a..a8f825911 100644 --- a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart +++ b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart @@ -10,7 +10,10 @@ import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/trade_card.dart'; -import 'package:tuple/tuple.dart'; + +import '../../../route_generator.dart'; +import '../../../widgets/desktop/desktop_dialog.dart'; +import '../../../widgets/desktop/desktop_dialog_close_button.dart'; class DesktopTradeHistory extends ConsumerStatefulWidget { const DesktopTradeHistory({Key? key}) : super(key: key); @@ -64,19 +67,122 @@ class _DesktopTradeHistoryState extends ConsumerState<DesktopTradeHistory> { final tx = txData.getAllTransactions()[txid]; if (mounted) { - unawaited( - Navigator.of(context).pushNamed( - TradeDetailsView.routeName, - arguments: Tuple4( - tradeId, tx, walletIds.first, manager.walletName), + await showDialog<void>( + context: context, + builder: (context) => Navigator( + initialRoute: TradeDetailsView.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + FadePageRoute( + DesktopDialog( + // maxHeight: + // MediaQuery.of(context).size.height - 64, + maxHeight: double.infinity, + maxWidth: 580, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 16, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Trade details", + style: STextStyles.desktopH3(context), + ), + DesktopDialogCloseButton( + onPressedOverride: Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + ], + ), + ), + Flexible( + child: TradeDetailsView( + tradeId: tradeId, + transactionIfSentFromStack: tx, + walletName: manager.walletName, + walletId: walletIds.first, + ), + ), + ], + ), + ), + const RouteSettings( + name: TradeDetailsView.routeName, + ), + ), + ]; + }, ), ); } } else { unawaited( - Navigator.of(context).pushNamed( - TradeDetailsView.routeName, - arguments: Tuple4(tradeId, null, walletIds?.first, null), + showDialog<void>( + context: context, + builder: (context) => Navigator( + initialRoute: TradeDetailsView.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + FadePageRoute( + DesktopDialog( + // maxHeight: + // MediaQuery.of(context).size.height - 64, + maxHeight: double.infinity, + maxWidth: 580, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 16, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Trade details", + style: STextStyles.desktopH3(context), + ), + DesktopDialogCloseButton( + onPressedOverride: Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + ], + ), + ), + Flexible( + child: TradeDetailsView( + tradeId: tradeId, + transactionIfSentFromStack: null, + walletName: null, + walletId: walletIds?.first, + ), + ), + ], + ), + ), + const RouteSettings( + name: TradeDetailsView.routeName, + ), + ), + ]; + }, + ), ), ); } diff --git a/lib/widgets/trade_card.dart b/lib/widgets/trade_card.dart index 0ac8e9346..5a14a0777 100644 --- a/lib/widgets/trade_card.dart +++ b/lib/widgets/trade_card.dart @@ -8,6 +8,7 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class TradeCard extends ConsumerWidget { @@ -49,68 +50,85 @@ class TradeCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return GestureDetector( - onTap: onTap, - child: RoundedWhiteContainer( - child: Row( - children: [ - Container( - width: 32, - height: 32, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(32), - ), - child: Center( - child: SvgPicture.asset( - _fetchIconAssetForStatus( - trade.status, - context, + final isDesktop = Util.isDesktop; + + return ConditionalParent( + condition: isDesktop, + builder: (child) => MouseRegion( + cursor: SystemMouseCursors.click, + child: child, + ), + child: GestureDetector( + onTap: onTap, + child: RoundedWhiteContainer( + padding: + isDesktop ? const EdgeInsets.all(16) : const EdgeInsets.all(12), + child: Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(32), + ), + child: Center( + child: SvgPicture.asset( + _fetchIconAssetForStatus( + trade.status, + context, + ), + width: 32, + height: 32, ), - width: 32, - height: 32, ), ), - ), - const SizedBox( - width: 12, - ), - Expanded( - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "${trade.payInCurrency.toUpperCase()} → ${trade.payOutCurrency.toUpperCase()}", - style: STextStyles.itemSubtitle12(context), - ), - Text( - "${Util.isDesktop ? "-" : ""}${Decimal.tryParse(trade.payInAmount) ?? "..."} ${trade.payInCurrency.toUpperCase()}", - style: STextStyles.itemSubtitle12(context), - ), - ], - ), - const SizedBox( - height: 2, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - trade.exchangeName, - style: STextStyles.label(context), - ), - Text( - Format.extractDateFrom( - trade.timestamp.millisecondsSinceEpoch ~/ 1000), - style: STextStyles.label(context), - ), - ], - ), - ], + const SizedBox( + width: 12, ), - ) - ], + Expanded( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "${trade.payInCurrency.toUpperCase()} → ${trade.payOutCurrency.toUpperCase()}", + style: STextStyles.itemSubtitle12(context), + ), + Text( + "${isDesktop ? "-" : ""}${Decimal.tryParse(trade.payInAmount) ?? "..."} ${trade.payInCurrency.toUpperCase()}", + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + const SizedBox( + height: 2, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (!isDesktop) + Text( + trade.exchangeName, + style: STextStyles.label(context), + ), + Text( + Format.extractDateFrom( + trade.timestamp.millisecondsSinceEpoch ~/ 1000), + style: STextStyles.label(context), + ), + if (isDesktop) + Text( + trade.exchangeName, + style: STextStyles.label(context), + ), + ], + ), + ], + ), + ) + ], + ), ), ), ); From 6552fc913db3d66782eddc02c54f6eb155e7a730 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 22 Nov 2022 09:11:18 -0600 Subject: [PATCH 374/426] WIP send auth for trade transactions --- lib/pages/exchange_view/send_from_view.dart | 281 ++++++++++++++++-- .../send_view/confirm_transaction_view.dart | 17 +- .../sub_widgets/desktop_auth_send.dart | 32 +- 3 files changed, 301 insertions(+), 29 deletions(-) diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart index c87175955..59675e4e4 100644 --- a/lib/pages/exchange_view/send_from_view.dart +++ b/lib/pages/exchange_view/send_from_view.dart @@ -7,6 +7,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/models/exchange/response_objects/trade.dart'; import 'package:stackwallet/pages/exchange_view/confirm_change_now_send.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; +import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart'; import 'package:stackwallet/pages/send_view/sub_widgets/building_transaction_dialog.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/route_generator.dart'; @@ -19,13 +20,18 @@ import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/animated_text.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:stackwallet/widgets/expandable.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; +import '../../pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart'; + class SendFromView extends ConsumerStatefulWidget { const SendFromView({ Key? key, @@ -90,21 +96,68 @@ class _SendFromViewState extends ConsumerState<SendFromView> { final walletIds = ref.watch(walletsChangeNotifierProvider .select((value) => value.getWalletIdsFor(coin: coin))); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + final isDesktop = Util.isDesktop; + + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Send from", + style: STextStyles.navBarTitle(context), + ), + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + ); + }, + child: ConditionalParent( + condition: isDesktop, + builder: (child) => DesktopDialog( + maxHeight: double.infinity, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Send from Stack", + style: STextStyles.desktopH3(context), + ), + ), + DesktopDialogCloseButton( + onPressedOverride: Navigator.of( + context, + rootNavigator: false, + ).pop, + ), + ], + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: child, + ), + ], + ), ), - title: Text( - "Send from", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(16), child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ @@ -112,15 +165,23 @@ class _SendFromViewState extends ConsumerState<SendFromView> { children: [ Text( "You need to send ${formatAmount(amount, coin)} ${coin.ticker}", - style: STextStyles.itemSubtitle(context), + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.itemSubtitle(context), ), ], ), const SizedBox( height: 16, ), - Expanded( + ConditionalParent( + condition: !isDesktop, + builder: (child) => Expanded( + child: child, + ), child: ListView.builder( + primary: isDesktop ? false : null, + shrinkWrap: isDesktop, itemCount: walletIds.length, itemBuilder: (context, index) { return Padding( @@ -339,10 +400,67 @@ class _SendFromCardState extends ConsumerState<SendFromCard> { Constants.size.circularBorderRadius, ), ), - onPressed: () => _send( - manager, - shouldSendPublicFiroFunds: false, - ), + onPressed: () async { + final dynamic unlocked; + + if (Util.isDesktop) { + unlocked = await showDialog<bool?>( + context: context, + builder: (context) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: const [ + DesktopDialogCloseButton(), + ], + ), + const Padding( + padding: EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: DesktopAuthSend(), + ), + ], + ), + ), + ); + } else { + unlocked = await Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator.useMaterialPageRoute, + builder: (_) => const LockscreenView( + showBackButton: true, + popOnSuccess: true, + routeOnSuccessArguments: true, + routeOnSuccess: "", + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: + "Authenticate to send transaction", + biometricsAuthenticationTitle: "Confirm Transaction", + ), + settings: + const RouteSettings(name: "/confirmsendlockscreen"), + ), + ); + } + + if (unlocked is bool && unlocked && mounted) { + unawaited( + _send( + manager, + shouldSendPublicFiroFunds: false, + ), + ); + } + }, child: Container( color: Colors.transparent, child: Padding( @@ -418,10 +536,67 @@ class _SendFromCardState extends ConsumerState<SendFromCard> { Constants.size.circularBorderRadius, ), ), - onPressed: () => _send( - manager, - shouldSendPublicFiroFunds: true, - ), + onPressed: () async { + final dynamic unlocked; + + if (Util.isDesktop) { + unlocked = await showDialog<bool?>( + context: context, + builder: (context) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: const [ + DesktopDialogCloseButton(), + ], + ), + const Padding( + padding: EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: DesktopAuthSend(), + ), + ], + ), + ), + ); + } else { + unlocked = await Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator.useMaterialPageRoute, + builder: (_) => const LockscreenView( + showBackButton: true, + popOnSuccess: true, + routeOnSuccessArguments: true, + routeOnSuccess: "", + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: + "Authenticate to send transaction", + biometricsAuthenticationTitle: "Confirm Transaction", + ), + settings: + const RouteSettings(name: "/confirmsendlockscreen"), + ), + ); + } + + if (unlocked is bool && unlocked && mounted) { + unawaited( + _send( + manager, + shouldSendPublicFiroFunds: true, + ), + ); + } + }, child: Container( color: Colors.transparent, child: Padding( @@ -504,7 +679,63 @@ class _SendFromCardState extends ConsumerState<SendFromCard> { Constants.size.circularBorderRadius, ), ), - onPressed: () => _send(manager), + onPressed: () async { + final dynamic unlocked; + + if (Util.isDesktop) { + unlocked = await showDialog<bool?>( + context: context, + builder: (context) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: const [ + DesktopDialogCloseButton(), + ], + ), + const Padding( + padding: EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: DesktopAuthSend(), + ), + ], + ), + ), + ); + } else { + unlocked = await Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, + builder: (_) => const LockscreenView( + showBackButton: true, + popOnSuccess: true, + routeOnSuccessArguments: true, + routeOnSuccess: "", + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: + "Authenticate to send transaction", + biometricsAuthenticationTitle: "Confirm Transaction", + ), + settings: + const RouteSettings(name: "/confirmsendlockscreen"), + ), + ); + } + + if (unlocked is bool && unlocked && mounted) { + unawaited( + _send(manager), + ); + } + }, child: child, ), child: Row( diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index 8f7afb0bb..276203804 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -37,6 +37,7 @@ class ConfirmTransactionView extends ConsumerStatefulWidget { required this.transactionInfo, required this.walletId, this.routeOnSuccessName = WalletView.routeName, + this.isTradeTransaction = false, }) : super(key: key); static const String routeName = "/confirmTransactionView"; @@ -44,6 +45,7 @@ class ConfirmTransactionView extends ConsumerStatefulWidget { final Map<String, dynamic> transactionInfo; final String walletId; final String routeOnSuccessName; + final bool isTradeTransaction; @override ConsumerState<ConfirmTransactionView> createState() => @@ -833,8 +835,19 @@ class _ConfirmTransactionViewState ); } - if (unlocked is bool && unlocked && mounted) { - unawaited(_attemptSend(context)); + if (mounted) { + if (unlocked == true) { + unawaited(_attemptSend(context)); + } else { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: Util.isDesktop + ? "Invalid passphrase" + : "Invalid PIN", + context: context), + ); + } } }, ), diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart index 9f863c8a4..a8d1ea497 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -10,6 +12,9 @@ import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; +import '../../../../../notifications/show_flush_bar.dart'; +import '../../../../../widgets/loading_indicator.dart'; + class DesktopAuthSend extends ConsumerStatefulWidget { const DesktopAuthSend({Key? key}) : super(key: key); @@ -155,12 +160,35 @@ class _DesktopAuthSendState extends ConsumerState<DesktopAuthSend> { label: "Confirm", buttonHeight: ButtonHeight.l, onPressed: () async { - // TODO show spinner while verifying passphrase + unawaited( + showDialog<void>( + context: context, + builder: (context) => Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: const [ + LoadingIndicator( + width: 200, + height: 200, + ), + ], + ), + ), + ); + + await Future<void>.delayed(const Duration(seconds: 1)); final passwordIsValid = await verifyPassphrase(); if (mounted) { - Navigator.of(context).pop(passwordIsValid); + Navigator.of(context).pop(); + Navigator.of( + context, + rootNavigator: true, + ).pop(passwordIsValid); + await Future<void>.delayed(const Duration( + milliseconds: 100, + )); } }, ), From 0bdf337ffbeed8c7ebb8a3aa00271c6ccadbacf1 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 22 Nov 2022 11:21:43 -0600 Subject: [PATCH 375/426] WIP send from stack desktop trade transaction navigation --- .../confirm_change_now_send.dart | 842 ++++++++++++------ lib/pages/exchange_view/send_from_view.dart | 196 +--- .../exchange_view/trade_details_view.dart | 7 +- .../subwidgets/desktop_step_2.dart | 92 +- .../subwidgets/desktop_step_4.dart | 36 +- 5 files changed, 700 insertions(+), 473 deletions(-) diff --git a/lib/pages/exchange_view/confirm_change_now_send.dart b/lib/pages/exchange_view/confirm_change_now_send.dart index e99cf2df4..9f62bd8ec 100644 --- a/lib/pages/exchange_view/confirm_change_now_send.dart +++ b/lib/pages/exchange_view/confirm_change_now_send.dart @@ -7,15 +7,23 @@ import 'package:stackwallet/models/trade_wallet_lookup.dart'; import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart'; import 'package:stackwallet/pages/send_view/sub_widgets/sending_transaction_dialog.dart'; import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart'; import 'package:stackwallet/providers/exchange/trade_sent_from_stack_lookup_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; +import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.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'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; @@ -52,14 +60,16 @@ class _ConfirmChangeNowSendViewState late final Trade trade; Future<void> _attemptSend(BuildContext context) async { - unawaited(showDialog<void>( - context: context, - useSafeArea: false, - barrierDismissible: false, - builder: (context) { - return const SendingTransactionDialog(); - }, - )); + unawaited( + showDialog<void>( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return const SendingTransactionDialog(); + }, + ), + ); final String note = transactionInfo["note"] as String? ?? ""; final manager = @@ -93,6 +103,9 @@ class _ConfirmChangeNowSendViewState // pop back to wallet if (mounted) { + if (Util.isDesktop) { + Navigator.of(context, rootNavigator: true).pop(); + } Navigator.of(context).popUntil(ModalRoute.withName(routeOnSuccessName)); } } catch (e) { @@ -129,6 +142,60 @@ class _ConfirmChangeNowSendViewState } } + Future<void> _confirmSend() async { + final dynamic unlocked; + + if (Util.isDesktop) { + unlocked = await showDialog<bool?>( + context: context, + builder: (context) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: const [ + DesktopDialogCloseButton(), + ], + ), + const Padding( + padding: EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: DesktopAuthSend(), + ), + ], + ), + ), + ); + } else { + unlocked = await Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, + builder: (_) => const LockscreenView( + showBackButton: true, + popOnSuccess: true, + routeOnSuccessArguments: true, + routeOnSuccess: "", + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: "Authenticate to send transaction", + biometricsAuthenticationTitle: "Confirm Transaction", + ), + settings: const RouteSettings(name: "/confirmsendlockscreen"), + ), + ); + } + + if (unlocked is bool && unlocked && mounted) { + await _attemptSend(context); + } + } + @override void initState() { transactionInfo = widget.transactionInfo; @@ -142,280 +209,503 @@ class _ConfirmChangeNowSendViewState Widget build(BuildContext context) { final managerProvider = ref.watch(walletsChangeNotifierProvider .select((value) => value.getManagerProvider(walletId))); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - leading: AppBarBackButton( - onPressed: () async { - // if (FocusScope.of(context).hasFocus) { - // FocusScope.of(context).unfocus(); - // await Future<void>.delayed(Duration(milliseconds: 50)); - // } - Navigator.of(context).pop(); - }, - ), - title: Text( - "Confirm transaction", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (builderContext, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - "Send ${ref.watch(managerProvider.select((value) => value.coin)).ticker}", - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - "Send from", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 4, - ), - Text( - ref - .watch(walletsChangeNotifierProvider) - .getManager(walletId) - .walletName, - style: STextStyles.itemSubtitle12(context), - ), - ], - ), - ), - const SizedBox( - height: 12, - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - "${trade.exchangeName} address", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 4, - ), - Text( - "${transactionInfo["address"] ?? "ERROR"}", - style: STextStyles.itemSubtitle12(context), - ), - ], - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Amount", - style: STextStyles.smallMed12(context), - ), - Text( - "${Format.satoshiAmountToPrettyString( - transactionInfo["recipientAmt"] as int, - ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), - )} ${ref.watch( - managerProvider - .select((value) => value.coin), - ).ticker}", - style: STextStyles.itemSubtitle12(context), - textAlign: TextAlign.right, - ), - ], - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Transaction fee", - style: STextStyles.smallMed12(context), - ), - Text( - "${Format.satoshiAmountToPrettyString( - transactionInfo["fee"] as int, - ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), - )} ${ref.watch( - managerProvider - .select((value) => value.coin), - ).ticker}", - style: STextStyles.itemSubtitle12(context), - textAlign: TextAlign.right, - ), - ], - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - "Note", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 4, - ), - Text( - transactionInfo["note"] as String? ?? "", - style: STextStyles.itemSubtitle12(context), - ), - ], - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Trade ID", - style: STextStyles.smallMed12(context), - ), - Text( - trade.tradeId, - style: STextStyles.itemSubtitle12(context), - textAlign: TextAlign.right, - ), - ], - ), - ), - const SizedBox( - height: 12, - ), - RoundedContainer( - color: Theme.of(context) - .extension<StackColors>()! - .snackBarBackSuccess, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Total amount", - style: - STextStyles.titleBold12(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textConfirmTotalAmount, - ), - ), - Text( - "${Format.satoshiAmountToPrettyString( - (transactionInfo["fee"] as int) + - (transactionInfo["recipientAmt"] as int), - ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), - )} ${ref.watch( - managerProvider - .select((value) => value.coin), - ).ticker}", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textConfirmTotalAmount, - ), - textAlign: TextAlign.right, - ), - ], - ), - ), - const SizedBox( - height: 16, - ), - const Spacer(), - TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () async { - final unlocked = await Navigator.push( - context, - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator.useMaterialPageRoute, - builder: (_) => const LockscreenView( - showBackButton: true, - popOnSuccess: true, - routeOnSuccessArguments: true, - routeOnSuccess: "", - biometricsCancelButtonString: "CANCEL", - biometricsLocalizedReason: - "Authenticate to send transaction", - biometricsAuthenticationTitle: - "Confirm Transaction", - ), - settings: const RouteSettings( - name: "/confirmsendlockscreen"), - ), - ); - if (unlocked is bool && unlocked && mounted) { - await _attemptSend(context); - } - }, - child: Text( - "Send", - style: STextStyles.button(context), - ), - ), - ], + final isDesktop = Util.isDesktop; + + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + leading: AppBarBackButton( + onPressed: () async { + // if (FocusScope.of(context).hasFocus) { + // FocusScope.of(context).unfocus(); + // await Future<void>.delayed(Duration(milliseconds: 50)); + // } + Navigator.of(context).pop(); + }, + ), + title: Text( + "Confirm transaction", + style: STextStyles.navBarTitle(context), + ), + ), + body: LayoutBuilder( + builder: (builderContext, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), ), ), ), + ); + }, + ), + ); + }, + child: ConditionalParent( + condition: isDesktop, + builder: (child) => DesktopDialog( + maxHeight: double.infinity, + maxWidth: 580, + child: Column( + children: [ + Row( + children: [ + const SizedBox( + width: 6, + ), + const AppBarBackButton( + isCompact: true, + iconSize: 23, + ), + const SizedBox( + width: 12, + ), + Text( + "Confirm ${ref.watch(managerProvider.select((value) => value.coin)).ticker} transaction", + style: STextStyles.desktopH3(context), + ) + ], + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: Column( + children: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + borderColor: Theme.of(context) + .extension<StackColors>()! + .background, + child: child, + ), + const SizedBox( + height: 16, + ), + Row( + children: [ + Text( + "Transaction fee", + style: + STextStyles.desktopTextExtraExtraSmall(context), + ), + ], + ), + const SizedBox( + height: 10, + ), + RoundedContainer( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + "${Format.satoshiAmountToPrettyString( + (transactionInfo["fee"] as int), + ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ), + )} ${ref.watch( + managerProvider.select((value) => value.coin), + ).ticker}", + style: + STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + ], + ), + ), + const SizedBox( + height: 16, + ), + RoundedContainer( + color: Theme.of(context) + .extension<StackColors>()! + .snackBarBackSuccess, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Total amount", + style: STextStyles.titleBold12(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textConfirmTotalAmount, + ), + ), + Text( + "${Format.satoshiAmountToPrettyString( + (transactionInfo["fee"] as int) + + (transactionInfo["recipientAmt"] as int), + ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ), + )} ${ref.watch( + managerProvider.select((value) => value.coin), + ).ticker}", + style: STextStyles.itemSubtitle12(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textConfirmTotalAmount, + ), + textAlign: TextAlign.right, + ), + ], + ), + ), + const SizedBox( + height: 16, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of(context).pop, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Send", + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: _confirmSend, + ), + ), + ], + ) + ], + ), + ), + ], + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ConditionalParent( + condition: isDesktop, + builder: (child) => Container( + decoration: BoxDecoration( + color: Theme.of(context).extension<StackColors>()!.background, + borderRadius: BorderRadius.vertical( + top: Radius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + child, + ], + ), + ), + ), + child: Text( + "Send ${ref.watch(managerProvider.select((value) => value.coin)).ticker}", + style: isDesktop + ? STextStyles.desktopTextMedium(context) + : STextStyles.pageTitleH1(context), ), ), - ); - }, + isDesktop + ? Container( + color: + Theme.of(context).extension<StackColors>()!.background, + height: 1, + ) + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Send from", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 4, + ), + Text( + ref + .watch(walletsChangeNotifierProvider) + .getManager(walletId) + .walletName, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + isDesktop + ? Container( + color: + Theme.of(context).extension<StackColors>()!.background, + height: 1, + ) + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "${trade.exchangeName} address", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 4, + ), + Text( + "${transactionInfo["address"] ?? "ERROR"}", + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + isDesktop + ? Container( + color: + Theme.of(context).extension<StackColors>()!.background, + height: 1, + ) + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Amount", + style: STextStyles.smallMed12(context), + ), + ConditionalParent( + condition: isDesktop, + builder: (child) => Row( + children: [ + child, + Builder(builder: (context) { + final coin = ref.watch( + walletsChangeNotifierProvider.select( + (value) => value.getManager(walletId).coin)); + final price = ref.watch( + priceAnd24hChangeNotifierProvider + .select((value) => value.getPrice(coin))); + final amount = Format.satoshisToAmount( + transactionInfo["recipientAmt"] as int, + coin: coin, + ); + final value = price.item1 * amount; + final currency = ref.watch(prefsChangeNotifierProvider + .select((value) => value.currency)); + + return Text( + " | ${value.toStringAsFixed(Constants.decimalPlacesForCoin(coin))} $currency", + style: + STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle2, + ), + ); + }) + ], + ), + child: Text( + "${Format.satoshiAmountToPrettyString( + transactionInfo["recipientAmt"] as int, + ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ), + )} ${ref.watch( + managerProvider.select((value) => value.coin), + ).ticker}", + style: STextStyles.itemSubtitle12(context), + textAlign: TextAlign.right, + ), + ), + ], + ), + ), + isDesktop + ? Container( + color: + Theme.of(context).extension<StackColors>()!.background, + height: 1, + ) + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transaction fee", + style: STextStyles.smallMed12(context), + ), + Text( + "${Format.satoshiAmountToPrettyString( + transactionInfo["fee"] as int, + ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ), + )} ${ref.watch( + managerProvider.select((value) => value.coin), + ).ticker}", + style: STextStyles.itemSubtitle12(context), + textAlign: TextAlign.right, + ), + ], + ), + ), + isDesktop + ? Container( + color: + Theme.of(context).extension<StackColors>()!.background, + height: 1, + ) + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Note", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 4, + ), + Text( + transactionInfo["note"] as String? ?? "", + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + isDesktop + ? Container( + color: + Theme.of(context).extension<StackColors>()!.background, + height: 1, + ) + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Trade ID", + style: STextStyles.smallMed12(context), + ), + Text( + trade.tradeId, + style: STextStyles.itemSubtitle12(context), + textAlign: TextAlign.right, + ), + ], + ), + ), + if (!isDesktop) + const SizedBox( + height: 12, + ), + if (!isDesktop) + RoundedContainer( + color: Theme.of(context) + .extension<StackColors>()! + .snackBarBackSuccess, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Total amount", + style: STextStyles.titleBold12(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textConfirmTotalAmount, + ), + ), + Text( + "${Format.satoshiAmountToPrettyString( + (transactionInfo["fee"] as int) + + (transactionInfo["recipientAmt"] as int), + ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ), + )} ${ref.watch( + managerProvider.select((value) => value.coin), + ).ticker}", + style: STextStyles.itemSubtitle12(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textConfirmTotalAmount, + ), + textAlign: TextAlign.right, + ), + ], + ), + ), + if (!isDesktop) + const SizedBox( + height: 16, + ), + if (!isDesktop) const Spacer(), + if (!isDesktop) + PrimaryButton( + label: "Send", + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: _confirmSend, + ), + ], + ), ), ); } diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart index 59675e4e4..7cbf38384 100644 --- a/lib/pages/exchange_view/send_from_view.dart +++ b/lib/pages/exchange_view/send_from_view.dart @@ -7,8 +7,8 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/models/exchange/response_objects/trade.dart'; import 'package:stackwallet/pages/exchange_view/confirm_change_now_send.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; -import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart'; import 'package:stackwallet/pages/send_view/sub_widgets/building_transaction_dialog.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; @@ -30,8 +30,6 @@ import 'package:stackwallet/widgets/expandable.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; -import '../../pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart'; - class SendFromView extends ConsumerStatefulWidget { const SendFromView({ Key? key, @@ -39,6 +37,7 @@ class SendFromView extends ConsumerStatefulWidget { required this.trade, required this.amount, required this.address, + this.shouldPopRoot = false, }) : super(key: key); static const String routeName = "/sendFrom"; @@ -47,6 +46,7 @@ class SendFromView extends ConsumerStatefulWidget { final Decimal amount; final String address; final Trade trade; + final bool shouldPopRoot; @override ConsumerState<SendFromView> createState() => _SendFromViewState(); @@ -142,7 +142,7 @@ class _SendFromViewState extends ConsumerState<SendFromView> { DesktopDialogCloseButton( onPressedOverride: Navigator.of( context, - rootNavigator: false, + rootNavigator: widget.shouldPopRoot, ).pop, ), ], @@ -239,12 +239,23 @@ class _SendFromCardState extends ConsumerState<SendFromCard> { useSafeArea: false, barrierDismissible: false, builder: (context) { - return BuildingTransactionDialog( - onCancel: () { - wasCancelled = true; + return ConditionalParent( + condition: Util.isDesktop, + builder: (child) => DesktopDialog( + maxWidth: 400, + maxHeight: double.infinity, + child: Padding( + padding: const EdgeInsets.all(32), + child: child, + ), + ), + child: BuildingTransactionDialog( + onCancel: () { + wasCancelled = true; - Navigator.of(context).pop(); - }, + Navigator.of(context).pop(); + }, + ), ); }, ), @@ -290,7 +301,10 @@ class _SendFromCardState extends ConsumerState<SendFromCard> { // pop building dialog if (mounted) { - Navigator.of(context).pop(); + Navigator.of( + context, + rootNavigator: Util.isDesktop, + ).pop(); } txData["note"] = @@ -304,7 +318,9 @@ class _SendFromCardState extends ConsumerState<SendFromCard> { builder: (_) => ConfirmChangeNowSendView( transactionInfo: txData, walletId: walletId, - routeOnSuccessName: HomeView.routeName, + routeOnSuccessName: Util.isDesktop + ? DesktopExchangeView.routeName + : HomeView.routeName, trade: trade, shouldSendPublicFiroFunds: shouldSendPublicFiroFunds, ), @@ -401,58 +417,7 @@ class _SendFromCardState extends ConsumerState<SendFromCard> { ), ), onPressed: () async { - final dynamic unlocked; - - if (Util.isDesktop) { - unlocked = await showDialog<bool?>( - context: context, - builder: (context) => DesktopDialog( - maxWidth: 580, - maxHeight: double.infinity, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: const [ - DesktopDialogCloseButton(), - ], - ), - const Padding( - padding: EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: DesktopAuthSend(), - ), - ], - ), - ), - ); - } else { - unlocked = await Navigator.push( - context, - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator.useMaterialPageRoute, - builder: (_) => const LockscreenView( - showBackButton: true, - popOnSuccess: true, - routeOnSuccessArguments: true, - routeOnSuccess: "", - biometricsCancelButtonString: "CANCEL", - biometricsLocalizedReason: - "Authenticate to send transaction", - biometricsAuthenticationTitle: "Confirm Transaction", - ), - settings: - const RouteSettings(name: "/confirmsendlockscreen"), - ), - ); - } - - if (unlocked is bool && unlocked && mounted) { + if (mounted) { unawaited( _send( manager, @@ -537,58 +502,7 @@ class _SendFromCardState extends ConsumerState<SendFromCard> { ), ), onPressed: () async { - final dynamic unlocked; - - if (Util.isDesktop) { - unlocked = await showDialog<bool?>( - context: context, - builder: (context) => DesktopDialog( - maxWidth: 580, - maxHeight: double.infinity, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: const [ - DesktopDialogCloseButton(), - ], - ), - const Padding( - padding: EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: DesktopAuthSend(), - ), - ], - ), - ), - ); - } else { - unlocked = await Navigator.push( - context, - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator.useMaterialPageRoute, - builder: (_) => const LockscreenView( - showBackButton: true, - popOnSuccess: true, - routeOnSuccessArguments: true, - routeOnSuccess: "", - biometricsCancelButtonString: "CANCEL", - biometricsLocalizedReason: - "Authenticate to send transaction", - biometricsAuthenticationTitle: "Confirm Transaction", - ), - settings: - const RouteSettings(name: "/confirmsendlockscreen"), - ), - ); - } - - if (unlocked is bool && unlocked && mounted) { + if (mounted) { unawaited( _send( manager, @@ -680,57 +594,7 @@ class _SendFromCardState extends ConsumerState<SendFromCard> { ), ), onPressed: () async { - final dynamic unlocked; - - if (Util.isDesktop) { - unlocked = await showDialog<bool?>( - context: context, - builder: (context) => DesktopDialog( - maxWidth: 580, - maxHeight: double.infinity, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: const [ - DesktopDialogCloseButton(), - ], - ), - const Padding( - padding: EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: DesktopAuthSend(), - ), - ], - ), - ), - ); - } else { - unlocked = await Navigator.push( - context, - RouteGenerator.getRoute( - shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: (_) => const LockscreenView( - showBackButton: true, - popOnSuccess: true, - routeOnSuccessArguments: true, - routeOnSuccess: "", - biometricsCancelButtonString: "CANCEL", - biometricsLocalizedReason: - "Authenticate to send transaction", - biometricsAuthenticationTitle: "Confirm Transaction", - ), - settings: - const RouteSettings(name: "/confirmsendlockscreen"), - ), - ); - } - - if (unlocked is bool && unlocked && mounted) { + if (mounted) { unawaited( _send(manager), ); diff --git a/lib/pages/exchange_view/trade_details_view.dart b/lib/pages/exchange_view/trade_details_view.dart index f28d1d617..0b7f4b502 100644 --- a/lib/pages/exchange_view/trade_details_view.dart +++ b/lib/pages/exchange_view/trade_details_view.dart @@ -219,7 +219,8 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> { children: children, ), ), - if (isStackCoin(trade.payInCurrency) && + if (!hasTx && + isStackCoin(trade.payInCurrency) && (trade.status == "New" || trade.status == "new" || trade.status == "waiting" || @@ -227,7 +228,8 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> { const SizedBox( height: 32, ), - if (isStackCoin(trade.payInCurrency) && + if (!hasTx && + isStackCoin(trade.payInCurrency) && (trade.status == "New" || trade.status == "new" || trade.status == "waiting" || @@ -1142,6 +1144,7 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> { height: 12, ), if (!isDesktop && + !hasTx && isStackCoin(trade.payInCurrency) && (trade.status == "New" || trade.status == "new" || diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart index 525d561fc..7b793210c 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart @@ -51,6 +51,8 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { late final FocusNode _toFocusNode; late final FocusNode _refundFocusNode; + bool enableNext = false; + bool isStackCoin(String ticker) { try { coinFromTickerCaseInsensitive(ticker); @@ -60,13 +62,13 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { } } - void selectRecipientAddressFromStack() { + void selectRecipientAddressFromStack() async { try { final coin = coinFromTickerCaseInsensitive( model.receiveTicker, ); - showDialog<String?>( + final address = await showDialog<String?>( context: context, barrierColor: Colors.transparent, builder: (context) => DesktopDialog( @@ -79,27 +81,31 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { ), ), ), - ).then((value) async { - if (value is String) { - final manager = - ref.read(walletsChangeNotifierProvider).getManager(value); + ); - _toController.text = manager.walletName; - model.recipientAddress = await manager.currentReceivingAddress; - } - }); + if (address is String) { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(address); + + _toController.text = manager.walletName; + model.recipientAddress = await manager.currentReceivingAddress; + } } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Info); } + setState(() { + enableNext = + _toController.text.isNotEmpty && _refundController.text.isNotEmpty; + }); } - void selectRefundAddressFromStack() { + void selectRefundAddressFromStack() async { try { final coin = coinFromTickerCaseInsensitive( model.sendTicker, ); - showDialog<String?>( + final address = await showDialog<String?>( context: context, barrierColor: Colors.transparent, builder: (context) => DesktopDialog( @@ -112,18 +118,21 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { ), ), ), - ).then((value) async { - if (value is String) { - final manager = - ref.read(walletsChangeNotifierProvider).getManager(value); + ); + if (address is String) { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(address); - _refundController.text = manager.walletName; - model.refundAddress = await manager.currentReceivingAddress; - } - }); + _refundController.text = manager.walletName; + model.refundAddress = await manager.currentReceivingAddress; + } } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Info); } + setState(() { + enableNext = + _toController.text.isNotEmpty && _refundController.text.isNotEmpty; + }); } void selectRecipientFromAddressBook() async { @@ -168,7 +177,10 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { if (entry != null) { _toController.text = entry.address; model.recipientAddress = entry.address; - setState(() {}); + setState(() { + enableNext = + _toController.text.isNotEmpty && _refundController.text.isNotEmpty; + }); } } @@ -214,7 +226,10 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { if (entry != null) { _refundController.text = entry.address; model.refundAddress = entry.address; - setState(() {}); + setState(() { + enableNext = + _toController.text.isNotEmpty && _refundController.text.isNotEmpty; + }); } } @@ -334,7 +349,10 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { focusNode: _toFocusNode, style: STextStyles.field(context), onChanged: (value) { - setState(() {}); + setState(() { + enableNext = _toController.text.isNotEmpty && + _refundController.text.isNotEmpty; + }); }, decoration: standardInputDecoration( "Enter the ${model.receiveTicker.toUpperCase()} payout address", @@ -363,7 +381,10 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { onTap: () { _toController.text = ""; model.recipientAddress = _toController.text; - setState(() {}); + setState(() { + enableNext = _toController.text.isNotEmpty && + _refundController.text.isNotEmpty; + }); }, child: const XIcon(), ) @@ -378,7 +399,11 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { final content = data.text!.trim(); _toController.text = content; model.recipientAddress = _toController.text; - setState(() {}); + setState(() { + enableNext = + _toController.text.isNotEmpty && + _refundController.text.isNotEmpty; + }); } }, child: _toController.text.isEmpty @@ -454,7 +479,10 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { focusNode: _refundFocusNode, style: STextStyles.field(context), onChanged: (value) { - setState(() {}); + setState(() { + enableNext = _toController.text.isNotEmpty && + _refundController.text.isNotEmpty; + }); }, decoration: standardInputDecoration( "Enter ${model.sendTicker.toUpperCase()} refund address", @@ -484,7 +512,10 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { _refundController.text = ""; model.refundAddress = _refundController.text; - setState(() {}); + setState(() { + enableNext = _toController.text.isNotEmpty && + _refundController.text.isNotEmpty; + }); }, child: const XIcon(), ) @@ -501,7 +532,11 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { _refundController.text = content; model.refundAddress = _refundController.text; - setState(() {}); + setState(() { + enableNext = + _toController.text.isNotEmpty && + _refundController.text.isNotEmpty; + }); } }, child: _refundController.text.isEmpty @@ -552,6 +587,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { Expanded( child: PrimaryButton( label: "Next", + enabled: enableNext, buttonHeight: ButtonHeight.l, onPressed: () async { await showDialog<void>( diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart index 7747f570f..c86713a76 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart @@ -1,11 +1,14 @@ import 'dart:async'; +import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; +import 'package:stackwallet/pages/exchange_view/send_from_view.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -199,7 +202,38 @@ class _DesktopStep4State extends ConsumerState<DesktopStep4> { child: SecondaryButton( label: "Send from Stack Wallet", buttonHeight: ButtonHeight.l, - onPressed: Navigator.of(context).pop, + onPressed: () { + final trade = model.trade!; + final amount = Decimal.parse(trade.payInAmount); + final address = trade.payInAddress; + + final coin = + coinFromTickerCaseInsensitive(trade.payInCurrency); + + showDialog<void>( + context: context, + builder: (context) => Navigator( + initialRoute: SendFromView.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + FadePageRoute( + SendFromView( + coin: coin, + trade: trade, + amount: amount, + address: address, + shouldPopRoot: true, + ), + const RouteSettings( + name: SendFromView.routeName, + ), + ), + ]; + }, + ), + ); + }, ), ), const SizedBox( From c5c0443d000d425b86056d27dda575a3b895fde6 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Tue, 22 Nov 2022 08:57:05 -0700 Subject: [PATCH 376/426] button sizing fix --- .../home/settings_menu/currency_settings/currency_settings.dart | 2 +- .../home/settings_menu/language_settings/language_settings.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart index d9c20d8fa..9f5f42b7c 100644 --- a/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/currency_settings/currency_settings.dart @@ -107,7 +107,7 @@ class _CurrencySettings extends ConsumerState<CurrencySettings> { 10, ), child: PrimaryButton( - width: 210, + width: 200, buttonHeight: ButtonHeight.m, enabled: true, label: "Change currency", diff --git a/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart b/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart index acddcb055..3c511236c 100644 --- a/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/language_settings/language_settings.dart @@ -84,7 +84,7 @@ class _LanguageOptionSettings extends ConsumerState<LanguageOptionSettings> { 10, ), child: PrimaryButton( - width: 210, + width: 200, buttonHeight: ButtonHeight.m, enabled: true, label: "Change language", From 9a47ce349e52a12d2be80ab7f48ede2b9ca7f379 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Tue, 22 Nov 2022 11:48:47 -0700 Subject: [PATCH 377/426] submit on enter passphrase --- .../desktop_attention_delete_wallet.dart | 3 +- .../desktop_delete_wallet_dialog.dart | 126 +++++++++--------- 2 files changed, 66 insertions(+), 63 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart index 6eb04502e..a614be3a6 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -10,8 +11,6 @@ import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:tuple/tuple.dart'; -import 'delete_wallet_keys_popup.dart'; - class DesktopAttentionDeleteWallet extends ConsumerStatefulWidget { const DesktopAttentionDeleteWallet({ Key? key, diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart index 6729d33b9..e8d9c2dc5 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart @@ -5,6 +5,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart'; +import 'package:stackwallet/providers/desktop/storage_crypto_handler_provider.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -16,9 +18,6 @@ import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; -import '../../../../../providers/desktop/storage_crypto_handler_provider.dart'; -import '../../../../../providers/global/wallets_provider.dart'; - class DesktopDeleteWalletDialog extends ConsumerStatefulWidget { const DesktopDeleteWalletDialog({ Key? key, @@ -42,6 +41,62 @@ class _DesktopDeleteWalletDialog bool hidePassword = true; bool _continueEnabled = false; + Future<void> enterPassphrase() async { + unawaited( + showDialog( + context: context, + builder: (context) => Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: const [ + LoadingIndicator( + width: 200, + height: 200, + ), + ], + ), + ), + ); + + await Future<void>.delayed(const Duration(seconds: 1)); + + final verified = await ref + .read(storageCryptoHandlerProvider) + .verifyPassphrase(passwordController.text); + + if (verified) { + Navigator.of(context, rootNavigator: true).pop(); + + final words = await ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .mnemonic; + + if (mounted) { + Navigator.of(context).pop(); + + unawaited( + Navigator.of(context).pushNamed( + DesktopAttentionDeleteWallet.routeName, + arguments: widget.walletId, + ), + ); + } + } else { + Navigator.of(context, rootNavigator: true).pop(); + + await Future<void>.delayed(const Duration(milliseconds: 300)); + + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Invalid passphrase!", + context: context, + ), + ); + } + } + @override void initState() { passwordController = TextEditingController(); @@ -106,6 +161,12 @@ class _DesktopDeleteWalletDialog obscureText: hidePassword, enableSuggestions: false, autocorrect: false, + autofocus: true, + onSubmitted: (_) { + if (_continueEnabled) { + enterPassphrase(); + } + }, decoration: standardInputDecoration( "Enter password", passwordFocusNode, @@ -179,64 +240,7 @@ class _DesktopDeleteWalletDialog onPressed: _continueEnabled ? () async { // add loading indicator - unawaited( - showDialog( - context: context, - builder: (context) => Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: - CrossAxisAlignment.center, - children: const [ - LoadingIndicator( - width: 200, - height: 200, - ), - ], - ), - ), - ); - - await Future<void>.delayed( - const Duration(seconds: 1)); - - final verified = await ref - .read(storageCryptoHandlerProvider) - .verifyPassphrase(passwordController.text); - - if (verified) { - Navigator.of(context, rootNavigator: true) - .pop(); - - final words = await ref - .read(walletsChangeNotifierProvider) - .getManager(widget.walletId) - .mnemonic; - - if (mounted) { - Navigator.of(context).pop(); - - unawaited( - Navigator.of(context).pushNamed( - DesktopAttentionDeleteWallet.routeName, - arguments: widget.walletId, - ), - ); - } - } else { - Navigator.of(context, rootNavigator: true) - .pop(); - - await Future<void>.delayed( - const Duration(milliseconds: 300)); - - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Invalid passphrase!", - context: context, - ), - ); - } + enterPassphrase(); } : null, ), From 8e2ff3883d3516ee561d9acb36a89a5de94a0400 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 22 Nov 2022 14:42:24 -0600 Subject: [PATCH 378/426] exchange amount field re style --- lib/pages/exchange_view/exchange_form.dart | 362 ++--------------- .../textfields/exchange_textfield.dart | 384 ++++++++++++++++++ 2 files changed, 424 insertions(+), 322 deletions(-) create mode 100644 lib/widgets/textfields/exchange_textfield.dart diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart index 7f0d9b5d2..921b35bf0 100644 --- a/lib/pages/exchange_view/exchange_form.dart +++ b/lib/pages/exchange_view/exchange_form.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/svg.dart'; @@ -25,6 +24,7 @@ import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart'; import 'package:stackwallet/services/exchange/simpleswap/simpleswap_exchange.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/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -37,10 +37,10 @@ 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/desktop/simple_desktop_dialog.dart'; -import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; +import 'package:stackwallet/widgets/textfields/exchange_textfield.dart'; import 'package:tuple/tuple.dart'; class ExchangeForm extends ConsumerStatefulWidget { @@ -1226,146 +1226,33 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { SizedBox( height: isDesktop ? 10 : 4, ), - TextFormField( - style: STextStyles.smallMed14(context).copyWith( + ExchangeTextField( + controller: _sendController, + focusNode: _sendFocusNode, + textStyle: STextStyles.smallMed14(context).copyWith( color: Theme.of(context).extension<StackColors>()!.textDark, ), - focusNode: _sendFocusNode, - controller: _sendController, - textAlign: TextAlign.right, - enableSuggestions: false, - autocorrect: false, + buttonColor: + Theme.of(context).extension<StackColors>()!.buttonBackSecondary, + borderRadius: Constants.size.circularBorderRadius, + background: + Theme.of(context).extension<StackColors>()!.textFieldDefaultBG, onTap: () { if (_sendController.text == "-") { _sendController.text = ""; } }, onChanged: sendFieldOnChanged, - keyboardType: const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), - inputFormatters: [ - // regex to validate a crypto amount with 8 decimal places - TextInputFormatter.withFunction((oldValue, newValue) => - RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') - .hasMatch(newValue.text) - ? newValue - : oldValue), - ], - decoration: InputDecoration( - contentPadding: const EdgeInsets.only( - top: 12, - right: 12, - ), - hintText: "0", - hintStyle: STextStyles.fieldLabel(context).copyWith( - fontSize: 14, - ), - prefixIcon: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerLeft, - child: GestureDetector( - onTap: selectSendCurrency, - child: Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - Container( - width: 18, - height: 18, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(18), - ), - child: Builder( - builder: (context) { - final image = _fetchIconUrlFromTicker(ref.watch( - exchangeFormStateProvider - .select((value) => value.fromTicker))); - - if (image != null && image.isNotEmpty) { - return Center( - child: SvgPicture.network( - image, - height: 18, - placeholderBuilder: (_) => Container( - width: 18, - height: 18, - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - borderRadius: BorderRadius.circular( - 18, - ), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular( - 18, - ), - child: const LoadingIndicator(), - ), - ), - ), - ); - } else { - return Container( - width: 18, - height: 18, - decoration: BoxDecoration( - // color: Theme.of(context).extension<StackColors>()!.accentColorDark - borderRadius: BorderRadius.circular(18), - ), - child: SvgPicture.asset( - Assets.svg.circleQuestion, - width: 18, - height: 18, - color: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - ), - ); - } - }, - ), - ), - const SizedBox( - width: 6, - ), - Text( - ref.watch(exchangeFormStateProvider.select((value) => - value.fromTicker?.toUpperCase())) ?? - "-", - style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ), - ), - if (!isWalletCoin(coin, true)) - const SizedBox( - width: 6, - ), - if (!isWalletCoin(coin, true)) - SvgPicture.asset( - Assets.svg.chevronDown, - width: 5, - height: 2.5, - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ), - ], - ), - ), - ), - ), - ), - ), + onButtonTap: selectSendCurrency, + isWalletCoin: isWalletCoin(coin, true), + image: _fetchIconUrlFromTicker(ref.watch( + exchangeFormStateProvider.select((value) => value.fromTicker))), + ticker: ref.watch( + exchangeFormStateProvider.select((value) => value.fromTicker)), + ), + SizedBox( + height: isDesktop ? 10 : 4, ), - SizedBox( height: isDesktop ? 10 : 4, ), @@ -1422,79 +1309,20 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { ), ], ), - // Stack( - // children: [ - // Positioned.fill( - // child: Align( - // alignment: Alignment.bottomLeft, - // child: Text( - // "You will receive", - // style: STextStyles.itemSubtitle(context).copyWith( - // color: - // Theme.of(context).extension<StackColors>()!.textDark3, - // ), - // ), - // ), - // ), - // Center( - // child: Column( - // children: [ - // const SizedBox( - // height: 6, - // ), - // GestureDetector( - // onTap: () async { - // await _swap(); - // }, - // child: Padding( - // padding: const EdgeInsets.all(4), - // child: SvgPicture.asset( - // Assets.svg.swap, - // width: 20, - // height: 20, - // color: Theme.of(context) - // .extension<StackColors>()! - // .accentColorDark, - // ), - // ), - // ), - // const SizedBox( - // height: 6, - // ), - // ], - // ), - // ), - // Positioned.fill( - // child: Align( - // alignment: ref.watch(exchangeFormStateProvider - // .select((value) => value.reversed)) - // ? Alignment.bottomRight - // : Alignment.topRight, - // child: Text( - // ref.watch(exchangeFormStateProvider - // .select((value) => value.warning)), - // style: STextStyles.errorSmall(context), - // ), - // ), - // ), - // ], - // ), SizedBox( height: isDesktop ? 10 : 4, ), - TextFormField( - style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context).extension<StackColors>()!.textDark, - ), + ExchangeTextField( focusNode: _receiveFocusNode, controller: _receiveController, - enableSuggestions: false, - autocorrect: false, - readOnly: ref.watch(prefsChangeNotifierProvider - .select((value) => value.exchangeRateType)) == - ExchangeRateType.estimated || - ref.watch(exchangeProvider).name == - SimpleSwapExchange.exchangeName, + textStyle: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context).extension<StackColors>()!.textDark, + ), + buttonColor: + Theme.of(context).extension<StackColors>()!.buttonBackSecondary, + borderRadius: Constants.size.circularBorderRadius, + background: + Theme.of(context).extension<StackColors>()!.textFieldDefaultBG, onTap: () { if (!(ref.read(prefsChangeNotifierProvider).exchangeRateType == ExchangeRateType.estimated) && @@ -1503,127 +1331,17 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { } }, onChanged: receiveFieldOnChanged, - textAlign: TextAlign.right, - keyboardType: const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), - inputFormatters: [ - // regex to validate a crypto amount with 8 decimal places - TextInputFormatter.withFunction((oldValue, newValue) => - RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') - .hasMatch(newValue.text) - ? newValue - : oldValue), - ], - decoration: InputDecoration( - contentPadding: const EdgeInsets.only( - top: 12, - right: 12, - ), - hintText: "0", - hintStyle: STextStyles.fieldLabel(context).copyWith( - fontSize: 14, - ), - prefixIcon: FittedBox( - fit: BoxFit.scaleDown, - child: GestureDetector( - onTap: selectReceiveCurrency, - child: Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - Container( - width: 18, - height: 18, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(18), - ), - child: Builder( - builder: (context) { - final image = _fetchIconUrlFromTicker(ref.watch( - exchangeFormStateProvider - .select((value) => value.toTicker))); - - if (image != null && image.isNotEmpty) { - return Center( - child: SvgPicture.network( - image, - height: 18, - placeholderBuilder: (_) => Container( - width: 18, - height: 18, - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - borderRadius: BorderRadius.circular(18), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular( - 18, - ), - child: const LoadingIndicator(), - ), - ), - ), - ); - } else { - return Container( - width: 18, - height: 18, - decoration: BoxDecoration( - // color: Theme.of(context).extension<StackColors>()!.accentColorDark - borderRadius: BorderRadius.circular(18), - ), - child: SvgPicture.asset( - Assets.svg.circleQuestion, - width: 18, - height: 18, - color: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - ), - ); - } - }, - ), - ), - const SizedBox( - width: 6, - ), - Text( - ref.watch(exchangeFormStateProvider.select( - (value) => value.toTicker?.toUpperCase())) ?? - "-", - style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ), - ), - if (!isWalletCoin(coin, false)) - const SizedBox( - width: 6, - ), - if (!isWalletCoin(coin, false)) - SvgPicture.asset( - Assets.svg.chevronDown, - width: 5, - height: 2.5, - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ), - ], - ), - ), - ), - ), - ), - ), + onButtonTap: selectReceiveCurrency, + isWalletCoin: isWalletCoin(coin, true), + image: _fetchIconUrlFromTicker(ref.watch( + exchangeFormStateProvider.select((value) => value.toTicker))), + ticker: ref.watch( + exchangeFormStateProvider.select((value) => value.toTicker)), + readOnly: ref.watch(prefsChangeNotifierProvider + .select((value) => value.exchangeRateType)) == + ExchangeRateType.estimated || + ref.watch(exchangeProvider).name == + SimpleSwapExchange.exchangeName, ), if (ref .watch( diff --git a/lib/widgets/textfields/exchange_textfield.dart b/lib/widgets/textfields/exchange_textfield.dart new file mode 100644 index 000000000..8d3c5d699 --- /dev/null +++ b/lib/widgets/textfields/exchange_textfield.dart @@ -0,0 +1,384 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.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/loading_indicator.dart'; + +class ExchangeTextField extends StatefulWidget { + const ExchangeTextField({ + Key? key, + this.borderRadius = 0, + this.background, + required this.controller, + this.buttonColor, + required this.focusNode, + this.buttonContent, + required this.textStyle, + this.onButtonTap, + this.onChanged, + this.onSubmitted, + this.onTap, + required this.isWalletCoin, + this.image, + this.ticker, + this.readOnly = false, + }) : super(key: key); + + final double borderRadius; + final Color? background; + final Color? buttonColor; + final Widget? buttonContent; + final TextEditingController controller; + final FocusNode focusNode; + final TextStyle textStyle; + final VoidCallback? onTap; + final VoidCallback? onButtonTap; + final void Function(String)? onChanged; + final void Function(String)? onSubmitted; + + final bool isWalletCoin; + final bool readOnly; + final String? image; + final String? ticker; + + @override + State<ExchangeTextField> createState() => _ExchangeTextFieldState(); +} + +class _ExchangeTextFieldState extends State<ExchangeTextField> { + late final TextEditingController controller; + late final FocusNode focusNode; + late final TextStyle textStyle; + + late final double borderRadius; + + late final Color? background; + late final Color? buttonColor; + late final Widget? buttonContent; + late final VoidCallback? onButtonTap; + late final VoidCallback? onTap; + late final void Function(String)? onChanged; + late final void Function(String)? onSubmitted; + + @override + void initState() { + borderRadius = widget.borderRadius; + background = widget.background; + buttonColor = widget.buttonColor; + controller = widget.controller; + focusNode = widget.focusNode; + buttonContent = widget.buttonContent; + textStyle = widget.textStyle; + onButtonTap = widget.onButtonTap; + onChanged = widget.onChanged; + onSubmitted = widget.onSubmitted; + onTap = widget.onTap; + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: background, + borderRadius: BorderRadius.circular(borderRadius), + ), + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: TextField( + style: textStyle, + controller: controller, + focusNode: focusNode, + onChanged: onChanged, + onTap: onTap, + enableSuggestions: false, + autocorrect: false, + readOnly: widget.readOnly, + keyboardType: const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), + decoration: InputDecoration( + contentPadding: const EdgeInsets.only( + top: 12, + left: 12, + ), + hintText: "0", + hintStyle: STextStyles.fieldLabel(context).copyWith( + fontSize: 14, + ), + ), + inputFormatters: [ + // regex to validate a crypto amount with 8 decimal places + TextInputFormatter.withFunction((oldValue, newValue) => + RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') + .hasMatch(newValue.text) + ? newValue + : oldValue), + ], + ), + ), + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () => onButtonTap?.call(), + child: Container( + decoration: BoxDecoration( + color: buttonColor, + borderRadius: BorderRadius.horizontal( + right: Radius.circular( + borderRadius, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: Row( + children: [ + Container( + width: 18, + height: 18, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(18), + ), + child: Builder( + builder: (context) { + final image = widget.image; + + if (image != null && image.isNotEmpty) { + return Center( + child: SvgPicture.network( + image, + height: 18, + placeholderBuilder: (_) => Container( + width: 18, + height: 18, + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + 18, + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular( + 18, + ), + child: const LoadingIndicator(), + ), + ), + ), + ); + } else { + return Container( + width: 18, + height: 18, + decoration: BoxDecoration( + // color: Theme.of(context).extension<StackColors>()!.accentColorDark + borderRadius: BorderRadius.circular(18), + ), + child: SvgPicture.asset( + Assets.svg.circleQuestion, + width: 18, + height: 18, + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + ), + ); + } + }, + ), + ), + const SizedBox( + width: 6, + ), + Text( + widget.ticker ?? "-", + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + if (!widget.isWalletCoin) + const SizedBox( + width: 6, + ), + if (!widget.isWalletCoin) + SvgPicture.asset( + Assets.svg.chevronDown, + width: 5, + height: 2.5, + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ); + } +} + +// experimental UNUSED +// class ExchangeTextField extends StatefulWidget { +// const ExchangeTextField({ +// Key? key, +// this.borderRadius = 0, +// this.background, +// required this.controller, +// this.buttonColor, +// required this.focusNode, +// this.buttonContent, +// required this.textStyle, +// this.onButtonTap, +// this.onChanged, +// this.onSubmitted, +// }) : super(key: key); +// +// final double borderRadius; +// final Color? background; +// final Color? buttonColor; +// final Widget? buttonContent; +// final TextEditingController controller; +// final FocusNode focusNode; +// final TextStyle textStyle; +// final VoidCallback? onButtonTap; +// final void Function(String)? onChanged; +// final void Function(String)? onSubmitted; +// +// @override +// State<ExchangeTextField> createState() => _ExchangeTextFieldState(); +// } +// +// class _ExchangeTextFieldState extends State<ExchangeTextField> { +// late final TextEditingController controller; +// late final FocusNode focusNode; +// late final TextStyle textStyle; +// +// late final double borderRadius; +// +// late final Color? background; +// late final Color? buttonColor; +// late final Widget? buttonContent; +// late final VoidCallback? onButtonTap; +// late final void Function(String)? onChanged; +// late final void Function(String)? onSubmitted; +// +// @override +// void initState() { +// borderRadius = widget.borderRadius; +// background = widget.background; +// buttonColor = widget.buttonColor; +// controller = widget.controller; +// focusNode = widget.focusNode; +// buttonContent = widget.buttonContent; +// textStyle = widget.textStyle; +// onButtonTap = widget.onButtonTap; +// onChanged = widget.onChanged; +// onSubmitted = widget.onSubmitted; +// +// super.initState(); +// } +// +// @override +// Widget build(BuildContext context) { +// return Container( +// decoration: BoxDecoration( +// color: background, +// borderRadius: BorderRadius.circular(borderRadius), +// ), +// child: IntrinsicHeight( +// child: Row( +// crossAxisAlignment: CrossAxisAlignment.stretch, +// children: [ +// Expanded( +// child: MouseRegion( +// cursor: SystemMouseCursors.text, +// child: GestureDetector( +// onTap: () { +// // +// }, +// child: Padding( +// padding: const EdgeInsets.only( +// left: 16, +// top: 18, +// bottom: 17, +// ), +// child: IgnorePointer( +// ignoring: true, +// child: EditableText( +// controller: controller, +// focusNode: focusNode, +// style: textStyle, +// onChanged: onChanged, +// onSubmitted: onSubmitted, +// onEditingComplete: () => print("lol"), +// autocorrect: false, +// enableSuggestions: false, +// keyboardType: const TextInputType.numberWithOptions( +// signed: false, +// decimal: true, +// ), +// inputFormatters: [ +// // regex to validate a crypto amount with 8 decimal places +// TextInputFormatter.withFunction((oldValue, +// newValue) => +// RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') +// .hasMatch(newValue.text) +// ? newValue +// : oldValue), +// ], +// cursorColor: textStyle.color ?? +// Theme.of(context).backgroundColor, +// backgroundCursorColor: background ?? Colors.transparent, +// ), +// ), +// ), +// ), +// ), +// ), +// MouseRegion( +// cursor: SystemMouseCursors.click, +// child: GestureDetector( +// onTap: () => onButtonTap?.call(), +// child: Container( +// decoration: BoxDecoration( +// color: buttonColor, +// borderRadius: BorderRadius.horizontal( +// right: Radius.circular( +// borderRadius, +// ), +// ), +// ), +// child: Padding( +// padding: const EdgeInsets.symmetric( +// horizontal: 16, +// ), +// child: buttonContent, +// ), +// ), +// ), +// ), +// ], +// ), +// ), +// ); +// } +// } From 0b6545645bdd7bdf1939c2de2b42397e47614210 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 22 Nov 2022 14:55:26 -0600 Subject: [PATCH 379/426] macos + windows app icon --- .../AppIcon.appiconset/Contents.json | 132 +++++++++--------- .../AppIcon.appiconset/app_icon_1024.png | Bin 46993 -> 69450 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 3276 -> 4664 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 1429 -> 926 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 5933 -> 10085 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 1243 -> 1441 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 14800 -> 26089 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 1874 -> 2443 bytes pubspec.lock | 9 +- pubspec.yaml | 9 +- windows/runner/resources/app_icon.ico | Bin 33772 -> 1968 bytes 11 files changed, 82 insertions(+), 68 deletions(-) diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index a2ec33f19..96d3fee1a 100644 --- a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,68 +1,68 @@ { - "images" : [ - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" + "info": { + "version": 1, + "author": "xcode" }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "1x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_64.png", - "scale" : "2x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_128.png", - "scale" : "1x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "2x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "1x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "2x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "1x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_1024.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} + "images": [ + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_16.png", + "scale": "1x" + }, + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "2x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "1x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_64.png", + "scale": "2x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_128.png", + "scale": "1x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "2x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "1x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "2x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "1x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_1024.png", + "scale": "2x" + } + ] +} \ No newline at end of file diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png index 3c4935a7ca84f0976aca34b7f2895d65fb94d1ea..55efad011aa22c651bd4eeb225bebd9c6fea31bd 100644 GIT binary patch literal 69450 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 PKlPC5u>%Drr*HfpG~rAC literal 46993 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 QOu#(cH>|q6Qt;3J0APUL!~g&Q diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png index ed4cc16421680a50164ba74381b4b35ceaa0ccfc..c8ba3b1a21bf014a0ed55ac2169dc48b4abb240f 100644 GIT binary patch literal 4664 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 Lx+=BLUw-%>(J$+( literal 3276 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 diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png index 483be61389733f2e5331c08db8ca245268610ccb..e85f014180ccfa3df01d669e08620973393bc259 100644 GIT binary patch delta 915 zcmV;E18n@23!Vp%8Gi-<001BJ|6u?C0fcEoLr_UWLm+T+Z)Rz1WdHzpoPCi!NW(xJ z#a~lPRazA6AmWgrI$01Eanvdlp+cw?T6HkF^b49aBq=VAf@{ISkHxBki?gl{u7V)= z0pjT7r060g{x2!Ci1pyOAMfrx?%n}Hz05SLYaGyY+e{_mVt*#PDh6K>L>T=Dphsqw zF(*k$bR1vz@bUF7#<Mz~`*VcVoW%g2NIc67(<a^^p5C+#&ilj>R+LrZbK)_RE=c^y zb;aX1&IOkRo*6OIsd?fEu~=whrHxt9)QG2uqpGG;zL4=)<-EmND_2?bp8SQOoW8Qm zb(+ISU=d4@Ab&zZ6(y8mBSx!EiiH&I$2<6kT)#vvg<K^tax9<%4YKP8|AXJ%TKUNd zFDVoU0xyp9F#-g4fkw@7zK<QJaRT_Cfh)b`uhfB=Ptt2GEqVm>Z37qAElt@2E_Z;z zCqp)6SMt*o@_FF>jJ_!g4BP^}YhG{7eVjf3Y3eF@1AiPG0;2`WUhnYk?)KjPJ=5&( z2kcmKaYeY=jQ{`v)k#D_RCocUQ@gHdQ4}56B;py7NIc`&_yCQ{2PhPXLL*VAG!z=O zs5KgeLR1>1L?aS9BGD+sBOwxm#9Q1uSF-nc+<Wf1$r;UFd#y3YoO7(Xxbyk^!Vkdx zB=}o_&3|Tt)oKN$QVFZo3W-GWe*(MR4$Wp0r_%{erxWY-8q4JpE|&`ig8?Fu=<fp8 z>lLk53+;9r_xl}oyInZDTrL=mMp!Hs@OV5>tJQA;uh$E`UXNux9*;uMY&L_<W`jzl z6884{J^KAVj7B4?j+@Wt$mjE{Y_r)6u~;mSpMTFMI-L&OZZ{kb$A>d&?{>R^t5hmX zkwE1US+Ca%eLfx!jK^bEjo0gCW)g?@f};hG-!p;Xa0s1F$4KNpkHg`BTCE0?$%Je+ zD<t3|kq8=%2FuQ4yWO&?h)hP(|70>@CQqkRghC-iqfw+%DR5e?7WH}^rBaD~sc+s` z5`U%ZqxE|bXD}GBb9M_FjRv_~4&iVZg+c+{ZdWMrb1?anPNxwJ2AQcJ=70vJTrRUY zC>D#L8{#3AN)eC8kxV9`P$-1`KPHhf2Lb{3{eD!dRVL{3`H;zEV6j*})4zTTGMNmC pL;|r`405^rE86$3@n6A&Ut0#p@f^_w=nMb=002ovPDHLkV1iY&s4f5i literal 1429 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLl<6e(pbstU$g(vPY0F z14ES>14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a><YADU|^aU;1l8sRFH7;2N1=B zQQV0iv0xN^;z#sxC`vr}6NoU?!~oTRQPlAta5Zq#;6?$JK~=((;W7=P3}^zxG`N{) zZUedys1hg$u?tff&}tw`Joy9Yut*q!D29`8Ga>GbI`Jdw*pG<g2rUo=$RxyAi0zQ@ zK{g*#CB!bc_u+~|kN*O?3St_>cA%L+*Q#&*YQOJ$_%U#(BDn<O1)%wG2LRc++rJs_ z{ARTCo88{;VIVC(fy&@kKq3(+!cg`Os0N4(foRuvi_Pzx*E}+qe75q_|L~(AaRZ1H zSXO7vE8*g;>``;rKxi&&)LfRxIZ*98z8UWRslDo@Xu)QVh}rB>bKwe@Bjzwg%m$hd zG)gFMgHZlPxGcm3paLLb44<vP<D2@r*P^96AbR1lK<$hXvw`IQ|Nj*t=9vH!<*kw+ zKTrZ^aK8WT>yHI|Ag0wdp!_yD5R<|B29Ui~27`?vfy#ktk_KyHWMDA42{J=Uq-o}i z*%kZ@45mQ-Rw?0?K+z{&5KFc}xc5Q%1PFAbL_xCmpj?JNAm>L6SjrCMpiK}5LG0ZE z<A9vK72<4{``^ETd}9%0zzx*KnB?v5a_b$lS`(1NS>O>_%)r1c48n{Iv*t(u1=&kH zeO=ifbFy+6aSK)V_5t;<dAc};NL)@%U|@7pOA`w-E93Li%QKkYKD|HQA;2TTCB#SO z$n*(QCQX|-b#iz>NKhE#$Iz=+Oii|KDJ}W>g}0%`Svgra*tnS6TRU4iTH*e=dj~I` zym|EM*}I1?pT2#3`oZ(|3I-Y$DkeHMN=8~%YSR?;>=X?(Emci*ZIz9+t<|S1>hE8$ zVa1LmTh{DZv}x6@Wz!a}+qZDz%AHHMuHCzM^XlEpr!QPzf9Qzk<mEnd&1#RGiQN30 zVeL%o{C`cY-D3KYyK;VZO-*PwUK6!9?d`6w(q?(L_M~b@B;5WcYhC{K-sbZA|CrhN z8(sP2Evw%A=$yRVZ@y(U-=dd)TDzy~#qX?o`>S_0!&1MPx*ICxe}RFdTH+c}l9E`G zYL#4+3Zxi}3=A!G4S>ir#L(2r)WFKnP}jiR%D`ZOPH`@ZhTQy=%(P0}8ZH)|z6jL7 N;OXk;vd$@?2>?>Ex^Vyi diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png index bcbf36df2f2aaaa0a63c7dabc94e600184229d0d..bfb090fc604d253944fcabee1e019ee460c70201 100644 GIT binary patch literal 10085 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 literal 5933 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 diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png index 9c0a652864769b250ed58097f2f6270b393f751f..877b87dc389f3f83852a0eaf258bbeea9122221c 100644 GIT binary patch delta 1424 zcmV;B1#kM>384#+BYy#eX+uL$Nkc;*aB^>EX>4Tx0C=2zkv&MmKpe$iQ%hA^6zm}4 zkfAzR5EXIMDionYs1;guFuC*#nlvOSE{=k0!NHHks)LKOt`4q(Aou~|=;Wm6A|?JW zDYS_7;J6>}?mh0_0YbgZG^=YI&~)2OCE{WxyDA1>5kwgM2!EhQW|lE0NlA1ZU-$6w z^)AM<I-mP<gw>qI0G~)a%M8;d-XNadv<=St#1U4MRpN8vF_SJx{K$31<2TL)mj#{~ zG1IAe;s~)=Xk(>~S<%#pr--Afrc=I<@mS@&#aSy?S@WL!g`u3jvdndw!$@EeOOPN! zK@}yGVIxMXPJfDp6z#`5_=jA-L@tF~B`|U<paKoD>j(dX-`!gI$q6qh6bAw?j`J}B z1b2Z(&2heu9j9>u_@99*z2&deftgRzYb`B$1oUkK7uPLK*#j<jfWaq2Hf2}x(-iV~ z;QfrgDGLnT0=;WqZ_Rz2J^*RzDtQAO90H>S%3kmA?tkv~-u^w)?C%HcSaNYixY~^X z00Y=bL_t(|0qs{=C}v?8esJuRec#taaxcwDjZ%u#Fg2q@8C(#GJC`UUxp5<z6uFQT zV&=vL5xF2)BeJjA_hruWO~3j19sjY+xS8k1a=!Du&-cF1`@G+gPEJm4@P?4yGWdT2 zuR>sNZ+{Q_`}?rAwuX(3&A%hCxw(n)@o`*ST!_DKZf@Y{=!l@8AXr#f{5=B4$H(aI z?#95t0AgZdU}$J4{%>GlfUT`9Y;SMF%gYPCzP^wo{oAW1aCv!&zP>(mb#)06mX?;c z!L`9mEG#Twb!7$q0ReDxa}(sAO@JGYjg6tLt$$6VU4zrpQ(RwPKS+W&mrFA-F@eR! zMFa;2!`9aJ=>(RRmeAVTikX=iol{(0T?s<Y&d!e>;PCJe!^6XHa&kgoU?9xR&Gi%5 z+ucQbM+b(6hC~LGQaCj=C4{Gx1#N6>YzU1|A3i=lxB9q4fOYQa=@BA3KR?&IDO+%K zbbl1#;o+)Cuw?V|^H^P7MS!di7Z;a51p52?(a_L<v$Hc;Sy}1bOas>V;NSpZVPT4O zz7NJ=Htg-~e-o&vs6c&v{p|u*S6BG^`@_`KRMm#Na>;sod*R{Xfsl|8n3<U=uBBno zD>P&!3QapW!4$D(-rnA#W=2Lvs)%qezJJRA^a1VnjE#*&+V8iew6?bP;AT|$+}s>i z<bLLhRPkf|to<I_k6sWO8Y;5s>FEhSKfec!Ng|`il+J#q576g%GNw=sj*gDd+1V)! ztEi|*(G?{!WZfnwCm-FAbsiiXguA=DIE{&kiQ-!J{`B-Tc6N5e!IF}ako@r@vVU`O zbOx5h1-ZGo$jZu6b+YB<Wvs8SiyBjO+U>`|#IkXXc2-%I#-=88$QjAb&PH-_@?9T@ zii$#BULLBds#KAnU(g*Hi0)=*XD5DVa6VH4eWSFr6wS@eqAZb-k$-$3BO^nUqP)CZ zY@`MpN+Tm9LgAXRtOm2Qv-mD|9)IKAQ3e^cw6vf}PFGD$jW8HBcuH`#bzxy4qNAfl ziFBmN)1;=RB0fGI)z#I45H0kj;3eg2W+py=`Ep0ViqnuzRZ>!dPbn!VEh`fppvDL8 zgYP6HBnY89I5_A$*Lxobl4p5cT^)Y@_<?Vgl|pQ45fKrA;^Ja4CzPP-Mt{Upe29(3 zyZ7&fZP(V;LjIspT+cf~ettgU;^OpX?MZh+4yR1Hj){qhsFbtO-rlbBJS{B^>FMb@ z7mrsVpaJJXnXHQxSs(mKtB#d3R8UYLhRsv(qVEFMxUsQOTrz0S&%uj7OlZZ=4gM+w eUWzv-@C)B~n#-8vW6l5o00{s|MNUMnLSTXy*2F#l delta 1224 zcmV;(1ULJk3)=~hBYyw{XF*Lt006O%3;baP0000WV@Og>004R>004l5008;`004mK z004C`008P>0026e000+ooVrmw00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU z3ljkVnw%H_000McNliru;|T=_G7A!Wjg9~S1IkH6K~z}7t$&wYXdG1($A4#b8}k9i z6oeKZ#P&&qlI)X$Amqiq`Dz7CgPVW|f)IT2t%#_hFMZ4g#MY*dRbMoOAcdADh?Jrw zCEB!!RQn+{A3NPmcIWtT=T7d-?(EKPdtsTGd*`0t|D1F0xjRCaZEUFusC^s)&Zu%+ zA+Vk5UX>hhLw{icSULY%F97*&;3t===sx^e;B(*v;H6Fy?x36!SaG}4C$xZrBKz>K zfXOzt)_@|yd~%>80pszoTlfT6J*n_XV8KrPHVl790t_6)usVmE$uT%&Tmu%~spsOt zfpf3+HoVj}0$Jgzv1yU#w!0a;t$=Pl$MIZRz*`DPgMTY3;4DQq3dx)UXIsEi1#}7z z!{0I7M(C-+?RXYAK+b(Q>FjesGKKv~;IRmdEme^gMW$)awK449bO4V8jKw^U!&AJD z!3A>y#SWkFrlBuvbMeC{8Xi>mk~Ui{>3Q9}{1R}K)J#S8t^@aNr&|g8fiFVCX<$gK zb~ToV6@O-cW6^QkD8mCT0w1}Z$x__ZYp2S^;K4q87AQ2#i(kJ7J_hb3RoUhPp8zFQ z1GT>-yvKtLU&%=vu1Rtn{sC||DKZuTRW^aC@UiF^SOX>n7HSGN{|itU-r8e!ATLef zj)n&KoO2>N+prROt1t*mhazrc7Ue=+;pSEVFn<ip_IMNy^$GoXX$sdi@P?efceUag zcrHxI&%k^V4ipqP;KAWOVIU_R!y5zuyc>#e#<i}nvK1g9T%84{K#vE9dxe3#DP>vV zO$8)HlxuLi9DsqAfRRLwA&P{@Kwd7Y>W5k2sRG<q_{q1op8}^+D<eYn@4J=q>gxEx zLx0a-xOVz$dS5S4IXm9cekK87u^9@hWU(21XZ<h!S^kAR-~UrYCV|BuFmiX#zn^M9 z)1Oi?A%BF~qNNC|2423i!Tl@C)ON}Ua#U4kRCQEB_YS->+r5Ba-|Tnuy$MXInqq|r z{@OYZFI}a+y%P#mfFr8Rs4{9N=1-KG+JA4G0^3HywO~H8WDs<^ytZC^aCw>C-3me8 zLqnIHdU6X>Ll!mctj)zUm{|b8@Q+5s-u4a}1cMv%i_^Q0{UQl)Bot89Tfl@Wi*~Gj zVP8Z?Uq;uC_uCIm9<Bbd>2GaLAyRVB42{D9j$ls8QTsjVy*(4z%w>h!cJJJ0Y=1xV zGLIiTM7&%TxC9&oM%0O@4S9GO0^R^_VA6vA_Ct390BYC2BY*5I`&d=D0=x*kpsHoe z$?B_`E8r{I2fU)nW#C_aHv;|#93kSZ<%JtK0000bbVXQnWMOn=I%9HWVRU5xGB7bY zEigGPFf~*$H99pgIx#UVFgH3dFhQU!b8P?s03~!qSaf7zbY(hiZ)9m^c>ppnF*z+T mIV~_XR53L=H846cF)c7RIxsNNam&#F0000<MNUMnLSTZ9Sr(fB diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png index e71a726136a47ed24125c7efc79d68a4a01961b4..f24fbe65de8627a88836014c48b7f8156a627594 100644 GIT binary patch literal 26089 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 BO?v<U literal 14800 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 OIc;`vc)7{z(*FShbtRtw diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png index 8a31fe2dd3f91d79cab6e0390356bb3c1a355f94..b81789038bb1b5e0704d296cdcab37af92284870 100644 GIT binary patch delta 2433 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 delta 1860 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 diff --git a/pubspec.lock b/pubspec.lock index 0b8f49c2c..f12b25487 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -205,6 +205,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.5" clock: dependency: transitive description: @@ -494,7 +501,7 @@ packages: name: flutter_launcher_icons url: "https://pub.dartlang.org" source: hosted - version: "0.9.3" + version: "0.11.0" flutter_libepiccash: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index af4370d99..b1312427d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -141,7 +141,7 @@ dev_dependencies: integration_test: sdk: flutter build_runner: ^2.1.7 - flutter_launcher_icons: ^0.9.3 + flutter_launcher_icons: ^0.11.0 hive_generator: ^1.1.2 dependency_validator: ^3.1.2 hive_test: ^1.0.1 @@ -160,6 +160,13 @@ flutter_icons: image_path_android: assets/icon/app_icon_alpha.png image_path_ios: assets/icon/icon.png remove_alpha_ios: true + windows: + generate: true + image_path: assets/icon/icon.png + icon_size: 48 # min:48, max:256, default: 48 + macos: + generate: true + image_path: assets/icon/icon.png flutter_native_splash: image: assets/images/splash.png diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico index c04e20caf6370ebb9253ad831cc31de4a9c965f6..eee73d91b8f7daeadfb6dcb917cdb15466910278 100644 GIT binary patch literal 1968 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 CG^h~( literal 33772 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 From a8b0901ec664e9f1bee21222cd5d4e319e92aeff Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 22 Nov 2022 15:32:06 -0600 Subject: [PATCH 380/426] desktop exchange navigation flow fix --- lib/pages/exchange_view/confirm_change_now_send.dart | 12 ++++++++++++ lib/pages/exchange_view/send_from_view.dart | 6 ++++++ .../exchange_steps/subwidgets/desktop_step_4.dart | 1 + 3 files changed, 19 insertions(+) diff --git a/lib/pages/exchange_view/confirm_change_now_send.dart b/lib/pages/exchange_view/confirm_change_now_send.dart index 9f62bd8ec..540067915 100644 --- a/lib/pages/exchange_view/confirm_change_now_send.dart +++ b/lib/pages/exchange_view/confirm_change_now_send.dart @@ -37,6 +37,7 @@ class ConfirmChangeNowSendView extends ConsumerStatefulWidget { this.routeOnSuccessName = WalletView.routeName, required this.trade, this.shouldSendPublicFiroFunds, + this.fromDesktopStep4 = false, }) : super(key: key); static const String routeName = "/confirmChangeNowSend"; @@ -46,6 +47,7 @@ class ConfirmChangeNowSendView extends ConsumerStatefulWidget { final String routeOnSuccessName; final Trade trade; final bool? shouldSendPublicFiroFunds; + final bool fromDesktopStep4; @override ConsumerState<ConfirmChangeNowSendView> createState() => @@ -105,7 +107,17 @@ class _ConfirmChangeNowSendViewState if (mounted) { if (Util.isDesktop) { Navigator.of(context, rootNavigator: true).pop(); + + // stupid hack + if (widget.fromDesktopStep4) { + Navigator.of(context, rootNavigator: true).pop(); + Navigator.of(context, rootNavigator: true).pop(); + Navigator.of(context, rootNavigator: true).pop(); + Navigator.of(context, rootNavigator: true).pop(); + Navigator.of(context, rootNavigator: true).pop(); + } } + Navigator.of(context).popUntil(ModalRoute.withName(routeOnSuccessName)); } } catch (e) { diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart index 7cbf38384..7c5f5541c 100644 --- a/lib/pages/exchange_view/send_from_view.dart +++ b/lib/pages/exchange_view/send_from_view.dart @@ -38,6 +38,7 @@ class SendFromView extends ConsumerStatefulWidget { required this.amount, required this.address, this.shouldPopRoot = false, + this.fromDesktopStep4 = false, }) : super(key: key); static const String routeName = "/sendFrom"; @@ -47,6 +48,7 @@ class SendFromView extends ConsumerStatefulWidget { final String address; final Trade trade; final bool shouldPopRoot; + final bool fromDesktopStep4; @override ConsumerState<SendFromView> createState() => _SendFromViewState(); @@ -191,6 +193,7 @@ class _SendFromViewState extends ConsumerState<SendFromView> { amount: amount, address: address, trade: trade, + fromDesktopStep4: widget.fromDesktopStep4, ), ); }, @@ -210,12 +213,14 @@ class SendFromCard extends ConsumerStatefulWidget { required this.amount, required this.address, required this.trade, + this.fromDesktopStep4 = false, }) : super(key: key); final String walletId; final Decimal amount; final String address; final Trade trade; + final bool fromDesktopStep4; @override ConsumerState<SendFromCard> createState() => _SendFromCardState(); @@ -323,6 +328,7 @@ class _SendFromCardState extends ConsumerState<SendFromCard> { : HomeView.routeName, trade: trade, shouldSendPublicFiroFunds: shouldSendPublicFiroFunds, + fromDesktopStep4: widget.fromDesktopStep4, ), settings: const RouteSettings( name: ConfirmChangeNowSendView.routeName, diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart index c86713a76..5b69f064d 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart @@ -224,6 +224,7 @@ class _DesktopStep4State extends ConsumerState<DesktopStep4> { amount: amount, address: address, shouldPopRoot: true, + fromDesktopStep4: true, ), const RouteSettings( name: SendFromView.routeName, From c3a3dd3180358f0f1e2faae0cf16c6d7a8b4ecb4 Mon Sep 17 00:00:00 2001 From: sneurlax <sneurlax@gmail.com> Date: Tue, 22 Nov 2022 11:48:52 -0600 Subject: [PATCH 381/426] remove Wownero if isDesktop or isLinux or isWindows or isMacOS, respectively --- .../add_wallet_view/sub_widgets/searchable_coin_list.dart | 5 +++++ lib/utilities/enums/coin_enum.dart | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart index d89d42bbf..38181b9e1 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart @@ -32,6 +32,11 @@ class SearchableCoinList extends ConsumerWidget { // remove firo testnet regardless _coins.remove(Coin.firoTestNet); + // Kidgloves for Wownero on desktop + if(isDesktop) { + _coins.remove(Coin.wownero); + } + return _coins; } diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index 48212bde8..f80c40f52 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -12,6 +12,7 @@ import 'package:stackwallet/services/coins/monero/monero_wallet.dart' as xmr; import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart' as nmc; import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart' as wow; +import 'dart:io' show Platform; enum Coin { bitcoin, @@ -36,8 +37,7 @@ enum Coin { firoTestNet, } -// remove firotestnet for now -const int kTestNetCoinCount = 4; +int kTestNetCoinCount = (Platform.isLinux || Platform.isWindows || Platform.isMacOS) ? 5 : 4; extension CoinExt on Coin { String get prettyName { From 3306cf8b99f4afd54030bdef8746eb5de89adf6c Mon Sep 17 00:00:00 2001 From: sneurlax <sneurlax@gmail.com> Date: Tue, 22 Nov 2022 11:56:13 -0600 Subject: [PATCH 382/426] expand the ternary for readability --- lib/utilities/enums/coin_enum.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index f80c40f52..648407809 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -37,7 +37,12 @@ enum Coin { firoTestNet, } -int kTestNetCoinCount = (Platform.isLinux || Platform.isWindows || Platform.isMacOS) ? 5 : 4; +if(Platform.isLinux || Platform.isWindows || Platform.isMacOS) { + int kTestNetCoinCount = 5; // Because we are removing Wownero from Desktop +} else { + // remove firotestnet for now + int kTestNetCoinCount = 4; +} extension CoinExt on Coin { String get prettyName { From 172b3d157bacc84dff2dece8b4fd0c11ca9e1487 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 22 Nov 2022 15:37:47 -0600 Subject: [PATCH 383/426] wownero disable on desktop fix --- lib/utilities/enums/coin_enum.dart | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index 648407809..543a193ee 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -12,7 +12,7 @@ import 'package:stackwallet/services/coins/monero/monero_wallet.dart' as xmr; import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart' as nmc; import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart' as wow; -import 'dart:io' show Platform; +import 'package:stackwallet/utilities/util.dart'; enum Coin { bitcoin, @@ -37,12 +37,7 @@ enum Coin { firoTestNet, } -if(Platform.isLinux || Platform.isWindows || Platform.isMacOS) { - int kTestNetCoinCount = 5; // Because we are removing Wownero from Desktop -} else { - // remove firotestnet for now - int kTestNetCoinCount = 4; -} +final int kTestNetCoinCount = Util.isDesktop ? 5 : 4; extension CoinExt on Coin { String get prettyName { From 157829a933da78229e6279aeb3ae1fcc8409307b Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 22 Nov 2022 15:53:16 -0600 Subject: [PATCH 384/426] fixed failing test --- test/widget_tests/address_book_card_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/widget_tests/address_book_card_test.dart b/test/widget_tests/address_book_card_test.dart index 7c53d8d50..07b1387df 100644 --- a/test/widget_tests/address_book_card_test.dart +++ b/test/widget_tests/address_book_card_test.dart @@ -70,7 +70,7 @@ void main() { await widgetTester.tap(find.byType(RawMaterialButton)); expect(find.byType(ContactPopUp), findsOneWidget); } else if (Util.isDesktop) { - expect(find.byType(RawMaterialButton), findsOneWidget); + expect(find.byType(RawMaterialButton), findsNothing); } }); } From 467d43d9f3c3c80b4dfc03c8232b56bcefeebc1d Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 22 Nov 2022 16:18:38 -0600 Subject: [PATCH 385/426] desktop trade history scroll fix --- .../desktop_exchange_view.dart | 16 +- .../subwidgets/desktop_trade_history.dart | 368 ++++++++++-------- 2 files changed, 217 insertions(+), 167 deletions(-) diff --git a/lib/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart b/lib/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart index 0f44eb59b..105c485f0 100644 --- a/lib/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart +++ b/lib/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart @@ -63,19 +63,9 @@ class _DesktopExchangeViewState extends State<DesktopExchangeView> { width: 16, ), Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Exchange details", - style: STextStyles.desktopTextExtraExtraSmall(context), - ), - const SizedBox( - height: 16, - ), - const RoundedWhiteContainer( - padding: EdgeInsets.all(0), + child: Row( + children: const [ + Expanded( child: DesktopTradeHistory(), ), ], diff --git a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart index a8f825911..e31a87dd4 100644 --- a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart +++ b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart @@ -6,6 +6,7 @@ import 'package:stackwallet/pages/exchange_view/trade_details_view.dart'; import 'package:stackwallet/providers/exchange/trade_sent_from_stack_lookup_provider.dart'; import 'package:stackwallet/providers/global/trades_service_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -24,6 +25,28 @@ class DesktopTradeHistory extends ConsumerStatefulWidget { } class _DesktopTradeHistoryState extends ConsumerState<DesktopTradeHistory> { + BorderRadius get _borderRadiusFirst { + return BorderRadius.only( + topLeft: Radius.circular( + Constants.size.circularBorderRadius, + ), + topRight: Radius.circular( + Constants.size.circularBorderRadius, + ), + ); + } + + BorderRadius get _borderRadiusLast { + return BorderRadius.only( + bottomLeft: Radius.circular( + Constants.size.circularBorderRadius, + ), + bottomRight: Radius.circular( + Constants.size.circularBorderRadius, + ), + ); + } + @override Widget build(BuildContext context) { final trades = @@ -33,169 +56,206 @@ class _DesktopTradeHistoryState extends ConsumerState<DesktopTradeHistory> { final hasHistory = tradeCount > 0; if (hasHistory) { - return ListView.separated( - shrinkWrap: true, - primary: false, - itemBuilder: (context, index) { - return TradeCard( - key: Key("tradeCard_${trades[index].uuid}"), - trade: trades[index], - onTap: () async { - final String tradeId = trades[index].tradeId; - - final lookup = ref.read(tradeSentFromStackLookupProvider).all; - - debugPrint("ALL: $lookup"); - - final String? txid = ref - .read(tradeSentFromStackLookupProvider) - .getTxidForTradeId(tradeId); - final List<String>? walletIds = ref - .read(tradeSentFromStackLookupProvider) - .getWalletIdsForTradeId(tradeId); - - if (txid != null && walletIds != null && walletIds.isNotEmpty) { - final manager = ref - .read(walletsChangeNotifierProvider) - .getManager(walletIds.first); - - debugPrint("name: ${manager.walletName}"); - - // TODO store tx data completely locally in isar so we don't lock up ui here when querying txData - final txData = await manager.transactionData; - - final tx = txData.getAllTransactions()[txid]; - - if (mounted) { - await showDialog<void>( - context: context, - builder: (context) => Navigator( - initialRoute: TradeDetailsView.routeName, - onGenerateRoute: RouteGenerator.generateRoute, - onGenerateInitialRoutes: (_, __) { - return [ - FadePageRoute( - DesktopDialog( - // maxHeight: - // MediaQuery.of(context).size.height - 64, - maxHeight: double.infinity, - maxWidth: 580, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, - bottom: 16, - ), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Trade details", - style: STextStyles.desktopH3(context), - ), - DesktopDialogCloseButton( - onPressedOverride: Navigator.of( - context, - rootNavigator: true, - ).pop, - ), - ], - ), - ), - Flexible( - child: TradeDetailsView( - tradeId: tradeId, - transactionIfSentFromStack: tx, - walletName: manager.walletName, - walletId: walletIds.first, - ), - ), - ], - ), - ), - const RouteSettings( - name: TradeDetailsView.routeName, - ), - ), - ]; - }, - ), - ); + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Exchange details", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + const SizedBox( + height: 16, + ), + Expanded( + child: ListView.separated( + shrinkWrap: true, + primary: false, + itemBuilder: (context, index) { + BorderRadius? radius; + if (index == tradeCount - 1) { + radius = _borderRadiusLast; + } else if (index == 0) { + radius = _borderRadiusFirst; } - } else { - unawaited( - showDialog<void>( - context: context, - builder: (context) => Navigator( - initialRoute: TradeDetailsView.routeName, - onGenerateRoute: RouteGenerator.generateRoute, - onGenerateInitialRoutes: (_, __) { - return [ - FadePageRoute( - DesktopDialog( - // maxHeight: - // MediaQuery.of(context).size.height - 64, - maxHeight: double.infinity, - maxWidth: 580, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, - bottom: 16, + + return Container( + decoration: BoxDecoration( + color: Theme.of(context).extension<StackColors>()!.popupBG, + borderRadius: radius, + ), + child: TradeCard( + key: Key("tradeCard_${trades[index].uuid}"), + trade: trades[index], + onTap: () async { + final String tradeId = trades[index].tradeId; + + final lookup = + ref.read(tradeSentFromStackLookupProvider).all; + + debugPrint("ALL: $lookup"); + + final String? txid = ref + .read(tradeSentFromStackLookupProvider) + .getTxidForTradeId(tradeId); + final List<String>? walletIds = ref + .read(tradeSentFromStackLookupProvider) + .getWalletIdsForTradeId(tradeId); + + if (txid != null && + walletIds != null && + walletIds.isNotEmpty) { + final manager = ref + .read(walletsChangeNotifierProvider) + .getManager(walletIds.first); + + debugPrint("name: ${manager.walletName}"); + + // TODO store tx data completely locally in isar so we don't lock up ui here when querying txData + final txData = await manager.transactionData; + + final tx = txData.getAllTransactions()[txid]; + + if (mounted) { + await showDialog<void>( + context: context, + builder: (context) => Navigator( + initialRoute: TradeDetailsView.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + FadePageRoute( + DesktopDialog( + // maxHeight: + // MediaQuery.of(context).size.height - 64, + maxHeight: double.infinity, + maxWidth: 580, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 16, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Text( + "Trade details", + style: STextStyles.desktopH3( + context), + ), + DesktopDialogCloseButton( + onPressedOverride: + Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + ], + ), + ), + Flexible( + child: TradeDetailsView( + tradeId: tradeId, + transactionIfSentFromStack: tx, + walletName: manager.walletName, + walletId: walletIds.first, + ), + ), + ], + ), ), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Trade details", - style: STextStyles.desktopH3(context), - ), - DesktopDialogCloseButton( - onPressedOverride: Navigator.of( - context, - rootNavigator: true, - ).pop, - ), - ], + const RouteSettings( + name: TradeDetailsView.routeName, ), ), - Flexible( - child: TradeDetailsView( - tradeId: tradeId, - transactionIfSentFromStack: null, - walletName: null, - walletId: walletIds?.first, - ), - ), - ], - ), + ]; + }, ), - const RouteSettings( - name: TradeDetailsView.routeName, + ); + } + } else { + unawaited( + showDialog<void>( + context: context, + builder: (context) => Navigator( + initialRoute: TradeDetailsView.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + FadePageRoute( + DesktopDialog( + // maxHeight: + // MediaQuery.of(context).size.height - 64, + maxHeight: double.infinity, + maxWidth: 580, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 16, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Text( + "Trade details", + style: STextStyles.desktopH3( + context), + ), + DesktopDialogCloseButton( + onPressedOverride: + Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + ], + ), + ), + Flexible( + child: TradeDetailsView( + tradeId: tradeId, + transactionIfSentFromStack: null, + walletName: null, + walletId: walletIds?.first, + ), + ), + ], + ), + ), + const RouteSettings( + name: TradeDetailsView.routeName, + ), + ), + ]; + }, ), ), - ]; - }, - ), + ); + } + }, ), ); - } - }, - ); - }, - separatorBuilder: (context, index) { - return Container( - height: 1, - color: Theme.of(context).extension<StackColors>()!.background, - ); - }, - itemCount: tradeCount, + }, + separatorBuilder: (context, index) { + return Container( + height: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ); + }, + itemCount: tradeCount, + ), + ), + ], ); } else { return RoundedWhiteContainer( From 4debb0fff9d67087f20743c4c3899996085c6b41 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 22 Nov 2022 16:34:31 -0600 Subject: [PATCH 386/426] desktop block explorer warning dialog navigation fix --- .../transaction_views/transaction_details_view.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart index dc4e41152..4d45428ff 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart @@ -266,7 +266,10 @@ class _TransactionDetailsViewState buttonHeight: ButtonHeight.l, label: "Cancel", onPressed: () { - Navigator.of(context).pop(false); + Navigator.of( + context, + rootNavigator: true, + ).pop(false); }, ), const SizedBox(width: 20), @@ -275,7 +278,10 @@ class _TransactionDetailsViewState buttonHeight: ButtonHeight.l, label: "Continue", onPressed: () { - Navigator.of(context).pop(true); + Navigator.of( + context, + rootNavigator: true, + ).pop(true); }, ), ], From 67d375dbd5ba0b994fa9d370dd6787e197a6dbf5 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Tue, 22 Nov 2022 16:37:47 -0600 Subject: [PATCH 387/426] desktop new notifications bell icon indicator --- .../home/desktop_menu.dart | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/lib/pages_desktop_specific/home/desktop_menu.dart b/lib/pages_desktop_specific/home/desktop_menu.dart index 60a424a06..d82d62883 100644 --- a/lib/pages_desktop_specific/home/desktop_menu.dart +++ b/lib/pages_desktop_specific/home/desktop_menu.dart @@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_menu_item.dart'; import 'package:stackwallet/providers/desktop/current_desktop_menu_item.dart'; +import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -149,20 +150,27 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { ), DesktopMenuItem( icon: SvgPicture.asset( - Assets.svg.bell, + ref.watch(notificationsProvider.select( + (value) => value.hasUnreadNotifications)) + ? Assets.svg.bellNew(context) + : Assets.svg.bell, width: 20, height: 20, - color: DesktopMenuItemId.notifications == - ref - .watch(currentDesktopMenuItemProvider.state) - .state - ? Theme.of(context) - .extension<StackColors>()! - .accentColorDark - : Theme.of(context) - .extension<StackColors>()! - .accentColorDark - .withOpacity(0.8), + color: ref.watch(notificationsProvider.select( + (value) => value.hasUnreadNotifications)) + ? null + : DesktopMenuItemId.notifications == + ref + .watch(currentDesktopMenuItemProvider + .state) + .state + ? Theme.of(context) + .extension<StackColors>()! + .accentColorDark + : Theme.of(context) + .extension<StackColors>()! + .accentColorDark + .withOpacity(0.8), ), label: "Notifications", value: DesktopMenuItemId.notifications, From 7719ad32a5973958e4e9ba6793fb5a110effb891 Mon Sep 17 00:00:00 2001 From: Diego Salazar <diego@cypherstack.com> Date: Tue, 22 Nov 2022 17:56:14 -0700 Subject: [PATCH 388/426] Bump version (1.5.18, build 90) --- pubspec.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index b1312427d..f79879f61 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.5.17+89 +version: 1.5.18+90 environment: sdk: ">=2.17.0 <3.0.0" @@ -410,4 +410,4 @@ import_sorter: ignored_files: # Optional, defaults to [] - \/test\/* - \/crypto_plugins\/* - - \/integration_test\/* \ No newline at end of file + - \/integration_test\/* From 140e68948f084a5435500e195712f3920f523409 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 23 Nov 2022 08:26:03 -0600 Subject: [PATCH 389/426] uppercase tickers on exchange form coin select field buttons --- lib/widgets/textfields/exchange_textfield.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/widgets/textfields/exchange_textfield.dart b/lib/widgets/textfields/exchange_textfield.dart index 8d3c5d699..399d077c4 100644 --- a/lib/widgets/textfields/exchange_textfield.dart +++ b/lib/widgets/textfields/exchange_textfield.dart @@ -203,7 +203,7 @@ class _ExchangeTextFieldState extends State<ExchangeTextField> { width: 6, ), Text( - widget.ticker ?? "-", + widget.ticker?.toUpperCase() ?? "-", style: STextStyles.smallMed14(context).copyWith( color: Theme.of(context) .extension<StackColors>()! From d50e1e4a1418a4fd1f6b6d697e73ebaf370bb8a6 Mon Sep 17 00:00:00 2001 From: Diego Salazar <diego@cypherstack.com> Date: Tue, 22 Nov 2022 17:56:14 -0700 Subject: [PATCH 390/426] Bump version (1.5.18, build 90) --- pubspec.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index b1312427d..f79879f61 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.5.17+89 +version: 1.5.18+90 environment: sdk: ">=2.17.0 <3.0.0" @@ -410,4 +410,4 @@ import_sorter: ignored_files: # Optional, defaults to [] - \/test\/* - \/crypto_plugins\/* - - \/integration_test\/* \ No newline at end of file + - \/integration_test\/* From b3a7b19b8e9430dd7ad6dfb851ac09b2e64129e4 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 23 Nov 2022 08:36:27 -0600 Subject: [PATCH 391/426] mobile exchange form layout fixes --- lib/pages/exchange_view/exchange_form.dart | 46 ++++++++++--------- .../sub_widgets/rate_type_toggle.dart | 4 +- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart index 921b35bf0..d818a73ff 100644 --- a/lib/pages/exchange_view/exchange_form.dart +++ b/lib/pages/exchange_view/exchange_form.dart @@ -1281,28 +1281,30 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { condition: isDesktop, builder: (child) => MouseRegion( cursor: SystemMouseCursors.click, - child: RoundedContainer( - padding: const EdgeInsets.all(6), - color: Theme.of(context) - .extension<StackColors>()! - .buttonBackSecondary, - radiusMultiplier: 0.75, - child: child, - ), + child: child, ), - child: GestureDetector( - onTap: () async { - await _swap(); - }, - child: Padding( - padding: const EdgeInsets.all(4), - child: SvgPicture.asset( - Assets.svg.swap, - width: 20, - height: 20, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, + child: RoundedContainer( + padding: isDesktop + ? const EdgeInsets.all(6) + : const EdgeInsets.all(2), + color: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + radiusMultiplier: 0.75, + child: GestureDetector( + onTap: () async { + await _swap(); + }, + child: Padding( + padding: const EdgeInsets.all(4), + child: SvgPicture.asset( + Assets.svg.swap, + width: 20, + height: 20, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), ), ), ), @@ -1310,7 +1312,7 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> { ], ), SizedBox( - height: isDesktop ? 10 : 4, + height: isDesktop ? 10 : 7, ), ExchangeTextField( focusNode: _receiveFocusNode, diff --git a/lib/pages/exchange_view/sub_widgets/rate_type_toggle.dart b/lib/pages/exchange_view/sub_widgets/rate_type_toggle.dart index 31ee01ce2..361d49e65 100644 --- a/lib/pages/exchange_view/sub_widgets/rate_type_toggle.dart +++ b/lib/pages/exchange_view/sub_widgets/rate_type_toggle.dart @@ -55,7 +55,7 @@ class RateTypeToggle extends ConsumerWidget { child: RoundedContainer( padding: isDesktop ? const EdgeInsets.all(17) - : const EdgeInsets.all(0), + : const EdgeInsets.all(12), color: estimated ? Theme.of(context) .extension<StackColors>()! @@ -136,7 +136,7 @@ class RateTypeToggle extends ConsumerWidget { child: RoundedContainer( padding: isDesktop ? const EdgeInsets.all(17) - : const EdgeInsets.all(0), + : const EdgeInsets.all(12), color: !estimated ? Theme.of(context) .extension<StackColors>()! From 4377c351d304d135303626f74ca7f175705ed2db Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 23 Nov 2022 08:45:29 -0600 Subject: [PATCH 392/426] mobile exchange step 2 only enable next button when all fields are filled out --- .../exchange_step_views/step_2_view.dart | 95 +++++++++++++++---- 1 file changed, 77 insertions(+), 18 deletions(-) diff --git a/lib/pages/exchange_view/exchange_step_views/step_2_view.dart b/lib/pages/exchange_view/exchange_step_views/step_2_view.dart index 800d1e146..030f91cb7 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_2_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_2_view.dart @@ -7,8 +7,6 @@ import 'package:stackwallet/pages/address_book_views/subviews/contact_popup.dart import 'package:stackwallet/pages/exchange_view/choose_from_stack_view.dart'; import 'package:stackwallet/pages/exchange_view/exchange_step_views/step_3_view.dart'; import 'package:stackwallet/pages/exchange_view/sub_widgets/step_row.dart'; -import 'package:stackwallet/providers/exchange/exchange_flow_is_active_state_provider.dart'; -import 'package:stackwallet/providers/exchange/exchange_send_from_wallet_id_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/address_utils.dart'; import 'package:stackwallet/utilities/barcode_scanner_interface.dart'; @@ -20,6 +18,7 @@ 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/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/icon_widgets/addressbook_icon.dart'; import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart'; import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart'; @@ -57,6 +56,8 @@ class _Step2ViewState extends ConsumerState<Step2View> { late final FocusNode _toFocusNode; late final FocusNode _refundFocusNode; + bool enableNext = false; + bool isStackCoin(String ticker) { try { coinFromTickerCaseInsensitive(ticker); @@ -207,6 +208,12 @@ class _Step2ViewState extends ConsumerState<Step2View> { _toController.text = manager.walletName; model.recipientAddress = await manager .currentReceivingAddress; + + setState(() { + enableNext = _toController + .text.isNotEmpty && + _refundController.text.isNotEmpty; + }); } }); } catch (e, s) { @@ -275,7 +282,12 @@ class _Step2ViewState extends ConsumerState<Step2View> { model.recipientAddress = _toController.text; - setState(() {}); + setState(() { + enableNext = _toController + .text.isNotEmpty && + _refundController + .text.isNotEmpty; + }); }, child: const XIcon(), ) @@ -295,7 +307,12 @@ class _Step2ViewState extends ConsumerState<Step2View> { model.recipientAddress = _toController.text; - setState(() {}); + setState(() { + enableNext = _toController + .text.isNotEmpty && + _refundController + .text.isNotEmpty; + }); } }, child: _toController.text.isEmpty @@ -338,6 +355,12 @@ class _Step2ViewState extends ConsumerState<Step2View> { .state) .state = ""; } + setState(() { + enableNext = _toController + .text.isNotEmpty && + _refundController + .text.isNotEmpty; + }); }); }, child: const AddressBookIcon(), @@ -361,14 +384,24 @@ class _Step2ViewState extends ConsumerState<Step2View> { model.recipientAddress = _toController.text; - setState(() {}); + setState(() { + enableNext = _toController + .text.isNotEmpty && + _refundController + .text.isNotEmpty; + }); } else { _toController.text = qrResult.rawContent; model.recipientAddress = _toController.text; - setState(() {}); + setState(() { + enableNext = _toController + .text.isNotEmpty && + _refundController + .text.isNotEmpty; + }); } } on PlatformException catch (e, s) { Logging.instance.log( @@ -429,6 +462,11 @@ class _Step2ViewState extends ConsumerState<Step2View> { model.refundAddress = await manager .currentReceivingAddress; } + setState(() { + enableNext = _toController + .text.isNotEmpty && + _refundController.text.isNotEmpty; + }); }); } catch (e, s) { Logging.instance @@ -495,7 +533,12 @@ class _Step2ViewState extends ConsumerState<Step2View> { model.refundAddress = _refundController.text; - setState(() {}); + setState(() { + enableNext = _toController + .text.isNotEmpty && + _refundController + .text.isNotEmpty; + }); }, child: const XIcon(), ) @@ -516,7 +559,12 @@ class _Step2ViewState extends ConsumerState<Step2View> { model.refundAddress = _refundController.text; - setState(() {}); + setState(() { + enableNext = _toController + .text.isNotEmpty && + _refundController + .text.isNotEmpty; + }); } }, child: @@ -555,6 +603,12 @@ class _Step2ViewState extends ConsumerState<Step2View> { model.refundAddress = _refundController.text; } + setState(() { + enableNext = _toController + .text.isNotEmpty && + _refundController + .text.isNotEmpty; + }); }); }, child: const AddressBookIcon(), @@ -578,14 +632,24 @@ class _Step2ViewState extends ConsumerState<Step2View> { model.refundAddress = _refundController.text; - setState(() {}); + setState(() { + enableNext = _toController + .text.isNotEmpty && + _refundController + .text.isNotEmpty; + }); } else { _refundController.text = qrResult.rawContent; model.refundAddress = _refundController.text; - setState(() {}); + setState(() { + enableNext = _toController + .text.isNotEmpty && + _refundController + .text.isNotEmpty; + }); } } on PlatformException catch (e, s) { Logging.instance.log( @@ -637,20 +701,15 @@ class _Step2ViewState extends ConsumerState<Step2View> { width: 16, ), Expanded( - child: TextButton( + child: PrimaryButton( + label: "Next", + enabled: enableNext, onPressed: () { Navigator.of(context).pushNamed( Step3View.routeName, arguments: model, ); }, - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - child: Text( - "Next", - style: STextStyles.button(context), - ), ), ), ], From 7011c6e1f68417d5e5fbee1ac53596bc50b618b0 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Tue, 22 Nov 2022 12:31:45 -0700 Subject: [PATCH 393/426] submit on enter for wallet keys --- .../unlock_wallet_keys_desktop.dart | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart index 739a3ebc4..e6d0ff390 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart @@ -43,6 +43,58 @@ class _UnlockWalletKeysDesktopState bool continueEnabled = false; bool hidePassword = true; + Future<void> enterPassphrase() async { + unawaited( + showDialog( + context: context, + builder: (context) => Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: const [ + LoadingIndicator( + width: 200, + height: 200, + ), + ], + ), + ), + ); + + await Future<void>.delayed(const Duration(seconds: 1)); + + final verified = await ref + .read(storageCryptoHandlerProvider) + .verifyPassphrase(passwordController.text); + + if (verified) { + Navigator.of(context, rootNavigator: true).pop(); + + final words = await ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .mnemonic; + + if (mounted) { + await Navigator.of(context).pushReplacementNamed( + WalletKeysDesktopPopup.routeName, + arguments: words, + ); + } + } else { + Navigator.of(context, rootNavigator: true).pop(); + + await Future<void>.delayed(const Duration(milliseconds: 300)); + + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Invalid passphrase!", + context: context, + ), + ); + } + } + @override void initState() { passwordController = TextEditingController(); @@ -120,6 +172,12 @@ class _UnlockWalletKeysDesktopState obscureText: hidePassword, enableSuggestions: false, autocorrect: false, + autofocus: true, + onSubmitted: (_) { + if (continueEnabled) { + enterPassphrase(); + } + }, decoration: standardInputDecoration( "Enter password", passwordFocusNode, From d7a7c706d2a066d6815088ba6228cdcd8fed2603 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Wed, 23 Nov 2022 11:24:07 -0700 Subject: [PATCH 394/426] adjusted restore calendar height --- .../restore_options_view/restore_options_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart index 1ce5d713a..ac84964ca 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart @@ -155,7 +155,7 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> { final date = await showRoundedDatePicker( context: context, initialDate: DateTime.now(), - height: height * 0.5, + height: height / 3.2, theme: ThemeData( primarySwatch: Util.createMaterialColor(fetchedColor), ), From adee71224b25bf9f8e72521dad0d289d55a16df1 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 23 Nov 2022 12:31:31 -0600 Subject: [PATCH 395/426] Format coin amounts improvements, fixed fee rates display issue, use hard coded xmr estimates for now --- lib/models/paymint/transactions_model.dart | 10 +- .../confirm_change_now_send.dart | 46 ++++---- .../exchange_step_views/step_4_view.dart | 2 +- lib/pages/exchange_view/send_from_view.dart | 35 ++---- .../exchange_provider_options.dart | 31 ++++-- .../send_view/confirm_transaction_view.dart | 36 +++---- lib/pages/send_view/send_view.dart | 83 +++++++++----- .../transaction_fee_selection_sheet.dart | 77 +++++++++---- .../all_transactions_view.dart | 17 +-- .../transaction_details_view.dart | 58 +++++----- .../transaction_search_filter_view.dart | 19 +--- .../sub_widgets/desktop_fee_dropdown.dart | 86 ++++++++------- .../wallet_view/sub_widgets/desktop_send.dart | 24 +++-- .../coins/bitcoin/bitcoin_wallet.dart | 52 +++++---- .../coins/bitcoincash/bitcoincash_wallet.dart | 53 ++++----- .../coins/dogecoin/dogecoin_wallet.dart | 53 ++++----- lib/services/coins/firo/firo_wallet.dart | 81 +++++++------- .../coins/litecoin/litecoin_wallet.dart | 52 +++++---- lib/services/coins/monero/monero_wallet.dart | 101 +++++++++--------- .../coins/namecoin/namecoin_wallet.dart | 52 +++++---- .../coins/wownero/wownero_wallet.dart | 50 ++++----- lib/utilities/constants.dart | 42 ++++++-- lib/utilities/format.dart | 50 +++------ lib/widgets/transaction_card.dart | 18 +--- 24 files changed, 602 insertions(+), 526 deletions(-) diff --git a/lib/models/paymint/transactions_model.dart b/lib/models/paymint/transactions_model.dart index 08b6eb7e2..382459922 100644 --- a/lib/models/paymint/transactions_model.dart +++ b/lib/models/paymint/transactions_model.dart @@ -2,6 +2,7 @@ import 'package:dart_numerics/dart_numerics.dart'; import 'package:decimal/decimal.dart'; import 'package:hive/hive.dart'; import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; part '../type_adaptors/transactions_model.g.dart'; @@ -220,14 +221,16 @@ class Transaction { (DateTime.now().millisecondsSinceEpoch ~/ 1000), txType: json['txType'] as String, amount: (Decimal.parse(json["amount"].toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(Coin + .firo))) // dirty hack but we need 8 decimal places here to keep consistent data structure .toBigInt() .toInt(), aliens: [], worthNow: json['worthNow'] as String, worthAtBlockTimestamp: json['worthAtBlockTimestamp'] as String? ?? "0", fees: (Decimal.parse(json["fees"].toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(Coin + .firo))) // dirty hack but we need 8 decimal places here to keep consistent data structure .toBigInt() .toInt(), inputSize: json['inputSize'] as int? ?? 0, @@ -386,7 +389,8 @@ class Output { scriptpubkeyType: json['scriptPubKey']['type'] as String?, scriptpubkeyAddress: address, value: (Decimal.parse(json["value"].toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(Coin + .firo))) // dirty hack but we need 8 decimal places here to keep consistent data structure .toBigInt() .toInt(), ); diff --git a/lib/pages/exchange_view/confirm_change_now_send.dart b/lib/pages/exchange_view/confirm_change_now_send.dart index 540067915..0e1eef755 100644 --- a/lib/pages/exchange_view/confirm_change_now_send.dart +++ b/lib/pages/exchange_view/confirm_change_now_send.dart @@ -342,6 +342,9 @@ class _ConfirmChangeNowSendViewState localeServiceChangeNotifierProvider .select((value) => value.locale), ), + ref.watch( + managerProvider.select((value) => value.coin), + ), )} ${ref.watch( managerProvider.select((value) => value.coin), ).ticker}", @@ -382,6 +385,9 @@ class _ConfirmChangeNowSendViewState localeServiceChangeNotifierProvider .select((value) => value.locale), ), + ref.watch( + managerProvider.select((value) => value.coin), + ), )} ${ref.watch( managerProvider.select((value) => value.coin), ).ticker}", @@ -563,13 +569,12 @@ class _ConfirmChangeNowSendViewState ], ), child: Text( - "${Format.satoshiAmountToPrettyString( - transactionInfo["recipientAmt"] as int, - ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), - )} ${ref.watch( + "${Format.satoshiAmountToPrettyString(transactionInfo["recipientAmt"] as int, ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ), ref.watch( + managerProvider.select((value) => value.coin), + ))} ${ref.watch( managerProvider.select((value) => value.coin), ).ticker}", style: STextStyles.itemSubtitle12(context), @@ -597,13 +602,12 @@ class _ConfirmChangeNowSendViewState style: STextStyles.smallMed12(context), ), Text( - "${Format.satoshiAmountToPrettyString( - transactionInfo["fee"] as int, - ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), - )} ${ref.watch( + "${Format.satoshiAmountToPrettyString(transactionInfo["fee"] as int, ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ), ref.watch( + managerProvider.select((value) => value.coin), + ))} ${ref.watch( managerProvider.select((value) => value.coin), ).ticker}", style: STextStyles.itemSubtitle12(context), @@ -685,14 +689,12 @@ class _ConfirmChangeNowSendViewState ), ), Text( - "${Format.satoshiAmountToPrettyString( - (transactionInfo["fee"] as int) + - (transactionInfo["recipientAmt"] as int), - ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), - )} ${ref.watch( + "${Format.satoshiAmountToPrettyString((transactionInfo["fee"] as int) + (transactionInfo["recipientAmt"] as int), ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ), ref.watch( + managerProvider.select((value) => value.coin), + ))} ${ref.watch( managerProvider.select((value) => value.coin), ).ticker}", style: STextStyles.itemSubtitle12(context).copyWith( 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 a8b403dcf..f5975a277 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 @@ -487,7 +487,7 @@ class _Step4ViewState extends ConsumerState<Step4View> { final amount = Format.decimalAmountToSatoshis( - model.sendAmount); + model.sendAmount, manager.coin); final address = model.trade!.payInAddress; diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart index 7c5f5541c..f13971a67 100644 --- a/lib/pages/exchange_view/send_from_view.dart +++ b/lib/pages/exchange_view/send_from_view.dart @@ -61,25 +61,7 @@ class _SendFromViewState extends ConsumerState<SendFromView> { late final Trade trade; String formatAmount(Decimal amount, Coin coin) { - switch (coin) { - case Coin.bitcoin: - case Coin.bitcoincash: - case Coin.litecoin: - case Coin.dogecoin: - case Coin.epicCash: - case Coin.firo: - case Coin.namecoin: - case Coin.bitcoinTestNet: - case Coin.litecoinTestNet: - case Coin.bitcoincashTestnet: - case Coin.dogecoinTestNet: - case Coin.firoTestNet: - return amount.toStringAsFixed(Constants.decimalPlaces); - case Coin.monero: - return amount.toStringAsFixed(Constants.decimalPlacesMonero); - case Coin.wownero: - return amount.toStringAsFixed(Constants.decimalPlacesWownero); - } + return amount.toStringAsFixed(Constants.decimalPlacesForCoin(coin)); } @override @@ -233,7 +215,7 @@ class _SendFromCardState extends ConsumerState<SendFromCard> { late final Trade trade; Future<void> _send(Manager manager, {bool? shouldSendPublicFiroFunds}) async { - final _amount = Format.decimalAmountToSatoshis(amount); + final _amount = Format.decimalAmountToSatoshis(amount, manager.coin); try { bool wasCancelled = false; @@ -464,7 +446,8 @@ class _SendFromCardState extends ConsumerState<SendFromCard> { "${Format.localizedStringAsFixed( value: snapshot.data!, locale: locale, - decimalPlaces: Constants.decimalPlaces, + decimalPlaces: + Constants.decimalPlacesForCoin(coin), )} ${coin.ticker}", style: STextStyles.itemSubtitle(context), ); @@ -549,7 +532,8 @@ class _SendFromCardState extends ConsumerState<SendFromCard> { "${Format.localizedStringAsFixed( value: snapshot.data!, locale: locale, - decimalPlaces: Constants.decimalPlaces, + decimalPlaces: + Constants.decimalPlacesForCoin(coin), )} ${coin.ticker}", style: STextStyles.itemSubtitle(context), ); @@ -657,11 +641,8 @@ class _SendFromCardState extends ConsumerState<SendFromCard> { "${Format.localizedStringAsFixed( value: snapshot.data!, locale: locale, - decimalPlaces: coin == Coin.monero - ? Constants.decimalPlacesMonero - : coin == Coin.wownero - ? Constants.decimalPlacesWownero - : Constants.decimalPlaces, + decimalPlaces: + Constants.decimalPlacesForCoin(coin), )} ${coin.ticker}", style: STextStyles.itemSubtitle(context), ); diff --git a/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart b/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart index 4dd768403..ebaf68066 100644 --- a/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart +++ b/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart @@ -159,6 +159,14 @@ class ExchangeProviderOptions extends ConsumerWidget { .toDecimal( scaleOnInfinitePrecision: 12); } + Coin coin; + try { + coin = + coinFromTickerCaseInsensitive(to!); + } catch (_) { + coin = Coin.bitcoin; + } + return Text( "1 ${from!.toUpperCase()} ~ ${Format.localizedStringAsFixed( value: rate, @@ -167,11 +175,9 @@ class ExchangeProviderOptions extends ConsumerWidget { .select( (value) => value.locale), ), - decimalPlaces: to!.toUpperCase() == - Coin.monero.ticker - .toUpperCase() - ? Constants.decimalPlacesMonero - : Constants.decimalPlaces, + decimalPlaces: + Constants.decimalPlacesForCoin( + coin), )} ${to!.toUpperCase()}", style: STextStyles.itemSubtitle12(context) @@ -354,6 +360,13 @@ class ExchangeProviderOptions extends ConsumerWidget { .toDecimal( scaleOnInfinitePrecision: 12); + Coin coin; + try { + coin = + coinFromTickerCaseInsensitive(to!); + } catch (_) { + coin = Coin.bitcoin; + } return Text( "1 ${from!.toUpperCase()} ~ ${Format.localizedStringAsFixed( value: rate, @@ -362,11 +375,9 @@ class ExchangeProviderOptions extends ConsumerWidget { .select( (value) => value.locale), ), - decimalPlaces: to!.toUpperCase() == - Coin.monero.ticker - .toUpperCase() - ? Constants.decimalPlacesMonero - : Constants.decimalPlaces, + decimalPlaces: + Constants.decimalPlacesForCoin( + coin), )} ${to!.toUpperCase()}", style: STextStyles.itemSubtitle12(context) diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index 276203804..1ddeb3c9f 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -317,13 +317,12 @@ class _ConfirmTransactionViewState style: STextStyles.smallMed12(context), ), Text( - "${Format.satoshiAmountToPrettyString( - transactionInfo["recipientAmt"] as int, - ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), - )} ${ref.watch( + "${Format.satoshiAmountToPrettyString(transactionInfo["recipientAmt"] as int, ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ), ref.watch( + managerProvider.select((value) => value.coin), + ))} ${ref.watch( managerProvider.select((value) => value.coin), ).ticker}", style: STextStyles.itemSubtitle12(context), @@ -344,13 +343,12 @@ class _ConfirmTransactionViewState style: STextStyles.smallMed12(context), ), Text( - "${Format.satoshiAmountToPrettyString( - transactionInfo["fee"] as int, - ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), - )} ${ref.watch( + "${Format.satoshiAmountToPrettyString(transactionInfo["fee"] as int, ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ), ref.watch( + managerProvider.select((value) => value.coin), + ))} ${ref.watch( managerProvider.select((value) => value.coin), ).ticker}", style: STextStyles.itemSubtitle12(context), @@ -494,6 +492,7 @@ class _ConfirmTransactionViewState localeServiceChangeNotifierProvider .select((value) => value.locale), ), + coin, )} ${coin.ticker}", style: STextStyles .desktopTextExtraExtraSmall( @@ -638,11 +637,7 @@ class _ConfirmTransactionViewState value: fee, locale: ref.watch(localeServiceChangeNotifierProvider .select((value) => value.locale)), - decimalPlaces: coin == Coin.monero - ? Constants.decimalPlacesMonero - : coin == Coin.wownero - ? Constants.decimalPlacesWownero - : Constants.decimalPlaces, + decimalPlaces: Constants.decimalPlacesForCoin(coin), )} ${coin.ticker}", style: STextStyles.itemSubtitle(context), ); @@ -750,6 +745,9 @@ class _ConfirmTransactionViewState localeServiceChangeNotifierProvider .select((value) => value.locale), ), + ref.watch( + managerProvider.select((value) => value.coin), + ), )} ${ref.watch( managerProvider.select((value) => value.coin), ).ticker}", diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index d91b7a3ea..2539b89ab 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:cw_core/monero_transaction_priority.dart'; import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -30,6 +31,7 @@ import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/animated_text.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; @@ -41,8 +43,6 @@ import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; -import 'package:stackwallet/utilities/util.dart'; - class SendView extends ConsumerStatefulWidget { const SendView({ Key? key, @@ -211,29 +211,47 @@ class _SendViewState extends ConsumerState<SendView> { } int fee; + if (coin == Coin.monero) { + MoneroTransactionPriority specialMoneroId; + switch (ref.read(feeRateTypeStateProvider.state).state) { + case FeeRateType.fast: + specialMoneroId = MoneroTransactionPriority.fast; + break; + case FeeRateType.average: + specialMoneroId = MoneroTransactionPriority.regular; + break; + case FeeRateType.slow: + specialMoneroId = MoneroTransactionPriority.slow; + break; + } - if (coin == Coin.firo || coin == Coin.firoTestNet) { + fee = await manager.estimateFeeFor(amount, specialMoneroId.raw!); + cachedFees[amount] = Format.satoshisToAmount(fee, coin: coin) + .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); + + return cachedFees[amount]!; + } else if (coin == Coin.firo || coin == Coin.firoTestNet) { if (ref.read(publicPrivateBalanceStateProvider.state).state == "Private") { fee = await manager.estimateFeeFor(amount, feeRate); - cachedFiroPrivateFees[amount] = Format.satoshisToAmount(fee) - .toStringAsFixed(Constants.decimalPlaces); + cachedFiroPrivateFees[amount] = Format.satoshisToAmount(fee, coin: coin) + .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); return cachedFiroPrivateFees[amount]!; } else { fee = await (manager.wallet as FiroWallet) .estimateFeeForPublic(amount, feeRate); - cachedFiroPublicFees[amount] = Format.satoshisToAmount(fee) - .toStringAsFixed(Constants.decimalPlaces); + cachedFiroPublicFees[amount] = Format.satoshisToAmount(fee, coin: coin) + .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); return cachedFiroPublicFees[amount]!; } } else { fee = await manager.estimateFeeFor(amount, feeRate); - cachedFees[amount] = - Format.satoshisToAmount(fee).toStringAsFixed(Constants.decimalPlaces); + cachedFees[amount] = Format.satoshisToAmount(fee, coin: coin) + .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); return cachedFees[amount]!; } @@ -296,8 +314,8 @@ class _SendViewState extends ConsumerState<SendView> { }); } else { setState(() { - _calculateFeesFuture = - calculateFees(Format.decimalAmountToSatoshis(_amountToSend!)); + _calculateFeesFuture = calculateFees( + Format.decimalAmountToSatoshis(_amountToSend!, coin)); }); } } @@ -311,8 +329,8 @@ class _SendViewState extends ConsumerState<SendView> { }); } else { setState(() { - _calculateFeesFuture = - calculateFees(Format.decimalAmountToSatoshis(_amountToSend!)); + _calculateFeesFuture = calculateFees( + Format.decimalAmountToSatoshis(_amountToSend!, coin)); }); } } @@ -354,8 +372,8 @@ class _SendViewState extends ConsumerState<SendView> { }); } else { setState(() { - _calculateFeesFuture = - calculateFees(Format.decimalAmountToSatoshis(_amountToSend!)); + _calculateFeesFuture = calculateFees( + Format.decimalAmountToSatoshis(_amountToSend!, coin)); }); } }); @@ -492,7 +510,9 @@ class _SendViewState extends ConsumerState<SendView> { onTap: () { cryptoAmountController.text = _cachedBalance!.toStringAsFixed( - Constants.decimalPlaces); + Constants + .decimalPlacesForCoin( + coin)); }, child: Container( color: Colors.transparent, @@ -781,8 +801,9 @@ class _SendViewState extends ConsumerState<SendView> { .read( localeServiceChangeNotifierProvider) .locale, - decimalPlaces: - Constants.decimalPlaces, + decimalPlaces: Constants + .decimalPlacesForCoin( + coin), ); amount.toString(); _amountToSend = amount; @@ -1044,19 +1065,22 @@ class _SendViewState extends ConsumerState<SendView> { (await firoWallet .availablePrivateBalance()) .toStringAsFixed( - Constants.decimalPlaces); + Constants.decimalPlacesForCoin( + coin)); } else { cryptoAmountController.text = (await firoWallet .availablePublicBalance()) .toStringAsFixed( - Constants.decimalPlaces); + Constants.decimalPlacesForCoin( + coin)); } } else { cryptoAmountController.text = (await ref .read(provider) .availableBalance) - .toStringAsFixed(Constants.decimalPlaces); + .toStringAsFixed( + Constants.decimalPlacesForCoin(coin)); } }, ), @@ -1167,7 +1191,8 @@ class _SendViewState extends ConsumerState<SendView> { ? Decimal.zero : (baseAmount / _price).toDecimal( scaleOnInfinitePrecision: - Constants.decimalPlaces); + Constants.decimalPlacesForCoin( + coin)); } if (_cachedAmountToSend != null && _cachedAmountToSend == _amountToSend) { @@ -1184,7 +1209,8 @@ class _SendViewState extends ConsumerState<SendView> { locale: ref .read(localeServiceChangeNotifierProvider) .locale, - decimalPlaces: Constants.decimalPlaces, + decimalPlaces: + Constants.decimalPlacesForCoin(coin), ); _cryptoAmountChangeLock = true; @@ -1506,7 +1532,7 @@ class _SendViewState extends ConsumerState<SendView> { } final amount = Format.decimalAmountToSatoshis( - _amountToSend!); + _amountToSend!, coin); int availableBalance; if ((coin == Coin.firo || coin == Coin.firoTestNet)) { @@ -1520,18 +1546,21 @@ class _SendViewState extends ConsumerState<SendView> { Format.decimalAmountToSatoshis( await (manager.wallet as FiroWallet) - .availablePrivateBalance()); + .availablePrivateBalance(), + coin); } else { availableBalance = Format.decimalAmountToSatoshis( await (manager.wallet as FiroWallet) - .availablePublicBalance()); + .availablePublicBalance(), + coin); } } else { availableBalance = Format.decimalAmountToSatoshis( - await manager.availableBalance); + await manager.availableBalance, + coin); } // confirm send all diff --git a/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart b/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart index 982086d3c..2f5ce2f3e 100644 --- a/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart +++ b/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart @@ -1,3 +1,4 @@ +import 'package:cw_core/monero_transaction_priority.dart'; import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -70,16 +71,27 @@ class _TransactionFeeSelectionSheetState final manager = ref.read(walletsChangeNotifierProvider).getManager(walletId); - if ((coin == Coin.firo || coin == Coin.firoTestNet) && + if (coin == Coin.monero || coin == Coin.wownero) { + final fee = await manager.estimateFeeFor( + amount, MoneroTransactionPriority.fast.raw!); + ref.read(feeSheetSessionCacheProvider).fast[amount] = + Format.satoshisToAmount( + fee, + coin: coin, + ); + } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && ref.read(publicPrivateBalanceStateProvider.state).state != "Private") { ref.read(feeSheetSessionCacheProvider).fast[amount] = - Format.satoshisToAmount(await (manager.wallet as FiroWallet) - .estimateFeeForPublic(amount, feeRate)); + Format.satoshisToAmount( + await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate), + coin: coin); } else { ref.read(feeSheetSessionCacheProvider).fast[amount] = Format.satoshisToAmount( - await manager.estimateFeeFor(amount, feeRate)); + await manager.estimateFeeFor(amount, feeRate), + coin: coin); } } return ref.read(feeSheetSessionCacheProvider).fast[amount]!; @@ -88,17 +100,27 @@ class _TransactionFeeSelectionSheetState if (ref.read(feeSheetSessionCacheProvider).average[amount] == null) { final manager = ref.read(walletsChangeNotifierProvider).getManager(walletId); - - if ((coin == Coin.firo || coin == Coin.firoTestNet) && + if (coin == Coin.monero || coin == Coin.wownero) { + final fee = await manager.estimateFeeFor( + amount, MoneroTransactionPriority.regular.raw!); + ref.read(feeSheetSessionCacheProvider).average[amount] = + Format.satoshisToAmount( + fee, + coin: coin, + ); + } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && ref.read(publicPrivateBalanceStateProvider.state).state != "Private") { ref.read(feeSheetSessionCacheProvider).average[amount] = - Format.satoshisToAmount(await (manager.wallet as FiroWallet) - .estimateFeeForPublic(amount, feeRate)); + Format.satoshisToAmount( + await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate), + coin: coin); } else { ref.read(feeSheetSessionCacheProvider).average[amount] = Format.satoshisToAmount( - await manager.estimateFeeFor(amount, feeRate)); + await manager.estimateFeeFor(amount, feeRate), + coin: coin); } } return ref.read(feeSheetSessionCacheProvider).average[amount]!; @@ -107,17 +129,27 @@ class _TransactionFeeSelectionSheetState if (ref.read(feeSheetSessionCacheProvider).slow[amount] == null) { final manager = ref.read(walletsChangeNotifierProvider).getManager(walletId); - - if ((coin == Coin.firo || coin == Coin.firoTestNet) && + if (coin == Coin.monero || coin == Coin.wownero) { + final fee = await manager.estimateFeeFor( + amount, MoneroTransactionPriority.slow.raw!); + ref.read(feeSheetSessionCacheProvider).slow[amount] = + Format.satoshisToAmount( + fee, + coin: coin, + ); + } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && ref.read(publicPrivateBalanceStateProvider.state).state != "Private") { ref.read(feeSheetSessionCacheProvider).slow[amount] = - Format.satoshisToAmount(await (manager.wallet as FiroWallet) - .estimateFeeForPublic(amount, feeRate)); + Format.satoshisToAmount( + await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate), + coin: coin); } else { ref.read(feeSheetSessionCacheProvider).slow[amount] = Format.satoshisToAmount( - await manager.estimateFeeFor(amount, feeRate)); + await manager.estimateFeeFor(amount, feeRate), + coin: coin); } } return ref.read(feeSheetSessionCacheProvider).slow[amount]!; @@ -225,7 +257,7 @@ class _TransactionFeeSelectionSheetState ref.read(feeRateTypeStateProvider.state).state = FeeRateType.fast; } - String? fee = getAmount(FeeRateType.fast); + String? fee = getAmount(FeeRateType.fast, manager.coin); if (fee != null) { widget.updateChosen(fee); } @@ -293,7 +325,7 @@ class _TransactionFeeSelectionSheetState feeRate: feeObject!.fast, amount: Format .decimalAmountToSatoshis( - amount)), + amount, manager.coin)), // future: manager.estimateFeeFor( // Format.decimalAmountToSatoshis( // amount), @@ -358,7 +390,8 @@ class _TransactionFeeSelectionSheetState ref.read(feeRateTypeStateProvider.state).state = FeeRateType.average; } - String? fee = getAmount(FeeRateType.average); + String? fee = + getAmount(FeeRateType.average, manager.coin); if (fee != null) { widget.updateChosen(fee); } @@ -424,7 +457,7 @@ class _TransactionFeeSelectionSheetState feeRate: feeObject!.medium, amount: Format .decimalAmountToSatoshis( - amount)), + amount, manager.coin)), // future: manager.estimateFeeFor( // Format.decimalAmountToSatoshis( // amount), @@ -489,7 +522,7 @@ class _TransactionFeeSelectionSheetState ref.read(feeRateTypeStateProvider.state).state = FeeRateType.slow; } - String? fee = getAmount(FeeRateType.slow); + String? fee = getAmount(FeeRateType.slow, manager.coin); print("fee $fee"); if (fee != null) { widget.updateChosen(fee); @@ -557,7 +590,7 @@ class _TransactionFeeSelectionSheetState feeRate: feeObject!.slow, amount: Format .decimalAmountToSatoshis( - amount)), + amount, manager.coin)), // future: manager.estimateFeeFor( // Format.decimalAmountToSatoshis( // amount), @@ -624,10 +657,10 @@ class _TransactionFeeSelectionSheetState ); } - String? getAmount(FeeRateType feeRateType) { + String? getAmount(FeeRateType feeRateType, Coin coin) { try { print(feeRateType); - var amount = Format.decimalAmountToSatoshis(this.amount); + var amount = Format.decimalAmountToSatoshis(this.amount, coin); print(amount); print(ref.read(feeSheetSessionCacheProvider).fast); print(ref.read(feeSheetSessionCacheProvider).average); diff --git a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart index 95dcc8126..b9ea02e79 100644 --- a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart +++ b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart @@ -937,13 +937,9 @@ class _DesktopTransactionCardRowState flex: 6, child: Builder( builder: (_) { - final amount = coin == Coin.monero - ? (_transaction.amount ~/ 10000) - : coin == Coin.wownero - ? (_transaction.amount ~/ 1000) - : _transaction.amount; + final amount = _transaction.amount; return Text( - "$prefix${Format.satoshiAmountToPrettyString(amount, locale)} ${coin.ticker}", + "$prefix${Format.satoshiAmountToPrettyString(amount, locale, coin)} ${coin.ticker}", style: STextStyles.desktopTextExtraExtraSmall(context) .copyWith( color: Theme.of(context) @@ -960,17 +956,12 @@ class _DesktopTransactionCardRowState flex: 4, child: Builder( builder: (_) { - // TODO: modify Format.<functions> to take optional Coin parameter so this type oif check isn't done in ui int value = _transaction.amount; - if (coin == Coin.monero) { - value = (value ~/ 10000); - } else if (coin == Coin.wownero) { - value = (value ~/ 1000); - } return Text( "$prefix${Format.localizedStringAsFixed( - value: Format.satoshisToAmount(value) * price, + value: Format.satoshisToAmount(value, coin: coin) * + price, locale: locale, decimalPlaces: 2, )} $baseCurrency", diff --git a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart index 4d45428ff..c2a0590e4 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart @@ -78,8 +78,8 @@ class _TransactionDetailsViewState walletId = widget.walletId; coin = widget.coin; - amount = Format.satoshisToAmount(_transaction.amount); - fee = Format.satoshisToAmount(_transaction.fees); + amount = Format.satoshisToAmount(_transaction.amount, coin: coin); + fee = Format.satoshisToAmount(_transaction.fees, coin: coin); if ((coin == Coin.firo || coin == Coin.firoTestNet) && _transaction.subType == "mint") { @@ -418,21 +418,15 @@ class _TransactionDetailsViewState children: [ SelectableText( "$amountPrefix${Format.localizedStringAsFixed( - value: coin == Coin.monero - ? (amount / 10000.toDecimal()) - .toDecimal() - : coin == Coin.wownero - ? (amount / - 1000.toDecimal()) - .toDecimal() - : amount, + value: amount, locale: ref.watch( localeServiceChangeNotifierProvider .select( (value) => value.locale), ), decimalPlaces: - Constants.decimalPlaces, + Constants.decimalPlacesForCoin( + coin), )} ${coin.ticker}", style: isDesktop ? STextStyles @@ -454,11 +448,21 @@ class _TransactionDetailsViewState (value) => value.externalCalls))) SelectableText( - "$amountPrefix${Format.localizedStringAsFixed(value: (coin == Coin.monero ? (amount / 10000.toDecimal()).toDecimal() : coin == Coin.wownero ? (amount / 1000.toDecimal()).toDecimal() : amount) * ref.watch(priceAnd24hChangeNotifierProvider.select((value) => value.getPrice(coin).item1)), locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => - value.locale), - ), decimalPlaces: 2)} ${ref.watch( + "$amountPrefix${Format.localizedStringAsFixed( + value: amount * + ref.watch( + priceAnd24hChangeNotifierProvider + .select((value) => value + .getPrice(coin) + .item1), + ), + locale: ref.watch( + localeServiceChangeNotifierProvider + .select((value) => + value.locale), + ), + decimalPlaces: 2, + )} ${ref.watch( prefsChangeNotifierProvider .select( (value) => value.currency, @@ -834,32 +838,22 @@ class _TransactionDetailsViewState final feeString = showFeePending ? _transaction.confirmedStatus ? Format.localizedStringAsFixed( - value: coin == Coin.monero - ? (fee / 10000.toDecimal()) - .toDecimal() - : coin == Coin.wownero - ? (fee / 1000.toDecimal()) - .toDecimal() - : fee, + value: fee, locale: ref.watch( localeServiceChangeNotifierProvider .select( (value) => value.locale)), decimalPlaces: - Constants.decimalPlaces) + Constants.decimalPlacesForCoin( + coin)) : "Pending" : Format.localizedStringAsFixed( - value: coin == Coin.monero - ? (fee / 10000.toDecimal()) - .toDecimal() - : coin == Coin.wownero - ? (fee / 1000.toDecimal()) - .toDecimal() - : fee, + value: fee, locale: ref.watch( localeServiceChangeNotifierProvider .select((value) => value.locale)), - decimalPlaces: Constants.decimalPlaces); + decimalPlaces: + Constants.decimalPlacesForCoin(coin)); return Row( mainAxisAlignment: diff --git a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart index d135ea276..0b3fd3acb 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart @@ -79,7 +79,7 @@ class _TransactionSearchViewState String amount = ""; if (filterState.amount != null) { amount = Format.satoshiAmountToPrettyString(filterState.amount!, - ref.read(localeServiceChangeNotifierProvider).locale); + ref.read(localeServiceChangeNotifierProvider).locale, widget.coin); } _amountTextEditingController.text = amount; } @@ -967,22 +967,7 @@ class _TransactionSearchViewState } int? amount; if (amountDecimal != null) { - if (widget.coin == Coin.monero) { - amount = (amountDecimal * Decimal.fromInt(Constants.satsPerCoinMonero)) - .floor() - .toBigInt() - .toInt(); - } else if (widget.coin == Coin.wownero) { - amount = (amountDecimal * Decimal.fromInt(Constants.satsPerCoinWownero)) - .floor() - .toBigInt() - .toInt(); - } else { - amount = (amountDecimal * Decimal.fromInt(Constants.satsPerCoin)) - .floor() - .toBigInt() - .toInt(); - } + amount = Format.decimalAmountToSatoshis(amountDecimal, widget.coin); } final TransactionFilter filter = TransactionFilter( diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart index 9acb3a6f9..25e1f47ab 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart @@ -1,3 +1,4 @@ +import 'package:cw_core/monero_transaction_priority.dart'; import 'package:decimal/decimal.dart'; import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/material.dart'; @@ -55,16 +56,27 @@ class _DesktopFeeDropDownState extends ConsumerState<DesktopFeeDropDown> { final manager = ref.read(walletsChangeNotifierProvider).getManager(walletId); - if ((coin == Coin.firo || coin == Coin.firoTestNet) && + if (coin == Coin.monero || coin == Coin.wownero) { + final fee = await manager.estimateFeeFor( + amount, MoneroTransactionPriority.fast.raw!); + ref.read(feeSheetSessionCacheProvider).fast[amount] = + Format.satoshisToAmount( + fee, + coin: coin, + ); + } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && ref.read(publicPrivateBalanceStateProvider.state).state != "Private") { ref.read(feeSheetSessionCacheProvider).fast[amount] = - Format.satoshisToAmount(await (manager.wallet as FiroWallet) - .estimateFeeForPublic(amount, feeRate)); + Format.satoshisToAmount( + await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate), + coin: coin); } else { ref.read(feeSheetSessionCacheProvider).fast[amount] = Format.satoshisToAmount( - await manager.estimateFeeFor(amount, feeRate)); + await manager.estimateFeeFor(amount, feeRate), + coin: coin); } } return ref.read(feeSheetSessionCacheProvider).fast[amount]!; @@ -74,16 +86,27 @@ class _DesktopFeeDropDownState extends ConsumerState<DesktopFeeDropDown> { final manager = ref.read(walletsChangeNotifierProvider).getManager(walletId); - if ((coin == Coin.firo || coin == Coin.firoTestNet) && + if (coin == Coin.monero || coin == Coin.wownero) { + final fee = await manager.estimateFeeFor( + amount, MoneroTransactionPriority.regular.raw!); + ref.read(feeSheetSessionCacheProvider).average[amount] = + Format.satoshisToAmount( + fee, + coin: coin, + ); + } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && ref.read(publicPrivateBalanceStateProvider.state).state != "Private") { ref.read(feeSheetSessionCacheProvider).average[amount] = - Format.satoshisToAmount(await (manager.wallet as FiroWallet) - .estimateFeeForPublic(amount, feeRate)); + Format.satoshisToAmount( + await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate), + coin: coin); } else { ref.read(feeSheetSessionCacheProvider).average[amount] = Format.satoshisToAmount( - await manager.estimateFeeFor(amount, feeRate)); + await manager.estimateFeeFor(amount, feeRate), + coin: coin); } } return ref.read(feeSheetSessionCacheProvider).average[amount]!; @@ -93,47 +116,33 @@ class _DesktopFeeDropDownState extends ConsumerState<DesktopFeeDropDown> { final manager = ref.read(walletsChangeNotifierProvider).getManager(walletId); - if ((coin == Coin.firo || coin == Coin.firoTestNet) && + if (coin == Coin.monero || coin == Coin.wownero) { + final fee = await manager.estimateFeeFor( + amount, MoneroTransactionPriority.slow.raw!); + ref.read(feeSheetSessionCacheProvider).slow[amount] = + Format.satoshisToAmount( + fee, + coin: coin, + ); + } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && ref.read(publicPrivateBalanceStateProvider.state).state != "Private") { ref.read(feeSheetSessionCacheProvider).slow[amount] = - Format.satoshisToAmount(await (manager.wallet as FiroWallet) - .estimateFeeForPublic(amount, feeRate)); + Format.satoshisToAmount( + await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate), + coin: coin); } else { ref.read(feeSheetSessionCacheProvider).slow[amount] = Format.satoshisToAmount( - await manager.estimateFeeFor(amount, feeRate)); + await manager.estimateFeeFor(amount, feeRate), + coin: coin); } } return ref.read(feeSheetSessionCacheProvider).slow[amount]!; } } - String estimatedTimeToBeIncludedInNextBlock( - int targetBlockTime, int estimatedNumberOfBlocks) { - int time = targetBlockTime * estimatedNumberOfBlocks; - - int hours = (time / 3600).floor(); - if (hours > 1) { - return "~$hours hours"; - } else if (hours == 1) { - return "~$hours hour"; - } - - // less than an hour - - final string = (time / 60).toStringAsFixed(1); - - if (string == "1.0") { - return "~1 minute"; - } else { - if (string.endsWith(".0")) { - return "~${(time / 60).floor()} minutes"; - } - return "~$string minutes"; - } - } - @override void initState() { walletId = widget.walletId; @@ -307,7 +316,7 @@ class FeeDropDownChild extends ConsumerWidget { return FutureBuilder( future: feeFor( coin: manager.coin, - feeRateType: FeeRateType.fast, + feeRateType: feeRateType, feeRate: feeRateType == FeeRateType.fast ? feeObject!.fast : feeRateType == FeeRateType.slow @@ -315,6 +324,7 @@ class FeeDropDownChild extends ConsumerWidget { : feeObject!.medium, amount: Format.decimalAmountToSatoshis( ref.watch(sendAmountProvider.state).state, + manager.coin, ), ), builder: (_, AsyncSnapshot<Decimal> snapshot) { diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index 336dd7b4e..72690c7e5 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -161,20 +161,22 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { return; } - final amount = Format.decimalAmountToSatoshis(_amountToSend!); + final amount = Format.decimalAmountToSatoshis(_amountToSend!, coin); int availableBalance; if ((coin == Coin.firo || coin == Coin.firoTestNet)) { if (ref.read(publicPrivateBalanceStateProvider.state).state == "Private") { availableBalance = Format.decimalAmountToSatoshis( - await (manager.wallet as FiroWallet).availablePrivateBalance()); + await (manager.wallet as FiroWallet).availablePrivateBalance(), + coin); } else { availableBalance = Format.decimalAmountToSatoshis( - await (manager.wallet as FiroWallet).availablePublicBalance()); + await (manager.wallet as FiroWallet).availablePublicBalance(), + coin); } } else { availableBalance = - Format.decimalAmountToSatoshis(await manager.availableBalance); + Format.decimalAmountToSatoshis(await manager.availableBalance, coin); } // confirm send all @@ -642,7 +644,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { cryptoAmountController.text = Format.localizedStringAsFixed( value: amount, locale: ref.read(localeServiceChangeNotifierProvider).locale, - decimalPlaces: Constants.decimalPlaces, + decimalPlaces: Constants.decimalPlacesForCoin(coin), ); amount.toString(); _amountToSend = amount; @@ -709,8 +711,8 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { } else { _amountToSend = baseAmount <= Decimal.zero ? Decimal.zero - : (baseAmount / _price) - .toDecimal(scaleOnInfinitePrecision: Constants.decimalPlaces); + : (baseAmount / _price).toDecimal( + scaleOnInfinitePrecision: Constants.decimalPlacesForCoin(coin)); } if (_cachedAmountToSend != null && _cachedAmountToSend == _amountToSend) { return; @@ -722,7 +724,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { final amountString = Format.localizedStringAsFixed( value: _amountToSend!, locale: ref.read(localeServiceChangeNotifierProvider).locale, - decimalPlaces: Constants.decimalPlaces, + decimalPlaces: Constants.decimalPlacesForCoin(coin), ); _cryptoAmountChangeLock = true; @@ -752,18 +754,18 @@ class _DesktopSendState extends ConsumerState<DesktopSend> { "Private") { cryptoAmountController.text = (await firoWallet.availablePrivateBalance()) - .toStringAsFixed(Constants.decimalPlaces); + .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); } else { cryptoAmountController.text = (await firoWallet.availablePublicBalance()) - .toStringAsFixed(Constants.decimalPlaces); + .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); } } else { cryptoAmountController.text = (await ref .read(walletsChangeNotifierProvider) .getManager(walletId) .availableBalance) - .toStringAsFixed(Constants.decimalPlaces); + .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); } } diff --git a/lib/services/coins/bitcoin/bitcoin_wallet.dart b/lib/services/coins/bitcoin/bitcoin_wallet.dart index dfd5ea180..25ec6b519 100644 --- a/lib/services/coins/bitcoin/bitcoin_wallet.dart +++ b/lib/services/coins/bitcoin/bitcoin_wallet.dart @@ -200,19 +200,21 @@ class BitcoinWallet extends CoinServiceAPI { Future<Decimal> get availableBalance async { final data = await utxoData; return Format.satoshisToAmount( - data.satoshiBalance - data.satoshiBalanceUnconfirmed); + data.satoshiBalance - data.satoshiBalanceUnconfirmed, + coin: coin); } @override Future<Decimal> get pendingBalance async { final data = await utxoData; - return Format.satoshisToAmount(data.satoshiBalanceUnconfirmed); + return Format.satoshisToAmount(data.satoshiBalanceUnconfirmed, coin: coin); } @override Future<Decimal> get balanceMinusMaxFee async => (await availableBalance) - - (Decimal.fromInt((await maxFee)) / Decimal.fromInt(Constants.satsPerCoin)) + (Decimal.fromInt((await maxFee)) / + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(); @override @@ -222,13 +224,13 @@ class BitcoinWallet extends CoinServiceAPI { .get<dynamic>(boxName: walletId, key: 'totalBalance') as int?; if (totalBalance == null) { final data = await utxoData; - return Format.satoshisToAmount(data.satoshiBalance); + return Format.satoshisToAmount(data.satoshiBalance, coin: coin); } else { - return Format.satoshisToAmount(totalBalance); + return Format.satoshisToAmount(totalBalance, coin: coin); } } final data = await utxoData; - return Format.satoshisToAmount(data.satoshiBalance); + return Format.satoshisToAmount(data.satoshiBalance, coin: coin); } @override @@ -266,7 +268,8 @@ class BitcoinWallet extends CoinServiceAPI { @override Future<int> get maxFee async { final fee = (await fees).fast as String; - final satsFee = Decimal.parse(fee) * Decimal.fromInt(Constants.satsPerCoin); + final satsFee = + Decimal.parse(fee) * Decimal.fromInt(Constants.satsPerCoin(coin)); return satsFee.floor().toBigInt().toInt(); } @@ -1093,7 +1096,8 @@ class BitcoinWallet extends CoinServiceAPI { // check for send all bool isSendAll = false; - final balance = Format.decimalAmountToSatoshis(await availableBalance); + final balance = + Format.decimalAmountToSatoshis(await availableBalance, coin); if (satoshiAmount == balance) { isSendAll = true; } @@ -1297,7 +1301,7 @@ class BitcoinWallet extends CoinServiceAPI { final String worthNow = Format.localizedStringAsFixed( value: ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2), decimalPlaces: 2, locale: locale!); @@ -1497,9 +1501,9 @@ class BitcoinWallet extends CoinServiceAPI { numberOfBlocksFast: f, numberOfBlocksAverage: m, numberOfBlocksSlow: s, - fast: Format.decimalAmountToSatoshis(fast), - medium: Format.decimalAmountToSatoshis(medium), - slow: Format.decimalAmountToSatoshis(slow), + fast: Format.decimalAmountToSatoshis(fast, coin), + medium: Format.decimalAmountToSatoshis(medium, coin), + slow: Format.decimalAmountToSatoshis(slow, coin), ); Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); @@ -1968,7 +1972,7 @@ class BitcoinWallet extends CoinServiceAPI { utxo["status"]["block_time"] = txn["blocktime"]; final fiatValue = ((Decimal.fromInt(value) * currentPrice) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2); utxo["rawWorth"] = fiatValue; utxo["fiatWorth"] = fiatValue.toString(); @@ -1978,15 +1982,16 @@ class BitcoinWallet extends CoinServiceAPI { Decimal currencyBalanceRaw = ((Decimal.fromInt(satoshiBalance) * currentPrice) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2); final Map<String, dynamic> result = { "total_user_currency": currencyBalanceRaw.toString(), "total_sats": satoshiBalance, "total_btc": (Decimal.fromInt(satoshiBalance) / - Decimal.fromInt(Constants.satsPerCoin)) - .toDecimal(scaleOnInfinitePrecision: Constants.decimalPlaces) + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal( + scaleOnInfinitePrecision: Constants.decimalPlacesForCoin(coin)) .toString(), "outputArray": outputArray, "unconfirmed": satoshiBalancePending, @@ -2532,7 +2537,7 @@ class BitcoinWallet extends CoinServiceAPI { if (prevOut == out["n"]) { inputAmtSentFromWallet += (Decimal.parse(out["value"]!.toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); } @@ -2545,7 +2550,7 @@ class BitcoinWallet extends CoinServiceAPI { final String address = output["scriptPubKey"]!["address"] as String; final value = output["value"]!; final _value = (Decimal.parse(value.toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); totalOutput += _value; @@ -2570,7 +2575,7 @@ class BitcoinWallet extends CoinServiceAPI { final address = output["scriptPubKey"]["address"]; if (address != null) { final value = (Decimal.parse(output["value"].toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); totalOut += value; @@ -2593,7 +2598,7 @@ class BitcoinWallet extends CoinServiceAPI { for (final out in tx["vout"] as List) { if (prevOut == out["n"]) { totalIn += (Decimal.parse(out["value"].toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); } @@ -2615,7 +2620,7 @@ class BitcoinWallet extends CoinServiceAPI { midSortedTx["amount"] = inputAmtSentFromWallet; final String worthNow = ((currentPrice * Decimal.fromInt(inputAmtSentFromWallet)) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2) .toStringAsFixed(2); midSortedTx["worthNow"] = worthNow; @@ -2625,7 +2630,7 @@ class BitcoinWallet extends CoinServiceAPI { midSortedTx["amount"] = outputAmtAddressedToWallet; final worthNow = ((currentPrice * Decimal.fromInt(outputAmtAddressedToWallet)) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2) .toStringAsFixed(2); midSortedTx["worthNow"] = worthNow; @@ -3753,7 +3758,8 @@ class BitcoinWallet extends CoinServiceAPI { @override Future<int> estimateFeeFor(int satoshiAmount, int feeRate) async { - final available = Format.decimalAmountToSatoshis(await availableBalance); + final available = + Format.decimalAmountToSatoshis(await availableBalance, coin); if (available == satoshiAmount) { return satoshiAmount - sweepAllEstimate(feeRate); diff --git a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart index 429af898e..59b2454b4 100644 --- a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart +++ b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart @@ -170,19 +170,21 @@ class BitcoinCashWallet extends CoinServiceAPI { Future<Decimal> get availableBalance async { final data = await utxoData; return Format.satoshisToAmount( - data.satoshiBalance - data.satoshiBalanceUnconfirmed); + data.satoshiBalance - data.satoshiBalanceUnconfirmed, + coin: coin); } @override Future<Decimal> get pendingBalance async { final data = await utxoData; - return Format.satoshisToAmount(data.satoshiBalanceUnconfirmed); + return Format.satoshisToAmount(data.satoshiBalanceUnconfirmed, coin: coin); } @override Future<Decimal> get balanceMinusMaxFee async => (await availableBalance) - - (Decimal.fromInt((await maxFee)) / Decimal.fromInt(Constants.satsPerCoin)) + (Decimal.fromInt((await maxFee)) / + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(); @override @@ -192,13 +194,13 @@ class BitcoinCashWallet extends CoinServiceAPI { .get<dynamic>(boxName: walletId, key: 'totalBalance') as int?; if (totalBalance == null) { final data = await utxoData; - return Format.satoshisToAmount(data.satoshiBalance); + return Format.satoshisToAmount(data.satoshiBalance, coin: coin); } else { - return Format.satoshisToAmount(totalBalance); + return Format.satoshisToAmount(totalBalance, coin: coin); } } final data = await utxoData; - return Format.satoshisToAmount(data.satoshiBalance); + return Format.satoshisToAmount(data.satoshiBalance, coin: coin); } @override @@ -232,8 +234,8 @@ class BitcoinCashWallet extends CoinServiceAPI { @override Future<int> get maxFee async { final fee = (await fees).fast; - final satsFee = - Format.satoshisToAmount(fee) * Decimal.fromInt(Constants.satsPerCoin); + final satsFee = Format.satoshisToAmount(fee, coin: coin) * + Decimal.fromInt(Constants.satsPerCoin(coin)); return satsFee.floor().toBigInt().toInt(); } @@ -988,7 +990,8 @@ class BitcoinCashWallet extends CoinServiceAPI { } // check for send all bool isSendAll = false; - final balance = Format.decimalAmountToSatoshis(await availableBalance); + final balance = + Format.decimalAmountToSatoshis(await availableBalance, coin); if (satoshiAmount == balance) { isSendAll = true; } @@ -1175,7 +1178,7 @@ class BitcoinCashWallet extends CoinServiceAPI { final String worthNow = Format.localizedStringAsFixed( value: ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2), decimalPlaces: 2, locale: locale!); @@ -1384,9 +1387,9 @@ class BitcoinCashWallet extends CoinServiceAPI { numberOfBlocksFast: f, numberOfBlocksAverage: m, numberOfBlocksSlow: s, - fast: Format.decimalAmountToSatoshis(fast), - medium: Format.decimalAmountToSatoshis(medium), - slow: Format.decimalAmountToSatoshis(slow), + fast: Format.decimalAmountToSatoshis(fast, coin), + medium: Format.decimalAmountToSatoshis(medium, coin), + slow: Format.decimalAmountToSatoshis(slow, coin), ); Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); @@ -1791,7 +1794,7 @@ class BitcoinCashWallet extends CoinServiceAPI { utxo["status"]["block_time"] = txn["blocktime"]; final fiatValue = ((Decimal.fromInt(value) * currentPrice) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2); utxo["rawWorth"] = fiatValue; utxo["fiatWorth"] = fiatValue.toString(); @@ -1801,15 +1804,16 @@ class BitcoinCashWallet extends CoinServiceAPI { Decimal currencyBalanceRaw = ((Decimal.fromInt(satoshiBalance) * currentPrice) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2); final Map<String, dynamic> result = { "total_user_currency": currencyBalanceRaw.toString(), "total_sats": satoshiBalance, "total_btc": (Decimal.fromInt(satoshiBalance) / - Decimal.fromInt(Constants.satsPerCoin)) - .toDecimal(scaleOnInfinitePrecision: Constants.decimalPlaces) + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal( + scaleOnInfinitePrecision: Constants.decimalPlacesForCoin(coin)) .toString(), "outputArray": outputArray, "unconfirmed": satoshiBalancePending, @@ -2332,7 +2336,7 @@ class BitcoinCashWallet extends CoinServiceAPI { if (prevOut == out["n"]) { inputAmtSentFromWallet += (Decimal.parse(out["value"].toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); } @@ -2345,7 +2349,7 @@ class BitcoinCashWallet extends CoinServiceAPI { final address = output["scriptPubKey"]["addresses"][0]; final value = output["value"]; final _value = (Decimal.parse(value.toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); totalOutput += _value; @@ -2370,7 +2374,7 @@ class BitcoinCashWallet extends CoinServiceAPI { final address = output["scriptPubKey"]["addresses"][0]; if (address != null) { final value = (Decimal.parse(output["value"].toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); totalOut += value; @@ -2394,7 +2398,7 @@ class BitcoinCashWallet extends CoinServiceAPI { for (final out in tx["vout"] as List) { if (prevOut == out["n"]) { totalIn += (Decimal.parse(out["value"].toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); } @@ -2416,7 +2420,7 @@ class BitcoinCashWallet extends CoinServiceAPI { midSortedTx["amount"] = inputAmtSentFromWallet; final String worthNow = ((currentPrice * Decimal.fromInt(inputAmtSentFromWallet)) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2) .toStringAsFixed(2); midSortedTx["worthNow"] = worthNow; @@ -2426,7 +2430,7 @@ class BitcoinCashWallet extends CoinServiceAPI { midSortedTx["amount"] = outputAmtAddressedToWallet; final worthNow = ((currentPrice * Decimal.fromInt(outputAmtAddressedToWallet)) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2) .toStringAsFixed(2); midSortedTx["worthNow"] = worthNow; @@ -3457,7 +3461,8 @@ class BitcoinCashWallet extends CoinServiceAPI { @override Future<int> estimateFeeFor(int satoshiAmount, int feeRate) async { - final available = Format.decimalAmountToSatoshis(await availableBalance); + final available = + Format.decimalAmountToSatoshis(await availableBalance, coin); if (available == satoshiAmount) { return satoshiAmount - sweepAllEstimate(feeRate); diff --git a/lib/services/coins/dogecoin/dogecoin_wallet.dart b/lib/services/coins/dogecoin/dogecoin_wallet.dart index 41778a9e0..f7372752b 100644 --- a/lib/services/coins/dogecoin/dogecoin_wallet.dart +++ b/lib/services/coins/dogecoin/dogecoin_wallet.dart @@ -167,19 +167,21 @@ class DogecoinWallet extends CoinServiceAPI { Future<Decimal> get availableBalance async { final data = await utxoData; return Format.satoshisToAmount( - data.satoshiBalance - data.satoshiBalanceUnconfirmed); + data.satoshiBalance - data.satoshiBalanceUnconfirmed, + coin: coin); } @override Future<Decimal> get pendingBalance async { final data = await utxoData; - return Format.satoshisToAmount(data.satoshiBalanceUnconfirmed); + return Format.satoshisToAmount(data.satoshiBalanceUnconfirmed, coin: coin); } @override Future<Decimal> get balanceMinusMaxFee async => (await availableBalance) - - (Decimal.fromInt((await maxFee)) / Decimal.fromInt(Constants.satsPerCoin)) + (Decimal.fromInt((await maxFee)) / + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(); @override @@ -189,13 +191,13 @@ class DogecoinWallet extends CoinServiceAPI { .get<dynamic>(boxName: walletId, key: 'totalBalance') as int?; if (totalBalance == null) { final data = await utxoData; - return Format.satoshisToAmount(data.satoshiBalance); + return Format.satoshisToAmount(data.satoshiBalance, coin: coin); } else { - return Format.satoshisToAmount(totalBalance); + return Format.satoshisToAmount(totalBalance, coin: coin); } } final data = await utxoData; - return Format.satoshisToAmount(data.satoshiBalance); + return Format.satoshisToAmount(data.satoshiBalance, coin: coin); } @override @@ -225,8 +227,8 @@ class DogecoinWallet extends CoinServiceAPI { @override Future<int> get maxFee async { final fee = (await fees).fast; - final satsFee = - Format.satoshisToAmount(fee) * Decimal.fromInt(Constants.satsPerCoin); + final satsFee = Format.satoshisToAmount(fee, coin: coin) * + Decimal.fromInt(Constants.satsPerCoin(coin)); return satsFee.floor().toBigInt().toInt(); } @@ -878,7 +880,8 @@ class DogecoinWallet extends CoinServiceAPI { } // check for send all bool isSendAll = false; - final balance = Format.decimalAmountToSatoshis(await availableBalance); + final balance = + Format.decimalAmountToSatoshis(await availableBalance, coin); if (satoshiAmount == balance) { isSendAll = true; } @@ -1065,7 +1068,7 @@ class DogecoinWallet extends CoinServiceAPI { final String worthNow = Format.localizedStringAsFixed( value: ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2), decimalPlaces: 2, locale: locale!); @@ -1247,9 +1250,9 @@ class DogecoinWallet extends CoinServiceAPI { numberOfBlocksFast: f, numberOfBlocksAverage: m, numberOfBlocksSlow: s, - fast: Format.decimalAmountToSatoshis(fast), - medium: Format.decimalAmountToSatoshis(medium), - slow: Format.decimalAmountToSatoshis(slow), + fast: Format.decimalAmountToSatoshis(fast, coin), + medium: Format.decimalAmountToSatoshis(medium, coin), + slow: Format.decimalAmountToSatoshis(slow, coin), ); Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); @@ -1650,7 +1653,7 @@ class DogecoinWallet extends CoinServiceAPI { utxo["status"]["block_time"] = txn["blocktime"]; final fiatValue = ((Decimal.fromInt(value) * currentPrice) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2); utxo["rawWorth"] = fiatValue; utxo["fiatWorth"] = fiatValue.toString(); @@ -1660,15 +1663,16 @@ class DogecoinWallet extends CoinServiceAPI { Decimal currencyBalanceRaw = ((Decimal.fromInt(satoshiBalance) * currentPrice) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2); final Map<String, dynamic> result = { "total_user_currency": currencyBalanceRaw.toString(), "total_sats": satoshiBalance, "total_btc": (Decimal.fromInt(satoshiBalance) / - Decimal.fromInt(Constants.satsPerCoin)) - .toDecimal(scaleOnInfinitePrecision: Constants.decimalPlaces) + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal( + scaleOnInfinitePrecision: Constants.decimalPlacesForCoin(coin)) .toString(), "outputArray": outputArray, "unconfirmed": satoshiBalancePending, @@ -2144,7 +2148,7 @@ class DogecoinWallet extends CoinServiceAPI { if (prevOut == out["n"]) { inputAmtSentFromWallet += (Decimal.parse(out["value"].toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); } @@ -2157,7 +2161,7 @@ class DogecoinWallet extends CoinServiceAPI { final address = output["scriptPubKey"]["addresses"][0]; final value = output["value"]; final _value = (Decimal.parse(value.toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); totalOutput += _value; @@ -2182,7 +2186,7 @@ class DogecoinWallet extends CoinServiceAPI { final address = output["scriptPubKey"]["addresses"][0]; if (address != null) { final value = (Decimal.parse(output["value"].toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); totalOut += value; @@ -2205,7 +2209,7 @@ class DogecoinWallet extends CoinServiceAPI { for (final out in tx["vout"] as List) { if (prevOut == out["n"]) { totalIn += (Decimal.parse(out["value"].toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); } @@ -2227,7 +2231,7 @@ class DogecoinWallet extends CoinServiceAPI { midSortedTx["amount"] = inputAmtSentFromWallet; final String worthNow = ((currentPrice * Decimal.fromInt(inputAmtSentFromWallet)) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2) .toStringAsFixed(2); midSortedTx["worthNow"] = worthNow; @@ -2237,7 +2241,7 @@ class DogecoinWallet extends CoinServiceAPI { midSortedTx["amount"] = outputAmtAddressedToWallet; final worthNow = ((currentPrice * Decimal.fromInt(outputAmtAddressedToWallet)) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2) .toStringAsFixed(2); midSortedTx["worthNow"] = worthNow; @@ -3050,7 +3054,8 @@ class DogecoinWallet extends CoinServiceAPI { @override Future<int> estimateFeeFor(int satoshiAmount, int feeRate) async { - final available = Format.decimalAmountToSatoshis(await availableBalance); + final available = + Format.decimalAmountToSatoshis(await availableBalance, coin); if (available == satoshiAmount) { return satoshiAmount - sweepAllEstimate(feeRate); diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index 4bd863f2c..3e0cba75d 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -620,7 +620,7 @@ Future<dynamic> isolateCreateJoinSplitTransaction( "txid": txId, "txHex": txHex, "value": amount, - "fees": Format.satoshisToAmount(fee).toDouble(), + "fees": Format.satoshisToAmount(fee, coin: coin).toDouble(), "fee": fee, "vSize": extTx.virtualSize(), "jmintValue": changeToMint, @@ -629,11 +629,11 @@ Future<dynamic> isolateCreateJoinSplitTransaction( "height": locktime, "txType": "Sent", "confirmed_status": false, - "amount": Format.satoshisToAmount(amount).toDouble(), + "amount": Format.satoshisToAmount(amount, coin: coin).toDouble(), "recipientAmt": amount, "worthNow": Format.localizedStringAsFixed( value: ((Decimal.fromInt(amount) * price) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2), decimalPlaces: 2, locale: locale), @@ -883,7 +883,7 @@ class FiroWallet extends CoinServiceAPI { Future<Decimal> get balanceMinusMaxFee async { final balances = await this.balances; final maxFee = await this.maxFee; - return balances[0] - Format.satoshisToAmount(maxFee); + return balances[0] - Format.satoshisToAmount(maxFee, coin: coin); } @override @@ -919,7 +919,7 @@ class FiroWallet extends CoinServiceAPI { final String worthNow = Format.localizedStringAsFixed( value: ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2), decimalPlaces: 2, locale: locale!); @@ -1089,8 +1089,8 @@ class FiroWallet extends CoinServiceAPI { // check for send all bool isSendAll = false; - final balance = - Format.decimalAmountToSatoshis(await availablePublicBalance()); + final balance = Format.decimalAmountToSatoshis( + await availablePublicBalance(), coin); if (satoshiAmount == balance) { isSendAll = true; } @@ -1176,7 +1176,7 @@ class FiroWallet extends CoinServiceAPI { // check for send all bool isSendAll = false; final balance = - Format.decimalAmountToSatoshis(await availablePrivateBalance()); + Format.decimalAmountToSatoshis(await availablePrivateBalance(), coin); if (satoshiAmount == balance) { // print("is send all"); isSendAll = true; @@ -1222,7 +1222,8 @@ class FiroWallet extends CoinServiceAPI { // temporarily update apdate available balance until a full refresh is done // TODO: something here causes an exception to be thrown giving user false info that the tx failed - Decimal sendTotal = Format.satoshisToAmount(txData["value"] as int); + Decimal sendTotal = + Format.satoshisToAmount(txData["value"] as int, coin: coin); sendTotal += Decimal.parse(txData["fees"].toString()); final bals = await balances; bals[0] -= sendTotal; @@ -1270,7 +1271,7 @@ class FiroWallet extends CoinServiceAPI { // temporarily update apdate available balance until a full refresh is done Decimal sendTotal = - Format.satoshisToAmount(txHexOrError["value"] as int); + Format.satoshisToAmount(txHexOrError["value"] as int, coin: coin); sendTotal += Decimal.parse(txHexOrError["fees"].toString()); final bals = await balances; bals[0] -= sendTotal; @@ -2333,8 +2334,9 @@ class FiroWallet extends CoinServiceAPI { Future<int> _fetchMaxFee() async { final balance = await availableBalance; - int spendAmount = - (balance * Decimal.fromInt(Constants.satsPerCoin)).toBigInt().toInt(); + int spendAmount = (balance * Decimal.fromInt(Constants.satsPerCoin(coin))) + .toBigInt() + .toInt(); int fee = await estimateJoinSplitFee(spendAmount); return fee; } @@ -2480,18 +2482,20 @@ class FiroWallet extends CoinServiceAPI { } final int utxosIntValue = utxos.satoshiBalance; - final Decimal utxosValue = Format.satoshisToAmount(utxosIntValue); + final Decimal utxosValue = + Format.satoshisToAmount(utxosIntValue, coin: coin); List<Decimal> balances = List.empty(growable: true); - Decimal lelantusBalance = Format.satoshisToAmount(intLelantusBalance); + Decimal lelantusBalance = + Format.satoshisToAmount(intLelantusBalance, coin: coin); balances.add(lelantusBalance); balances.add(lelantusBalance * price); Decimal _unconfirmedLelantusBalance = - Format.satoshisToAmount(unconfirmedLelantusBalance); + Format.satoshisToAmount(unconfirmedLelantusBalance, coin: coin); balances.add(lelantusBalance + utxosValue + _unconfirmedLelantusBalance); @@ -2503,7 +2507,7 @@ class FiroWallet extends CoinServiceAPI { if (availableSats < 0) { availableSats = 0; } - balances.add(Format.satoshisToAmount(availableSats)); + balances.add(Format.satoshisToAmount(availableSats, coin: coin)); Logging.instance.log("balances $balances", level: LogLevel.Info); await DB.instance.put<dynamic>( @@ -2601,7 +2605,8 @@ class FiroWallet extends CoinServiceAPI { final feesObject = await fees; - final Decimal fastFee = Format.satoshisToAmount(feesObject.fast); + final Decimal fastFee = + Format.satoshisToAmount(feesObject.fast, coin: coin); int firoFee = (dvsize * fastFee * Decimal.fromInt(100000)).toDouble().ceil(); // int firoFee = (vsize * feesObject.fast * (1 / 1000.0) * 100000000).ceil(); @@ -2789,15 +2794,15 @@ class FiroWallet extends CoinServiceAPI { "txid": txId, "txHex": txHex, "value": amount - fee, - "fees": Format.satoshisToAmount(fee).toDouble(), + "fees": Format.satoshisToAmount(fee, coin: coin).toDouble(), "publicCoin": "", "height": height, "txType": "Sent", "confirmed_status": false, - "amount": Format.satoshisToAmount(amount).toDouble(), + "amount": Format.satoshisToAmount(amount, coin: coin).toDouble(), "worthNow": Format.localizedStringAsFixed( value: ((Decimal.fromInt(amount) * price) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2), decimalPlaces: 2, locale: locale!), @@ -3040,9 +3045,9 @@ class FiroWallet extends CoinServiceAPI { numberOfBlocksFast: f, numberOfBlocksAverage: m, numberOfBlocksSlow: s, - fast: Format.decimalAmountToSatoshis(fast), - medium: Format.decimalAmountToSatoshis(medium), - slow: Format.decimalAmountToSatoshis(slow), + fast: Format.decimalAmountToSatoshis(fast, coin), + medium: Format.decimalAmountToSatoshis(medium, coin), + slow: Format.decimalAmountToSatoshis(slow, coin), ); Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); @@ -3328,7 +3333,7 @@ class FiroWallet extends CoinServiceAPI { if (nFees != null) { nFeesUsed = true; fees = (Decimal.parse(nFees.toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); } @@ -3353,7 +3358,7 @@ class FiroWallet extends CoinServiceAPI { if (value != null) { if (changeAddresses.contains(address)) { inputAmtSentFromWallet -= (Decimal.parse(value.toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); } else { @@ -3363,7 +3368,7 @@ class FiroWallet extends CoinServiceAPI { } if (value != null) { outAmount += (Decimal.parse(value.toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); } @@ -3376,7 +3381,7 @@ class FiroWallet extends CoinServiceAPI { final nFees = input["nFees"]; if (nFees != null) { fees += (Decimal.parse(nFees.toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); } @@ -3391,7 +3396,7 @@ class FiroWallet extends CoinServiceAPI { if (allAddresses.contains(address)) { outputAmtAddressedToWallet += (Decimal.parse(value.toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); outAddress = address; @@ -3413,7 +3418,7 @@ class FiroWallet extends CoinServiceAPI { midSortedTx["amount"] = inputAmtSentFromWallet; final String worthNow = Format.localizedStringAsFixed( value: ((currentPrice * Decimal.fromInt(inputAmtSentFromWallet)) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2), decimalPlaces: 2, locale: locale!); @@ -3428,7 +3433,7 @@ class FiroWallet extends CoinServiceAPI { final worthNow = Format.localizedStringAsFixed( value: ((currentPrice * Decimal.fromInt(outputAmtAddressedToWallet)) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2), decimalPlaces: 2, locale: locale!); @@ -3589,7 +3594,7 @@ class FiroWallet extends CoinServiceAPI { utxo["status"]["block_time"] = txn["blocktime"]; final fiatValue = ((Decimal.fromInt(value) * currentPrice) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2); utxo["rawWorth"] = fiatValue; utxo["fiatWorth"] = fiatValue.toString(); @@ -3600,15 +3605,16 @@ class FiroWallet extends CoinServiceAPI { Decimal currencyBalanceRaw = ((Decimal.fromInt(satoshiBalance) * currentPrice) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2); final Map<String, dynamic> result = { "total_user_currency": currencyBalanceRaw.toString(), "total_sats": satoshiBalance, "total_btc": (Decimal.fromInt(satoshiBalance) / - Decimal.fromInt(Constants.satsPerCoin)) - .toDecimal(scaleOnInfinitePrecision: Constants.decimalPlaces) + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal( + scaleOnInfinitePrecision: Constants.decimalPlacesForCoin(coin)) .toString(), "outputArray": outputArray, "unconfirmed": satoshiBalancePending, @@ -4571,8 +4577,9 @@ class FiroWallet extends CoinServiceAPI { ) async { var lelantusEntry = await _getLelantusEntry(); final balance = await availableBalance; - int spendAmount = - (balance * Decimal.fromInt(Constants.satsPerCoin)).toBigInt().toInt(); + int spendAmount = (balance * Decimal.fromInt(Constants.satsPerCoin(coin))) + .toBigInt() + .toInt(); if (spendAmount == 0 || lelantusEntry.isEmpty) { return LelantusFeeData(0, 0, []).fee; } @@ -4633,7 +4640,7 @@ class FiroWallet extends CoinServiceAPI { Future<int> estimateFeeForPublic(int satoshiAmount, int feeRate) async { final available = - Format.decimalAmountToSatoshis(await availablePublicBalance()); + Format.decimalAmountToSatoshis(await availablePublicBalance(), coin); if (available == satoshiAmount) { return satoshiAmount - sweepAllEstimate(feeRate); diff --git a/lib/services/coins/litecoin/litecoin_wallet.dart b/lib/services/coins/litecoin/litecoin_wallet.dart index db7c9d1fa..9c4bb2305 100644 --- a/lib/services/coins/litecoin/litecoin_wallet.dart +++ b/lib/services/coins/litecoin/litecoin_wallet.dart @@ -200,19 +200,21 @@ class LitecoinWallet extends CoinServiceAPI { Future<Decimal> get availableBalance async { final data = await utxoData; return Format.satoshisToAmount( - data.satoshiBalance - data.satoshiBalanceUnconfirmed); + data.satoshiBalance - data.satoshiBalanceUnconfirmed, + coin: coin); } @override Future<Decimal> get pendingBalance async { final data = await utxoData; - return Format.satoshisToAmount(data.satoshiBalanceUnconfirmed); + return Format.satoshisToAmount(data.satoshiBalanceUnconfirmed, coin: coin); } @override Future<Decimal> get balanceMinusMaxFee async => (await availableBalance) - - (Decimal.fromInt((await maxFee)) / Decimal.fromInt(Constants.satsPerCoin)) + (Decimal.fromInt((await maxFee)) / + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(); @override @@ -222,13 +224,13 @@ class LitecoinWallet extends CoinServiceAPI { .get<dynamic>(boxName: walletId, key: 'totalBalance') as int?; if (totalBalance == null) { final data = await utxoData; - return Format.satoshisToAmount(data.satoshiBalance); + return Format.satoshisToAmount(data.satoshiBalance, coin: coin); } else { - return Format.satoshisToAmount(totalBalance); + return Format.satoshisToAmount(totalBalance, coin: coin); } } final data = await utxoData; - return Format.satoshisToAmount(data.satoshiBalance); + return Format.satoshisToAmount(data.satoshiBalance, coin: coin); } @override @@ -266,7 +268,8 @@ class LitecoinWallet extends CoinServiceAPI { @override Future<int> get maxFee async { final fee = (await fees).fast as String; - final satsFee = Decimal.parse(fee) * Decimal.fromInt(Constants.satsPerCoin); + final satsFee = + Decimal.parse(fee) * Decimal.fromInt(Constants.satsPerCoin(coin)); return satsFee.floor().toBigInt().toInt(); } @@ -1095,7 +1098,8 @@ class LitecoinWallet extends CoinServiceAPI { // check for send all bool isSendAll = false; - final balance = Format.decimalAmountToSatoshis(await availableBalance); + final balance = + Format.decimalAmountToSatoshis(await availableBalance, coin); if (satoshiAmount == balance) { isSendAll = true; } @@ -1299,7 +1303,7 @@ class LitecoinWallet extends CoinServiceAPI { final String worthNow = Format.localizedStringAsFixed( value: ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2), decimalPlaces: 2, locale: locale!); @@ -1499,9 +1503,9 @@ class LitecoinWallet extends CoinServiceAPI { numberOfBlocksFast: f, numberOfBlocksAverage: m, numberOfBlocksSlow: s, - fast: Format.decimalAmountToSatoshis(fast), - medium: Format.decimalAmountToSatoshis(medium), - slow: Format.decimalAmountToSatoshis(slow), + fast: Format.decimalAmountToSatoshis(fast, coin), + medium: Format.decimalAmountToSatoshis(medium, coin), + slow: Format.decimalAmountToSatoshis(slow, coin), ); Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); @@ -1978,7 +1982,7 @@ class LitecoinWallet extends CoinServiceAPI { utxo["status"]["block_time"] = txn["blocktime"]; final fiatValue = ((Decimal.fromInt(value) * currentPrice) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2); utxo["rawWorth"] = fiatValue; utxo["fiatWorth"] = fiatValue.toString(); @@ -1988,15 +1992,16 @@ class LitecoinWallet extends CoinServiceAPI { Decimal currencyBalanceRaw = ((Decimal.fromInt(satoshiBalance) * currentPrice) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2); final Map<String, dynamic> result = { "total_user_currency": currencyBalanceRaw.toString(), "total_sats": satoshiBalance, "total_btc": (Decimal.fromInt(satoshiBalance) / - Decimal.fromInt(Constants.satsPerCoin)) - .toDecimal(scaleOnInfinitePrecision: Constants.decimalPlaces) + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal( + scaleOnInfinitePrecision: Constants.decimalPlacesForCoin(coin)) .toString(), "outputArray": outputArray, "unconfirmed": satoshiBalancePending, @@ -2543,7 +2548,7 @@ class LitecoinWallet extends CoinServiceAPI { if (prevOut == out["n"]) { inputAmtSentFromWallet += (Decimal.parse(out["value"]!.toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); } @@ -2557,7 +2562,7 @@ class LitecoinWallet extends CoinServiceAPI { output["scriptPubKey"]!["addresses"][0] as String; final value = output["value"]!; final _value = (Decimal.parse(value.toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); totalOutput += _value; @@ -2582,7 +2587,7 @@ class LitecoinWallet extends CoinServiceAPI { final address = output["scriptPubKey"]["addresses"][0]; if (address != null) { final value = (Decimal.parse(output["value"].toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); totalOut += value; @@ -2605,7 +2610,7 @@ class LitecoinWallet extends CoinServiceAPI { for (final out in tx["vout"] as List) { if (prevOut == out["n"]) { totalIn += (Decimal.parse(out["value"].toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); } @@ -2627,7 +2632,7 @@ class LitecoinWallet extends CoinServiceAPI { midSortedTx["amount"] = inputAmtSentFromWallet; final String worthNow = ((currentPrice * Decimal.fromInt(inputAmtSentFromWallet)) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2) .toStringAsFixed(2); midSortedTx["worthNow"] = worthNow; @@ -2637,7 +2642,7 @@ class LitecoinWallet extends CoinServiceAPI { midSortedTx["amount"] = outputAmtAddressedToWallet; final worthNow = ((currentPrice * Decimal.fromInt(outputAmtAddressedToWallet)) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2) .toStringAsFixed(2); midSortedTx["worthNow"] = worthNow; @@ -3769,7 +3774,8 @@ class LitecoinWallet extends CoinServiceAPI { @override Future<int> estimateFeeFor(int satoshiAmount, int feeRate) async { - final available = Format.decimalAmountToSatoshis(await availableBalance); + final available = + Format.decimalAmountToSatoshis(await availableBalance, coin); if (available == satoshiAmount) { return satoshiAmount - sweepAllEstimate(feeRate); diff --git a/lib/services/coins/monero/monero_wallet.dart b/lib/services/coins/monero/monero_wallet.dart index f94f0cd2a..58bd36c72 100644 --- a/lib/services/coins/monero/monero_wallet.dart +++ b/lib/services/coins/monero/monero_wallet.dart @@ -44,6 +44,7 @@ import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/stack_file_system.dart'; @@ -533,7 +534,8 @@ class MoneroWallet extends CoinServiceAPI { @override Future<Decimal> get balanceMinusMaxFee async => (await availableBalance) - - (Decimal.fromInt((await maxFee)) / Decimal.fromInt(Constants.satsPerCoin)) + (Decimal.fromInt((await maxFee)) / + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(); @override @@ -542,16 +544,16 @@ class MoneroWallet extends CoinServiceAPI { @override Future<void> exit() async { - await stopSyncPercentTimer(); _hasCalledExit = true; - isActive = false; - await walletBase?.save(prioritySave: true); - walletBase?.close(); + stopNetworkAlivePinging(); moneroAutosaveTimer?.cancel(); moneroAutosaveTimer = null; timer?.cancel(); timer = null; - stopNetworkAlivePinging(); + await stopSyncPercentTimer(); + await walletBase?.save(prioritySave: true); + walletBase?.close(); + isActive = false; } bool _hasCalledExit = false; @@ -562,13 +564,15 @@ class MoneroWallet extends CoinServiceAPI { Future<String>? _currentReceivingAddress; Future<FeeObject> _getFees() async { + // TODO: not use hard coded values here return FeeObject( - numberOfBlocksFast: 10, - numberOfBlocksAverage: 10, - numberOfBlocksSlow: 10, - fast: 4, - medium: 2, - slow: 0); + numberOfBlocksFast: 10, + numberOfBlocksAverage: 10, + numberOfBlocksSlow: 10, + fast: MoneroTransactionPriority.fast.raw!, + medium: MoneroTransactionPriority.regular.raw!, + slow: MoneroTransactionPriority.slow.raw!, + ); } @override @@ -868,8 +872,9 @@ class MoneroWallet extends CoinServiceAPI { Future<int> get maxFee async { var bal = await availableBalance; var fee = walletBase!.calculateEstimatedFee( - monero.getDefaultTransactionPriority(), bal.toBigInt().toInt()) ~/ - 10000; + monero.getDefaultTransactionPriority(), + Format.decimalAmountToSatoshis(bal, coin), + ); return fee; } @@ -1372,7 +1377,6 @@ class MoneroWallet extends CoinServiceAPI { } @override - // TODO: implement availableBalance Future<Decimal> get availableBalance async { var bal = 0; for (var element in walletBase!.balance!.entries) { @@ -1421,13 +1425,13 @@ class MoneroWallet extends CoinServiceAPI { try { final feeRate = args?["feeRate"]; if (feeRate is FeeRateType) { - MoneroTransactionPriority feePriority = MoneroTransactionPriority.slow; + MoneroTransactionPriority feePriority; switch (feeRate) { case FeeRateType.fast: - feePriority = MoneroTransactionPriority.fastest; + feePriority = MoneroTransactionPriority.fast; break; case FeeRateType.average: - feePriority = MoneroTransactionPriority.medium; + feePriority = MoneroTransactionPriority.regular; break; case FeeRateType.slow: feePriority = MoneroTransactionPriority.slow; @@ -1440,15 +1444,14 @@ class MoneroWallet extends CoinServiceAPI { bool isSendAll = false; final balance = await availableBalance; final satInDecimal = ((Decimal.fromInt(satoshiAmount) / - Decimal.fromInt(Constants.satsPerCoinMonero)) - .toDecimal() * - Decimal.fromInt(10000)); + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal()); if (satInDecimal == balance) { isSendAll = true; } Logging.instance .log("$toAddress $amount $args", level: LogLevel.Info); - String amountToSend = moneroAmountToString(amount: amount * 10000); + String amountToSend = moneroAmountToString(amount: amount); Logging.instance.log("$amount $amountToSend", level: LogLevel.Info); monero_output.Output output = monero_output.Output(walletBase!); @@ -1470,10 +1473,9 @@ class MoneroWallet extends CoinServiceAPI { PendingMoneroTransaction pendingMoneroTransaction = await (awaitPendingTransaction!) as PendingMoneroTransaction; - int realfee = (Decimal.parse(pendingMoneroTransaction.feeFormatted) * - 100000000.toDecimal()) - .toBigInt() - .toInt(); + + int realfee = Format.decimalAmountToSatoshis( + Decimal.parse(pendingMoneroTransaction.feeFormatted), coin); debugPrint("fee? $realfee"); Map<String, dynamic> txData = { "pendingMoneroTransaction": pendingMoneroTransaction, @@ -1506,12 +1508,13 @@ class MoneroWallet extends CoinServiceAPI { @override Future<int> estimateFeeFor(int satoshiAmount, int feeRate) async { - MoneroTransactionPriority? priority; - FeeRateType feeRateType = FeeRateType.slow; + MoneroTransactionPriority priority; + FeeRateType feeRateType; + switch (feeRate) { case 1: priority = MoneroTransactionPriority.regular; - feeRateType = FeeRateType.slow; + feeRateType = FeeRateType.average; break; case 2: priority = MoneroTransactionPriority.medium; @@ -1519,7 +1522,7 @@ class MoneroWallet extends CoinServiceAPI { break; case 3: priority = MoneroTransactionPriority.fast; - feeRateType = FeeRateType.average; + feeRateType = FeeRateType.fast; break; case 4: priority = MoneroTransactionPriority.fastest; @@ -1531,27 +1534,29 @@ class MoneroWallet extends CoinServiceAPI { feeRateType = FeeRateType.slow; break; } - var aprox; + // int? aprox; - await estimateFeeMutex.protect(() async { - { - try { - aprox = (await prepareSend( - // This address is only used for getting an approximate fee, never for sending - address: - "8347huhmj6Ggzr1BpZPJAD5oa96ob5Fe8GtQdGZDYVVYVsCgtUNH3pEEzExDuaAVZdC16D4FkAb24J6wUfsKkcZtC8EPXB7", - satoshiAmount: satoshiAmount, - args: {"feeRate": feeRateType}))['fee']; - await Future.delayed(const Duration(milliseconds: 1000)); - } catch (e, s) { - Logging.instance.log("$feeRateType $e $s", level: LogLevel.Error); - aprox = -9999999999999999; - } - } - }); + // corrupted size vs. prev_size occurs but not sure if related to fees or just generating monero transactions in general + + // await estimateFeeMutex.protect(() async { + // { + // try { + // aprox = (await prepareSend( + // // This address is only used for getting an approximate fee, never for sending + // address: + // "8347huhmj6Ggzr1BpZPJAD5oa96ob5Fe8GtQdGZDYVVYVsCgtUNH3pEEzExDuaAVZdC16D4FkAb24J6wUfsKkcZtC8EPXB7", + // satoshiAmount: satoshiAmount, + // args: {"feeRate": feeRateType}))['fee'] as int?; + // await Future<void>.delayed(const Duration(milliseconds: 1000)); + // } catch (e, s) { + // Logging.instance.log("$feeRateType $e $s", level: LogLevel.Error); + final aprox = walletBase!.calculateEstimatedFee(priority, satoshiAmount); + // } + // } + // }); print("this is the aprox fee $aprox for $satoshiAmount"); - final fee = (aprox as int); + final fee = aprox; return fee; } diff --git a/lib/services/coins/namecoin/namecoin_wallet.dart b/lib/services/coins/namecoin/namecoin_wallet.dart index c8c84fb27..142bfb379 100644 --- a/lib/services/coins/namecoin/namecoin_wallet.dart +++ b/lib/services/coins/namecoin/namecoin_wallet.dart @@ -196,19 +196,21 @@ class NamecoinWallet extends CoinServiceAPI { Future<Decimal> get availableBalance async { final data = await utxoData; return Format.satoshisToAmount( - data.satoshiBalance - data.satoshiBalanceUnconfirmed); + data.satoshiBalance - data.satoshiBalanceUnconfirmed, + coin: coin); } @override Future<Decimal> get pendingBalance async { final data = await utxoData; - return Format.satoshisToAmount(data.satoshiBalanceUnconfirmed); + return Format.satoshisToAmount(data.satoshiBalanceUnconfirmed, coin: coin); } @override Future<Decimal> get balanceMinusMaxFee async => (await availableBalance) - - (Decimal.fromInt((await maxFee)) / Decimal.fromInt(Constants.satsPerCoin)) + (Decimal.fromInt((await maxFee)) / + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(); @override @@ -218,13 +220,13 @@ class NamecoinWallet extends CoinServiceAPI { .get<dynamic>(boxName: walletId, key: 'totalBalance') as int?; if (totalBalance == null) { final data = await utxoData; - return Format.satoshisToAmount(data.satoshiBalance); + return Format.satoshisToAmount(data.satoshiBalance, coin: coin); } else { - return Format.satoshisToAmount(totalBalance); + return Format.satoshisToAmount(totalBalance, coin: coin); } } final data = await utxoData; - return Format.satoshisToAmount(data.satoshiBalance); + return Format.satoshisToAmount(data.satoshiBalance, coin: coin); } @override @@ -262,7 +264,8 @@ class NamecoinWallet extends CoinServiceAPI { @override Future<int> get maxFee async { final fee = (await fees).fast as String; - final satsFee = Decimal.parse(fee) * Decimal.fromInt(Constants.satsPerCoin); + final satsFee = + Decimal.parse(fee) * Decimal.fromInt(Constants.satsPerCoin(coin)); return satsFee.floor().toBigInt().toInt(); } @@ -1086,7 +1089,8 @@ class NamecoinWallet extends CoinServiceAPI { // check for send all bool isSendAll = false; - final balance = Format.decimalAmountToSatoshis(await availableBalance); + final balance = + Format.decimalAmountToSatoshis(await availableBalance, coin); if (satoshiAmount == balance) { isSendAll = true; } @@ -1290,7 +1294,7 @@ class NamecoinWallet extends CoinServiceAPI { final String worthNow = Format.localizedStringAsFixed( value: ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2), decimalPlaces: 2, locale: locale!); @@ -1490,9 +1494,9 @@ class NamecoinWallet extends CoinServiceAPI { numberOfBlocksFast: f, numberOfBlocksAverage: m, numberOfBlocksSlow: s, - fast: Format.decimalAmountToSatoshis(fast), - medium: Format.decimalAmountToSatoshis(medium), - slow: Format.decimalAmountToSatoshis(slow), + fast: Format.decimalAmountToSatoshis(fast, coin), + medium: Format.decimalAmountToSatoshis(medium, coin), + slow: Format.decimalAmountToSatoshis(slow, coin), ); Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); @@ -1965,7 +1969,7 @@ class NamecoinWallet extends CoinServiceAPI { utxo["status"]["block_time"] = txn["blocktime"]; final fiatValue = ((Decimal.fromInt(value) * currentPrice) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2); utxo["rawWorth"] = fiatValue; utxo["fiatWorth"] = fiatValue.toString(); @@ -1975,15 +1979,16 @@ class NamecoinWallet extends CoinServiceAPI { Decimal currencyBalanceRaw = ((Decimal.fromInt(satoshiBalance) * currentPrice) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2); final Map<String, dynamic> result = { "total_user_currency": currencyBalanceRaw.toString(), "total_sats": satoshiBalance, "total_btc": (Decimal.fromInt(satoshiBalance) / - Decimal.fromInt(Constants.satsPerCoin)) - .toDecimal(scaleOnInfinitePrecision: Constants.decimalPlaces) + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal( + scaleOnInfinitePrecision: Constants.decimalPlacesForCoin(coin)) .toString(), "outputArray": outputArray, "unconfirmed": satoshiBalancePending, @@ -2540,7 +2545,7 @@ class NamecoinWallet extends CoinServiceAPI { if (prevOut == out["n"]) { inputAmtSentFromWallet += (Decimal.parse(out["value"]!.toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); } @@ -2554,7 +2559,7 @@ class NamecoinWallet extends CoinServiceAPI { final address = output["scriptPubKey"]["address"]; final value = output["value"]; final _value = (Decimal.parse(value.toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); totalOutput += _value; @@ -2582,7 +2587,7 @@ class NamecoinWallet extends CoinServiceAPI { } if (address != null) { final value = (Decimal.parse(output["value"].toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); totalOut += value; @@ -2605,7 +2610,7 @@ class NamecoinWallet extends CoinServiceAPI { for (final out in tx["vout"] as List) { if (prevOut == out["n"]) { totalIn += (Decimal.parse(out["value"].toString()) * - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); } @@ -2627,7 +2632,7 @@ class NamecoinWallet extends CoinServiceAPI { midSortedTx["amount"] = inputAmtSentFromWallet; final String worthNow = ((currentPrice * Decimal.fromInt(inputAmtSentFromWallet)) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2) .toStringAsFixed(2); midSortedTx["worthNow"] = worthNow; @@ -2637,7 +2642,7 @@ class NamecoinWallet extends CoinServiceAPI { midSortedTx["amount"] = outputAmtAddressedToWallet; final worthNow = ((currentPrice * Decimal.fromInt(outputAmtAddressedToWallet)) / - Decimal.fromInt(Constants.satsPerCoin)) + Decimal.fromInt(Constants.satsPerCoin(coin))) .toDecimal(scaleOnInfinitePrecision: 2) .toStringAsFixed(2); midSortedTx["worthNow"] = worthNow; @@ -3772,7 +3777,8 @@ class NamecoinWallet extends CoinServiceAPI { @override Future<int> estimateFeeFor(int satoshiAmount, int feeRate) async { - final available = Format.decimalAmountToSatoshis(await availableBalance); + final available = + Format.decimalAmountToSatoshis(await availableBalance, coin); if (available == satoshiAmount) { return satoshiAmount - sweepAllEstimate(feeRate); diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart index e6a531b78..686dcd08c 100644 --- a/lib/services/coins/wownero/wownero_wallet.dart +++ b/lib/services/coins/wownero/wownero_wallet.dart @@ -45,6 +45,7 @@ import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/stack_file_system.dart'; @@ -534,8 +535,7 @@ class WowneroWallet extends CoinServiceAPI { @override Future<Decimal> get balanceMinusMaxFee async => (await availableBalance) - - (Decimal.fromInt((await maxFee)) / Decimal.fromInt(Constants.satsPerCoin)) - .toDecimal(); + Format.satoshisToAmount(await maxFee, coin: Coin.wownero); @override Future<String> get currentReceivingAddress => @@ -563,13 +563,15 @@ class WowneroWallet extends CoinServiceAPI { Future<String>? _currentReceivingAddress; Future<FeeObject> _getFees() async { + // TODO: not use hard coded values here return FeeObject( - numberOfBlocksFast: 10, - numberOfBlocksAverage: 10, - numberOfBlocksSlow: 10, - fast: 4, - medium: 2, - slow: 0); + numberOfBlocksFast: 10, + numberOfBlocksAverage: 10, + numberOfBlocksSlow: 10, + fast: MoneroTransactionPriority.fast.raw!, + medium: MoneroTransactionPriority.regular.raw!, + slow: MoneroTransactionPriority.slow.raw!, + ); } @override @@ -873,8 +875,9 @@ class WowneroWallet extends CoinServiceAPI { Future<int> get maxFee async { var bal = await availableBalance; var fee = walletBase!.calculateEstimatedFee( - wownero.getDefaultTransactionPriority(), bal.toBigInt().toInt()) ~/ - 10000; + wownero.getDefaultTransactionPriority(), + Format.decimalAmountToSatoshis(bal, coin), + ); return fee; } @@ -1446,13 +1449,13 @@ class WowneroWallet extends CoinServiceAPI { try { final feeRate = args?["feeRate"]; if (feeRate is FeeRateType) { - MoneroTransactionPriority feePriority = MoneroTransactionPriority.slow; + MoneroTransactionPriority feePriority; switch (feeRate) { case FeeRateType.fast: - feePriority = MoneroTransactionPriority.fastest; + feePriority = MoneroTransactionPriority.fast; break; case FeeRateType.average: - feePriority = MoneroTransactionPriority.medium; + feePriority = MoneroTransactionPriority.regular; break; case FeeRateType.slow: feePriority = MoneroTransactionPriority.slow; @@ -1465,15 +1468,14 @@ class WowneroWallet extends CoinServiceAPI { bool isSendAll = false; final balance = await availableBalance; final satInDecimal = ((Decimal.fromInt(satoshiAmount) / - Decimal.fromInt(Constants.satsPerCoinWownero)) - .toDecimal() * - Decimal.fromInt(1000)); + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal()); if (satInDecimal == balance) { isSendAll = true; } Logging.instance .log("$toAddress $amount $args", level: LogLevel.Info); - String amountToSend = wowneroAmountToString(amount: amount * 1000); + String amountToSend = wowneroAmountToString(amount: amount); Logging.instance.log("$amount $amountToSend", level: LogLevel.Info); wownero_output.Output output = wownero_output.Output(walletBase!); @@ -1495,10 +1497,8 @@ class WowneroWallet extends CoinServiceAPI { PendingWowneroTransaction pendingWowneroTransaction = await (awaitPendingTransaction!) as PendingWowneroTransaction; - int realfee = (Decimal.parse(pendingWowneroTransaction.feeFormatted) * - 100000000.toDecimal()) - .toBigInt() - .toInt(); + int realfee = Format.decimalAmountToSatoshis( + Decimal.parse(pendingWowneroTransaction.feeFormatted), coin); debugPrint("fee? $realfee"); Map<String, dynamic> txData = { "pendingWowneroTransaction": pendingWowneroTransaction, @@ -1531,12 +1531,12 @@ class WowneroWallet extends CoinServiceAPI { @override Future<int> estimateFeeFor(int satoshiAmount, int feeRate) async { - MoneroTransactionPriority? priority; + MoneroTransactionPriority priority; FeeRateType feeRateType = FeeRateType.slow; switch (feeRate) { case 1: priority = MoneroTransactionPriority.regular; - feeRateType = FeeRateType.slow; + feeRateType = FeeRateType.average; break; case 2: priority = MoneroTransactionPriority.medium; @@ -1544,7 +1544,7 @@ class WowneroWallet extends CoinServiceAPI { break; case 3: priority = MoneroTransactionPriority.fast; - feeRateType = FeeRateType.average; + feeRateType = FeeRateType.fast; break; case 4: priority = MoneroTransactionPriority.fastest; @@ -1568,7 +1568,7 @@ class WowneroWallet extends CoinServiceAPI { args: {"feeRate": feeRateType}))['fee']; await Future.delayed(const Duration(milliseconds: 500)); } catch (e, s) { - aprox = -9999999999999999; + aprox = walletBase!.calculateEstimatedFee(priority, satoshiAmount); } } }); diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index 0a062de67..3263d526e 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -23,12 +23,12 @@ abstract class Constants { static bool enableExchange = Util.isDesktop || !Platform.isIOS; //TODO: correct for monero? - static const int satsPerCoinMonero = 1000000000000; - static const int satsPerCoinWownero = 100000000000; - static const int satsPerCoin = 100000000; - static const int decimalPlaces = 8; - static const int decimalPlacesWownero = 11; - static const int decimalPlacesMonero = 12; + static const int _satsPerCoinMonero = 1000000000000; + static const int _satsPerCoinWownero = 100000000000; + static const int _satsPerCoin = 100000000; + static const int _decimalPlaces = 8; + static const int _decimalPlacesWownero = 11; + static const int _decimalPlacesMonero = 12; static const int notificationsMax = 0xFFFFFFFF; static const Duration networkAliveTimerDuration = Duration(seconds: 10); @@ -40,6 +40,30 @@ abstract class Constants { static const int currentHiveDbVersion = 3; + static int satsPerCoin(Coin coin) { + switch (coin) { + case Coin.bitcoin: + case Coin.litecoin: + case Coin.litecoinTestNet: + case Coin.bitcoincash: + case Coin.bitcoincashTestnet: + case Coin.dogecoin: + case Coin.firo: + case Coin.bitcoinTestNet: + case Coin.dogecoinTestNet: + case Coin.firoTestNet: + case Coin.epicCash: + case Coin.namecoin: + return _satsPerCoin; + + case Coin.wownero: + return _satsPerCoinWownero; + + case Coin.monero: + return _satsPerCoinMonero; + } + } + static int decimalPlacesForCoin(Coin coin) { switch (coin) { case Coin.bitcoin: @@ -54,13 +78,13 @@ abstract class Constants { case Coin.firoTestNet: case Coin.epicCash: case Coin.namecoin: - return decimalPlaces; + return _decimalPlaces; case Coin.wownero: - return decimalPlacesWownero; + return _decimalPlacesWownero; case Coin.monero: - return decimalPlacesMonero; + return _decimalPlacesMonero; } } diff --git a/lib/utilities/format.dart b/lib/utilities/format.dart index 775780833..136ec5b95 100644 --- a/lib/utilities/format.dart +++ b/lib/utilities/format.dart @@ -8,46 +8,28 @@ import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; abstract class Format { - static Decimal satoshisToAmount(int sats, {Coin? coin}) { - late final int satsPerCoin; - - switch (coin) { - case Coin.wownero: - satsPerCoin = Constants.satsPerCoinWownero; - break; - case Coin.monero: - satsPerCoin = Constants.satsPerCoinMonero; - break; - case Coin.bitcoin: - case Coin.bitcoincash: - case Coin.dogecoin: - case Coin.epicCash: - case Coin.firo: - case Coin.litecoin: - case Coin.namecoin: - case Coin.bitcoinTestNet: - case Coin.litecoinTestNet: - case Coin.bitcoincashTestnet: - case Coin.dogecoinTestNet: - case Coin.firoTestNet: - default: - satsPerCoin = Constants.satsPerCoin; - } - - return (Decimal.fromInt(sats) / Decimal.fromInt(satsPerCoin)) - .toDecimal(scaleOnInfinitePrecision: Constants.decimalPlaces); + static Decimal satoshisToAmount(int sats, {required Coin coin}) { + return (Decimal.fromInt(sats) / + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal( + scaleOnInfinitePrecision: Constants.decimalPlacesForCoin(coin)); } /// - static String satoshiAmountToPrettyString(int sats, String locale) { - final amount = satoshisToAmount(sats); + static String satoshiAmountToPrettyString( + int sats, String locale, Coin coin) { + final amount = satoshisToAmount(sats, coin: coin); return localizedStringAsFixed( - value: amount, locale: locale, decimalPlaces: Constants.decimalPlaces); + value: amount, + locale: locale, + decimalPlaces: Constants.decimalPlacesForCoin(coin), + ); } - static int decimalAmountToSatoshis(Decimal amount) { - final value = - (Decimal.fromInt(Constants.satsPerCoin) * amount).floor().toBigInt(); + static int decimalAmountToSatoshis(Decimal amount, Coin coin) { + final value = (Decimal.fromInt(Constants.satsPerCoin(coin)) * amount) + .floor() + .toBigInt(); return value.toInt(); } diff --git a/lib/widgets/transaction_card.dart b/lib/widgets/transaction_card.dart index 15dcf2b4d..4389573c3 100644 --- a/lib/widgets/transaction_card.dart +++ b/lib/widgets/transaction_card.dart @@ -9,7 +9,6 @@ import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_deta import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -198,13 +197,9 @@ class _TransactionCardState extends ConsumerState<TransactionCard> { fit: BoxFit.scaleDown, child: Builder( builder: (_) { - final amount = coin == Coin.monero - ? (_transaction.amount ~/ 10000) - : coin == Coin.wownero - ? (_transaction.amount ~/ 1000) - : _transaction.amount; + final amount = _transaction.amount; return Text( - "$prefix${Format.satoshiAmountToPrettyString(amount, locale)} ${coin.ticker}", + "$prefix${Format.satoshiAmountToPrettyString(amount, locale, coin)} ${coin.ticker}", style: STextStyles.itemSubtitle12_600(context), ); @@ -242,17 +237,12 @@ class _TransactionCardState extends ConsumerState<TransactionCard> { fit: BoxFit.scaleDown, child: Builder( builder: (_) { - // TODO: modify Format.<functions> to take optional Coin parameter so this type oif check isn't done in ui int value = _transaction.amount; - if (coin == Coin.monero) { - value = (value ~/ 10000); - } else if (coin == Coin.wownero) { - value = (value ~/ 1000); - } return Text( "$prefix${Format.localizedStringAsFixed( - value: Format.satoshisToAmount(value) * + value: Format.satoshisToAmount(value, + coin: coin) * price, locale: locale, decimalPlaces: 2, From ffe9a83abf170b7082a24f030b5219daefe9d203 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 23 Nov 2022 12:38:36 -0600 Subject: [PATCH 396/426] Format tests updated --- test/formet_test.dart | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/test/formet_test.dart b/test/formet_test.dart index 4f7136cd4..e27293114 100644 --- a/test/formet_test.dart +++ b/test/formet_test.dart @@ -1,54 +1,64 @@ import 'package:decimal/decimal.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/format.dart'; void main() { group("satoshisToAmount", () { test("12345", () { - expect(Format.satoshisToAmount(12345), Decimal.parse("0.00012345")); + expect(Format.satoshisToAmount(12345, coin: Coin.bitcoin), + Decimal.parse("0.00012345")); }); test("100012345", () { - expect(Format.satoshisToAmount(100012345), Decimal.parse("1.00012345")); + expect(Format.satoshisToAmount(100012345, coin: Coin.bitcoin), + Decimal.parse("1.00012345")); }); test("0", () { - expect(Format.satoshisToAmount(0), Decimal.zero); + expect(Format.satoshisToAmount(0, coin: Coin.bitcoin), Decimal.zero); }); test("1000000000", () { - expect(Format.satoshisToAmount(1000000000), Decimal.parse("10")); + expect(Format.satoshisToAmount(1000000000, coin: Coin.bitcoin), + Decimal.parse("10")); }); }); group("satoshiAmountToPrettyString", () { const locale = "en_US"; test("12345", () { - expect(Format.satoshiAmountToPrettyString(12345, locale), "0.00012345"); + expect(Format.satoshiAmountToPrettyString(12345, locale, Coin.bitcoin), + "0.00012345"); }); test("100012345", () { expect( - Format.satoshiAmountToPrettyString(100012345, locale), "1.00012345"); + Format.satoshiAmountToPrettyString(100012345, locale, Coin.bitcoin), + "1.00012345"); }); test("123450000", () { expect( - Format.satoshiAmountToPrettyString(123450000, locale), "1.23450000"); + Format.satoshiAmountToPrettyString(123450000, locale, Coin.bitcoin), + "1.23450000"); }); test("1230045000", () { - expect(Format.satoshiAmountToPrettyString(1230045000, locale), + expect( + Format.satoshiAmountToPrettyString(1230045000, locale, Coin.bitcoin), "12.30045000"); }); test("1000000000", () { - expect(Format.satoshiAmountToPrettyString(1000000000, locale), + expect( + Format.satoshiAmountToPrettyString(1000000000, locale, Coin.bitcoin), "10.00000000"); }); test("0", () { - expect(Format.satoshiAmountToPrettyString(0, locale), "0.00000000"); + expect(Format.satoshiAmountToPrettyString(0, locale, Coin.bitcoin), + "0.00000000"); }); }); From 85b9fdc2f3c07f353166deb2a087bbf52ca50ef4 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 23 Nov 2022 12:42:08 -0600 Subject: [PATCH 397/426] random hardcoded values :/ --- lib/services/coins/monero/monero_wallet.dart | 6 +++--- lib/services/coins/wownero/wownero_wallet.dart | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/services/coins/monero/monero_wallet.dart b/lib/services/coins/monero/monero_wallet.dart index 58bd36c72..6f1e49ee5 100644 --- a/lib/services/coins/monero/monero_wallet.dart +++ b/lib/services/coins/monero/monero_wallet.dart @@ -564,11 +564,11 @@ class MoneroWallet extends CoinServiceAPI { Future<String>? _currentReceivingAddress; Future<FeeObject> _getFees() async { - // TODO: not use hard coded values here + // TODO: not use random hard coded values here return FeeObject( numberOfBlocksFast: 10, - numberOfBlocksAverage: 10, - numberOfBlocksSlow: 10, + numberOfBlocksAverage: 15, + numberOfBlocksSlow: 20, fast: MoneroTransactionPriority.fast.raw!, medium: MoneroTransactionPriority.regular.raw!, slow: MoneroTransactionPriority.slow.raw!, diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart index 686dcd08c..72580ea4a 100644 --- a/lib/services/coins/wownero/wownero_wallet.dart +++ b/lib/services/coins/wownero/wownero_wallet.dart @@ -563,11 +563,11 @@ class WowneroWallet extends CoinServiceAPI { Future<String>? _currentReceivingAddress; Future<FeeObject> _getFees() async { - // TODO: not use hard coded values here + // TODO: not use random hard coded values here return FeeObject( numberOfBlocksFast: 10, - numberOfBlocksAverage: 10, - numberOfBlocksSlow: 10, + numberOfBlocksAverage: 15, + numberOfBlocksSlow: 20, fast: MoneroTransactionPriority.fast.raw!, medium: MoneroTransactionPriority.regular.raw!, slow: MoneroTransactionPriority.slow.raw!, From d14593407df61ebf656028bc88985fa0e27172ad Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Wed, 23 Nov 2022 17:48:58 -0700 Subject: [PATCH 398/426] v1.5.19 build 91 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index f79879f61..7ca368d29 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.5.18+90 +version: 1.5.19+91 environment: sdk: ">=2.17.0 <3.0.0" From 08c6fb72ac8fe2cd4eed91c403d83be65459e874 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Wed, 23 Nov 2022 14:55:04 -0600 Subject: [PATCH 399/426] mobile confirm send button height fix --- lib/pages/send_view/confirm_transaction_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index 1ddeb3c9f..fd5341dd9 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -780,7 +780,7 @@ class _ConfirmTransactionViewState : const EdgeInsets.all(0), child: PrimaryButton( label: "Send", - buttonHeight: ButtonHeight.l, + buttonHeight: isDesktop ? ButtonHeight.l : null, onPressed: () async { final dynamic unlocked; From 3bda6620efdb24210667d207cd151c7b601e6142 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 24 Nov 2022 18:07:17 -0600 Subject: [PATCH 400/426] reduce minimum height and set starting height lower on linux --- lib/main.dart | 2 +- linux/my_application.cc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 728152951..11a851b79 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -77,7 +77,7 @@ void main() async { if (Util.isDesktop) { setWindowTitle('Stack Wallet'); - setWindowMinSize(const Size(1220, 900)); + setWindowMinSize(const Size(1220, 100)); setWindowMaxSize(Size.infinite); } diff --git a/linux/my_application.cc b/linux/my_application.cc index 9cb3acebd..d342c1506 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -47,7 +47,7 @@ static void my_application_activate(GApplication* application) { gtk_window_set_title(window, "Stack Wallet"); } - gtk_window_set_default_size(window, 1220, 900); + gtk_window_set_default_size(window, 1220, 500); gtk_widget_show(GTK_WIDGET(window)); g_autoptr(FlDartProject) project = fl_dart_project_new(); From 286f6a552b07294ad9f4d6795f74e7d834162376 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 24 Nov 2022 18:16:26 -0600 Subject: [PATCH 401/426] desktop send to coin ticker fixed --- .../exchange_view/confirm_change_now_send.dart | 11 ++++++++--- lib/pages/send_view/confirm_transaction_view.dart | 13 ++++++++++--- .../wallet_view/sub_widgets/desktop_auth_send.dart | 14 +++++++++----- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/lib/pages/exchange_view/confirm_change_now_send.dart b/lib/pages/exchange_view/confirm_change_now_send.dart index 0e1eef755..810ea2541 100644 --- a/lib/pages/exchange_view/confirm_change_now_send.dart +++ b/lib/pages/exchange_view/confirm_change_now_send.dart @@ -157,6 +157,9 @@ class _ConfirmChangeNowSendViewState Future<void> _confirmSend() async { final dynamic unlocked; + final coin = + ref.read(walletsChangeNotifierProvider).getManager(walletId).coin; + if (Util.isDesktop) { unlocked = await showDialog<bool?>( context: context, @@ -172,13 +175,15 @@ class _ConfirmChangeNowSendViewState DesktopDialogCloseButton(), ], ), - const Padding( - padding: EdgeInsets.only( + Padding( + padding: const EdgeInsets.only( left: 32, right: 32, bottom: 32, ), - child: DesktopAuthSend(), + child: DesktopAuthSend( + coin: coin, + ), ), ], ), diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index fd5341dd9..16de6cf55 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -784,6 +784,11 @@ class _ConfirmTransactionViewState onPressed: () async { final dynamic unlocked; + final coin = ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .coin; + if (isDesktop) { unlocked = await showDialog<bool?>( context: context, @@ -799,13 +804,15 @@ class _ConfirmTransactionViewState DesktopDialogCloseButton(), ], ), - const Padding( - padding: EdgeInsets.only( + Padding( + padding: const EdgeInsets.only( left: 32, right: 32, bottom: 32, ), - child: DesktopAuthSend(), + child: DesktopAuthSend( + coin: coin, + ), ), ], ), diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart index a8d1ea497..20bb3f95a 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart @@ -6,17 +6,21 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/providers/desktop/storage_crypto_handler_provider.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/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; -import '../../../../../notifications/show_flush_bar.dart'; -import '../../../../../widgets/loading_indicator.dart'; - class DesktopAuthSend extends ConsumerStatefulWidget { - const DesktopAuthSend({Key? key}) : super(key: key); + const DesktopAuthSend({ + Key? key, + required this.coin, + }) : super(key: key); + + final Coin coin; @override ConsumerState<DesktopAuthSend> createState() => _DesktopAuthSendState(); @@ -72,7 +76,7 @@ class _DesktopAuthSendState extends ConsumerState<DesktopAuthSend> { height: 16, ), Text( - "Enter your wallet password to send BTC", + "Enter your wallet password to send ${widget.coin.ticker.toUpperCase()}", style: STextStyles.desktopTextMedium(context).copyWith( color: Theme.of(context).extension<StackColors>()!.textDark3, ), From d71899d1dff48ded1ec45d02abb3c88a0b1c37c8 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Thu, 24 Nov 2022 18:22:35 -0600 Subject: [PATCH 402/426] mobile exchange form top padding added --- lib/pages/exchange_view/exchange_view.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/pages/exchange_view/exchange_view.dart b/lib/pages/exchange_view/exchange_view.dart index 84e054eac..ed99047b2 100644 --- a/lib/pages/exchange_view/exchange_view.dart +++ b/lib/pages/exchange_view/exchange_view.dart @@ -43,7 +43,11 @@ class _ExchangeViewState extends ConsumerState<ExchangeView> { handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), sliver: const SliverToBoxAdapter( child: Padding( - padding: EdgeInsets.symmetric(horizontal: 16), + padding: EdgeInsets.only( + left: 16, + right: 16, + top: 16, + ), child: ExchangeForm(), ), ), From 7db3abab473af403c156d26be580713e3d1e4a4f Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 25 Nov 2022 09:01:09 -0600 Subject: [PATCH 403/426] desktop starting to height be 3/4 screen height or 900, whichever is smaller --- lib/main.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/main.dart b/lib/main.dart index 11a851b79..d5980409f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math'; import 'package:cw_core/node.dart'; import 'package:cw_core/unspent_coins_info.dart'; @@ -79,6 +80,15 @@ void main() async { setWindowTitle('Stack Wallet'); setWindowMinSize(const Size(1220, 100)); setWindowMaxSize(Size.infinite); + final screen = await getCurrentScreen(); + final screenHeight = screen?.frame.height; + if (screenHeight != null) { + // starting to height be 3/4 screen height or 900, whichever is smaller + final height = min<double>(screenHeight * 0.75, 900); + setWindowFrame( + Rect.fromLTWH(0, 0, 1220, height), + ); + } } // FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); From 42aad5dcd53c9367ffd9f6672299b81b29af8f8a Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 25 Nov 2022 13:24:01 -0600 Subject: [PATCH 404/426] themed background gradient option and background image, as well as various button height fixes for mobile --- assets/svg/oceanBreeze/bg.svg | 11 + .../add_wallet_view/add_wallet_view.dart | 67 +- .../create_or_restore_wallet_view.dart | 83 +- .../name_your_wallet_view.dart | 65 +- .../new_wallet_recovery_phrase_view.dart | 1 - .../address_book_views/address_book_view.dart | 184 +- .../subviews/add_address_book_entry_view.dart | 115 +- .../add_new_contact_address_view.dart | 80 +- .../subviews/address_book_filter_view.dart | 97 +- .../subviews/contact_details_view.dart | 764 ++--- .../subviews/edit_contact_address_view.dart | 80 +- .../edit_contact_name_emoji_view.dart | 80 +- .../exchange_view/choose_from_stack_view.dart | 154 +- .../confirm_change_now_send.dart | 77 +- .../exchange_view/edit_trade_note_view.dart | 190 +- .../fixed_rate_pair_coin_selection_view.dart | 50 +- ...floating_rate_currency_selection_view.dart | 50 +- .../exchange_step_views/step_1_view.dart | 304 +- .../exchange_step_views/step_2_view.dart | 1077 +++---- .../exchange_step_views/step_3_view.dart | 522 ++-- .../exchange_step_views/step_4_view.dart | 1034 +++---- lib/pages/exchange_view/send_from_view.dart | 33 +- .../exchange_view/trade_details_view.dart | 40 +- .../wallet_initiated_exchange_view.dart | 131 +- lib/pages/home_view/home_view.dart | 232 +- lib/pages/intro_view.dart | 193 +- lib/pages/loading_view.dart | 37 +- .../notifications_view.dart | 113 +- lib/pages/pinpad_views/create_pin_view.dart | 403 +-- lib/pages/pinpad_views/lock_screen_view.dart | 299 +- .../generate_receiving_uri_qr_code_view.dart | 87 +- lib/pages/receive_view/receive_view.dart | 266 +- .../send_view/confirm_transaction_view.dart | 76 +- lib/pages/send_view/send_view.dart | 2567 +++++++++-------- .../global_settings_view/about_view.dart | 802 ++--- .../advanced_settings_view.dart | 289 +- .../advanced_views/debug_view.dart | 869 +++--- .../appearance_settings_view.dart | 223 +- .../global_settings_view/currency_view.dart | 54 +- .../global_settings_view.dart | 478 +-- .../global_settings_view/hidden_settings.dart | 270 +- .../global_settings_view/language_view.dart | 368 +-- .../add_edit_node_view.dart | 160 +- .../manage_nodes_views/coin_nodes_view.dart | 117 +- .../manage_nodes_views/manage_nodes_view.dart | 156 +- .../manage_nodes_views/node_details_view.dart | 147 +- .../change_pin_view/change_pin_view.dart | 335 +-- .../security_views/security_view.dart | 226 +- .../stack_backup_views/auto_backup_view.dart | 450 +-- .../create_auto_backup_view.dart | 1051 +++---- .../create_backup_information_view.dart | 129 +- .../create_backup_view.dart | 70 +- .../edit_auto_backup_view.dart | 54 +- .../restore_from_encrypted_string_view.dart | 365 +-- .../restore_from_file_view.dart | 71 +- .../stack_backup_views/stack_backup_view.dart | 259 +- .../startup_preferences_view.dart | 463 +-- .../startup_wallet_selection_view.dart | 308 +- .../global_settings_view/support_view.dart | 33 +- .../syncing_options_view.dart | 57 +- .../syncing_preferences_view.dart | 240 +- .../wallet_syncing_options_view.dart | 45 +- .../wallet_backup_view.dart | 333 +-- .../wallet_network_settings_view.dart | 209 +- .../wallet_settings_view.dart | 370 +-- .../delete_wallet_recovery_phrase_view.dart | 308 +- .../delete_wallet_warning_view.dart | 162 +- .../rename_wallet_view.dart | 186 +- .../wallet_settings_wallet_settings_view.dart | 275 +- .../transaction_views/edit_note_view.dart | 271 +- .../transaction_details_view.dart | 1962 ++++++------- .../transaction_search_filter_view.dart | 73 +- lib/pages/wallet_view/wallet_view.dart | 737 ++--- .../desktop_login_view.dart | 1 - .../home/desktop_home_view.dart | 33 +- .../home/my_stack_view/my_stack_view.dart | 59 +- lib/utilities/assets.dart | 11 + lib/utilities/theme/color_theme.dart | 7 +- lib/utilities/theme/dark_colors.dart | 5 + lib/utilities/theme/light_colors.dart | 5 + lib/utilities/theme/ocean_breeze_colors.dart | 14 +- lib/utilities/theme/stack_colors.dart | 17 + lib/widgets/background.dart | 67 + lib/widgets/desktop/desktop_scaffold.dart | 36 +- pubspec.yaml | 1 + 85 files changed, 11664 insertions(+), 11129 deletions(-) create mode 100644 assets/svg/oceanBreeze/bg.svg create mode 100644 lib/widgets/background.dart diff --git a/assets/svg/oceanBreeze/bg.svg b/assets/svg/oceanBreeze/bg.svg new file mode 100644 index 000000000..35fbda281 --- /dev/null +++ b/assets/svg/oceanBreeze/bg.svg @@ -0,0 +1,11 @@ +<svg width="360" height="480" viewBox="0 0 360 480" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M48 174.503C45.4098 171.437 39.6642 169.267 34.578 176.625C29.4917 183.983 22.3192 180.197 19.7431 176.625C16.682 172.38 8.86422 166.438 2.08257 176.625C-0.507645 179.926 -7.2422 184.549 -13.4587 176.625C-19.6752 168.702 -26.4098 171.909 -29 174.503" stroke="#D3EAF3" stroke-width="2" stroke-linecap="round"/> +<path d="M48 188.503C45.4098 185.437 39.6642 183.267 34.578 190.625C29.4917 197.983 22.3192 194.197 19.7431 190.625C16.682 186.38 8.86422 180.438 2.08257 190.625C-0.507645 193.926 -7.2422 198.549 -13.4587 190.625C-19.6752 182.702 -26.4098 185.909 -29 188.503" stroke="#D3EAF3" stroke-width="2" stroke-linecap="round"/> +<path d="M48 202.503C45.4098 199.437 39.6642 197.267 34.578 204.625C29.4917 211.983 22.3192 208.197 19.7431 204.625C16.682 200.38 8.86422 194.438 2.08257 204.625C-0.507645 207.926 -7.2422 212.549 -13.4587 204.625C-19.6752 196.702 -26.4098 199.909 -29 202.503" stroke="#D3EAF3" stroke-width="2" stroke-linecap="round"/> +<path d="M389 444.503C386.41 441.437 380.664 439.267 375.578 446.625C370.492 453.983 363.319 450.197 360.743 446.625C357.682 442.38 349.864 436.438 343.083 446.625C340.492 449.926 333.758 454.549 327.541 446.625C321.325 438.702 314.59 441.909 312 444.503" stroke="#D3EAF3" stroke-width="2" stroke-linecap="round"/> +<path d="M389 458.503C386.41 455.437 380.664 453.267 375.578 460.625C370.492 467.983 363.319 464.197 360.743 460.625C357.682 456.38 349.864 450.438 343.083 460.625C340.492 463.926 333.758 468.549 327.541 460.625C321.325 452.702 314.59 455.909 312 458.503" stroke="#D3EAF3" stroke-width="2" stroke-linecap="round"/> +<path d="M389 472.503C386.41 469.437 380.664 467.267 375.578 474.625C370.492 481.983 363.319 478.197 360.743 474.625C357.682 470.38 349.864 464.438 343.083 474.625C340.492 477.926 333.758 482.549 327.541 474.625C321.325 466.702 314.59 469.909 312 472.503" stroke="#D3EAF3" stroke-width="2" stroke-linecap="round"/> +<path d="M389 4.5028C386.41 1.4371 380.664 -0.732664 375.578 6.62502C370.492 13.9827 363.319 10.1971 360.743 6.62502C357.682 2.38025 349.864 -3.56244 343.083 6.62502C340.492 9.92648 333.758 14.5485 327.541 6.62502C321.325 -1.29849 314.59 1.90875 312 4.5028" stroke="#D3EAF3" stroke-width="2" stroke-linecap="round"/> +<path d="M389 18.5028C386.41 15.4371 380.664 13.2673 375.578 20.625C370.492 27.9827 363.319 24.1971 360.743 20.625C357.682 16.3802 349.864 10.4376 343.083 20.625C340.492 23.9265 333.758 28.5485 327.541 20.625C321.325 12.7015 314.59 15.9087 312 18.5028" stroke="#D3EAF3" stroke-width="2" stroke-linecap="round"/> +<path d="M389 32.5028C386.41 29.4371 380.664 27.2673 375.578 34.625C370.492 41.9827 363.319 38.1971 360.743 34.625C357.682 30.3802 349.864 24.4376 343.083 34.625C340.492 37.9265 333.758 42.5485 327.541 34.625C321.325 26.7015 314.59 29.9087 312 32.5028" stroke="#D3EAF3" stroke-width="2" stroke-linecap="round"/> +</svg> diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index e964610ae..29bae26c1 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -11,6 +11,7 @@ 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/utilities/util.dart'; +import 'package:stackwallet/widgets/background.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'; @@ -182,39 +183,43 @@ class _AddWalletViewState extends State<AddWalletView> { ), ); } else { - return Scaffold( - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), ), - ), - body: Container( - color: Theme.of(context).extension<StackColors>()!.background, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const AddWalletText( - isDesktop: false, - ), - const SizedBox( - height: 16, - ), - Expanded( - child: MobileCoinList( - coins: coins, + body: Container( + color: Theme.of(context).extension<StackColors>()!.background, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const AddWalletText( + isDesktop: false, ), - ), - const SizedBox( - height: 16, - ), - const AddWalletNextButton( - isDesktop: false, - ), - ], + const SizedBox( + height: 16, + ), + Expanded( + child: MobileCoinList( + coins: coins, + ), + ), + const SizedBox( + height: 16, + ), + const AddWalletNextButton( + isDesktop: false, + ), + ], + ), ), ), ), diff --git a/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart b/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart index b3be99e25..1dfdc2a53 100644 --- a/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart @@ -7,6 +7,7 @@ import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/exit_to_my import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.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'; @@ -77,49 +78,53 @@ class CreateOrRestoreWalletView extends StatelessWidget { ), ); } else { - return Scaffold( - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), ), - ), - body: Container( - color: Theme.of(context).extension<StackColors>()!.background, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: const EdgeInsets.all(31), - child: CoinImage( + body: Container( + color: Theme.of(context).extension<StackColors>()!.background, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.all(31), + child: CoinImage( + coin: coin, + isDesktop: isDesktop, + ), + ), + const Spacer( + flex: 2, + ), + CreateRestoreWalletTitle( coin: coin, isDesktop: isDesktop, ), - ), - const Spacer( - flex: 2, - ), - CreateRestoreWalletTitle( - coin: coin, - isDesktop: isDesktop, - ), - const SizedBox( - height: 8, - ), - CreateRestoreWalletSubTitle( - isDesktop: isDesktop, - ), - const Spacer( - flex: 5, - ), - CreateWalletButtonGroup( - coin: coin, - isDesktop: isDesktop, - ), - ], + const SizedBox( + height: 8, + ), + CreateRestoreWalletSubTitle( + isDesktop: isDesktop, + ), + const Spacer( + flex: 5, + ), + CreateWalletButtonGroup( + coin: coin, + isDesktop: isDesktop, + ), + ], + ), ), ), ), diff --git a/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart b/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart index 8bc01b124..e435e285d 100644 --- a/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart +++ b/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart @@ -17,6 +17,7 @@ import 'package:stackwallet/utilities/name_generator.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.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'; @@ -108,40 +109,44 @@ class _NameYourWalletViewState extends ConsumerState<NameYourWalletView> { ), ); } else { - return Scaffold( - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - if (textFieldFocusNode.hasFocus) { - textFieldFocusNode.unfocus(); - Future<void>.delayed(const Duration(milliseconds: 100)) - .then((value) => Navigator.of(context).pop()); - } else { - if (mounted) { - Navigator.of(context).pop(); + return Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + if (textFieldFocusNode.hasFocus) { + textFieldFocusNode.unfocus(); + Future<void>.delayed(const Duration(milliseconds: 100)) + .then((value) => Navigator.of(context).pop()); + } else { + if (mounted) { + Navigator.of(context).pop(); + } } - } - }, - ), - ), - body: Container( - color: Theme.of(context).extension<StackColors>()!.background, - child: Padding( - padding: const EdgeInsets.all(16), - child: LayoutBuilder( - builder: (ctx, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: - BoxConstraints(minHeight: constraints.maxHeight), - child: IntrinsicHeight( - child: _content(), - ), - ), - ); }, ), ), + body: Container( + color: Theme.of(context).extension<StackColors>()!.background, + child: Padding( + padding: const EdgeInsets.all(16), + child: LayoutBuilder( + builder: (ctx, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: + BoxConstraints(minHeight: constraints.maxHeight), + child: IntrinsicHeight( + child: _content(), + ), + ), + ); + }, + ), + ), + ), ), ); } 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 faab6d08c..b3ceb0968 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 @@ -16,7 +16,6 @@ import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; diff --git a/lib/pages/address_book_views/address_book_view.dart b/lib/pages/address_book_views/address_book_view.dart index c87906870..0fbf334a7 100644 --- a/lib/pages/address_book_views/address_book_view.dart +++ b/lib/pages/address_book_views/address_book_view.dart @@ -15,6 +15,7 @@ import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/address_book_card.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; @@ -108,110 +109,115 @@ class _AddressBookViewState extends ConsumerState<AddressBookView> { return ConditionalParent( condition: !isDesktop, builder: (child) { - return Scaffold( - backgroundColor: - Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Address book", - style: STextStyles.navBarTitle(context), - ), - actions: [ - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("addressBookFilterViewButton"), - size: 36, - shadows: const [], - color: - Theme.of(context).extension<StackColors>()!.background, - icon: SvgPicture.asset( - Assets.svg.filter, + return Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Address book", + style: STextStyles.navBarTitle(context), + ), + actions: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("addressBookFilterViewButton"), + size: 36, + shadows: const [], color: Theme.of(context) .extension<StackColors>()! - .accentColorDark, - width: 20, - height: 20, + .background, + icon: SvgPicture.asset( + Assets.svg.filter, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + width: 20, + height: 20, + ), + onPressed: () { + Navigator.of(context).pushNamed( + AddressBookFilterView.routeName, + ); + }, ), - onPressed: () { - Navigator.of(context).pushNamed( - AddressBookFilterView.routeName, - ); - }, ), ), - ), - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("addressBookAddNewContactViewButton"), - size: 36, - shadows: const [], - color: - Theme.of(context).extension<StackColors>()!.background, - icon: SvgPicture.asset( - Assets.svg.plus, + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("addressBookAddNewContactViewButton"), + size: 36, + shadows: const [], color: Theme.of(context) .extension<StackColors>()! - .accentColorDark, - width: 20, - height: 20, + .background, + icon: SvgPicture.asset( + Assets.svg.plus, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + width: 20, + height: 20, + ), + onPressed: () { + Navigator.of(context).pushNamed( + AddAddressBookEntryView.routeName, + ); + }, ), - onPressed: () { - Navigator.of(context).pushNamed( - AddAddressBookEntryView.routeName, - ); - }, ), ), - ), - ], - ), - body: LayoutBuilder( - builder: (builderContext, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: MediaQuery.of(context).size.height - 271, + ], + ), + body: LayoutBuilder( + builder: (builderContext, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: + MediaQuery.of(context).size.height - 271, + ), + child: child, ), - child: child, ), ), ), ), - ), - ); - }, + ); + }, + ), ), ); }, diff --git a/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart b/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart index 2759e9cb1..36191e0b7 100644 --- a/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart +++ b/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart @@ -16,6 +16,7 @@ import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; @@ -116,65 +117,67 @@ class _AddAddressBookEntryViewState return ConditionalParent( condition: !isDesktop, builder: (child) { - return Scaffold( - backgroundColor: - Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "New contact", - style: STextStyles.navBarTitle(context), - ), - actions: [ - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("addAddressBookEntryFavoriteButtonKey"), - size: 36, - shadows: const [], - color: Theme.of(context) - .extension<StackColors>()! - .background, - icon: SvgPicture.asset( - Assets.svg.star, - color: _isFavorite - ? Theme.of(context) - .extension<StackColors>()! - .favoriteStarActive - : Theme.of(context) - .extension<StackColors>()! - .favoriteStarInactive, - width: 20, - height: 20, + return Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "New contact", + style: STextStyles.navBarTitle(context), + ), + actions: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("addAddressBookEntryFavoriteButtonKey"), + size: 36, + shadows: const [], + color: Theme.of(context) + .extension<StackColors>()! + .background, + icon: SvgPicture.asset( + Assets.svg.star, + color: _isFavorite + ? Theme.of(context) + .extension<StackColors>()! + .favoriteStarActive + : Theme.of(context) + .extension<StackColors>()! + .favoriteStarInactive, + width: 20, + height: 20, + ), + onPressed: () { + setState(() { + _isFavorite = !_isFavorite; + }); + }, ), - onPressed: () { - setState(() { - _isFavorite = !_isFavorite; - }); - }, ), ), - ), - ], - ), - body: child); + ], + ), + body: child), + ); }, child: ConditionalParent( condition: isDesktop, diff --git a/lib/pages/address_book_views/subviews/add_new_contact_address_view.dart b/lib/pages/address_book_views/subviews/add_new_contact_address_view.dart index dc25c3dc1..de2dfe90c 100644 --- a/lib/pages/address_book_views/subviews/add_new_contact_address_view.dart +++ b/lib/pages/address_book_views/subviews/add_new_contact_address_view.dart @@ -13,6 +13,7 @@ import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; @@ -63,48 +64,51 @@ class _AddNewContactAddressViewState return ConditionalParent( condition: !isDesktop, - builder: (child) => Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Add new address", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Add new address", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (context, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: child, + body: LayoutBuilder( + builder: (context, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), ), ), ), - ), - ); - }, + ); + }, + ), ), ), child: Column( diff --git a/lib/pages/address_book_views/subviews/address_book_filter_view.dart b/lib/pages/address_book_views/subviews/address_book_filter_view.dart index 55c3d47ac..9f410aae7 100644 --- a/lib/pages/address_book_views/subviews/address_book_filter_view.dart +++ b/lib/pages/address_book_views/subviews/address_book_filter_view.dart @@ -6,6 +6,7 @@ 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/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; @@ -50,60 +51,62 @@ class _AddressBookFilterViewState extends ConsumerState<AddressBookFilterView> { return ConditionalParent( condition: !isDesktop, builder: (child) { - return Scaffold( - backgroundColor: - Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( + return Background( + child: Scaffold( backgroundColor: Theme.of(context).extension<StackColors>()!.background, - leading: AppBarBackButton( - onPressed: () async { - Navigator.of(context).pop(); - }, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Filter addresses", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Filter addresses", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(12), - child: LayoutBuilder(builder: (builderContext, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - child: Text( - "Only selected cryptocurrency addresses will be displayed.", - style: STextStyles.itemSubtitle(context), + body: Padding( + padding: const EdgeInsets.all(12), + child: LayoutBuilder(builder: (builderContext, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + child: Text( + "Only selected cryptocurrency addresses will be displayed.", + style: STextStyles.itemSubtitle(context), + ), ), - ), - const SizedBox( - height: 12, - ), - Text( - "Select cryptocurrency", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 12, - ), - child, - ], + const SizedBox( + height: 12, + ), + Text( + "Select cryptocurrency", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 12, + ), + child, + ], + ), ), ), ), - ), - ); - }), + ); + }), + ), ), ); }, diff --git a/lib/pages/address_book_views/subviews/contact_details_view.dart b/lib/pages/address_book_views/subviews/contact_details_view.dart index a48a535c6..0ab6ebba9 100644 --- a/lib/pages/address_book_views/subviews/contact_details_view.dart +++ b/lib/pages/address_book_views/subviews/contact_details_view.dart @@ -18,6 +18,7 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; @@ -104,335 +105,203 @@ class _ContactDetailsViewState extends ConsumerState<ContactDetailsView> { final _contact = ref.watch(addressBookServiceProvider .select((value) => value.getContactById(_contactId))); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Contact details", - style: STextStyles.navBarTitle(context), - ), - actions: [ - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("contactDetails"), - size: 36, - shadows: const [], - color: Theme.of(context).extension<StackColors>()!.background, - icon: SvgPicture.asset( - Assets.svg.star, - color: _contact.isFavorite - ? Theme.of(context) - .extension<StackColors>()! - .favoriteStarActive - : Theme.of(context) - .extension<StackColors>()! - .favoriteStarInactive, - width: 20, - height: 20, - ), - onPressed: () { - bool isFavorite = _contact.isFavorite; + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Contact details", + style: STextStyles.navBarTitle(context), + ), + actions: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("contactDetails"), + size: 36, + shadows: const [], + color: Theme.of(context).extension<StackColors>()!.background, + icon: SvgPicture.asset( + Assets.svg.star, + color: _contact.isFavorite + ? Theme.of(context) + .extension<StackColors>()! + .favoriteStarActive + : Theme.of(context) + .extension<StackColors>()! + .favoriteStarInactive, + width: 20, + height: 20, + ), + onPressed: () { + bool isFavorite = _contact.isFavorite; - ref - .read(addressBookServiceProvider) - .editContact(_contact.copyWith(isFavorite: !isFavorite)); - }, + ref.read(addressBookServiceProvider).editContact( + _contact.copyWith(isFavorite: !isFavorite)); + }, + ), ), ), - ), - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("contactDetailsViewDeleteContactButtonKey"), - size: 36, - shadows: const [], - color: Theme.of(context).extension<StackColors>()!.background, - icon: SvgPicture.asset( - Assets.svg.trash, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - width: 20, - height: 20, - ), - onPressed: () { - showDialog<dynamic>( - context: context, - useSafeArea: true, - barrierDismissible: true, - builder: (_) => StackDialog( - title: "Delete ${_contact.name}?", - message: "Contact will be deleted permanently!", - leftButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Cancel", - style: STextStyles.itemSubtitle12(context), + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("contactDetailsViewDeleteContactButtonKey"), + size: 36, + shadows: const [], + color: Theme.of(context).extension<StackColors>()!.background, + icon: SvgPicture.asset( + Assets.svg.trash, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + width: 20, + height: 20, + ), + onPressed: () { + showDialog<dynamic>( + context: context, + useSafeArea: true, + barrierDismissible: true, + builder: (_) => StackDialog( + title: "Delete ${_contact.name}?", + message: "Contact will be deleted permanently!", + leftButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Cancel", + style: STextStyles.itemSubtitle12(context), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + rightButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + child: Text( + "Delete", + style: STextStyles.button(context), + ), + onPressed: () { + ref + .read(addressBookServiceProvider) + .removeContact(_contact.id); + Navigator.of(context).pop(); + Navigator.of(context).pop(); + showFloatingFlushBar( + type: FlushBarType.success, + message: "${_contact.name} deleted", + context: context, + ); + }, ), - onPressed: () { - Navigator.of(context).pop(); - }, ), - rightButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - child: Text( - "Delete", - style: STextStyles.button(context), + ); + }, + ), + ), + ), + ], + ), + body: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + ), + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 12, + ), + Row( + children: [ + Container( + height: 48, + width: 48, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveBG, ), + child: Center( + child: _contact.emojiChar == null + ? SvgPicture.asset( + Assets.svg.user, + height: 24, + width: 24, + ) + : Text( + _contact.emojiChar!, + style: STextStyles.pageTitleH1(context), + ), + ), + ), + const SizedBox( + width: 16, + ), + FittedBox( + fit: BoxFit.scaleDown, + child: Text( + _contact.name, + textAlign: TextAlign.left, + style: STextStyles.pageTitleH2(context), + ), + ), + const Spacer(), + TextButton( onPressed: () { - ref - .read(addressBookServiceProvider) - .removeContact(_contact.id); - Navigator.of(context).pop(); - Navigator.of(context).pop(); - showFloatingFlushBar( - type: FlushBarType.success, - message: "${_contact.name} deleted", - context: context, + Navigator.of(context).pushNamed( + EditContactNameEmojiView.routeName, + arguments: _contact.id, ); }, - ), - ), - ); - }, - ), - ), - ), - ], - ), - body: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - ), - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 12, - ), - Row( - children: [ - Container( - height: 48, - width: 48, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - color: Theme.of(context) + style: Theme.of(context) .extension<StackColors>()! - .textFieldActiveBG, - ), - child: Center( - child: _contact.emojiChar == null - ? SvgPicture.asset( - Assets.svg.user, - height: 24, - width: 24, - ) - : Text( - _contact.emojiChar!, - style: STextStyles.pageTitleH1(context), - ), - ), - ), - const SizedBox( - width: 16, - ), - FittedBox( - fit: BoxFit.scaleDown, - child: Text( - _contact.name, - textAlign: TextAlign.left, - style: STextStyles.pageTitleH2(context), - ), - ), - const Spacer(), - TextButton( - onPressed: () { - Navigator.of(context).pushNamed( - EditContactNameEmojiView.routeName, - arguments: _contact.id, - ); - }, - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context)! - .copyWith( - minimumSize: MaterialStateProperty.all<Size>( - const Size(46, 32)), - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Row( - children: [ - SvgPicture.asset(Assets.svg.pencil, - width: 10, - height: 10, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - const SizedBox( - width: 4, + .getSecondaryEnabledButtonColor(context)! + .copyWith( + minimumSize: MaterialStateProperty.all<Size>( + const Size(46, 32)), ), - Text( - "Edit", - style: STextStyles.buttonSmall(context), - ), - ], - ), - ), - ), - ], - ), - const SizedBox( - height: 24, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Addresses", - style: STextStyles.itemSubtitle(context), - ), - BlueTextButton( - text: "Add new", - onTap: () { - Navigator.of(context).pushNamed( - AddNewContactAddressView.routeName, - arguments: _contact.id, - ); - }, - ), - ], - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: Column( - children: [ - ..._contact.addresses.map( - (e) => Padding( - padding: const EdgeInsets.all(12), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), child: Row( children: [ - SvgPicture.asset( - Assets.svg.iconFor(coin: e.coin), - height: 24, - ), - const SizedBox( - width: 12, - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "${e.label} (${e.coin.ticker})", - style: - STextStyles.itemSubtitle12(context), - ), - const SizedBox( - height: 2, - ), - FittedBox( - fit: BoxFit.scaleDown, - child: Text( - e.address, - style: STextStyles.itemSubtitle(context) - .copyWith( - fontSize: 8, - ), - ), - ), - ], - ), - ), - GestureDetector( - onTap: () { - ref - .read(addressEntryDataProvider(0)) - .address = e.address; - ref - .read(addressEntryDataProvider(0)) - .addressLabel = e.label; - ref.read(addressEntryDataProvider(0)).coin = - e.coin; - - Navigator.of(context).pushNamed( - EditContactAddressView.routeName, - arguments: Tuple2(_contact.id, e), - ); - }, - child: RoundedContainer( + SvgPicture.asset(Assets.svg.pencil, + width: 10, + height: 10, color: Theme.of(context) .extension<StackColors>()! - .textFieldDefaultBG, - padding: const EdgeInsets.all(6), - child: SvgPicture.asset( - Assets.svg.pencil, - width: 14, - height: 14, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - ), - ), - ), + .accentColorDark), const SizedBox( width: 4, ), - GestureDetector( - onTap: () { - clipboard.setData( - ClipboardData(text: e.address), - ); - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - iconAsset: Assets.svg.copy, - context: context, - ); - }, - child: RoundedContainer( - color: Theme.of(context) - .extension<StackColors>()! - .textFieldDefaultBG, - padding: const EdgeInsets.all(6), - child: SvgPicture.asset( - Assets.svg.copy, - width: 16, - height: 16, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - ), - ), + Text( + "Edit", + style: STextStyles.buttonSmall(context), ), ], ), @@ -440,81 +309,216 @@ class _ContactDetailsViewState extends ConsumerState<ContactDetailsView> { ), ], ), - ), - const SizedBox( - height: 24, - ), - Text( - "Transaction history", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 12, - ), - FutureBuilder( - future: _filteredTransactionsByContact( - ref.watch(walletsChangeNotifierProvider).managers), - builder: (_, - AsyncSnapshot<List<Tuple2<String, Transaction>>> - snapshot) { - if (snapshot.connectionState == ConnectionState.done && - snapshot.hasData) { - _cachedTransactions = snapshot.data!; - - if (_cachedTransactions.isNotEmpty) { - return RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: Column( - children: [ - ..._cachedTransactions.map( - (e) => TransactionCard( - key: Key( - "contactDetailsTransaction_${e.item1}_${e.item2.txid}_cardKey"), - transaction: e.item2, - walletId: e.item1, + const SizedBox( + height: 24, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Addresses", + style: STextStyles.itemSubtitle(context), + ), + BlueTextButton( + text: "Add new", + onTap: () { + Navigator.of(context).pushNamed( + AddNewContactAddressView.routeName, + arguments: _contact.id, + ); + }, + ), + ], + ), + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: Column( + children: [ + ..._contact.addresses.map( + (e) => Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.iconFor(coin: e.coin), + height: 24, ), - ), - ], - ), - ); - } else { - return RoundedWhiteContainer( - child: Center( - child: Text( - "No transactions found", - style: STextStyles.itemSubtitle(context), + const SizedBox( + width: 12, + ), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "${e.label} (${e.coin.ticker})", + style: + STextStyles.itemSubtitle12(context), + ), + const SizedBox( + height: 2, + ), + FittedBox( + fit: BoxFit.scaleDown, + child: Text( + e.address, + style: + STextStyles.itemSubtitle(context) + .copyWith( + fontSize: 8, + ), + ), + ), + ], + ), + ), + GestureDetector( + onTap: () { + ref + .read(addressEntryDataProvider(0)) + .address = e.address; + ref + .read(addressEntryDataProvider(0)) + .addressLabel = e.label; + ref.read(addressEntryDataProvider(0)).coin = + e.coin; + + Navigator.of(context).pushNamed( + EditContactAddressView.routeName, + arguments: Tuple2(_contact.id, e), + ); + }, + child: RoundedContainer( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + padding: const EdgeInsets.all(6), + child: SvgPicture.asset( + Assets.svg.pencil, + width: 14, + height: 14, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + ), + ), + const SizedBox( + width: 4, + ), + GestureDetector( + onTap: () { + clipboard.setData( + ClipboardData(text: e.address), + ); + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ); + }, + child: RoundedContainer( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + padding: const EdgeInsets.all(6), + child: SvgPicture.asset( + Assets.svg.copy, + width: 16, + height: 16, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + ), + ), + ], ), ), - ); - } - } else { - // TODO: proper loading animation - if (_cachedTransactions.isEmpty) { - return const LoadingIndicator(); - } else { - return RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: Column( - children: [ - ..._cachedTransactions.map( - (e) => TransactionCard( - key: Key( - "contactDetailsTransaction_${e.item1}_${e.item2.txid}_cardKey"), - transaction: e.item2, - walletId: e.item1, + ), + ], + ), + ), + const SizedBox( + height: 24, + ), + Text( + "Transaction history", + style: STextStyles.itemSubtitle(context), + ), + const SizedBox( + height: 12, + ), + FutureBuilder( + future: _filteredTransactionsByContact( + ref.watch(walletsChangeNotifierProvider).managers), + builder: (_, + AsyncSnapshot<List<Tuple2<String, Transaction>>> + snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData) { + _cachedTransactions = snapshot.data!; + + if (_cachedTransactions.isNotEmpty) { + return RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: Column( + children: [ + ..._cachedTransactions.map( + (e) => TransactionCard( + key: Key( + "contactDetailsTransaction_${e.item1}_${e.item2.txid}_cardKey"), + transaction: e.item2, + walletId: e.item1, + ), ), + ], + ), + ); + } else { + return RoundedWhiteContainer( + child: Center( + child: Text( + "No transactions found", + style: STextStyles.itemSubtitle(context), ), - ], - ), - ); + ), + ); + } + } else { + // TODO: proper loading animation + if (_cachedTransactions.isEmpty) { + return const LoadingIndicator(); + } else { + return RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: Column( + children: [ + ..._cachedTransactions.map( + (e) => TransactionCard( + key: Key( + "contactDetailsTransaction_${e.item1}_${e.item2.txid}_cardKey"), + transaction: e.item2, + walletId: e.item1, + ), + ), + ], + ), + ); + } } - } - }, - ), - const SizedBox( - height: 16, - ), - ], + }, + ), + const SizedBox( + height: 16, + ), + ], + ), ), ), ), diff --git a/lib/pages/address_book_views/subviews/edit_contact_address_view.dart b/lib/pages/address_book_views/subviews/edit_contact_address_view.dart index f0143d39d..0454903c3 100644 --- a/lib/pages/address_book_views/subviews/edit_contact_address_view.dart +++ b/lib/pages/address_book_views/subviews/edit_contact_address_view.dart @@ -13,6 +13,7 @@ import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; @@ -103,48 +104,51 @@ class _EditContactAddressViewState return ConditionalParent( condition: !isDesktop, - builder: (child) => Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Edit address", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Edit address", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (context, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: child, + body: LayoutBuilder( + builder: (context, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), ), ), ), - ), - ); - }, + ); + }, + ), ), ), child: Column( diff --git a/lib/pages/address_book_views/subviews/edit_contact_name_emoji_view.dart b/lib/pages/address_book_views/subviews/edit_contact_name_emoji_view.dart index a9b264b3c..99638ad2c 100644 --- a/lib/pages/address_book_views/subviews/edit_contact_name_emoji_view.dart +++ b/lib/pages/address_book_views/subviews/edit_contact_name_emoji_view.dart @@ -10,6 +10,7 @@ import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; @@ -79,48 +80,51 @@ class _EditContactNameEmojiViewState return ConditionalParent( condition: !isDesktop, - builder: (child) => Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Edit contact", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Edit contact", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (context, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: child, + body: LayoutBuilder( + builder: (context, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), ), ), ), - ), - ); - }, + ); + }, + ), ), ), child: Column( diff --git a/lib/pages/exchange_view/choose_from_stack_view.dart b/lib/pages/exchange_view/choose_from_stack_view.dart index f54a7552c..7c7669430 100644 --- a/lib/pages/exchange_view/choose_from_stack_view.dart +++ b/lib/pages/exchange_view/choose_from_stack_view.dart @@ -5,6 +5,7 @@ import 'package:stackwallet/utilities/constants.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/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance_future.dart'; @@ -39,89 +40,92 @@ class _ChooseFromStackViewState extends ConsumerState<ChooseFromStackView> { final walletIds = ref.watch(walletsChangeNotifierProvider .select((value) => value.getWalletIdsFor(coin: coin))); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: const AppBarBackButton(), - title: Text( - "Choose your ${coin.ticker.toUpperCase()} wallet", - style: STextStyles.navBarTitle(context), + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: const AppBarBackButton(), + title: Text( + "Choose your ${coin.ticker.toUpperCase()} wallet", + style: STextStyles.navBarTitle(context), + ), ), - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: walletIds.isEmpty - ? Column( - children: [ - RoundedWhiteContainer( - child: Center( - child: Text( - "No ${coin.ticker.toUpperCase()} wallets", - style: STextStyles.itemSubtitle(context), + body: Padding( + padding: const EdgeInsets.all(16), + child: walletIds.isEmpty + ? Column( + children: [ + RoundedWhiteContainer( + child: Center( + child: Text( + "No ${coin.ticker.toUpperCase()} wallets", + style: STextStyles.itemSubtitle(context), + ), ), ), - ), - ], - ) - : ListView.builder( - itemCount: walletIds.length, - itemBuilder: (context, index) { - final manager = ref.watch(walletsChangeNotifierProvider - .select((value) => value.getManager(walletIds[index]))); + ], + ) + : ListView.builder( + itemCount: walletIds.length, + itemBuilder: (context, index) { + final manager = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletIds[index]))); - return Padding( - padding: const EdgeInsets.symmetric(vertical: 5.0), - child: RawMaterialButton( - splashColor: - Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + return Padding( + padding: const EdgeInsets.symmetric(vertical: 5.0), + child: RawMaterialButton( + splashColor: Theme.of(context) + .extension<StackColors>()! + .highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - ), - padding: const EdgeInsets.all(0), - // color: Theme.of(context).extension<StackColors>()!.popupBG, - elevation: 0, - onPressed: () async { - if (mounted) { - Navigator.of(context).pop(manager.walletId); - } - }, - child: RoundedWhiteContainer( - // color: Colors.transparent, - child: Row( - children: [ - WalletInfoCoinIcon(coin: coin), - const SizedBox( - width: 12, - ), - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - manager.walletName, - style: STextStyles.titleBold12(context), - overflow: TextOverflow.ellipsis, - ), - const SizedBox( - height: 2, - ), - WalletInfoRowBalanceFuture( - walletId: walletIds[index], - ), - ], + padding: const EdgeInsets.all(0), + // color: Theme.of(context).extension<StackColors>()!.popupBG, + elevation: 0, + onPressed: () async { + if (mounted) { + Navigator.of(context).pop(manager.walletId); + } + }, + child: RoundedWhiteContainer( + // color: Colors.transparent, + child: Row( + children: [ + WalletInfoCoinIcon(coin: coin), + const SizedBox( + width: 12, ), - ) - ], + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + manager.walletName, + style: STextStyles.titleBold12(context), + overflow: TextOverflow.ellipsis, + ), + const SizedBox( + height: 2, + ), + WalletInfoRowBalanceFuture( + walletId: walletIds[index], + ), + ], + ), + ) + ], + ), ), ), - ), - ); - }, - ), + ); + }, + ), + ), ), ); } diff --git a/lib/pages/exchange_view/confirm_change_now_send.dart b/lib/pages/exchange_view/confirm_change_now_send.dart index 810ea2541..25ae51ecf 100644 --- a/lib/pages/exchange_view/confirm_change_now_send.dart +++ b/lib/pages/exchange_view/confirm_change_now_send.dart @@ -18,6 +18,7 @@ import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; @@ -232,49 +233,51 @@ class _ConfirmChangeNowSendViewState return ConditionalParent( condition: !isDesktop, builder: (child) { - return Scaffold( - backgroundColor: - Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( + return Background( + child: Scaffold( backgroundColor: Theme.of(context).extension<StackColors>()!.background, - leading: AppBarBackButton( - onPressed: () async { - // if (FocusScope.of(context).hasFocus) { - // FocusScope.of(context).unfocus(); - // await Future<void>.delayed(Duration(milliseconds: 50)); - // } - Navigator.of(context).pop(); - }, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + leading: AppBarBackButton( + onPressed: () async { + // if (FocusScope.of(context).hasFocus) { + // FocusScope.of(context).unfocus(); + // await Future<void>.delayed(Duration(milliseconds: 50)); + // } + Navigator.of(context).pop(); + }, + ), + title: Text( + "Confirm transaction", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Confirm transaction", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (builderContext, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: child, + body: LayoutBuilder( + builder: (builderContext, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), ), ), ), - ), - ); - }, + ); + }, + ), ), ); }, diff --git a/lib/pages/exchange_view/edit_trade_note_view.dart b/lib/pages/exchange_view/edit_trade_note_view.dart index e2a72d1b4..19804b7c5 100644 --- a/lib/pages/exchange_view/edit_trade_note_view.dart +++ b/lib/pages/exchange_view/edit_trade_note_view.dart @@ -4,13 +4,13 @@ import 'package:stackwallet/providers/exchange/trade_note_service_provider.dart' import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; -import 'package:stackwallet/utilities/util.dart'; - class EditTradeNoteView extends ConsumerStatefulWidget { const EditTradeNoteView({ Key? key, @@ -47,105 +47,109 @@ class _EditNoteViewState extends ConsumerState<EditTradeNoteView> { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( + return Background( + child: Scaffold( backgroundColor: Theme.of(context).extension<StackColors>()!.background, - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Edit trade note", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Edit trade note", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(12), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: _noteController, - style: STextStyles.field(context), - focusNode: noteFieldFocusNode, - onChanged: (_) => setState(() {}), - decoration: standardInputDecoration( - "Note", - noteFieldFocusNode, - context, - ).copyWith( - suffixIcon: _noteController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _noteController.text = ""; - }); - }, - ), - ], + body: Padding( + padding: const EdgeInsets.all(12), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _noteController, + style: STextStyles.field(context), + focusNode: noteFieldFocusNode, + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Note", + noteFieldFocusNode, + context, + ).copyWith( + suffixIcon: _noteController.text.isNotEmpty + ? Padding( + padding: + const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _noteController.text = ""; + }); + }, + ), + ], + ), ), - ), - ) - : null, + ) + : null, + ), ), ), - ), - const Spacer(), - TextButton( - onPressed: () async { - await ref.read(tradeNoteServiceProvider).set( - tradeId: widget.tradeId, - note: _noteController.text, - ); - if (mounted) { - Navigator.of(context).pop(); - } - }, - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - child: Text( - "Save", - style: STextStyles.button(context), - ), - ) - ], + const Spacer(), + TextButton( + onPressed: () async { + await ref.read(tradeNoteServiceProvider).set( + tradeId: widget.tradeId, + note: _noteController.text, + ); + if (mounted) { + Navigator.of(context).pop(); + } + }, + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + child: Text( + "Save", + style: STextStyles.button(context), + ), + ) + ], + ), ), ), ), - ), - ); - }, + ); + }, + ), ), ), ); diff --git a/lib/pages/exchange_view/exchange_coin_selection/fixed_rate_pair_coin_selection_view.dart b/lib/pages/exchange_view/exchange_coin_selection/fixed_rate_pair_coin_selection_view.dart index 779d99306..fdc1e027a 100644 --- a/lib/pages/exchange_view/exchange_coin_selection/fixed_rate_pair_coin_selection_view.dart +++ b/lib/pages/exchange_view/exchange_coin_selection/fixed_rate_pair_coin_selection_view.dart @@ -9,6 +9,7 @@ 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/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; @@ -124,32 +125,35 @@ class _FixedRateMarketPairCoinSelectionViewState return ConditionalParent( condition: !isDesktop, builder: (child) { - return Scaffold( - backgroundColor: - Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 50)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, + return Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 50)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Choose a coin to exchange", + style: STextStyles.pageTitleH2(context), + ), ), - title: Text( - "Choose a coin to exchange", - style: STextStyles.pageTitleH2(context), + body: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: child, ), ), - body: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), - child: child, - ), ); }, child: Column( diff --git a/lib/pages/exchange_view/exchange_coin_selection/floating_rate_currency_selection_view.dart b/lib/pages/exchange_view/exchange_coin_selection/floating_rate_currency_selection_view.dart index eb7a99299..1a82e5ec5 100644 --- a/lib/pages/exchange_view/exchange_coin_selection/floating_rate_currency_selection_view.dart +++ b/lib/pages/exchange_view/exchange_coin_selection/floating_rate_currency_selection_view.dart @@ -7,6 +7,7 @@ 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/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; @@ -80,32 +81,35 @@ class _FloatingRateCurrencySelectionViewState return ConditionalParent( condition: !isDesktop, builder: (child) { - return Scaffold( - backgroundColor: - Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 50)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, + return Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 50)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Choose a coin to exchange", + style: STextStyles.pageTitleH2(context), + ), ), - title: Text( - "Choose a coin to exchange", - style: STextStyles.pageTitleH2(context), + body: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: child, ), ), - body: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), - child: child, - ), ); }, child: Column( diff --git a/lib/pages/exchange_view/exchange_step_views/step_1_view.dart b/lib/pages/exchange_view/exchange_step_views/step_1_view.dart index 95f086003..b51bccc55 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_1_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_1_view.dart @@ -6,6 +6,7 @@ import 'package:stackwallet/pages/exchange_view/sub_widgets/step_row.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -39,166 +40,169 @@ class _Step1ViewState extends State<Step1View> { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Exchange", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Exchange", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (context, constraints) { - final width = MediaQuery.of(context).size.width - 32; - return Padding( - padding: const EdgeInsets.all(12), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - StepRow( - count: 4, - current: 0, - width: width, - ), - const SizedBox( - height: 14, - ), - Text( - "Confirm amount", - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 8, - ), - Text( - "Network fees and other exchange charges are included in the rate.", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 24, - ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "You send", - style: STextStyles.itemSubtitle(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .infoItemText), - ), - Text( - "${model.sendAmount.toStringAsFixed(8)} ${model.sendTicker.toUpperCase()}", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .infoItemText), - ), - ], + body: LayoutBuilder( + builder: (context, constraints) { + final width = MediaQuery.of(context).size.width - 32; + return Padding( + padding: const EdgeInsets.all(12), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + StepRow( + count: 4, + current: 0, + width: width, ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "You receive", - style: STextStyles.itemSubtitle(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .infoItemText), - ), - Text( - "~${model.receiveAmount.toStringAsFixed(8)} ${model.receiveTicker.toUpperCase()}", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .infoItemText), - ), - ], + const SizedBox( + height: 14, ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - model.rateType == ExchangeRateType.estimated - ? "Estimated rate" - : "Fixed rate", - style: - STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .infoItemLabel, + Text( + "Confirm amount", + style: STextStyles.pageTitleH1(context), + ), + const SizedBox( + height: 8, + ), + Text( + "Network fees and other exchange charges are included in the rate.", + style: STextStyles.itemSubtitle(context), + ), + const SizedBox( + height: 24, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "You send", + style: STextStyles.itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .infoItemText), ), - ), - Text( - model.rateInfo, - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .infoItemText), - ), - ], + Text( + "${model.sendAmount.toStringAsFixed(8)} ${model.sendTicker.toUpperCase()}", + style: STextStyles.itemSubtitle12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .infoItemText), + ), + ], + ), ), - ), - const SizedBox( - height: 12, - ), - const Spacer(), - TextButton( - onPressed: () { - Navigator.of(context).pushNamed(Step2View.routeName, - arguments: model); - }, - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - child: Text( - "Next", - style: STextStyles.button(context), + const SizedBox( + height: 12, ), - ), - ], + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "You receive", + style: STextStyles.itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .infoItemText), + ), + Text( + "~${model.receiveAmount.toStringAsFixed(8)} ${model.receiveTicker.toUpperCase()}", + style: STextStyles.itemSubtitle12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .infoItemText), + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + model.rateType == ExchangeRateType.estimated + ? "Estimated rate" + : "Fixed rate", + style: STextStyles.itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .infoItemLabel, + ), + ), + Text( + model.rateInfo, + style: STextStyles.itemSubtitle12(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .infoItemText), + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + const Spacer(), + TextButton( + onPressed: () { + Navigator.of(context).pushNamed( + Step2View.routeName, + arguments: model); + }, + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + child: Text( + "Next", + style: STextStyles.button(context), + ), + ), + ], + ), ), ), ), ), - ), - ); - }, + ); + }, + ), ), ); } diff --git a/lib/pages/exchange_view/exchange_step_views/step_2_view.dart b/lib/pages/exchange_view/exchange_step_views/step_2_view.dart index 030f91cb7..d943adcfa 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_2_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_2_view.dart @@ -16,6 +16,7 @@ import 'package:stackwallet/utilities/enums/coin_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/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; @@ -122,188 +123,285 @@ class _Step2ViewState extends ConsumerState<Step2View> { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Exchange", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Exchange", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (context, constraints) { - final width = MediaQuery.of(context).size.width - 32; - return Padding( - padding: const EdgeInsets.all(12), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - StepRow( - count: 4, - current: 1, - width: width, - ), - const SizedBox( - height: 14, - ), - Text( - "Exchange details", - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 8, - ), - Text( - "Enter your recipient and refund addresses", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 24, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Recipient Wallet", - style: STextStyles.smallMed12(context), - ), - if (isStackCoin(model.receiveTicker)) - BlueTextButton( - text: "Choose from stack", - onTap: () { - try { - final coin = coinFromTickerCaseInsensitive( - model.receiveTicker, - ); - Navigator.of(context) - .pushNamed( - ChooseFromStackView.routeName, - arguments: coin, - ) - .then((value) async { - if (value is String) { - final manager = ref - .read(walletsChangeNotifierProvider) - .getManager(value); - - _toController.text = manager.walletName; - model.recipientAddress = await manager - .currentReceivingAddress; - - setState(() { - enableNext = _toController - .text.isNotEmpty && - _refundController.text.isNotEmpty; - }); - } - }); - } catch (e, s) { - Logging.instance - .log("$e\n$s", level: LogLevel.Info); - } - }, - ), - ], - ), - const SizedBox( - height: 4, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + body: LayoutBuilder( + builder: (context, constraints) { + final width = MediaQuery.of(context).size.width - 32; + return Padding( + padding: const EdgeInsets.all(12), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + StepRow( + count: 4, + current: 1, + width: width, ), - child: TextField( - onTap: () {}, - key: const Key( - "recipientExchangeStep2ViewAddressFieldKey"), - controller: _toController, - readOnly: false, - autocorrect: false, - enableSuggestions: false, - // inputFormatters: <TextInputFormatter>[ - // FilteringTextInputFormatter.allow(RegExp("[a-zA-Z0-9]{34}")), - // ], - toolbarOptions: const ToolbarOptions( - copy: false, - cut: false, - paste: true, - selectAll: false, - ), - focusNode: _toFocusNode, - style: STextStyles.field(context), - onChanged: (value) { - setState(() {}); - }, - decoration: standardInputDecoration( - "Enter the ${model.receiveTicker.toUpperCase()} payout address", - _toFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, + const SizedBox( + height: 14, + ), + Text( + "Exchange details", + style: STextStyles.pageTitleH1(context), + ), + const SizedBox( + height: 8, + ), + Text( + "Enter your recipient and refund addresses", + style: STextStyles.itemSubtitle(context), + ), + const SizedBox( + height: 24, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Recipient Wallet", + style: STextStyles.smallMed12(context), ), - suffixIcon: Padding( - padding: _toController.text.isEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - _toController.text.isNotEmpty - ? TextFieldIconButton( - key: const Key( - "sendViewClearAddressFieldButtonKey"), - onTap: () { - _toController.text = ""; - model.recipientAddress = - _toController.text; + if (isStackCoin(model.receiveTicker)) + BlueTextButton( + text: "Choose from stack", + onTap: () { + try { + final coin = + coinFromTickerCaseInsensitive( + model.receiveTicker, + ); + Navigator.of(context) + .pushNamed( + ChooseFromStackView.routeName, + arguments: coin, + ) + .then((value) async { + if (value is String) { + final manager = ref + .read( + walletsChangeNotifierProvider) + .getManager(value); + _toController.text = + manager.walletName; + model.recipientAddress = await manager + .currentReceivingAddress; + + setState(() { + enableNext = + _toController.text.isNotEmpty && + _refundController + .text.isNotEmpty; + }); + } + }); + } catch (e, s) { + Logging.instance + .log("$e\n$s", level: LogLevel.Info); + } + }, + ), + ], + ), + const SizedBox( + height: 4, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + onTap: () {}, + key: const Key( + "recipientExchangeStep2ViewAddressFieldKey"), + controller: _toController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + // inputFormatters: <TextInputFormatter>[ + // FilteringTextInputFormatter.allow(RegExp("[a-zA-Z0-9]{34}")), + // ], + toolbarOptions: const ToolbarOptions( + copy: false, + cut: false, + paste: true, + selectAll: false, + ), + focusNode: _toFocusNode, + style: STextStyles.field(context), + onChanged: (value) { + setState(() {}); + }, + decoration: standardInputDecoration( + "Enter the ${model.receiveTicker.toUpperCase()} payout address", + _toFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _toController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceAround, + children: [ + _toController.text.isNotEmpty + ? TextFieldIconButton( + key: const Key( + "sendViewClearAddressFieldButtonKey"), + onTap: () { + _toController.text = ""; + model.recipientAddress = + _toController.text; + + setState(() { + enableNext = _toController + .text.isNotEmpty && + _refundController + .text.isNotEmpty; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "sendViewPasteAddressFieldButtonKey"), + onTap: () async { + final ClipboardData? data = + await clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + final content = + data.text!.trim(); + + _toController.text = + content; + model.recipientAddress = + _toController.text; + + setState(() { + enableNext = _toController + .text + .isNotEmpty && + _refundController + .text.isNotEmpty; + }); + } + }, + child: + _toController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_toController.text.isEmpty) + TextFieldIconButton( + key: const Key( + "sendViewAddressBookButtonKey"), + onTap: () { + ref + .read( + exchangeFlowIsActiveStateProvider + .state) + .state = true; + Navigator.of(context) + .pushNamed( + AddressBookView.routeName, + ) + .then((_) { + ref + .read( + exchangeFlowIsActiveStateProvider + .state) + .state = false; + + final address = ref + .read( + exchangeFromAddressBookAddressStateProvider + .state) + .state; + if (address.isNotEmpty) { + _toController.text = address; + model.recipientAddress = + _toController.text; + ref + .read( + exchangeFromAddressBookAddressStateProvider + .state) + .state = ""; + } setState(() { enableNext = _toController .text.isNotEmpty && _refundController .text.isNotEmpty; }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - key: const Key( - "sendViewPasteAddressFieldButtonKey"), - onTap: () async { - final ClipboardData? data = - await clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - final content = - data.text!.trim(); + }); + }, + child: const AddressBookIcon(), + ), + if (_toController.text.isEmpty) + TextFieldIconButton( + key: const Key( + "sendViewScanQrButtonKey"), + onTap: () async { + try { + final qrResult = + await scanner.scan(); - _toController.text = content; + final results = + AddressUtils.parseUri( + qrResult.rawContent); + if (results.isNotEmpty) { + // auto fill address + _toController.text = + results["address"] ?? ""; + model.recipientAddress = + _toController.text; + + setState(() { + enableNext = _toController + .text.isNotEmpty && + _refundController + .text.isNotEmpty; + }); + } else { + _toController.text = + qrResult.rawContent; model.recipientAddress = _toController.text; @@ -314,248 +412,247 @@ class _Step2ViewState extends ConsumerState<Step2View> { .text.isNotEmpty; }); } - }, - child: _toController.text.isEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - if (_toController.text.isEmpty) - TextFieldIconButton( - key: const Key( - "sendViewAddressBookButtonKey"), - onTap: () { - ref - .read( - exchangeFlowIsActiveStateProvider - .state) - .state = true; - Navigator.of(context) - .pushNamed( - AddressBookView.routeName, - ) - .then((_) { - ref - .read( - exchangeFlowIsActiveStateProvider - .state) - .state = false; - - final address = ref - .read( - exchangeFromAddressBookAddressStateProvider - .state) - .state; - if (address.isNotEmpty) { - _toController.text = address; - model.recipientAddress = - _toController.text; - ref - .read( - exchangeFromAddressBookAddressStateProvider - .state) - .state = ""; + } on PlatformException catch (e, s) { + Logging.instance.log( + "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", + level: LogLevel.Warning, + ); } - setState(() { - enableNext = _toController - .text.isNotEmpty && - _refundController - .text.isNotEmpty; - }); - }); - }, - child: const AddressBookIcon(), - ), - if (_toController.text.isEmpty) - TextFieldIconButton( - key: const Key( - "sendViewScanQrButtonKey"), - onTap: () async { - try { - final qrResult = - await scanner.scan(); - - final results = - AddressUtils.parseUri( - qrResult.rawContent); - if (results.isNotEmpty) { - // auto fill address - _toController.text = - results["address"] ?? ""; - model.recipientAddress = - _toController.text; - - setState(() { - enableNext = _toController - .text.isNotEmpty && - _refundController - .text.isNotEmpty; - }); - } else { - _toController.text = - qrResult.rawContent; - model.recipientAddress = - _toController.text; - - setState(() { - enableNext = _toController - .text.isNotEmpty && - _refundController - .text.isNotEmpty; - }); - } - } on PlatformException catch (e, s) { - Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", - level: LogLevel.Warning, - ); - } - }, - child: const QrCodeIcon(), - ), - ], + }, + child: const QrCodeIcon(), + ), + ], + ), ), ), ), ), ), - ), - const SizedBox( - height: 6, - ), - RoundedWhiteContainer( - child: Text( - "This is the wallet where your ${model.receiveTicker.toUpperCase()} will be sent to.", - style: STextStyles.label(context), + const SizedBox( + height: 6, ), - ), - const SizedBox( - height: 24, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Refund Wallet (required)", - style: STextStyles.smallMed12(context), + RoundedWhiteContainer( + child: Text( + "This is the wallet where your ${model.receiveTicker.toUpperCase()} will be sent to.", + style: STextStyles.label(context), ), - if (isStackCoin(model.sendTicker)) - BlueTextButton( - text: "Choose from stack", - onTap: () { - try { - final coin = coinFromTickerCaseInsensitive( - model.sendTicker, - ); - Navigator.of(context) - .pushNamed( - ChooseFromStackView.routeName, - arguments: coin, - ) - .then((value) async { - if (value is String) { - final manager = ref - .read(walletsChangeNotifierProvider) - .getManager(value); + ), + const SizedBox( + height: 24, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Refund Wallet (required)", + style: STextStyles.smallMed12(context), + ), + if (isStackCoin(model.sendTicker)) + BlueTextButton( + text: "Choose from stack", + onTap: () { + try { + final coin = + coinFromTickerCaseInsensitive( + model.sendTicker, + ); + Navigator.of(context) + .pushNamed( + ChooseFromStackView.routeName, + arguments: coin, + ) + .then((value) async { + if (value is String) { + final manager = ref + .read( + walletsChangeNotifierProvider) + .getManager(value); - _refundController.text = - manager.walletName; - model.refundAddress = await manager - .currentReceivingAddress; - } - setState(() { - enableNext = _toController - .text.isNotEmpty && - _refundController.text.isNotEmpty; + _refundController.text = + manager.walletName; + model.refundAddress = await manager + .currentReceivingAddress; + } + setState(() { + enableNext = _toController + .text.isNotEmpty && + _refundController.text.isNotEmpty; + }); }); - }); - } catch (e, s) { - Logging.instance - .log("$e\n$s", level: LogLevel.Info); - } - }, - ), - ], - ), - const SizedBox( - height: 4, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + } catch (e, s) { + Logging.instance + .log("$e\n$s", level: LogLevel.Info); + } + }, + ), + ], ), - child: TextField( - key: const Key( - "refundExchangeStep2ViewAddressFieldKey"), - controller: _refundController, - readOnly: false, - autocorrect: false, - enableSuggestions: false, - // inputFormatters: <TextInputFormatter>[ - // FilteringTextInputFormatter.allow(RegExp("[a-zA-Z0-9]{34}")), - // ], - toolbarOptions: const ToolbarOptions( - copy: false, - cut: false, - paste: true, - selectAll: false, + const SizedBox( + height: 4, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - focusNode: _refundFocusNode, - style: STextStyles.field(context), - onChanged: (value) { - setState(() {}); - }, - decoration: standardInputDecoration( - "Enter ${model.sendTicker.toUpperCase()} refund address", - _refundFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, + child: TextField( + key: const Key( + "refundExchangeStep2ViewAddressFieldKey"), + controller: _refundController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + // inputFormatters: <TextInputFormatter>[ + // FilteringTextInputFormatter.allow(RegExp("[a-zA-Z0-9]{34}")), + // ], + toolbarOptions: const ToolbarOptions( + copy: false, + cut: false, + paste: true, + selectAll: false, ), - suffixIcon: Padding( - padding: _refundController.text.isEmpty - ? const EdgeInsets.only(right: 16) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - _refundController.text.isNotEmpty - ? TextFieldIconButton( - key: const Key( - "sendViewClearAddressFieldButtonKey"), - onTap: () { - _refundController.text = ""; - model.refundAddress = - _refundController.text; + focusNode: _refundFocusNode, + style: STextStyles.field(context), + onChanged: (value) { + setState(() {}); + }, + decoration: standardInputDecoration( + "Enter ${model.sendTicker.toUpperCase()} refund address", + _refundFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _refundController.text.isEmpty + ? const EdgeInsets.only(right: 16) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceAround, + children: [ + _refundController.text.isNotEmpty + ? TextFieldIconButton( + key: const Key( + "sendViewClearAddressFieldButtonKey"), + onTap: () { + _refundController.text = ""; + model.refundAddress = + _refundController.text; + setState(() { + enableNext = _toController + .text.isNotEmpty && + _refundController + .text.isNotEmpty; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "sendViewPasteAddressFieldButtonKey"), + onTap: () async { + final ClipboardData? data = + await clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + final content = + data.text!.trim(); + + _refundController.text = + content; + model.refundAddress = + _refundController.text; + + setState(() { + enableNext = _toController + .text + .isNotEmpty && + _refundController + .text.isNotEmpty; + }); + } + }, + child: _refundController + .text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_refundController.text.isEmpty) + TextFieldIconButton( + key: const Key( + "sendViewAddressBookButtonKey"), + onTap: () { + ref + .read( + exchangeFlowIsActiveStateProvider + .state) + .state = true; + Navigator.of(context) + .pushNamed( + AddressBookView.routeName, + ) + .then((_) { + ref + .read( + exchangeFlowIsActiveStateProvider + .state) + .state = false; + final address = ref + .read( + exchangeFromAddressBookAddressStateProvider + .state) + .state; + if (address.isNotEmpty) { + _refundController.text = + address; + model.refundAddress = + _refundController.text; + } setState(() { enableNext = _toController .text.isNotEmpty && _refundController .text.isNotEmpty; }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - key: const Key( - "sendViewPasteAddressFieldButtonKey"), - onTap: () async { - final ClipboardData? data = - await clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - final content = - data.text!.trim(); + }); + }, + child: const AddressBookIcon(), + ), + if (_refundController.text.isEmpty) + TextFieldIconButton( + key: const Key( + "sendViewScanQrButtonKey"), + onTap: () async { + try { + final qrResult = + await scanner.scan(); + final results = + AddressUtils.parseUri( + qrResult.rawContent); + if (results.isNotEmpty) { + // auto fill address _refundController.text = - content; + results["address"] ?? ""; + model.refundAddress = + _refundController.text; + + setState(() { + enableNext = _toController + .text.isNotEmpty && + _refundController + .text.isNotEmpty; + }); + } else { + _refundController.text = + qrResult.rawContent; model.refundAddress = _refundController.text; @@ -566,162 +663,78 @@ class _Step2ViewState extends ConsumerState<Step2View> { .text.isNotEmpty; }); } - }, - child: - _refundController.text.isEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - if (_refundController.text.isEmpty) - TextFieldIconButton( - key: const Key( - "sendViewAddressBookButtonKey"), - onTap: () { - ref - .read( - exchangeFlowIsActiveStateProvider - .state) - .state = true; - Navigator.of(context) - .pushNamed( - AddressBookView.routeName, - ) - .then((_) { - ref - .read( - exchangeFlowIsActiveStateProvider - .state) - .state = false; - final address = ref - .read( - exchangeFromAddressBookAddressStateProvider - .state) - .state; - if (address.isNotEmpty) { - _refundController.text = - address; - model.refundAddress = - _refundController.text; + } on PlatformException catch (e, s) { + Logging.instance.log( + "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", + level: LogLevel.Warning, + ); } - setState(() { - enableNext = _toController - .text.isNotEmpty && - _refundController - .text.isNotEmpty; - }); - }); - }, - child: const AddressBookIcon(), - ), - if (_refundController.text.isEmpty) - TextFieldIconButton( - key: const Key( - "sendViewScanQrButtonKey"), - onTap: () async { - try { - final qrResult = - await scanner.scan(); - - final results = - AddressUtils.parseUri( - qrResult.rawContent); - if (results.isNotEmpty) { - // auto fill address - _refundController.text = - results["address"] ?? ""; - model.refundAddress = - _refundController.text; - - setState(() { - enableNext = _toController - .text.isNotEmpty && - _refundController - .text.isNotEmpty; - }); - } else { - _refundController.text = - qrResult.rawContent; - model.refundAddress = - _refundController.text; - - setState(() { - enableNext = _toController - .text.isNotEmpty && - _refundController - .text.isNotEmpty; - }); - } - } on PlatformException catch (e, s) { - Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", - level: LogLevel.Warning, - ); - } - }, - child: const QrCodeIcon(), - ), - ], + }, + child: const QrCodeIcon(), + ), + ], + ), ), ), ), ), ), - ), - const SizedBox( - height: 6, - ), - RoundedWhiteContainer( - child: Text( - "In case something goes wrong during the exchange, we might need a refund address so we can return your coins back to you.", - style: STextStyles.label(context), + const SizedBox( + height: 6, ), - ), - const Spacer(), - Row( - children: [ - Expanded( - child: TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Back", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .buttonTextSecondary, + RoundedWhiteContainer( + child: Text( + "In case something goes wrong during the exchange, we might need a refund address so we can return your coins back to you.", + style: STextStyles.label(context), + ), + ), + const Spacer(), + Row( + children: [ + Expanded( + child: TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Back", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, + ), ), ), ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: PrimaryButton( - label: "Next", - enabled: enableNext, - onPressed: () { - Navigator.of(context).pushNamed( - Step3View.routeName, - arguments: model, - ); - }, + const SizedBox( + width: 16, ), - ), - ], - ), - ], + Expanded( + child: PrimaryButton( + label: "Next", + enabled: enableNext, + onPressed: () { + Navigator.of(context).pushNamed( + Step3View.routeName, + arguments: model, + ); + }, + ), + ), + ], + ), + ], + ), ), ), ), ), - ), - ); - }, + ); + }, + ), ), ); } 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 0f03d4216..467a3b9e7 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 @@ -15,6 +15,7 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_loading_overlay.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -50,290 +51,295 @@ class _Step3ViewState extends ConsumerState<Step3View> { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Exchange", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Exchange", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (context, constraints) { - final width = MediaQuery.of(context).size.width - 32; - return Padding( - padding: const EdgeInsets.all(12), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - StepRow( - count: 4, - current: 2, - width: width, - ), - const SizedBox( - height: 14, - ), - Text( - "Confirm exchange details", - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 24, - ), - RoundedWhiteContainer( - child: Row( - children: [ - Text( - "You send", - style: STextStyles.itemSubtitle(context), - ), - const Spacer(), - Text( - "${model.sendAmount.toString()} ${model.sendTicker.toUpperCase()}", - style: STextStyles.itemSubtitle12(context), - ) - ], + body: LayoutBuilder( + builder: (context, constraints) { + final width = MediaQuery.of(context).size.width - 32; + return Padding( + padding: const EdgeInsets.all(12), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + StepRow( + count: 4, + current: 2, + width: width, ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - child: Row( - children: [ - Text( - "You receive", - style: STextStyles.itemSubtitle(context), - ), - const Spacer(), - Text( - "${model.receiveAmount.toString()} ${model.receiveTicker.toUpperCase()}", - style: STextStyles.itemSubtitle12(context), - ) - ], + const SizedBox( + height: 14, ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - child: Row( - children: [ - Text( - "Estimated rate", - style: STextStyles.itemSubtitle(context), - ), - const Spacer(), - Text( - model.rateInfo, - style: STextStyles.itemSubtitle12(context), - ) - ], + Text( + "Confirm exchange details", + style: STextStyles.pageTitleH1(context), ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Recipient ${model.receiveTicker.toUpperCase()} address", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 4, - ), - Text( - model.recipientAddress!, - style: STextStyles.itemSubtitle12(context), - ) - ], + const SizedBox( + height: 24, ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Refund ${model.sendTicker.toUpperCase()} address", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 4, - ), - Text( - model.refundAddress!, - style: STextStyles.itemSubtitle12(context), - ) - ], + RoundedWhiteContainer( + child: Row( + children: [ + Text( + "You send", + style: STextStyles.itemSubtitle(context), + ), + const Spacer(), + Text( + "${model.sendAmount.toString()} ${model.sendTicker.toUpperCase()}", + style: STextStyles.itemSubtitle12(context), + ) + ], + ), ), - ), - const SizedBox( - height: 8, - ), - const Spacer(), - Row( - children: [ - Expanded( - child: TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Back", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .buttonTextSecondary, + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + child: Row( + children: [ + Text( + "You receive", + style: STextStyles.itemSubtitle(context), + ), + const Spacer(), + Text( + "${model.receiveAmount.toString()} ${model.receiveTicker.toUpperCase()}", + style: STextStyles.itemSubtitle12(context), + ) + ], + ), + ), + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + child: Row( + children: [ + Text( + "Estimated rate", + style: STextStyles.itemSubtitle(context), + ), + const Spacer(), + Text( + model.rateInfo, + style: STextStyles.itemSubtitle12(context), + ) + ], + ), + ), + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Recipient ${model.receiveTicker.toUpperCase()} address", + style: STextStyles.itemSubtitle(context), + ), + const SizedBox( + height: 4, + ), + Text( + model.recipientAddress!, + style: STextStyles.itemSubtitle12(context), + ) + ], + ), + ), + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Refund ${model.sendTicker.toUpperCase()} address", + style: STextStyles.itemSubtitle(context), + ), + const SizedBox( + height: 4, + ), + Text( + model.refundAddress!, + style: STextStyles.itemSubtitle12(context), + ) + ], + ), + ), + const SizedBox( + height: 8, + ), + const Spacer(), + Row( + children: [ + Expanded( + child: TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Back", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, + ), ), ), ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: TextButton( - onPressed: () async { - unawaited( - showDialog<void>( - context: context, - barrierDismissible: false, - builder: (_) => WillPopScope( - onWillPop: () async => false, - child: Container( - color: Theme.of(context) - .extension<StackColors>()! - .overlay - .withOpacity(0.6), - child: const CustomLoadingOverlay( - message: "Creating a trade", - eventBus: null, + const SizedBox( + width: 16, + ), + Expanded( + child: TextButton( + onPressed: () async { + unawaited( + showDialog<void>( + context: context, + barrierDismissible: false, + builder: (_) => WillPopScope( + onWillPop: () async => false, + child: Container( + color: Theme.of(context) + .extension<StackColors>()! + .overlay + .withOpacity(0.6), + child: const CustomLoadingOverlay( + message: "Creating a trade", + eventBus: null, + ), ), ), ), - ), - ); + ); - final ExchangeResponse<Trade> response = - await ref - .read(exchangeProvider) - .createTrade( - from: model.sendTicker, - to: model.receiveTicker, - fixedRate: model.rateType != - ExchangeRateType.estimated, - amount: model.reversed - ? model.receiveAmount - : model.sendAmount, - addressTo: model.recipientAddress!, - extraId: null, - addressRefund: model.refundAddress!, - refundExtraId: "", - rateId: model.rateId, - reversed: model.reversed, - ); + final ExchangeResponse<Trade> response = + await ref + .read(exchangeProvider) + .createTrade( + from: model.sendTicker, + to: model.receiveTicker, + fixedRate: model.rateType != + ExchangeRateType.estimated, + amount: model.reversed + ? model.receiveAmount + : model.sendAmount, + addressTo: + model.recipientAddress!, + extraId: null, + addressRefund: + model.refundAddress!, + refundExtraId: "", + rateId: model.rateId, + reversed: model.reversed, + ); + + if (response.value == null) { + if (mounted) { + Navigator.of(context).pop(); + } + + unawaited(showDialog<void>( + context: context, + barrierDismissible: true, + builder: (_) => StackDialog( + title: "Failed to create trade", + message: + response.exception?.toString(), + ), + )); + return; + } + + // save trade to hive + await ref.read(tradesServiceProvider).add( + trade: response.value!, + shouldNotifyListeners: true, + ); + + String status = response.value!.status; + + model.trade = response.value!; + + // extra info if status is waiting + if (status == "Waiting") { + status += " for deposit"; + } - if (response.value == null) { if (mounted) { Navigator.of(context).pop(); } - unawaited(showDialog<void>( - context: context, - barrierDismissible: true, - builder: (_) => StackDialog( - title: "Failed to create trade", - message: response.exception?.toString(), - ), + unawaited(NotificationApi.showNotification( + changeNowId: model.trade!.tradeId, + title: status, + body: "Trade ID ${model.trade!.tradeId}", + walletId: "", + iconAssetName: Assets.svg.arrowRotate, + date: model.trade!.timestamp, + shouldWatchForUpdates: true, + coinName: "coinName", )); - return; - } - // save trade to hive - await ref.read(tradesServiceProvider).add( - trade: response.value!, - shouldNotifyListeners: true, - ); - - String status = response.value!.status; - - model.trade = response.value!; - - // extra info if status is waiting - if (status == "Waiting") { - status += " for deposit"; - } - - if (mounted) { - Navigator.of(context).pop(); - } - - unawaited(NotificationApi.showNotification( - changeNowId: model.trade!.tradeId, - title: status, - body: "Trade ID ${model.trade!.tradeId}", - walletId: "", - iconAssetName: Assets.svg.arrowRotate, - date: model.trade!.timestamp, - shouldWatchForUpdates: true, - coinName: "coinName", - )); - - if (mounted) { - unawaited(Navigator.of(context).pushNamed( - Step4View.routeName, - arguments: model, - )); - } - }, - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - child: Text( - "Next", - style: STextStyles.button(context), + if (mounted) { + unawaited(Navigator.of(context).pushNamed( + Step4View.routeName, + arguments: model, + )); + } + }, + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + child: Text( + "Next", + style: STextStyles.button(context), + ), ), ), - ), - ], - ), - ], + ], + ), + ], + ), ), ), ), ), - ), - ); - }, + ); + }, + ), ), ); } 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 f5975a277..101ac637f 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_4_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_4_view.dart @@ -21,6 +21,7 @@ import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -106,539 +107,548 @@ class _Step4ViewState extends ConsumerState<Step4View> { Widget build(BuildContext context) { final bool isWalletCoin = _isWalletCoinAndHasWallet(model.trade!.payInCurrency, ref); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Exchange", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Exchange", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (context, constraints) { - final width = MediaQuery.of(context).size.width - 32; - return Padding( - padding: const EdgeInsets.all(12), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - StepRow( - count: 4, - current: 3, - width: width, - ), - const SizedBox( - height: 14, - ), - Text( - "Send ${model.sendTicker.toUpperCase()} to the address below", - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 8, - ), - Text( - "Send ${model.sendTicker.toUpperCase()} to the address below. Once it is received, ${model.trade!.exchangeName} will send the ${model.receiveTicker.toUpperCase()} to the recipient address you provided. You can find this trade details and check its status in the list of trades.", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 12, - ), - RoundedContainer( - color: Theme.of(context) - .extension<StackColors>()! - .warningBackground, - child: RichText( - text: TextSpan( - text: - "You must send at least ${model.sendAmount.toString()} ${model.sendTicker}. ", - style: STextStyles.label700(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .warningForeground, + body: LayoutBuilder( + builder: (context, constraints) { + final width = MediaQuery.of(context).size.width - 32; + return Padding( + padding: const EdgeInsets.all(12), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + StepRow( + count: 4, + current: 3, + width: width, + ), + const SizedBox( + height: 14, + ), + Text( + "Send ${model.sendTicker.toUpperCase()} to the address below", + style: STextStyles.pageTitleH1(context), + ), + const SizedBox( + height: 8, + ), + Text( + "Send ${model.sendTicker.toUpperCase()} to the address below. Once it is received, ${model.trade!.exchangeName} will send the ${model.receiveTicker.toUpperCase()} to the recipient address you provided. You can find this trade details and check its status in the list of trades.", + style: STextStyles.itemSubtitle(context), + ), + const SizedBox( + height: 12, + ), + RoundedContainer( + color: Theme.of(context) + .extension<StackColors>()! + .warningBackground, + child: RichText( + text: TextSpan( + text: + "You must send at least ${model.sendAmount.toString()} ${model.sendTicker}. ", + style: STextStyles.label700(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .warningForeground, + ), + children: [ + TextSpan( + text: + "If you send less than ${model.sendAmount.toString()} ${model.sendTicker}, your transaction may not be converted and it may not be refunded.", + style: STextStyles.label(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .warningForeground, + ), + ), + ], ), + ), + ), + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - TextSpan( - text: - "If you send less than ${model.sendAmount.toString()} ${model.sendTicker}, your transaction may not be converted and it may not be refunded.", - style: STextStyles.label(context).copyWith( + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Amount", + style: STextStyles.itemSubtitle(context), + ), + GestureDetector( + onTap: () async { + final data = ClipboardData( + text: model.sendAmount.toString()); + await clipboard.setData(data); + unawaited(showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, + )); + }, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.copy, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + width: 10, + ), + const SizedBox( + width: 4, + ), + Text( + "Copy", + style: STextStyles.link2(context), + ), + ], + ), + ), + ], + ), + const SizedBox( + height: 4, + ), + Text( + "${model.sendAmount.toString()} ${model.sendTicker.toUpperCase()}", + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Send ${model.sendTicker.toUpperCase()} to this address", + style: STextStyles.itemSubtitle(context), + ), + GestureDetector( + onTap: () async { + final data = ClipboardData( + text: model.trade!.payInAddress); + await clipboard.setData(data); + unawaited(showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, + )); + }, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.copy, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + width: 10, + ), + const SizedBox( + width: 4, + ), + Text( + "Copy", + style: STextStyles.link2(context), + ), + ], + ), + ), + ], + ), + const SizedBox( + height: 4, + ), + Text( + model.trade!.payInAddress, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + const SizedBox( + height: 6, + ), + RoundedWhiteContainer( + child: Row( + children: [ + Text( + "Trade ID", + style: STextStyles.itemSubtitle(context), + ), + const Spacer(), + Row( + children: [ + Text( + model.trade!.tradeId, + style: + STextStyles.itemSubtitle12(context), + ), + const SizedBox( + width: 10, + ), + GestureDetector( + onTap: () async { + final data = ClipboardData( + text: model.trade!.tradeId); + await clipboard.setData(data); + unawaited(showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, + )); + }, + child: SvgPicture.asset( + Assets.svg.copy, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + width: 12, + ), + ) + ], + ) + ], + ), + ), + const SizedBox( + height: 6, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Status", + style: STextStyles.itemSubtitle(context), + ), + Text( + _statusString, + style: STextStyles.itemSubtitle(context) + .copyWith( color: Theme.of(context) .extension<StackColors>()! - .warningForeground, + .colorForStatus(_statusString), ), ), ], ), ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Amount", - style: STextStyles.itemSubtitle(context), - ), - GestureDetector( - onTap: () async { - final data = ClipboardData( - text: model.sendAmount.toString()); - await clipboard.setData(data); - unawaited(showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - context: context, - )); - }, - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.copy, - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - width: 10, - ), - const SizedBox( - width: 4, - ), - Text( - "Copy", - style: STextStyles.link2(context), - ), - ], - ), - ), - ], - ), - const SizedBox( - height: 4, - ), - Text( - "${model.sendAmount.toString()} ${model.sendTicker.toUpperCase()}", - style: STextStyles.itemSubtitle12(context), - ), - ], - ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Send ${model.sendTicker.toUpperCase()} to this address", - style: STextStyles.itemSubtitle(context), - ), - GestureDetector( - onTap: () async { - final data = ClipboardData( - text: model.trade!.payInAddress); - await clipboard.setData(data); - unawaited(showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - context: context, - )); - }, - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.copy, - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - width: 10, - ), - const SizedBox( - width: 4, - ), - Text( - "Copy", - style: STextStyles.link2(context), - ), - ], - ), - ), - ], - ), - const SizedBox( - height: 4, - ), - Text( - model.trade!.payInAddress, - style: STextStyles.itemSubtitle12(context), - ), - ], - ), - ), - const SizedBox( - height: 6, - ), - RoundedWhiteContainer( - child: Row( - children: [ - Text( - "Trade ID", - style: STextStyles.itemSubtitle(context), - ), - const Spacer(), - Row( - children: [ - Text( - model.trade!.tradeId, - style: STextStyles.itemSubtitle12(context), - ), - const SizedBox( - width: 10, - ), - GestureDetector( - onTap: () async { - final data = ClipboardData( - text: model.trade!.tradeId); - await clipboard.setData(data); - unawaited(showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - context: context, - )); - }, - child: SvgPicture.asset( - Assets.svg.copy, - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - width: 12, - ), - ) - ], - ) - ], - ), - ), - const SizedBox( - height: 6, - ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Status", - style: STextStyles.itemSubtitle(context), - ), - Text( - _statusString, - style: - STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .colorForStatus(_statusString), - ), - ), - ], - ), - ), - const Spacer(), - const SizedBox( - height: 12, - ), - TextButton( - onPressed: () { - showDialog<dynamic>( - context: context, - barrierDismissible: true, - builder: (_) { - return StackDialogBase( - child: Column( - children: [ - const SizedBox( - height: 8, - ), - Center( - child: Text( - "Send ${model.sendTicker} to this address", - style: - STextStyles.pageTitleH2(context), - ), - ), - const SizedBox( - height: 24, - ), - Center( - child: QrImage( - // TODO: grab coin uri scheme from somewhere - // data: "${coin.uriScheme}:$receivingAddress", - data: model.trade!.payInAddress, - size: MediaQuery.of(context) - .size - .width / - 2, - foregroundColor: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - ), - ), - const SizedBox( - height: 24, - ), - Row( - children: [ - const Spacer(), - Expanded( - child: TextButton( - onPressed: () => - Navigator.of(context).pop(), - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor( - context), - child: Text( - "Cancel", - style: - STextStyles.button(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .buttonTextSecondary, - ), - ), - ), - ), - ], - ) - ], - ), - ); - }, - ); - }, - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - child: Text( - "Show QR Code", - style: STextStyles.button(context), - ), - ), - if (isWalletCoin) + const Spacer(), const SizedBox( height: 12, ), - if (isWalletCoin) - Builder( - builder: (context) { - String buttonTitle = "Send from Stack Wallet"; - - final tuple = ref - .read(exchangeSendFromWalletIdStateProvider - .state) - .state; - if (tuple != null && - model.sendTicker.toLowerCase() == - tuple.item2.ticker.toLowerCase()) { - final walletName = ref - .read(walletsChangeNotifierProvider) - .getManager(tuple.item1) - .walletName; - buttonTitle = "Send from $walletName"; - } - - return TextButton( - onPressed: tuple != null && - model.sendTicker.toLowerCase() == - tuple.item2.ticker.toLowerCase() - ? () async { - final manager = ref - .read(walletsChangeNotifierProvider) - .getManager(tuple.item1); - - final amount = - Format.decimalAmountToSatoshis( - model.sendAmount, manager.coin); - final address = - model.trade!.payInAddress; - - try { - bool wasCancelled = false; - - unawaited(showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: false, - builder: (context) { - return BuildingTransactionDialog( - onCancel: () { - wasCancelled = true; - - Navigator.of(context).pop(); - }, - ); - }, - )); - - final txData = - await manager.prepareSend( - address: address, - satoshiAmount: amount, - args: { - "feeRate": FeeRateType.average, - // ref.read(feeRateTypeStateProvider) - }, - ); - - if (!wasCancelled) { - // pop building dialog - - if (mounted) { - Navigator.of(context).pop(); - } - - txData["note"] = - "${model.trade!.payInCurrency.toUpperCase()}/${model.trade!.payOutCurrency.toUpperCase()} exchange"; - txData["address"] = address; - - if (mounted) { - unawaited( - Navigator.of(context).push( - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator - .useMaterialPageRoute, - builder: (_) => - ConfirmChangeNowSendView( - transactionInfo: txData, - walletId: tuple.item1, - routeOnSuccessName: - HomeView.routeName, - trade: model.trade!, - ), - settings: const RouteSettings( - name: - ConfirmChangeNowSendView - .routeName, - ), - ), - )); - } - } - } catch (e) { - // if (mounted) { - // pop building dialog - Navigator.of(context).pop(); - - unawaited(showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return StackDialog( - title: "Transaction failed", - message: e.toString(), - rightButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor( - context), - child: Text( - "Ok", - style: STextStyles.button( - context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .buttonTextSecondary, - ), - ), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ); - }, - )); - // } - } - } - : () { - Navigator.of(context).push( - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator - .useMaterialPageRoute, - builder: (BuildContext context) { - return SendFromView( - coin: - coinFromTickerCaseInsensitive( - model.trade! - .payInCurrency), - amount: model.sendAmount, - address: - model.trade!.payInAddress, - trade: model.trade!, - ); - }, - settings: const RouteSettings( - name: SendFromView.routeName, - ), + TextButton( + onPressed: () { + showDialog<dynamic>( + context: context, + barrierDismissible: true, + builder: (_) { + return StackDialogBase( + child: Column( + children: [ + const SizedBox( + height: 8, + ), + Center( + child: Text( + "Send ${model.sendTicker} to this address", + style: STextStyles.pageTitleH2( + context), ), - ); - }, - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - buttonTitle, - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .buttonTextSecondary, - ), - ), + ), + const SizedBox( + height: 24, + ), + Center( + child: QrImage( + // TODO: grab coin uri scheme from somewhere + // data: "${coin.uriScheme}:$receivingAddress", + data: model.trade!.payInAddress, + size: MediaQuery.of(context) + .size + .width / + 2, + foregroundColor: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + ), + const SizedBox( + height: 24, + ), + Row( + children: [ + const Spacer(), + Expanded( + child: TextButton( + onPressed: () => + Navigator.of(context).pop(), + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor( + context), + child: Text( + "Cancel", + style: STextStyles.button( + context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .buttonTextSecondary, + ), + ), + ), + ), + ], + ) + ], + ), + ); + }, ); }, + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + child: Text( + "Show QR Code", + style: STextStyles.button(context), + ), ), - ], + if (isWalletCoin) + const SizedBox( + height: 12, + ), + if (isWalletCoin) + Builder( + builder: (context) { + String buttonTitle = "Send from Stack Wallet"; + + final tuple = ref + .read(exchangeSendFromWalletIdStateProvider + .state) + .state; + if (tuple != null && + model.sendTicker.toLowerCase() == + tuple.item2.ticker.toLowerCase()) { + final walletName = ref + .read(walletsChangeNotifierProvider) + .getManager(tuple.item1) + .walletName; + buttonTitle = "Send from $walletName"; + } + + return TextButton( + onPressed: tuple != null && + model.sendTicker.toLowerCase() == + tuple.item2.ticker.toLowerCase() + ? () async { + final manager = ref + .read( + walletsChangeNotifierProvider) + .getManager(tuple.item1); + + final amount = + Format.decimalAmountToSatoshis( + model.sendAmount, + manager.coin); + final address = + model.trade!.payInAddress; + + try { + bool wasCancelled = false; + + unawaited(showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return BuildingTransactionDialog( + onCancel: () { + wasCancelled = true; + + Navigator.of(context).pop(); + }, + ); + }, + )); + + final txData = + await manager.prepareSend( + address: address, + satoshiAmount: amount, + args: { + "feeRate": FeeRateType.average, + // ref.read(feeRateTypeStateProvider) + }, + ); + + if (!wasCancelled) { + // pop building dialog + + if (mounted) { + Navigator.of(context).pop(); + } + + txData["note"] = + "${model.trade!.payInCurrency.toUpperCase()}/${model.trade!.payOutCurrency.toUpperCase()} exchange"; + txData["address"] = address; + + if (mounted) { + unawaited( + Navigator.of(context).push( + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator + .useMaterialPageRoute, + builder: (_) => + ConfirmChangeNowSendView( + transactionInfo: txData, + walletId: tuple.item1, + routeOnSuccessName: + HomeView.routeName, + trade: model.trade!, + ), + settings: + const RouteSettings( + name: + ConfirmChangeNowSendView + .routeName, + ), + ), + )); + } + } + } catch (e) { + // if (mounted) { + // pop building dialog + Navigator.of(context).pop(); + + unawaited(showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return StackDialog( + title: "Transaction failed", + message: e.toString(), + rightButton: TextButton( + style: Theme.of(context) + .extension< + StackColors>()! + .getSecondaryEnabledButtonColor( + context), + child: Text( + "Ok", + style: STextStyles.button( + context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .buttonTextSecondary, + ), + ), + onPressed: () { + Navigator.of(context) + .pop(); + }, + ), + ); + }, + )); + // } + } + } + : () { + Navigator.of(context).push( + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator + .useMaterialPageRoute, + builder: (BuildContext context) { + return SendFromView( + coin: + coinFromTickerCaseInsensitive( + model.trade! + .payInCurrency), + amount: model.sendAmount, + address: + model.trade!.payInAddress, + trade: model.trade!, + ); + }, + settings: const RouteSettings( + name: SendFromView.routeName, + ), + ), + ); + }, + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Text( + buttonTitle, + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, + ), + ), + ); + }, + ), + ], + ), ), ), ), ), - ), - ); - }, + ); + }, + ), ), ); } diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart index f13971a67..6a7fe5285 100644 --- a/lib/pages/exchange_view/send_from_view.dart +++ b/lib/pages/exchange_view/send_from_view.dart @@ -22,6 +22,7 @@ import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/animated_text.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; @@ -85,24 +86,26 @@ class _SendFromViewState extends ConsumerState<SendFromView> { return ConditionalParent( condition: !isDesktop, builder: (child) { - return Scaffold( - backgroundColor: - Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Send from", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Send from", - style: STextStyles.navBarTitle(context), + body: Padding( + padding: const EdgeInsets.all(16), + child: child, ), ), - body: Padding( - padding: const EdgeInsets.all(16), - child: child, - ), ); }, child: ConditionalParent( diff --git a/lib/pages/exchange_view/trade_details_view.dart b/lib/pages/exchange_view/trade_details_view.dart index 0b7f4b502..5679d5be5 100644 --- a/lib/pages/exchange_view/trade_details_view.dart +++ b/lib/pages/exchange_view/trade_details_view.dart @@ -28,6 +28,7 @@ import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; @@ -167,27 +168,30 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> { return ConditionalParent( condition: !isDesktop, - builder: (child) => Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( + builder: (child) => Background( + child: Scaffold( backgroundColor: Theme.of(context).extension<StackColors>()!.background, - leading: AppBarBackButton( - onPressed: () async { - Navigator.of(context).pop(); - }, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Trade details", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Trade details", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(12), - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(4), - child: child, + body: Padding( + padding: const EdgeInsets.all(12), + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), ), ), ), diff --git a/lib/pages/exchange_view/wallet_initiated_exchange_view.dart b/lib/pages/exchange_view/wallet_initiated_exchange_view.dart index c816d4fe8..915fb33ba 100644 --- a/lib/pages/exchange_view/wallet_initiated_exchange_view.dart +++ b/lib/pages/exchange_view/wallet_initiated_exchange_view.dart @@ -7,6 +7,7 @@ import 'package:stackwallet/pages/exchange_view/sub_widgets/step_row.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/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; class WalletInitiatedExchangeView extends ConsumerStatefulWidget { @@ -47,75 +48,77 @@ class _WalletInitiatedExchangeViewState Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Exchange", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Exchange", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (context, constraints) { - final width = MediaQuery.of(context).size.width - 32; - return Padding( - padding: const EdgeInsets.all(12), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - StepRow( - count: 4, - current: 0, - width: width, - ), - const SizedBox( - height: 14, - ), - Text( - "Exchange amount", - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 8, - ), - Text( - "Network fees and other exchange charges are included in the rate.", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 24, - ), - ExchangeForm( - walletId: walletId, - coin: coin, - ), - ], + body: LayoutBuilder( + builder: (context, constraints) { + final width = MediaQuery.of(context).size.width - 32; + return Padding( + padding: const EdgeInsets.all(12), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + StepRow( + count: 4, + current: 0, + width: width, + ), + const SizedBox( + height: 14, + ), + Text( + "Exchange amount", + style: STextStyles.pageTitleH1(context), + ), + const SizedBox( + height: 8, + ), + Text( + "Network fees and other exchange charges are included in the rate.", + style: STextStyles.itemSubtitle(context), + ), + const SizedBox( + height: 24, + ), + ExchangeForm( + walletId: walletId, + coin: coin, + ), + ], + ), ), ), ), ), - ), - ); - }, + ); + }, + ), ), ); } diff --git a/lib/pages/home_view/home_view.dart b/lib/pages/home_view/home_view.dart index 33744570b..5f41bfb16 100644 --- a/lib/pages/home_view/home_view.dart +++ b/lib/pages/home_view/home_view.dart @@ -20,6 +20,7 @@ import 'package:stackwallet/utilities/constants.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/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; @@ -141,129 +142,138 @@ class _HomeViewState extends ConsumerState<HomeView> { debugPrint("BUILD: $runtimeType"); return WillPopScope( onWillPop: _onWillPop, - child: Scaffold( - key: _key, - appBar: AppBar( - automaticallyImplyLeading: false, - title: Row( - children: [ - GestureDetector( - onTap: _hiddenOptions, - child: SvgPicture.asset( - Assets.svg.stackIcon(context), - width: 24, - height: 24, - ), - ), - const SizedBox( - width: 16, - ), - Text( - "My Stack", - style: STextStyles.navBarTitle(context), - ) - ], - ), - actions: [ - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("walletsViewAlertsButton"), - size: 36, - shadows: const [], - color: Theme.of(context).extension<StackColors>()!.background, - icon: SvgPicture.asset( - ref.watch(notificationsProvider - .select((value) => value.hasUnreadNotifications)) - ? Assets.svg.bellNew(context) - : Assets.svg.bell, - width: 20, - height: 20, - color: ref.watch(notificationsProvider - .select((value) => value.hasUnreadNotifications)) - ? null - : Theme.of(context) - .extension<StackColors>()! - .topNavIconPrimary, + child: Background( + child: Scaffold( + backgroundColor: Colors.transparent, + key: _key, + appBar: AppBar( + automaticallyImplyLeading: false, + backgroundColor: + Theme.of(context).extension<StackColors>()!.backgroundAppBar, + title: Row( + children: [ + GestureDetector( + onTap: _hiddenOptions, + child: SvgPicture.asset( + Assets.svg.stackIcon(context), + width: 24, + height: 24, ), - onPressed: () { - // reset unread state - ref.refresh(unreadNotificationsStateProvider); - - Navigator.of(context) - .pushNamed(NotificationsView.routeName) - .then((_) { - final Set<int> unreadNotificationIds = ref - .read(unreadNotificationsStateProvider.state) - .state; - if (unreadNotificationIds.isEmpty) return; - - List<Future<void>> futures = []; - for (int i = 0; - i < unreadNotificationIds.length - 1; - i++) { - futures.add(ref.read(notificationsProvider).markAsRead( - unreadNotificationIds.elementAt(i), false)); - } - - // wait for multiple to update if any - Future.wait(futures).then((_) { - // only notify listeners once - ref - .read(notificationsProvider) - .markAsRead(unreadNotificationIds.last, true); - }); - }); - }, ), - ), + const SizedBox( + width: 16, + ), + Text( + "My Stack", + style: STextStyles.navBarTitle(context), + ) + ], ), - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("walletsViewSettingsButton"), - size: 36, - shadows: const [], - color: Theme.of(context).extension<StackColors>()!.background, - icon: SvgPicture.asset( - Assets.svg.gear, + actions: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("walletsViewAlertsButton"), + size: 36, + shadows: const [], color: Theme.of(context) .extension<StackColors>()! - .topNavIconPrimary, - width: 20, - height: 20, + .backgroundAppBar, + icon: SvgPicture.asset( + ref.watch(notificationsProvider + .select((value) => value.hasUnreadNotifications)) + ? Assets.svg.bellNew(context) + : Assets.svg.bell, + width: 20, + height: 20, + color: ref.watch(notificationsProvider + .select((value) => value.hasUnreadNotifications)) + ? null + : Theme.of(context) + .extension<StackColors>()! + .topNavIconPrimary, + ), + onPressed: () { + // reset unread state + ref.refresh(unreadNotificationsStateProvider); + + Navigator.of(context) + .pushNamed(NotificationsView.routeName) + .then((_) { + final Set<int> unreadNotificationIds = ref + .read(unreadNotificationsStateProvider.state) + .state; + if (unreadNotificationIds.isEmpty) return; + + List<Future<void>> futures = []; + for (int i = 0; + i < unreadNotificationIds.length - 1; + i++) { + futures.add(ref + .read(notificationsProvider) + .markAsRead( + unreadNotificationIds.elementAt(i), false)); + } + + // wait for multiple to update if any + Future.wait(futures).then((_) { + // only notify listeners once + ref + .read(notificationsProvider) + .markAsRead(unreadNotificationIds.last, true); + }); + }); + }, ), - onPressed: () { - debugPrint("main view settings tapped"); - Navigator.of(context) - .pushNamed(GlobalSettingsView.routeName); - }, ), ), - ), - ], - ), - body: Container( - color: Theme.of(context).extension<StackColors>()!.background, - child: Column( + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("walletsViewSettingsButton"), + size: 36, + shadows: const [], + color: Theme.of(context) + .extension<StackColors>()! + .backgroundAppBar, + icon: SvgPicture.asset( + Assets.svg.gear, + color: Theme.of(context) + .extension<StackColors>()! + .topNavIconPrimary, + width: 20, + height: 20, + ), + onPressed: () { + debugPrint("main view settings tapped"); + Navigator.of(context) + .pushNamed(GlobalSettingsView.routeName); + }, + ), + ), + ), + ], + ), + body: Column( children: [ if (Constants.enableExchange) Container( decoration: BoxDecoration( - color: - Theme.of(context).extension<StackColors>()!.background, + color: Theme.of(context) + .extension<StackColors>()! + .backgroundAppBar, boxShadow: [ Theme.of(context) .extension<StackColors>()! diff --git a/lib/pages/intro_view.dart b/lib/pages/intro_view.dart index 494f23973..be0a9b82a 100644 --- a/lib/pages/intro_view.dart +++ b/lib/pages/intro_view.dart @@ -3,14 +3,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/stack_privacy_calls.dart'; import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; -import 'package:tuple/tuple.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'package:stackwallet/utilities/prefs.dart'; - class IntroView extends StatefulWidget { const IntroView({Key? key}) : super(key: key); @@ -32,118 +31,120 @@ class _IntroViewState extends State<IntroView> { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType "); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - body: Center( - child: !isDesktop - ? Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Spacer( - flex: 2, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 300, - ), - child: Image( - image: AssetImage( - Assets.png.stack, - ), - ), - ), - ), - const Spacer( - flex: 1, - ), - AppNameText( - isDesktop: isDesktop, - ), - const SizedBox( - height: 8, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 48, - ), - child: IntroAboutText( - isDesktop: isDesktop, - ), - ), - const Spacer( - flex: 4, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - ), - child: PrivacyAndTOSText( - isDesktop: isDesktop, - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 16, - ), - child: Row( - children: [ - Expanded( - child: GetStartedButton( - isDesktop: isDesktop, - ), - ), - ], - ), - ), - ], - ) - : SizedBox( - width: 350, - height: 540, - child: Column( + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + body: Center( + child: !isDesktop + ? Column( + crossAxisAlignment: CrossAxisAlignment.center, children: [ const Spacer( flex: 2, ), - SizedBox( - width: 130, - height: 130, - child: SvgPicture.asset( - Assets.svg.stackIcon(context), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 300, + ), + child: Image( + image: AssetImage( + Assets.png.stack, + ), + ), ), ), const Spacer( - flex: 42, + flex: 1, ), AppNameText( isDesktop: isDesktop, ), - const Spacer( - flex: 24, + const SizedBox( + height: 8, ), - IntroAboutText( - isDesktop: isDesktop, + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 48, + ), + child: IntroAboutText( + isDesktop: isDesktop, + ), ), const Spacer( - flex: 42, + flex: 4, ), - GetStartedButton( - isDesktop: isDesktop, + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: PrivacyAndTOSText( + isDesktop: isDesktop, + ), ), - const Spacer( - flex: 65, - ), - PrivacyAndTOSText( - isDesktop: isDesktop, + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 16, + ), + child: Row( + children: [ + Expanded( + child: GetStartedButton( + isDesktop: isDesktop, + ), + ), + ], + ), ), ], + ) + : SizedBox( + width: 350, + height: 540, + child: Column( + children: [ + const Spacer( + flex: 2, + ), + SizedBox( + width: 130, + height: 130, + child: SvgPicture.asset( + Assets.svg.stackIcon(context), + ), + ), + const Spacer( + flex: 42, + ), + AppNameText( + isDesktop: isDesktop, + ), + const Spacer( + flex: 24, + ), + IntroAboutText( + isDesktop: isDesktop, + ), + const Spacer( + flex: 42, + ), + GetStartedButton( + isDesktop: isDesktop, + ), + const Spacer( + flex: 65, + ), + PrivacyAndTOSText( + isDesktop: isDesktop, + ), + ], + ), ), - ), + ), ), ); } diff --git a/lib/pages/loading_view.dart b/lib/pages/loading_view.dart index c252913df..b7db1aa58 100644 --- a/lib/pages/loading_view.dart +++ b/lib/pages/loading_view.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:lottie/lottie.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; class LoadingView extends StatelessWidget { const LoadingView({Key? key}) : super(key: key); @@ -11,25 +12,27 @@ class LoadingView extends StatelessWidget { @override Widget build(BuildContext context) { final size = MediaQuery.of(context).size; - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - body: Container( - color: Theme.of(context).extension<StackColors>()!.background, - child: Center( - child: SizedBox( - width: min(size.width, size.height) * 0.5, - child: Lottie.asset( - Assets.lottie.test2, - animate: true, - repeat: true, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + body: Container( + color: Theme.of(context).extension<StackColors>()!.background, + child: Center( + child: SizedBox( + width: min(size.width, size.height) * 0.5, + child: Lottie.asset( + Assets.lottie.test2, + animate: true, + repeat: true, + ), ), + // child: Image( + // image: AssetImage( + // Assets.png.splash, + // ), + // width: MediaQuery.of(context).size.width * 0.5, + // ), ), - // child: Image( - // image: AssetImage( - // Assets.png.splash, - // ), - // width: MediaQuery.of(context).size.width * 0.5, - // ), ), ), ); diff --git a/lib/pages/notification_views/notifications_view.dart b/lib/pages/notification_views/notifications_view.dart index a034a34e4..a574e083d 100644 --- a/lib/pages/notification_views/notifications_view.dart +++ b/lib/pages/notification_views/notifications_view.dart @@ -5,6 +5,7 @@ import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/unread_notifications_provider.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -43,66 +44,68 @@ class _NotificationsViewState extends ConsumerState<NotificationsView> { .where((element) => element.walletId == widget.walletId) .toList(growable: false); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - title: Text( - "Notifications", - style: STextStyles.navBarTitle(context), + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + title: Text( + "Notifications", + style: STextStyles.navBarTitle(context), + ), + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).pop(); + }, + ), ), - leading: AppBarBackButton( - onPressed: () async { - Navigator.of(context).pop(); - }, - ), - ), - body: Padding( - padding: const EdgeInsets.all(12), - child: notifications.isNotEmpty - ? Column( - children: [ - Expanded( - child: ListView.builder( - shrinkWrap: true, - itemCount: notifications.length, - itemBuilder: (builderContext, index) { - final notification = notifications[index]; - if (notification.read == false) { - ref - .read(unreadNotificationsStateProvider.state) - .state - .add(notification.id); - } - return Padding( - padding: const EdgeInsets.all(4), - child: NotificationCard( - notification: notifications[index], - ), - ); - }, + body: Padding( + padding: const EdgeInsets.all(12), + child: notifications.isNotEmpty + ? Column( + children: [ + Expanded( + child: ListView.builder( + shrinkWrap: true, + itemCount: notifications.length, + itemBuilder: (builderContext, index) { + final notification = notifications[index]; + if (notification.read == false) { + ref + .read(unreadNotificationsStateProvider.state) + .state + .add(notification.id); + } + return Padding( + padding: const EdgeInsets.all(4), + child: NotificationCard( + notification: notifications[index], + ), + ); + }, + ), ), - ), - ], - ) - : Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.all(4), - child: RoundedWhiteContainer( - child: Center( - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - "Notifications will appear here", - style: STextStyles.itemSubtitle(context), + ], + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(4), + child: RoundedWhiteContainer( + child: Center( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + "Notifications will appear here", + style: STextStyles.itemSubtitle(context), + ), ), ), ), - ), - ) - ], - ), + ) + ], + ), + ), ), ); } diff --git a/lib/pages/pinpad_views/create_pin_view.dart b/lib/pages/pinpad_views/create_pin_view.dart index 766f10fa5..3180689c2 100644 --- a/lib/pages/pinpad_views/create_pin_view.dart +++ b/lib/pages/pinpad_views/create_pin_view.dart @@ -13,6 +13,7 @@ import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_pin_put/custom_pin_put.dart'; @@ -76,215 +77,219 @@ class _CreatePinViewState extends ConsumerState<CreatePinView> { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 70)); - } - if (mounted) { - Navigator.of(context).pop(widget.popOnSuccess); - } - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 70)); + } + if (mounted) { + Navigator.of(context).pop(widget.popOnSuccess); + } + }, + ), ), - ), - body: SafeArea( - child: PageView( - controller: _pageController, - physics: const NeverScrollableScrollPhysics(), - children: [ - // page 1 - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - "Create a PIN", - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 8, - ), - Text( - "This PIN protects access to your wallet.", - style: STextStyles.subtitle(context), - ), - const SizedBox( - height: 36, - ), - CustomPinPut( - fieldsCount: Constants.pinLength, - eachFieldHeight: 12, - eachFieldWidth: 12, - textStyle: STextStyles.label(context).copyWith( - fontSize: 1, + body: SafeArea( + child: PageView( + controller: _pageController, + physics: const NeverScrollableScrollPhysics(), + children: [ + // page 1 + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Create a PIN", + style: STextStyles.pageTitleH1(context), ), - focusNode: _pinPutFocusNode1, - controller: _pinPutController1, - useNativeKeyboard: false, - obscureText: "", - inputDecoration: InputDecoration( - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - disabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - focusedErrorBorder: InputBorder.none, - fillColor: - Theme.of(context).extension<StackColors>()!.background, - counterText: "", + const SizedBox( + height: 8, ), - submittedFieldDecoration: _pinPutDecoration.copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - border: Border.all( - width: 1, + Text( + "This PIN protects access to your wallet.", + style: STextStyles.subtitle(context), + ), + const SizedBox( + height: 36, + ), + CustomPinPut( + fieldsCount: Constants.pinLength, + eachFieldHeight: 12, + eachFieldWidth: 12, + textStyle: STextStyles.label(context).copyWith( + fontSize: 1, + ), + focusNode: _pinPutFocusNode1, + controller: _pinPutController1, + useNativeKeyboard: false, + obscureText: "", + inputDecoration: InputDecoration( + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + fillColor: Theme.of(context) + .extension<StackColors>()! + .background, + counterText: "", + ), + submittedFieldDecoration: _pinPutDecoration.copyWith( color: Theme.of(context) .extension<StackColors>()! .infoItemIcons, + border: Border.all( + width: 1, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + ), ), - ), - selectedFieldDecoration: _pinPutDecoration, - followingFieldDecoration: _pinPutDecoration, - onSubmit: (String pin) { - if (pin.length == Constants.pinLength) { - _pageController.nextPage( - duration: const Duration(milliseconds: 300), - curve: Curves.linear, - ); - } - }, - ), - ], - ), - - // page 2 - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - "Confirm PIN", - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 8, - ), - Text( - "This PIN protects access to your wallet.", - style: STextStyles.subtitle(context), - ), - const SizedBox( - height: 36, - ), - CustomPinPut( - fieldsCount: Constants.pinLength, - eachFieldHeight: 12, - eachFieldWidth: 12, - textStyle: STextStyles.infoSmall(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle3, - fontSize: 1, - ), - focusNode: _pinPutFocusNode2, - controller: _pinPutController2, - useNativeKeyboard: false, - obscureText: "", - inputDecoration: InputDecoration( - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - disabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - focusedErrorBorder: InputBorder.none, - fillColor: - Theme.of(context).extension<StackColors>()!.background, - counterText: "", - ), - submittedFieldDecoration: _pinPutDecoration.copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - border: Border.all( - width: 1, - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - ), - ), - selectedFieldDecoration: _pinPutDecoration, - followingFieldDecoration: _pinPutDecoration, - onSubmit: (String pin) async { - // _onSubmitCount++; - // if (_onSubmitCount - _onSubmitFailCount > 1) return; - - if (_pinPutController1.text == _pinPutController2.text) { - // ask if want to use biometrics - final bool useBiometrics = (Platform.isLinux) - ? false - : await biometrics.authenticate( - cancelButtonText: "SKIP", - localizedReason: - "You can use your fingerprint to unlock the wallet and confirm transactions.", - title: "Enable fingerprint authentication", - ); - - //TODO investigate why this crashes IOS, maybe ios persists securestorage even after an uninstall? - // This should never fail as we are writing a new pin - // assert( - // (await _secureStore.read(key: "stack_pin")) == null); - // possible alternative to the above but it does not guarantee we aren't overwriting a pin - // if (!Platform.isLinux) - // assert((await _secureStore.read(key: "stack_pin")) == - // null); - assert(ref.read(prefsChangeNotifierProvider).hasPin == - false); - - await _secureStore.write(key: "stack_pin", value: pin); - - ref.read(prefsChangeNotifierProvider).useBiometrics = - useBiometrics; - ref.read(prefsChangeNotifierProvider).hasPin = true; - - await Future<void>.delayed( - const Duration(milliseconds: 200)); - - if (mounted) { - if (!widget.popOnSuccess) { - Navigator.of(context).pushNamedAndRemoveUntil( - HomeView.routeName, - (route) => false, - ); - } else { - Navigator.of(context).pop(); - } + selectedFieldDecoration: _pinPutDecoration, + followingFieldDecoration: _pinPutDecoration, + onSubmit: (String pin) { + if (pin.length == Constants.pinLength) { + _pageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.linear, + ); } - } else { - // _onSubmitFailCount++; - _pageController.animateTo( - 0, - duration: const Duration(milliseconds: 300), - curve: Curves.linear, - ); + }, + ), + ], + ), - showFloatingFlushBar( - type: FlushBarType.warning, - message: "PIN codes do not match. Try again.", - context: context, - iconAsset: Assets.svg.alertCircle, - ); + // page 2 + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Confirm PIN", + style: STextStyles.pageTitleH1(context), + ), + const SizedBox( + height: 8, + ), + Text( + "This PIN protects access to your wallet.", + style: STextStyles.subtitle(context), + ), + const SizedBox( + height: 36, + ), + CustomPinPut( + fieldsCount: Constants.pinLength, + eachFieldHeight: 12, + eachFieldWidth: 12, + textStyle: STextStyles.infoSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle3, + fontSize: 1, + ), + focusNode: _pinPutFocusNode2, + controller: _pinPutController2, + useNativeKeyboard: false, + obscureText: "", + inputDecoration: InputDecoration( + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + fillColor: Theme.of(context) + .extension<StackColors>()! + .background, + counterText: "", + ), + submittedFieldDecoration: _pinPutDecoration.copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + border: Border.all( + width: 1, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + ), + ), + selectedFieldDecoration: _pinPutDecoration, + followingFieldDecoration: _pinPutDecoration, + onSubmit: (String pin) async { + // _onSubmitCount++; + // if (_onSubmitCount - _onSubmitFailCount > 1) return; - _pinPutController1.text = ''; - _pinPutController2.text = ''; - } - }, - ), - ], - ), - ], + if (_pinPutController1.text == _pinPutController2.text) { + // ask if want to use biometrics + final bool useBiometrics = (Platform.isLinux) + ? false + : await biometrics.authenticate( + cancelButtonText: "SKIP", + localizedReason: + "You can use your fingerprint to unlock the wallet and confirm transactions.", + title: "Enable fingerprint authentication", + ); + + //TODO investigate why this crashes IOS, maybe ios persists securestorage even after an uninstall? + // This should never fail as we are writing a new pin + // assert( + // (await _secureStore.read(key: "stack_pin")) == null); + // possible alternative to the above but it does not guarantee we aren't overwriting a pin + // if (!Platform.isLinux) + // assert((await _secureStore.read(key: "stack_pin")) == + // null); + assert(ref.read(prefsChangeNotifierProvider).hasPin == + false); + + await _secureStore.write(key: "stack_pin", value: pin); + + ref.read(prefsChangeNotifierProvider).useBiometrics = + useBiometrics; + ref.read(prefsChangeNotifierProvider).hasPin = true; + + await Future<void>.delayed( + const Duration(milliseconds: 200)); + + if (mounted) { + if (!widget.popOnSuccess) { + Navigator.of(context).pushNamedAndRemoveUntil( + HomeView.routeName, + (route) => false, + ); + } else { + Navigator.of(context).pop(); + } + } + } else { + // _onSubmitFailCount++; + _pageController.animateTo( + 0, + duration: const Duration(milliseconds: 300), + curve: Curves.linear, + ); + + showFloatingFlushBar( + type: FlushBarType.warning, + message: "PIN codes do not match. Try again.", + context: context, + iconAsset: Assets.svg.alertCircle, + ); + + _pinPutController1.text = ''; + _pinPutController2.text = ''; + } + }, + ), + ], + ), + ], + ), ), ), ); diff --git a/lib/pages/pinpad_views/lock_screen_view.dart b/lib/pages/pinpad_views/lock_screen_view.dart index 60d317e21..455d5ee3b 100644 --- a/lib/pages/pinpad_views/lock_screen_view.dart +++ b/lib/pages/pinpad_views/lock_screen_view.dart @@ -17,6 +17,7 @@ import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_pin_put/custom_pin_put.dart'; import 'package:stackwallet/widgets/shake/shake.dart'; @@ -161,173 +162,177 @@ class _LockscreenViewState extends ConsumerState<LockscreenView> { late SecureStorageInterface _secureStore; late Biometrics biometrics; - Scaffold get _body => Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: widget.showBackButton - ? AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration(milliseconds: 70)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ) - : Container(), - ), - body: SafeArea( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Shake( - animationDuration: const Duration(milliseconds: 700), - animationRange: 12, - controller: _shakeController, - child: Center( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Center( - child: Text( - "Enter PIN", - style: STextStyles.pageTitleH1(context), + Widget get _body => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: widget.showBackButton + ? AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 70)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ) + : Container(), + ), + body: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Shake( + animationDuration: const Duration(milliseconds: 700), + animationRange: 12, + controller: _shakeController, + child: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: Text( + "Enter PIN", + style: STextStyles.pageTitleH1(context), + ), ), - ), - const SizedBox( - height: 52, - ), - CustomPinPut( - fieldsCount: Constants.pinLength, - eachFieldHeight: 12, - eachFieldWidth: 12, - textStyle: STextStyles.label(context).copyWith( - fontSize: 1, + const SizedBox( + height: 52, ), - focusNode: _pinFocusNode, - controller: _pinTextController, - useNativeKeyboard: false, - obscureText: "", - inputDecoration: InputDecoration( - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - disabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - focusedErrorBorder: InputBorder.none, - fillColor: Theme.of(context) - .extension<StackColors>()! - .background, - counterText: "", - ), - submittedFieldDecoration: _pinPutDecoration.copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - border: Border.all( - width: 1, + CustomPinPut( + fieldsCount: Constants.pinLength, + eachFieldHeight: 12, + eachFieldWidth: 12, + textStyle: STextStyles.label(context).copyWith( + fontSize: 1, + ), + focusNode: _pinFocusNode, + controller: _pinTextController, + useNativeKeyboard: false, + obscureText: "", + inputDecoration: InputDecoration( + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + fillColor: Theme.of(context) + .extension<StackColors>()! + .background, + counterText: "", + ), + submittedFieldDecoration: _pinPutDecoration.copyWith( color: Theme.of(context) .extension<StackColors>()! .infoItemIcons, + border: Border.all( + width: 1, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + ), ), - ), - selectedFieldDecoration: _pinPutDecoration, - followingFieldDecoration: _pinPutDecoration, - onSubmit: (String pin) async { - _attempts++; + selectedFieldDecoration: _pinPutDecoration, + followingFieldDecoration: _pinPutDecoration, + onSubmit: (String pin) async { + _attempts++; - if (_attempts > maxAttemptsBeforeThrottling) { - _attemptLock = true; - switch (_attempts) { - case 4: - _timeout = const Duration(seconds: 30); - break; + if (_attempts > maxAttemptsBeforeThrottling) { + _attemptLock = true; + switch (_attempts) { + case 4: + _timeout = const Duration(seconds: 30); + break; - case 5: - _timeout = const Duration(seconds: 60); - break; + case 5: + _timeout = const Duration(seconds: 60); + break; - case 6: - _timeout = const Duration(minutes: 5); - break; + case 6: + _timeout = const Duration(minutes: 5); + break; - case 7: - _timeout = const Duration(minutes: 10); - break; + case 7: + _timeout = const Duration(minutes: 10); + break; - case 8: - _timeout = const Duration(minutes: 20); - break; + case 8: + _timeout = const Duration(minutes: 20); + break; - case 9: - _timeout = const Duration(minutes: 30); - break; + case 9: + _timeout = const Duration(minutes: 30); + break; - default: - _timeout = const Duration(minutes: 60); + default: + _timeout = const Duration(minutes: 60); + } + + unawaited( + Future<void>.delayed(_timeout).then((_) { + _attemptLock = false; + _attempts = 0; + })); } - unawaited(Future<void>.delayed(_timeout).then((_) { - _attemptLock = false; - _attempts = 0; - })); - } + if (_attemptLock) { + String prettyTime = ""; + if (_timeout.inSeconds >= 60) { + prettyTime += "${_timeout.inMinutes} minutes"; + } else { + prettyTime += "${_timeout.inSeconds} seconds"; + } - if (_attemptLock) { - String prettyTime = ""; - if (_timeout.inSeconds >= 60) { - prettyTime += "${_timeout.inMinutes} minutes"; + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: + "Incorrect PIN entered too many times. Please wait $prettyTime", + context: context, + iconAsset: Assets.svg.alertCircle, + )); + + await Future<void>.delayed( + const Duration(milliseconds: 100)); + + _pinTextController.text = ''; + + return; + } + + final storedPin = + await _secureStore.read(key: 'stack_pin'); + + if (storedPin == pin) { + await Future<void>.delayed( + const Duration(milliseconds: 200)); + unawaited(_onUnlock()); } else { - prettyTime += "${_timeout.inSeconds} seconds"; + unawaited(_shakeController.shake()); + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "Incorrect PIN. Please try again", + context: context, + iconAsset: Assets.svg.alertCircle, + )); + + await Future<void>.delayed( + const Duration(milliseconds: 100)); + + _pinTextController.text = ''; } - - unawaited(showFloatingFlushBar( - type: FlushBarType.warning, - message: - "Incorrect PIN entered too many times. Please wait $prettyTime", - context: context, - iconAsset: Assets.svg.alertCircle, - )); - - await Future<void>.delayed( - const Duration(milliseconds: 100)); - - _pinTextController.text = ''; - - return; - } - - final storedPin = - await _secureStore.read(key: 'stack_pin'); - - if (storedPin == pin) { - await Future<void>.delayed( - const Duration(milliseconds: 200)); - unawaited(_onUnlock()); - } else { - unawaited(_shakeController.shake()); - unawaited(showFloatingFlushBar( - type: FlushBarType.warning, - message: "Incorrect PIN. Please try again", - context: context, - iconAsset: Assets.svg.alertCircle, - )); - - await Future<void>.delayed( - const Duration(milliseconds: 100)); - - _pinTextController.text = ''; - } - }, - ), - ], + }, + ), + ], + ), ), ), - ), - ], + ], + ), ), ), ); diff --git a/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart b/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart index 05cedb148..46b48bb42 100644 --- a/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart +++ b/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart @@ -23,6 +23,7 @@ import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; @@ -305,48 +306,51 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> { return ConditionalParent( condition: !isDesktop, - builder: (child) => Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 70)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 70)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Generate QR code", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Generate QR code", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (buildContext, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: child, + body: LayoutBuilder( + builder: (buildContext, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), ), ), ), - ), - ); - }, + ); + }, + ), ), ), child: Padding( @@ -530,7 +534,7 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> { }); } : onGeneratePressed, - buttonHeight: ButtonHeight.l, + buttonHeight: isDesktop ? ButtonHeight.l : null, ), if (isDesktop && didGenerate) Row( @@ -586,6 +590,8 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> { if (!isDesktop) SecondaryButton( width: 170, + buttonHeight: + isDesktop ? ButtonHeight.l : null, onPressed: () async { await _capturePng(false); }, @@ -605,7 +611,8 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> { ), PrimaryButton( width: 170, - buttonHeight: ButtonHeight.l, + buttonHeight: + isDesktop ? ButtonHeight.l : null, onPressed: () async { // TODO: add save functionality instead of share // save works on linux at the moment diff --git a/lib/pages/receive_view/receive_view.dart b/lib/pages/receive_view/receive_view.dart index 50e0ffd5e..1ba00f8ee 100644 --- a/lib/pages/receive_view/receive_view.dart +++ b/lib/pages/receive_view/receive_view.dart @@ -12,9 +12,9 @@ import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/custom_loading_overlay.dart'; @@ -115,147 +115,149 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> { } }); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Receive ${coin.ticker}", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Receive ${coin.ticker}", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(12), - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - GestureDetector( - onTap: () { - clipboard.setData( - ClipboardData(text: receivingAddress), - ); - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - iconAsset: Assets.svg.copy, - context: context, - ); - }, - child: RoundedWhiteContainer( - child: Column( - children: [ - Row( - children: [ - Text( - "Your ${coin.ticker} address", - style: STextStyles.itemSubtitle(context), - ), - const Spacer(), - Row( - children: [ - SvgPicture.asset( - Assets.svg.copy, - width: 10, - height: 10, - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - ), - const SizedBox( - width: 4, - ), - Text( - "Copy", - style: STextStyles.link2(context), - ), - ], - ), - ], - ), - const SizedBox( - height: 4, - ), - Row( - children: [ - Expanded( - child: Text( - receivingAddress, - style: STextStyles.itemSubtitle12(context), - ), - ), - ], - ), - ], - ), - ), - ), - if (coin != Coin.epicCash) - const SizedBox( - height: 12, - ), - if (coin != Coin.epicCash) - TextButton( - onPressed: generateNewAddress, - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Generate new address", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - ), - ), - const SizedBox( - height: 30, - ), - RoundedWhiteContainer( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Center( + body: Padding( + padding: const EdgeInsets.all(12), + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + GestureDetector( + onTap: () { + clipboard.setData( + ClipboardData(text: receivingAddress), + ); + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ); + }, + child: RoundedWhiteContainer( child: Column( children: [ - QrImage( - data: "${coin.uriScheme}:$receivingAddress", - size: MediaQuery.of(context).size.width / 2, - foregroundColor: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - const SizedBox( - height: 20, + Row( + children: [ + Text( + "Your ${coin.ticker} address", + style: STextStyles.itemSubtitle(context), + ), + const Spacer(), + Row( + children: [ + SvgPicture.asset( + Assets.svg.copy, + width: 10, + height: 10, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + ), + const SizedBox( + width: 4, + ), + Text( + "Copy", + style: STextStyles.link2(context), + ), + ], + ), + ], ), - BlueTextButton( - text: "Create new QR code", - onTap: () async { - unawaited(Navigator.of(context).push( - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator.useMaterialPageRoute, - builder: (_) => GenerateUriQrCodeView( - coin: coin, - receivingAddress: receivingAddress, - ), - settings: const RouteSettings( - name: GenerateUriQrCodeView.routeName, - ), + const SizedBox( + height: 4, + ), + Row( + children: [ + Expanded( + child: Text( + receivingAddress, + style: STextStyles.itemSubtitle12(context), ), - )); - }, + ), + ], ), ], ), ), ), - ), - ], + if (coin != Coin.epicCash) + const SizedBox( + height: 12, + ), + if (coin != Coin.epicCash) + TextButton( + onPressed: generateNewAddress, + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Generate new address", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + const SizedBox( + height: 30, + ), + RoundedWhiteContainer( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Center( + child: Column( + children: [ + QrImage( + data: "${coin.uriScheme}:$receivingAddress", + size: MediaQuery.of(context).size.width / 2, + foregroundColor: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + const SizedBox( + height: 20, + ), + BlueTextButton( + text: "Create new QR code", + onTap: () async { + unawaited(Navigator.of(context).push( + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator.useMaterialPageRoute, + builder: (_) => GenerateUriQrCodeView( + coin: coin, + receivingAddress: receivingAddress, + ), + settings: const RouteSettings( + name: GenerateUriQrCodeView.routeName, + ), + ), + )); + }, + ), + ], + ), + ), + ), + ), + ], + ), ), ), ), diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index 16de6cf55..f1075b6e6 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -22,6 +22,7 @@ import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; @@ -203,48 +204,51 @@ class _ConfirmTransactionViewState return ConditionalParent( condition: !isDesktop, - builder: (child) => Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( + builder: (child) => Background( + child: Scaffold( backgroundColor: Theme.of(context).extension<StackColors>()!.background, - leading: AppBarBackButton( - onPressed: () async { - // if (FocusScope.of(context).hasFocus) { - // FocusScope.of(context).unfocus(); - // await Future<void>.delayed(Duration(milliseconds: 50)); - // } - Navigator.of(context).pop(); - }, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + leading: AppBarBackButton( + onPressed: () async { + // if (FocusScope.of(context).hasFocus) { + // FocusScope.of(context).unfocus(); + // await Future<void>.delayed(Duration(milliseconds: 50)); + // } + Navigator.of(context).pop(); + }, + ), + title: Text( + "Confirm transaction", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Confirm transaction", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (builderContext, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: child, + body: LayoutBuilder( + builder: (builderContext, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), ), ), ), - ), - ); - }, + ); + }, + ), ), ), child: ConditionalParent( diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 2539b89ab..a89a20d85 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -33,6 +33,7 @@ import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/animated_text.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/icon_widgets/addressbook_icon.dart'; @@ -379,325 +380,464 @@ class _SendViewState extends ConsumerState<SendView> { }); } - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 50)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 50)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Send ${coin.ticker}", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Send ${coin.ticker}", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (builderContext, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - // subtract top and bottom padding set in parent - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Container( - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .popupBG, + body: LayoutBuilder( + builder: (builderContext, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + // subtract top and bottom padding set in parent + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .popupBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.iconFor(coin: coin), + width: 22, + height: 22, + ), + const SizedBox( + width: 6, + ), + if (coin != Coin.firo && + coin != Coin.firoTestNet) + Expanded( + child: Text( + ref.watch(provider.select( + (value) => value.walletName)), + style: STextStyles.titleBold12(context), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + if (coin == Coin.firo || + coin == Coin.firoTestNet) + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + ref.watch(provider.select( + (value) => value.walletName)), + style: + STextStyles.titleBold12(context) + .copyWith(fontSize: 14), + ), + // const SizedBox( + // height: 2, + // ), + Text( + "${ref.watch(publicPrivateBalanceStateProvider.state).state} balance", + style: STextStyles.label(context) + .copyWith(fontSize: 10), + ), + ], + ), + if (coin != Coin.firo && + coin != Coin.firoTestNet) + const SizedBox( + width: 10, + ), + if (coin == Coin.firo || + coin == Coin.firoTestNet) + const Spacer(), + FutureBuilder( + future: (coin != Coin.firo && + coin != Coin.firoTestNet) + ? ref.watch(provider.select( + (value) => value.availableBalance)) + : ref + .watch( + publicPrivateBalanceStateProvider + .state) + .state == + "Private" + ? (ref.watch(provider).wallet + as FiroWallet) + .availablePrivateBalance() + : (ref.watch(provider).wallet + as FiroWallet) + .availablePublicBalance(), + builder: + (_, AsyncSnapshot<Decimal> snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + _cachedBalance = snapshot.data!; + } + + if (_cachedBalance != null) { + return GestureDetector( + onTap: () { + cryptoAmountController.text = + _cachedBalance!.toStringAsFixed( + Constants + .decimalPlacesForCoin( + coin)); + }, + child: Container( + color: Colors.transparent, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.end, + children: [ + Text( + "${Format.localizedStringAsFixed( + value: _cachedBalance!, + locale: locale, + decimalPlaces: 8, + )} ${coin.ticker}", + style: + STextStyles.titleBold12( + context) + .copyWith( + fontSize: 10, + ), + textAlign: TextAlign.right, + ), + Text( + "${Format.localizedStringAsFixed( + value: _cachedBalance! * + ref.watch(priceAnd24hChangeNotifierProvider + .select((value) => + value + .getPrice( + coin) + .item1)), + locale: locale, + decimalPlaces: 2, + )} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", + style: STextStyles + .titleBold12_400( + context) + .copyWith( + fontSize: 8, + ), + textAlign: TextAlign.right, + ) + ], + ), + ), + ); + } else { + return Column( + crossAxisAlignment: + CrossAxisAlignment.end, + children: [ + AnimatedText( + stringsToLoopThrough: const [ + "Loading balance ", + "Loading balance. ", + "Loading balance.. ", + "Loading balance...", + ], + style: STextStyles.itemSubtitle( + context) + .copyWith( + fontSize: 10, + ), + ), + const SizedBox( + height: 2, + ), + AnimatedText( + stringsToLoopThrough: const [ + "Loading balance ", + "Loading balance. ", + "Loading balance.. ", + "Loading balance...", + ], + style: STextStyles.itemSubtitle( + context) + .copyWith( + fontSize: 8, + ), + ) + ], + ); + } + }, + ), + ], + ), + ), + ), + const SizedBox( + height: 16, + ), + Text( + "Send to", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + const SizedBox( + height: 8, + ), + ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), - ), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.iconFor(coin: coin), - width: 22, - height: 22, - ), - const SizedBox( - width: 6, - ), - if (coin != Coin.firo && - coin != Coin.firoTestNet) - Expanded( - child: Text( - ref.watch(provider - .select((value) => value.walletName)), - style: STextStyles.titleBold12(context), - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - ), - if (coin == Coin.firo || - coin == Coin.firoTestNet) - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - ref.watch(provider.select( - (value) => value.walletName)), - style: STextStyles.titleBold12(context) - .copyWith(fontSize: 14), - ), - // const SizedBox( - // height: 2, - // ), - Text( - "${ref.watch(publicPrivateBalanceStateProvider.state).state} balance", - style: STextStyles.label(context) - .copyWith(fontSize: 10), - ), - ], - ), - if (coin != Coin.firo && - coin != Coin.firoTestNet) - const SizedBox( - width: 10, - ), - if (coin == Coin.firo || - coin == Coin.firoTestNet) - const Spacer(), - FutureBuilder( - future: (coin != Coin.firo && - coin != Coin.firoTestNet) - ? ref.watch(provider.select( - (value) => value.availableBalance)) - : ref - .watch( - publicPrivateBalanceStateProvider - .state) - .state == - "Private" - ? (ref.watch(provider).wallet - as FiroWallet) - .availablePrivateBalance() - : (ref.watch(provider).wallet - as FiroWallet) - .availablePublicBalance(), - builder: - (_, AsyncSnapshot<Decimal> snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - _cachedBalance = snapshot.data!; - } - - if (_cachedBalance != null) { - return GestureDetector( - onTap: () { - cryptoAmountController.text = - _cachedBalance!.toStringAsFixed( - Constants - .decimalPlacesForCoin( - coin)); - }, - child: Container( - color: Colors.transparent, - child: Column( - crossAxisAlignment: - CrossAxisAlignment.end, - children: [ - Text( - "${Format.localizedStringAsFixed( - value: _cachedBalance!, - locale: locale, - decimalPlaces: 8, - )} ${coin.ticker}", - style: STextStyles.titleBold12( - context) - .copyWith( - fontSize: 10, - ), - textAlign: TextAlign.right, - ), - Text( - "${Format.localizedStringAsFixed( - value: _cachedBalance! * - ref.watch( - priceAnd24hChangeNotifierProvider - .select((value) => - value - .getPrice( - coin) - .item1)), - locale: locale, - decimalPlaces: 2, - )} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", - style: - STextStyles.titleBold12_400( - context) - .copyWith( - fontSize: 8, - ), - textAlign: TextAlign.right, - ) - ], - ), - ), - ); - } else { - return Column( - crossAxisAlignment: - CrossAxisAlignment.end, - children: [ - AnimatedText( - stringsToLoopThrough: const [ - "Loading balance ", - "Loading balance. ", - "Loading balance.. ", - "Loading balance...", - ], - style: STextStyles.itemSubtitle( - context) - .copyWith( - fontSize: 10, - ), - ), - const SizedBox( - height: 2, - ), - AnimatedText( - stringsToLoopThrough: const [ - "Loading balance ", - "Loading balance. ", - "Loading balance.. ", - "Loading balance...", - ], - style: STextStyles.itemSubtitle( - context) - .copyWith( - fontSize: 8, - ), - ) - ], - ); - } - }, - ), - ], - ), - ), - ), - const SizedBox( - height: 16, - ), - Text( - "Send to", - style: STextStyles.smallMed12(context), - textAlign: TextAlign.left, - ), - const SizedBox( - height: 8, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("sendViewAddressFieldKey"), - controller: sendToController, - readOnly: false, - autocorrect: false, - enableSuggestions: false, - // inputFormatters: <TextInputFormatter>[ - // FilteringTextInputFormatter.allow( - // RegExp("[a-zA-Z0-9]{34}")), - // ], - toolbarOptions: const ToolbarOptions( - copy: false, - cut: false, - paste: true, - selectAll: false, - ), - onChanged: (newValue) { - _address = newValue; - _updatePreviewButtonState( - _address, _amountToSend); - - setState(() { - _addressToggleFlag = newValue.isNotEmpty; - }); - }, - focusNode: _addressFocusNode, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter ${coin.ticker} address", - _addressFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, + child: TextField( + key: const Key("sendViewAddressFieldKey"), + controller: sendToController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + // inputFormatters: <TextInputFormatter>[ + // FilteringTextInputFormatter.allow( + // RegExp("[a-zA-Z0-9]{34}")), + // ], + toolbarOptions: const ToolbarOptions( + copy: false, + cut: false, + paste: true, + selectAll: false, ), - suffixIcon: Padding( - padding: sendToController.text.isEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - _addressToggleFlag - ? TextFieldIconButton( - key: const Key( - "sendViewClearAddressFieldButtonKey"), - onTap: () { - sendToController.text = ""; - _address = ""; - _updatePreviewButtonState( - _address, _amountToSend); - setState(() { - _addressToggleFlag = false; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - key: const Key( - "sendViewPasteAddressFieldButtonKey"), - onTap: () async { - final ClipboardData? data = - await clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - String content = - data.text!.trim(); - if (content.contains("\n")) { - content = content.substring( - 0, - content.indexOf("\n")); + onChanged: (newValue) { + _address = newValue; + _updatePreviewButtonState( + _address, _amountToSend); + + setState(() { + _addressToggleFlag = newValue.isNotEmpty; + }); + }, + focusNode: _addressFocusNode, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Enter ${coin.ticker} address", + _addressFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: sendToController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceAround, + children: [ + _addressToggleFlag + ? TextFieldIconButton( + key: const Key( + "sendViewClearAddressFieldButtonKey"), + onTap: () { + sendToController.text = ""; + _address = ""; + _updatePreviewButtonState( + _address, _amountToSend); + setState(() { + _addressToggleFlag = false; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "sendViewPasteAddressFieldButtonKey"), + onTap: () async { + final ClipboardData? data = + await clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + String content = + data.text!.trim(); + if (content + .contains("\n")) { + content = + content.substring( + 0, + content.indexOf( + "\n")); + } + + sendToController.text = + content; + _address = content; + + _updatePreviewButtonState( + _address, + _amountToSend); + setState(() { + _addressToggleFlag = + sendToController + .text.isNotEmpty; + }); + } + }, + child: sendToController + .text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (sendToController.text.isEmpty) + TextFieldIconButton( + key: const Key( + "sendViewAddressBookButtonKey"), + onTap: () { + Navigator.of(context).pushNamed( + AddressBookView.routeName, + arguments: widget.coin, + ); + }, + child: const AddressBookIcon(), + ), + if (sendToController.text.isEmpty) + TextFieldIconButton( + key: const Key( + "sendViewScanQrButtonKey"), + onTap: () async { + try { + // ref + // .read( + // shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = false; + if (FocusScope.of(context) + .hasFocus) { + FocusScope.of(context) + .unfocus(); + await Future<void>.delayed( + const Duration( + milliseconds: 75)); + } + + final qrResult = + await scanner.scan(); + + // Future<void>.delayed( + // const Duration(seconds: 2), + // () => ref + // .read( + // shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = true, + // ); + + Logging.instance.log( + "qrResult content: ${qrResult.rawContent}", + level: LogLevel.Info); + + final results = + AddressUtils.parseUri( + qrResult.rawContent); + + Logging.instance.log( + "qrResult parsed: $results", + level: LogLevel.Info); + + if (results.isNotEmpty && + results["scheme"] == + coin.uriScheme) { + // auto fill address + _address = + results["address"] ?? ""; + sendToController.text = + _address!; + + // autofill notes field + if (results["message"] != + null) { + noteController.text = + results["message"]!; + } else if (results["label"] != + null) { + noteController.text = + results["label"]!; } + // autofill amount field + if (results["amount"] != + null) { + final amount = + Decimal.parse( + results["amount"]!); + cryptoAmountController + .text = + Format + .localizedStringAsFixed( + value: amount, + locale: ref + .read( + localeServiceChangeNotifierProvider) + .locale, + decimalPlaces: Constants + .decimalPlacesForCoin( + coin), + ); + amount.toString(); + _amountToSend = amount; + } + + _updatePreviewButtonState( + _address, _amountToSend); + setState(() { + _addressToggleFlag = + sendToController + .text.isNotEmpty; + }); + + // now check for non standard encoded basic address + } else if (ref + .read( + walletsChangeNotifierProvider) + .getManager(walletId) + .validateAddress( + qrResult.rawContent)) { + _address = + qrResult.rawContent; sendToController.text = - content; - _address = content; + _address ?? ""; _updatePreviewButtonState( _address, _amountToSend); @@ -707,211 +847,498 @@ class _SendViewState extends ConsumerState<SendView> { .text.isNotEmpty; }); } - }, - child: - sendToController.text.isEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - if (sendToController.text.isEmpty) - TextFieldIconButton( - key: const Key( - "sendViewAddressBookButtonKey"), - onTap: () { - Navigator.of(context).pushNamed( - AddressBookView.routeName, - arguments: widget.coin, - ); - }, - child: const AddressBookIcon(), - ), - if (sendToController.text.isEmpty) - TextFieldIconButton( - key: const Key( - "sendViewScanQrButtonKey"), - onTap: () async { - try { - // ref - // .read( - // shouldShowLockscreenOnResumeStateProvider - // .state) - // .state = false; - if (FocusScope.of(context) - .hasFocus) { - FocusScope.of(context) - .unfocus(); - await Future<void>.delayed( - const Duration( - milliseconds: 75)); + } on PlatformException catch (e, s) { + // ref + // .read( + // shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = true; + // here we ignore the exception caused by not giving permission + // to use the camera to scan a qr code + Logging.instance.log( + "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", + level: LogLevel.Warning); } - - final qrResult = - await scanner.scan(); - - // Future<void>.delayed( - // const Duration(seconds: 2), - // () => ref - // .read( - // shouldShowLockscreenOnResumeStateProvider - // .state) - // .state = true, - // ); - - Logging.instance.log( - "qrResult content: ${qrResult.rawContent}", - level: LogLevel.Info); - - final results = - AddressUtils.parseUri( - qrResult.rawContent); - - Logging.instance.log( - "qrResult parsed: $results", - level: LogLevel.Info); - - if (results.isNotEmpty && - results["scheme"] == - coin.uriScheme) { - // auto fill address - _address = - results["address"] ?? ""; - sendToController.text = - _address!; - - // autofill notes field - if (results["message"] != - null) { - noteController.text = - results["message"]!; - } else if (results["label"] != - null) { - noteController.text = - results["label"]!; - } - - // autofill amount field - if (results["amount"] != null) { - final amount = Decimal.parse( - results["amount"]!); - cryptoAmountController.text = - Format - .localizedStringAsFixed( - value: amount, - locale: ref - .read( - localeServiceChangeNotifierProvider) - .locale, - decimalPlaces: Constants - .decimalPlacesForCoin( - coin), - ); - amount.toString(); - _amountToSend = amount; - } - - _updatePreviewButtonState( - _address, _amountToSend); - setState(() { - _addressToggleFlag = - sendToController - .text.isNotEmpty; - }); - - // now check for non standard encoded basic address - } else if (ref - .read( - walletsChangeNotifierProvider) - .getManager(walletId) - .validateAddress( - qrResult.rawContent)) { - _address = qrResult.rawContent; - sendToController.text = - _address ?? ""; - - _updatePreviewButtonState( - _address, _amountToSend); - setState(() { - _addressToggleFlag = - sendToController - .text.isNotEmpty; - }); - } - } on PlatformException catch (e, s) { - // ref - // .read( - // shouldShowLockscreenOnResumeStateProvider - // .state) - // .state = true; - // here we ignore the exception caused by not giving permission - // to use the camera to scan a qr code - Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", - level: LogLevel.Warning); - } - }, - child: const QrCodeIcon(), - ) - ], + }, + child: const QrCodeIcon(), + ) + ], + ), ), ), ), ), ), - ), - Builder( - builder: (_) { - final error = _updateInvalidAddressText( - _address ?? "", - ref - .read(walletsChangeNotifierProvider) - .getManager(walletId), - ); + Builder( + builder: (_) { + final error = _updateInvalidAddressText( + _address ?? "", + ref + .read(walletsChangeNotifierProvider) + .getManager(walletId), + ); - if (error == null || error.isEmpty) { - return Container(); - } else { - return Align( - alignment: Alignment.topLeft, - child: Padding( - padding: const EdgeInsets.only( - left: 12.0, - top: 4.0, - ), - child: Text( - error, - textAlign: TextAlign.left, - style: STextStyles.label(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textError, + if (error == null || error.isEmpty) { + return Container(); + } else { + return Align( + alignment: Alignment.topLeft, + child: Padding( + padding: const EdgeInsets.only( + left: 12.0, + top: 4.0, + ), + child: Text( + error, + textAlign: TextAlign.left, + style: + STextStyles.label(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textError, + ), ), ), + ); + } + }, + ), + if (coin == Coin.firo) + const SizedBox( + height: 12, + ), + if (coin == Coin.firo) + Text( + "Send from", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + if (coin == Coin.firo) + const SizedBox( + height: 8, + ), + if (coin == Coin.firo) + Stack( + children: [ + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: + Util.isDesktop ? false : true, + readOnly: true, + textInputAction: TextInputAction.none, ), - ); - } - }, - ), - if (coin == Coin.firo) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + ), + child: RawMaterialButton( + splashColor: Theme.of(context) + .extension<StackColors>()! + .highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + showModalBottomSheet<dynamic>( + backgroundColor: Colors.transparent, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + builder: (_) => + FiroBalanceSelectionSheet( + walletId: walletId, + ), + ); + }, + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + "${ref.watch(publicPrivateBalanceStateProvider.state).state} balance", + style: STextStyles.itemSubtitle12( + context), + ), + const SizedBox( + width: 10, + ), + FutureBuilder( + future: _firoBalanceFuture( + provider, locale), + builder: (context, + AsyncSnapshot<String?> + snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + if (ref + .read( + publicPrivateBalanceStateProvider + .state) + .state == + "Private") { + _privateBalanceString = + snapshot.data!; + } else { + _publicBalanceString = + snapshot.data!; + } + } + if (ref + .read( + publicPrivateBalanceStateProvider + .state) + .state == + "Private" && + _privateBalanceString != + null) { + return Text( + "$_privateBalanceString ${coin.ticker}", + style: STextStyles + .itemSubtitle(context), + ); + } else if (ref + .read( + publicPrivateBalanceStateProvider + .state) + .state == + "Public" && + _publicBalanceString != + null) { + return Text( + "$_publicBalanceString ${coin.ticker}", + style: STextStyles + .itemSubtitle(context), + ); + } else { + return AnimatedText( + stringsToLoopThrough: const [ + "Loading balance", + "Loading balance.", + "Loading balance..", + "Loading balance...", + ], + style: STextStyles + .itemSubtitle(context), + ); + } + }, + ), + ], + ), + SvgPicture.asset( + Assets.svg.chevronDown, + width: 8, + height: 4, + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle2, + ), + ], + ), + ), + ) + ], + ), const SizedBox( height: 12, ), - if (coin == Coin.firo) - Text( - "Send from", - style: STextStyles.smallMed12(context), - textAlign: TextAlign.left, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Amount", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + BlueTextButton( + text: "Send all ${coin.ticker}", + onTap: () async { + if (coin == Coin.firo || + coin == Coin.firoTestNet) { + final firoWallet = + ref.read(provider).wallet as FiroWallet; + if (ref + .read( + publicPrivateBalanceStateProvider + .state) + .state == + "Private") { + cryptoAmountController.text = + (await firoWallet + .availablePrivateBalance()) + .toStringAsFixed(Constants + .decimalPlacesForCoin(coin)); + } else { + cryptoAmountController.text = + (await firoWallet + .availablePublicBalance()) + .toStringAsFixed(Constants + .decimalPlacesForCoin(coin)); + } + } else { + cryptoAmountController.text = (await ref + .read(provider) + .availableBalance) + .toStringAsFixed( + Constants.decimalPlacesForCoin( + coin)); + } + }, + ), + ], + ), + const SizedBox( + height: 8, + ), + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + key: + const Key("amountInputFieldCryptoTextFieldKey"), + controller: cryptoAmountController, + focusNode: _cryptoFocus, + keyboardType: const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), + textAlign: TextAlign.right, + inputFormatters: [ + // regex to validate a crypto amount with 8 decimal places + TextInputFormatter.withFunction((oldValue, + newValue) => + RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') + .hasMatch(newValue.text) + ? newValue + : oldValue), + ], + decoration: InputDecoration( + contentPadding: const EdgeInsets.only( + top: 12, + right: 12, + ), + hintText: "0", + hintStyle: + STextStyles.fieldLabel(context).copyWith( + fontSize: 14, + ), + prefixIcon: FittedBox( + fit: BoxFit.scaleDown, + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + coin.ticker, + style: STextStyles.smallMed14(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + ), + ), + ), + if (Prefs.instance.externalCalls) + const SizedBox( + height: 8, + ), + if (Prefs.instance.externalCalls) + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + key: + const Key("amountInputFieldFiatTextFieldKey"), + controller: baseAmountController, + focusNode: _baseFocus, + keyboardType: + const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), + textAlign: TextAlign.right, + inputFormatters: [ + // regex to validate a fiat amount with 2 decimal places + TextInputFormatter.withFunction((oldValue, + newValue) => + RegExp(r'^([0-9]*[,.]?[0-9]{0,2}|[,.][0-9]{0,2})$') + .hasMatch(newValue.text) + ? newValue + : oldValue), + ], + onChanged: (baseAmountString) { + if (baseAmountString.isNotEmpty && + baseAmountString != "." && + baseAmountString != ",") { + final baseAmount = + baseAmountString.contains(",") + ? Decimal.parse(baseAmountString + .replaceFirst(",", ".")) + : Decimal.parse(baseAmountString); + + var _price = ref + .read(priceAnd24hChangeNotifierProvider) + .getPrice(coin) + .item1; + + if (_price == Decimal.zero) { + _amountToSend = Decimal.zero; + } else { + _amountToSend = baseAmount <= Decimal.zero + ? Decimal.zero + : (baseAmount / _price).toDecimal( + scaleOnInfinitePrecision: + Constants.decimalPlacesForCoin( + coin)); + } + if (_cachedAmountToSend != null && + _cachedAmountToSend == _amountToSend) { + return; + } + _cachedAmountToSend = _amountToSend; + Logging.instance.log( + "it changed $_amountToSend $_cachedAmountToSend", + level: LogLevel.Info); + + final amountString = + Format.localizedStringAsFixed( + value: _amountToSend!, + locale: ref + .read( + localeServiceChangeNotifierProvider) + .locale, + decimalPlaces: + Constants.decimalPlacesForCoin(coin), + ); + + _cryptoAmountChangeLock = true; + cryptoAmountController.text = amountString; + _cryptoAmountChangeLock = false; + } else { + _amountToSend = Decimal.zero; + _cryptoAmountChangeLock = true; + cryptoAmountController.text = ""; + _cryptoAmountChangeLock = false; + } + // setState(() { + // _calculateFeesFuture = calculateFees( + // Format.decimalAmountToSatoshis( + // _amountToSend!)); + // }); + _updatePreviewButtonState( + _address, _amountToSend); + }, + decoration: InputDecoration( + contentPadding: const EdgeInsets.only( + top: 12, + right: 12, + ), + hintText: "0", + hintStyle: + STextStyles.fieldLabel(context).copyWith( + fontSize: 14, + ), + prefixIcon: FittedBox( + fit: BoxFit.scaleDown, + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + ref.watch(prefsChangeNotifierProvider + .select((value) => value.currency)), + style: STextStyles.smallMed14(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + ), + ), + ), + const SizedBox( + height: 12, + ), + Text( + "Note (optional)", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + const SizedBox( + height: 8, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: noteController, + focusNode: _noteFocusNode, + style: STextStyles.field(context), + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Type something...", + _noteFocusNode, + context, + ).copyWith( + suffixIcon: noteController.text.isNotEmpty + ? Padding( + padding: + const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + noteController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + const SizedBox( + height: 12, + ), + Text( + "Transaction fee (estimated)", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, ), - if (coin == Coin.firo) const SizedBox( height: 8, ), - if (coin == Coin.firo) Stack( children: [ TextField( autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, + controller: feeController, readOnly: true, textInputAction: TextInputAction.none, ), @@ -928,780 +1355,172 @@ class _SendViewState extends ConsumerState<SendView> { Constants.size.circularBorderRadius, ), ), - onPressed: () { - showModalBottomSheet<dynamic>( - backgroundColor: Colors.transparent, - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - builder: (_) => FiroBalanceSelectionSheet( - walletId: walletId, - ), - ); - }, - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Text( - "${ref.watch(publicPrivateBalanceStateProvider.state).state} balance", - style: STextStyles.itemSubtitle12( - context), - ), - const SizedBox( - width: 10, - ), - FutureBuilder( - future: _firoBalanceFuture( - provider, locale), - builder: (context, - AsyncSnapshot<String?> - snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - if (ref - .read( - publicPrivateBalanceStateProvider - .state) - .state == - "Private") { - _privateBalanceString = - snapshot.data!; + onPressed: (coin == Coin.firo || + coin == Coin.firoTestNet) && + ref + .watch( + publicPrivateBalanceStateProvider + .state) + .state == + "Private" + ? null + : () { + showModalBottomSheet<dynamic>( + backgroundColor: Colors.transparent, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: + BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + builder: (_) => + TransactionFeeSelectionSheet( + walletId: walletId, + amount: Decimal.tryParse( + cryptoAmountController + .text) ?? + Decimal.zero, + updateChosen: (String fee) { + setState(() { + _calculateFeesFuture = + Future(() => fee); + }); + }, + ), + ); + }, + child: ((coin == Coin.firo || + coin == Coin.firoTestNet) && + ref + .watch( + publicPrivateBalanceStateProvider + .state) + .state == + "Private") + ? Row( + children: [ + FutureBuilder( + future: _calculateFeesFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + return Text( + "~${snapshot.data! as String} ${coin.ticker}", + style: STextStyles + .itemSubtitle(context), + ); } else { - _publicBalanceString = - snapshot.data!; + return AnimatedText( + stringsToLoopThrough: const [ + "Calculating", + "Calculating.", + "Calculating..", + "Calculating...", + ], + style: STextStyles + .itemSubtitle(context), + ); } - } - if (ref - .read( - publicPrivateBalanceStateProvider - .state) - .state == - "Private" && - _privateBalanceString != - null) { - return Text( - "$_privateBalanceString ${coin.ticker}", - style: - STextStyles.itemSubtitle( - context), - ); - } else if (ref - .read( - publicPrivateBalanceStateProvider - .state) - .state == - "Public" && - _publicBalanceString != - null) { - return Text( - "$_publicBalanceString ${coin.ticker}", - style: - STextStyles.itemSubtitle( - context), - ); - } else { - return AnimatedText( - stringsToLoopThrough: const [ - "Loading balance", - "Loading balance.", - "Loading balance..", - "Loading balance...", - ], - style: - STextStyles.itemSubtitle( - context), - ); - } - }, - ), - ], - ), - SvgPicture.asset( - Assets.svg.chevronDown, - width: 8, - height: 4, - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle2, - ), - ], - ), + }, + ), + ], + ) + : Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + ref + .watch( + feeRateTypeStateProvider + .state) + .state + .prettyName, + style: STextStyles + .itemSubtitle12(context), + ), + const SizedBox( + width: 10, + ), + FutureBuilder( + future: _calculateFeesFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == + ConnectionState + .done && + snapshot.hasData) { + return Text( + "~${snapshot.data! as String} ${coin.ticker}", + style: STextStyles + .itemSubtitle( + context), + ); + } else { + return AnimatedText( + stringsToLoopThrough: const [ + "Calculating", + "Calculating.", + "Calculating..", + "Calculating...", + ], + style: STextStyles + .itemSubtitle( + context), + ); + } + }, + ), + ], + ), + SvgPicture.asset( + Assets.svg.chevronDown, + width: 8, + height: 4, + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle2, + ), + ], + ), ), ) ], ), - const SizedBox( - height: 12, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Amount", - style: STextStyles.smallMed12(context), - textAlign: TextAlign.left, - ), - BlueTextButton( - text: "Send all ${coin.ticker}", - onTap: () async { - if (coin == Coin.firo || - coin == Coin.firoTestNet) { - final firoWallet = - ref.read(provider).wallet as FiroWallet; - if (ref - .read( - publicPrivateBalanceStateProvider - .state) - .state == - "Private") { - cryptoAmountController.text = - (await firoWallet - .availablePrivateBalance()) - .toStringAsFixed( - Constants.decimalPlacesForCoin( - coin)); - } else { - cryptoAmountController.text = - (await firoWallet - .availablePublicBalance()) - .toStringAsFixed( - Constants.decimalPlacesForCoin( - coin)); - } - } else { - cryptoAmountController.text = (await ref - .read(provider) - .availableBalance) - .toStringAsFixed( - Constants.decimalPlacesForCoin(coin)); - } - }, - ), - ], - ), - const SizedBox( - height: 8, - ), - TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ), - key: const Key("amountInputFieldCryptoTextFieldKey"), - controller: cryptoAmountController, - focusNode: _cryptoFocus, - keyboardType: const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), - textAlign: TextAlign.right, - inputFormatters: [ - // regex to validate a crypto amount with 8 decimal places - TextInputFormatter.withFunction((oldValue, - newValue) => - RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') - .hasMatch(newValue.text) - ? newValue - : oldValue), - ], - decoration: InputDecoration( - contentPadding: const EdgeInsets.only( - top: 12, - right: 12, - ), - hintText: "0", - hintStyle: STextStyles.fieldLabel(context).copyWith( - fontSize: 14, - ), - prefixIcon: FittedBox( - fit: BoxFit.scaleDown, - child: Padding( - padding: const EdgeInsets.all(12), - child: Text( - coin.ticker, - style: STextStyles.smallMed14(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - ), - ), - ), - ), - ), - if (Prefs.instance.externalCalls) + const Spacer(), const SizedBox( - height: 8, + height: 12, ), - if (Prefs.instance.externalCalls) - TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ), - key: const Key("amountInputFieldFiatTextFieldKey"), - controller: baseAmountController, - focusNode: _baseFocus, - keyboardType: const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), - textAlign: TextAlign.right, - inputFormatters: [ - // regex to validate a fiat amount with 2 decimal places - TextInputFormatter.withFunction((oldValue, - newValue) => - RegExp(r'^([0-9]*[,.]?[0-9]{0,2}|[,.][0-9]{0,2})$') - .hasMatch(newValue.text) - ? newValue - : oldValue), - ], - onChanged: (baseAmountString) { - if (baseAmountString.isNotEmpty && - baseAmountString != "." && - baseAmountString != ",") { - final baseAmount = baseAmountString - .contains(",") - ? Decimal.parse( - baseAmountString.replaceFirst(",", ".")) - : Decimal.parse(baseAmountString); - - var _price = ref - .read(priceAnd24hChangeNotifierProvider) - .getPrice(coin) - .item1; - - if (_price == Decimal.zero) { - _amountToSend = Decimal.zero; - } else { - _amountToSend = baseAmount <= Decimal.zero - ? Decimal.zero - : (baseAmount / _price).toDecimal( - scaleOnInfinitePrecision: - Constants.decimalPlacesForCoin( - coin)); - } - if (_cachedAmountToSend != null && - _cachedAmountToSend == _amountToSend) { - return; - } - _cachedAmountToSend = _amountToSend; - Logging.instance.log( - "it changed $_amountToSend $_cachedAmountToSend", - level: LogLevel.Info); - - final amountString = - Format.localizedStringAsFixed( - value: _amountToSend!, - locale: ref - .read(localeServiceChangeNotifierProvider) - .locale, - decimalPlaces: - Constants.decimalPlacesForCoin(coin), - ); - - _cryptoAmountChangeLock = true; - cryptoAmountController.text = amountString; - _cryptoAmountChangeLock = false; - } else { - _amountToSend = Decimal.zero; - _cryptoAmountChangeLock = true; - cryptoAmountController.text = ""; - _cryptoAmountChangeLock = false; - } - // setState(() { - // _calculateFeesFuture = calculateFees( - // Format.decimalAmountToSatoshis( - // _amountToSend!)); - // }); - _updatePreviewButtonState( - _address, _amountToSend); - }, - decoration: InputDecoration( - contentPadding: const EdgeInsets.only( - top: 12, - right: 12, - ), - hintText: "0", - hintStyle: - STextStyles.fieldLabel(context).copyWith( - fontSize: 14, - ), - prefixIcon: FittedBox( - fit: BoxFit.scaleDown, - child: Padding( - padding: const EdgeInsets.all(12), - child: Text( - ref.watch(prefsChangeNotifierProvider - .select((value) => value.currency)), - style: STextStyles.smallMed14(context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - ), - ), - ), - ), - ), - const SizedBox( - height: 12, - ), - Text( - "Note (optional)", - style: STextStyles.smallMed12(context), - textAlign: TextAlign.left, - ), - const SizedBox( - height: 8, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: noteController, - focusNode: _noteFocusNode, - style: STextStyles.field(context), - onChanged: (_) => setState(() {}), - decoration: standardInputDecoration( - "Type something...", - _noteFocusNode, - context, - ).copyWith( - suffixIcon: noteController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - noteController.text = ""; - }); - }, - ), - ], - ), - ), - ) - : null, - ), - ), - ), - const SizedBox( - height: 12, - ), - Text( - "Transaction fee (estimated)", - style: STextStyles.smallMed12(context), - textAlign: TextAlign.left, - ), - const SizedBox( - height: 8, - ), - Stack( - children: [ - TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: feeController, - readOnly: true, - textInputAction: TextInputAction.none, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - ), - child: RawMaterialButton( - splashColor: Theme.of(context) - .extension<StackColors>()! - .highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: (coin == Coin.firo || - coin == Coin.firoTestNet) && - ref - .watch( - publicPrivateBalanceStateProvider - .state) - .state == - "Private" - ? null - : () { - showModalBottomSheet<dynamic>( - backgroundColor: Colors.transparent, - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - builder: (_) => - TransactionFeeSelectionSheet( - walletId: walletId, - amount: Decimal.tryParse( - cryptoAmountController - .text) ?? - Decimal.zero, - updateChosen: (String fee) { - setState(() { - _calculateFeesFuture = - Future(() => fee); - }); - }, - ), - ); - }, - child: ((coin == Coin.firo || - coin == Coin.firoTestNet) && - ref - .watch( - publicPrivateBalanceStateProvider - .state) - .state == - "Private") - ? Row( - children: [ - FutureBuilder( - future: _calculateFeesFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - return Text( - "~${snapshot.data! as String} ${coin.ticker}", - style: - STextStyles.itemSubtitle( - context), - ); - } else { - return AnimatedText( - stringsToLoopThrough: const [ - "Calculating", - "Calculating.", - "Calculating..", - "Calculating...", - ], - style: - STextStyles.itemSubtitle( - context), - ); - } - }, - ), - ], - ) - : Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Text( - ref - .watch( - feeRateTypeStateProvider - .state) - .state - .prettyName, - style: - STextStyles.itemSubtitle12( - context), - ), - const SizedBox( - width: 10, - ), - FutureBuilder( - future: _calculateFeesFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == - ConnectionState - .done && - snapshot.hasData) { - return Text( - "~${snapshot.data! as String} ${coin.ticker}", - style: STextStyles - .itemSubtitle( - context), - ); - } else { - return AnimatedText( - stringsToLoopThrough: const [ - "Calculating", - "Calculating.", - "Calculating..", - "Calculating...", - ], - style: STextStyles - .itemSubtitle( - context), - ); - } - }, - ), - ], - ), - SvgPicture.asset( - Assets.svg.chevronDown, - width: 8, - height: 4, - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle2, - ), - ], - ), - ), - ) - ], - ), - const Spacer(), - const SizedBox( - height: 12, - ), - TextButton( - onPressed: ref - .watch(previewTxButtonStateProvider.state) - .state - ? () async { - // wait for keyboard to disappear - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration(milliseconds: 100), - ); - final manager = ref - .read(walletsChangeNotifierProvider) - .getManager(walletId); - - // TODO: remove the need for this!! - final bool isOwnAddress = - await manager.isOwnAddress(_address!); - if (isOwnAddress) { - await showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return StackDialog( - title: "Transaction failed", - message: - "Sending to self is currently disabled", - rightButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor( - context), - child: Text( - "Ok", - style: STextStyles.button(context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .accentColorDark), - ), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ); - }, + TextButton( + onPressed: ref + .watch(previewTxButtonStateProvider.state) + .state + ? () async { + // wait for keyboard to disappear + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 100), ); - return; - } + final manager = ref + .read(walletsChangeNotifierProvider) + .getManager(walletId); - final amount = Format.decimalAmountToSatoshis( - _amountToSend!, coin); - int availableBalance; - if ((coin == Coin.firo || - coin == Coin.firoTestNet)) { - if (ref - .read( - publicPrivateBalanceStateProvider - .state) - .state == - "Private") { - availableBalance = - Format.decimalAmountToSatoshis( - await (manager.wallet - as FiroWallet) - .availablePrivateBalance(), - coin); - } else { - availableBalance = - Format.decimalAmountToSatoshis( - await (manager.wallet - as FiroWallet) - .availablePublicBalance(), - coin); - } - } else { - availableBalance = - Format.decimalAmountToSatoshis( - await manager.availableBalance, - coin); - } - - // confirm send all - if (amount == availableBalance) { - final bool? shouldSendAll = - await showDialog<bool>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return StackDialog( - title: "Confirm send all", - message: - "You are about to send your entire balance. Would you like to continue?", - leftButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor( - context), - child: Text( - "Cancel", - style: STextStyles.button(context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .accentColorDark), - ), - onPressed: () { - Navigator.of(context).pop(false); - }, - ), - rightButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor( - context), - child: Text( - "Yes", - style: - STextStyles.button(context), - ), - onPressed: () { - Navigator.of(context).pop(true); - }, - ), - ); - }, - ); - - if (shouldSendAll == null || - shouldSendAll == false) { - // cancel preview - return; - } - } - - try { - bool wasCancelled = false; - - unawaited(showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: false, - builder: (context) { - return BuildingTransactionDialog( - onCancel: () { - wasCancelled = true; - - Navigator.of(context).pop(); - }, - ); - }, - )); - - Map<String, dynamic> txData; - - if ((coin == Coin.firo || - coin == Coin.firoTestNet) && - ref - .read( - publicPrivateBalanceStateProvider - .state) - .state != - "Private") { - txData = - await (manager.wallet as FiroWallet) - .prepareSendPublic( - address: _address!, - satoshiAmount: amount, - args: { - "feeRate": - ref.read(feeRateTypeStateProvider) - }, - ); - } else { - txData = await manager.prepareSend( - address: _address!, - satoshiAmount: amount, - args: { - "feeRate": - ref.read(feeRateTypeStateProvider) - }, - ); - } - - if (!wasCancelled && mounted) { - // pop building dialog - Navigator.of(context).pop(); - txData["note"] = noteController.text; - txData["address"] = _address; - - unawaited(Navigator.of(context).push( - RouteGenerator.getRoute( - shouldUseMaterialRoute: RouteGenerator - .useMaterialPageRoute, - builder: (_) => - ConfirmTransactionView( - transactionInfo: txData, - walletId: walletId, - ), - settings: const RouteSettings( - name: ConfirmTransactionView - .routeName, - ), - ), - )); - } - } catch (e) { - if (mounted) { - // pop building dialog - Navigator.of(context).pop(); - - unawaited(showDialog<dynamic>( + // TODO: remove the need for this!! + final bool isOwnAddress = + await manager.isOwnAddress(_address!); + if (isOwnAddress) { + await showDialog<dynamic>( context: context, useSafeArea: false, barrierDismissible: true, builder: (context) { return StackDialog( title: "Transaction failed", - message: e.toString(), + message: + "Sending to self is currently disabled", rightButton: TextButton( style: Theme.of(context) .extension<StackColors>()! @@ -1723,36 +1542,238 @@ class _SendViewState extends ConsumerState<SendView> { ), ); }, + ); + return; + } + + final amount = + Format.decimalAmountToSatoshis( + _amountToSend!, coin); + int availableBalance; + if ((coin == Coin.firo || + coin == Coin.firoTestNet)) { + if (ref + .read( + publicPrivateBalanceStateProvider + .state) + .state == + "Private") { + availableBalance = + Format.decimalAmountToSatoshis( + await (manager.wallet + as FiroWallet) + .availablePrivateBalance(), + coin); + } else { + availableBalance = + Format.decimalAmountToSatoshis( + await (manager.wallet + as FiroWallet) + .availablePublicBalance(), + coin); + } + } else { + availableBalance = + Format.decimalAmountToSatoshis( + await manager.availableBalance, + coin); + } + + // confirm send all + if (amount == availableBalance) { + final bool? shouldSendAll = + await showDialog<bool>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return StackDialog( + title: "Confirm send all", + message: + "You are about to send your entire balance. Would you like to continue?", + leftButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor( + context), + child: Text( + "Cancel", + style: STextStyles.button( + context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .accentColorDark), + ), + onPressed: () { + Navigator.of(context) + .pop(false); + }, + ), + rightButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor( + context), + child: Text( + "Yes", + style: + STextStyles.button(context), + ), + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ); + }, + ); + + if (shouldSendAll == null || + shouldSendAll == false) { + // cancel preview + return; + } + } + + try { + bool wasCancelled = false; + + unawaited(showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return BuildingTransactionDialog( + onCancel: () { + wasCancelled = true; + + Navigator.of(context).pop(); + }, + ); + }, )); + + Map<String, dynamic> txData; + + if ((coin == Coin.firo || + coin == Coin.firoTestNet) && + ref + .read( + publicPrivateBalanceStateProvider + .state) + .state != + "Private") { + txData = + await (manager.wallet as FiroWallet) + .prepareSendPublic( + address: _address!, + satoshiAmount: amount, + args: { + "feeRate": ref + .read(feeRateTypeStateProvider) + }, + ); + } else { + txData = await manager.prepareSend( + address: _address!, + satoshiAmount: amount, + args: { + "feeRate": ref + .read(feeRateTypeStateProvider) + }, + ); + } + + if (!wasCancelled && mounted) { + // pop building dialog + Navigator.of(context).pop(); + txData["note"] = noteController.text; + txData["address"] = _address; + + unawaited(Navigator.of(context).push( + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator + .useMaterialPageRoute, + builder: (_) => + ConfirmTransactionView( + transactionInfo: txData, + walletId: walletId, + ), + settings: const RouteSettings( + name: ConfirmTransactionView + .routeName, + ), + ), + )); + } + } catch (e) { + if (mounted) { + // pop building dialog + Navigator.of(context).pop(); + + unawaited(showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return StackDialog( + title: "Transaction failed", + message: e.toString(), + rightButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor( + context), + child: Text( + "Ok", + style: STextStyles.button( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .accentColorDark), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ); + }, + )); + } } } - } - : null, - style: ref - .watch(previewTxButtonStateProvider.state) - .state - ? Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context) - : Theme.of(context) - .extension<StackColors>()! - .getPrimaryDisabledButtonColor(context), - child: Text( - "Preview", - style: STextStyles.button(context), + : null, + style: ref + .watch(previewTxButtonStateProvider.state) + .state + ? Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context) + : Theme.of(context) + .extension<StackColors>()! + .getPrimaryDisabledButtonColor(context), + child: Text( + "Preview", + style: STextStyles.button(context), + ), ), - ), - const SizedBox( - height: 4, - ), - ], + const SizedBox( + height: 4, + ), + ], + ), ), ), ), ), - ), - ); - }, + ); + }, + ), ), ); } diff --git a/lib/pages/settings_views/global_settings_view/about_view.dart b/lib/pages/settings_views/global_settings_view/about_view.dart index dc2da2488..a1e78ba7d 100644 --- a/lib/pages/settings_views/global_settings_view/about_view.dart +++ b/lib/pages/settings_views/global_settings_view/about_view.dart @@ -11,6 +11,7 @@ import 'package:package_info_plus/package_info_plus.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/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -117,408 +118,431 @@ class AboutView extends ConsumerWidget { ]; Future commitMoneroFuture = Future.wait(futureMoneroList); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).pop(); + }, + ), + title: Text( + "About", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "About", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - FutureBuilder( - future: PackageInfo.fromPlatform(), - builder: - (context, AsyncSnapshot<PackageInfo> snapshot) { - String version = ""; - String signature = ""; - String appName = ""; - String build = ""; + body: Padding( + padding: const EdgeInsets.all(16), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FutureBuilder( + future: PackageInfo.fromPlatform(), + builder: + (context, AsyncSnapshot<PackageInfo> snapshot) { + String version = ""; + String signature = ""; + String appName = ""; + String build = ""; - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - version = snapshot.data!.version; - build = snapshot.data!.buildNumber; - signature = snapshot.data!.buildSignature; - appName = snapshot.data!.appName; - } + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + version = snapshot.data!.version; + build = snapshot.data!.buildNumber; + signature = snapshot.data!.buildSignature; + appName = snapshot.data!.appName; + } - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: Text( + appName, + style: STextStyles.pageTitleH2(context), + ), + ), + const SizedBox( + height: 24, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + Text( + "Version", + style: STextStyles.titleBold12(context), + ), + const SizedBox( + height: 4, + ), + SelectableText( + version, + style: + STextStyles.itemSubtitle(context), + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + Text( + "Build number", + style: STextStyles.titleBold12(context), + ), + const SizedBox( + height: 4, + ), + SelectableText( + build, + style: + STextStyles.itemSubtitle(context), + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + Text( + "Build signature", + style: STextStyles.titleBold12(context), + ), + const SizedBox( + height: 4, + ), + SelectableText( + signature, + style: + STextStyles.itemSubtitle(context), + ), + ], + ), + ), + ], + ); + }, + ), + const SizedBox( + height: 12, + ), + FutureBuilder( + future: commitFiroFuture, + builder: + (context, AsyncSnapshot<dynamic> snapshot) { + bool commitExists = false; + bool isHead = false; + CommitStatus stateOfCommit = + CommitStatus.notLoaded; + + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + commitExists = snapshot.data![0] as bool; + isHead = snapshot.data![1] as bool; + if (commitExists && isHead) { + stateOfCommit = CommitStatus.isHead; + } else if (commitExists) { + stateOfCommit = CommitStatus.isOldCommit; + } else { + stateOfCommit = CommitStatus.notACommit; + } + } + TextStyle indicationStyle = + STextStyles.itemSubtitle(context); + switch (stateOfCommit) { + case CommitStatus.isHead: + indicationStyle = + STextStyles.itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorGreen); + break; + case CommitStatus.isOldCommit: + indicationStyle = + STextStyles.itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorYellow); + break; + case CommitStatus.notACommit: + indicationStyle = + STextStyles.itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorRed); + break; + default: + break; + } + return RoundedWhiteContainer( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + Text( + "Firo Build Commit", + style: STextStyles.titleBold12(context), + ), + const SizedBox( + height: 4, + ), + SelectableText( + firoCommit, + style: indicationStyle, + ), + ], + ), + ); + }), + const SizedBox( + height: 12, + ), + FutureBuilder( + future: commitEpicFuture, + builder: + (context, AsyncSnapshot<dynamic> snapshot) { + bool commitExists = false; + bool isHead = false; + CommitStatus stateOfCommit = + CommitStatus.notLoaded; + + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + commitExists = snapshot.data![0] as bool; + isHead = snapshot.data![1] as bool; + if (commitExists && isHead) { + stateOfCommit = CommitStatus.isHead; + } else if (commitExists) { + stateOfCommit = CommitStatus.isOldCommit; + } else { + stateOfCommit = CommitStatus.notACommit; + } + } + TextStyle indicationStyle = + STextStyles.itemSubtitle(context); + switch (stateOfCommit) { + case CommitStatus.isHead: + indicationStyle = + STextStyles.itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorGreen); + break; + case CommitStatus.isOldCommit: + indicationStyle = + STextStyles.itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorYellow); + break; + case CommitStatus.notACommit: + indicationStyle = + STextStyles.itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorRed); + break; + default: + break; + } + return RoundedWhiteContainer( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + Text( + "Epic Cash Build Commit", + style: STextStyles.titleBold12(context), + ), + const SizedBox( + height: 4, + ), + SelectableText( + epicCashCommit, + style: indicationStyle, + ), + ], + ), + ); + }), + const SizedBox( + height: 12, + ), + FutureBuilder( + future: commitMoneroFuture, + builder: + (context, AsyncSnapshot<dynamic> snapshot) { + bool commitExists = false; + bool isHead = false; + CommitStatus stateOfCommit = + CommitStatus.notLoaded; + + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + commitExists = snapshot.data![0] as bool; + isHead = snapshot.data![1] as bool; + if (commitExists && isHead) { + stateOfCommit = CommitStatus.isHead; + } else if (commitExists) { + stateOfCommit = CommitStatus.isOldCommit; + } else { + stateOfCommit = CommitStatus.notACommit; + } + } + TextStyle indicationStyle = + STextStyles.itemSubtitle(context); + switch (stateOfCommit) { + case CommitStatus.isHead: + indicationStyle = + STextStyles.itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorGreen); + break; + case CommitStatus.isOldCommit: + indicationStyle = + STextStyles.itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorYellow); + break; + case CommitStatus.notACommit: + indicationStyle = + STextStyles.itemSubtitle(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorRed); + break; + default: + break; + } + return RoundedWhiteContainer( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + Text( + "Monero Build Commit", + style: STextStyles.titleBold12(context), + ), + const SizedBox( + height: 4, + ), + SelectableText( + moneroCommit, + style: indicationStyle, + ), + ], + ), + ); + }), + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Center( - child: Text( - appName, - style: STextStyles.pageTitleH2(context), - ), + Text( + "Website", + style: STextStyles.titleBold12(context), ), const SizedBox( - height: 24, + height: 4, ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: [ - Text( - "Version", - style: STextStyles.titleBold12(context), - ), - const SizedBox( - height: 4, - ), - SelectableText( - version, - style: STextStyles.itemSubtitle(context), - ), - ], - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: [ - Text( - "Build number", - style: STextStyles.titleBold12(context), - ), - const SizedBox( - height: 4, - ), - SelectableText( - build, - style: STextStyles.itemSubtitle(context), - ), - ], - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: [ - Text( - "Build signature", - style: STextStyles.titleBold12(context), - ), - const SizedBox( - height: 4, - ), - SelectableText( - signature, - style: STextStyles.itemSubtitle(context), - ), - ], - ), + BlueTextButton( + text: "https://stackwallet.com", + onTap: () { + launchUrl( + Uri.parse("https://stackwallet.com"), + mode: LaunchMode.externalApplication, + ); + }, ), ], - ); - }, - ), - const SizedBox( - height: 12, - ), - FutureBuilder( - future: commitFiroFuture, - builder: (context, AsyncSnapshot<dynamic> snapshot) { - bool commitExists = false; - bool isHead = false; - CommitStatus stateOfCommit = CommitStatus.notLoaded; - - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - commitExists = snapshot.data![0] as bool; - isHead = snapshot.data![1] as bool; - if (commitExists && isHead) { - stateOfCommit = CommitStatus.isHead; - } else if (commitExists) { - stateOfCommit = CommitStatus.isOldCommit; - } else { - stateOfCommit = CommitStatus.notACommit; - } - } - TextStyle indicationStyle = - STextStyles.itemSubtitle(context); - switch (stateOfCommit) { - case CommitStatus.isHead: - indicationStyle = - STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorGreen); - break; - case CommitStatus.isOldCommit: - indicationStyle = - STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorYellow); - break; - case CommitStatus.notACommit: - indicationStyle = - STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorRed); - break; - default: - break; - } - return RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - "Firo Build Commit", - style: STextStyles.titleBold12(context), - ), - const SizedBox( - height: 4, - ), - SelectableText( - firoCommit, - style: indicationStyle, - ), - ], - ), - ); - }), - const SizedBox( - height: 12, - ), - FutureBuilder( - future: commitEpicFuture, - builder: (context, AsyncSnapshot<dynamic> snapshot) { - bool commitExists = false; - bool isHead = false; - CommitStatus stateOfCommit = CommitStatus.notLoaded; - - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - commitExists = snapshot.data![0] as bool; - isHead = snapshot.data![1] as bool; - if (commitExists && isHead) { - stateOfCommit = CommitStatus.isHead; - } else if (commitExists) { - stateOfCommit = CommitStatus.isOldCommit; - } else { - stateOfCommit = CommitStatus.notACommit; - } - } - TextStyle indicationStyle = - STextStyles.itemSubtitle(context); - switch (stateOfCommit) { - case CommitStatus.isHead: - indicationStyle = - STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorGreen); - break; - case CommitStatus.isOldCommit: - indicationStyle = - STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorYellow); - break; - case CommitStatus.notACommit: - indicationStyle = - STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorRed); - break; - default: - break; - } - return RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - "Epic Cash Build Commit", - style: STextStyles.titleBold12(context), - ), - const SizedBox( - height: 4, - ), - SelectableText( - epicCashCommit, - style: indicationStyle, - ), - ], - ), - ); - }), - const SizedBox( - height: 12, - ), - FutureBuilder( - future: commitMoneroFuture, - builder: (context, AsyncSnapshot<dynamic> snapshot) { - bool commitExists = false; - bool isHead = false; - CommitStatus stateOfCommit = CommitStatus.notLoaded; - - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - commitExists = snapshot.data![0] as bool; - isHead = snapshot.data![1] as bool; - if (commitExists && isHead) { - stateOfCommit = CommitStatus.isHead; - } else if (commitExists) { - stateOfCommit = CommitStatus.isOldCommit; - } else { - stateOfCommit = CommitStatus.notACommit; - } - } - TextStyle indicationStyle = - STextStyles.itemSubtitle(context); - switch (stateOfCommit) { - case CommitStatus.isHead: - indicationStyle = - STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorGreen); - break; - case CommitStatus.isOldCommit: - indicationStyle = - STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorYellow); - break; - case CommitStatus.notACommit: - indicationStyle = - STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorRed); - break; - default: - break; - } - return RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - "Monero Build Commit", - style: STextStyles.titleBold12(context), - ), - const SizedBox( - height: 4, - ), - SelectableText( - moneroCommit, - style: indicationStyle, - ), - ], - ), - ); - }), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Website", - style: STextStyles.titleBold12(context), - ), - const SizedBox( - height: 4, - ), - BlueTextButton( - text: "https://stackwallet.com", - onTap: () { - launchUrl( - Uri.parse("https://stackwallet.com"), - mode: LaunchMode.externalApplication, - ); - }, - ), - ], + ), ), - ), - const SizedBox( - height: 12, - ), - const Spacer(), - RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: STextStyles.label(context), - children: [ - const TextSpan( - text: - "By using Stack Wallet, you agree to the "), - TextSpan( - text: "Terms of service", - style: STextStyles.richLink(context), - recognizer: TapGestureRecognizer() - ..onTap = () { - launchUrl( - Uri.parse( - "https://stackwallet.com/terms-of-service.html"), - mode: LaunchMode.externalApplication, - ); - }, - ), - const TextSpan(text: " and "), - TextSpan( - text: "Privacy policy", - style: STextStyles.richLink(context), - recognizer: TapGestureRecognizer() - ..onTap = () { - launchUrl( - Uri.parse( - "https://stackwallet.com/privacy-policy.html"), - mode: LaunchMode.externalApplication, - ); - }, - ), - ], + const SizedBox( + height: 12, ), - ), - ], + const Spacer(), + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: STextStyles.label(context), + children: [ + const TextSpan( + text: + "By using Stack Wallet, you agree to the "), + TextSpan( + text: "Terms of service", + style: STextStyles.richLink(context), + recognizer: TapGestureRecognizer() + ..onTap = () { + launchUrl( + Uri.parse( + "https://stackwallet.com/terms-of-service.html"), + mode: LaunchMode.externalApplication, + ); + }, + ), + const TextSpan(text: " and "), + TextSpan( + text: "Privacy policy", + style: STextStyles.richLink(context), + recognizer: TapGestureRecognizer() + ..onTap = () { + launchUrl( + Uri.parse( + "https://stackwallet.com/privacy-policy.html"), + mode: LaunchMode.externalApplication, + ); + }, + ), + ], + ), + ), + ], + ), ), ), - ), - ); - }, + ); + }, + ), ), ), ); diff --git a/lib/pages/settings_views/global_settings_view/advanced_views/advanced_settings_view.dart b/lib/pages/settings_views/global_settings_view/advanced_views/advanced_settings_view.dart index 86212e0c7..06b798c36 100644 --- a/lib/pages/settings_views/global_settings_view/advanced_views/advanced_settings_view.dart +++ b/lib/pages/settings_views/global_settings_view/advanced_views/advanced_settings_view.dart @@ -1,16 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/advanced_views/debug_view.dart'; +import 'package:stackwallet/pages/stack_privacy_calls.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; -import 'package:tuple/tuple.dart'; - -import 'package:stackwallet/pages/stack_privacy_calls.dart'; class AdvancedSettingsView extends StatelessWidget { const AdvancedSettingsView({ @@ -23,158 +22,160 @@ class AdvancedSettingsView extends StatelessWidget { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Advanced", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Advanced", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - ), - onPressed: () { - Navigator.of(context).pushNamed(DebugView.routeName); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), - child: Row( - children: [ - Text( - "Debug info", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - ], + onPressed: () { + Navigator.of(context).pushNamed(DebugView.routeName); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + children: [ + Text( + "Debug info", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + ], + ), ), ), ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - child: Consumer( - builder: (_, ref, __) { - return RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + child: Consumer( + builder: (_, ref, __) { + return RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - ), - onPressed: null, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Toggle testnet coins", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - SizedBox( - height: 20, - width: 40, - child: DraggableSwitchButton( - isOn: ref.watch( - prefsChangeNotifierProvider - .select((value) => value.showTestNetCoins), - ), - onValueChanged: (newValue) { - ref - .read(prefsChangeNotifierProvider) - .showTestNetCoins = newValue; - }, + onPressed: null, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Toggle testnet coins", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, ), - ), - ], - ), - ), - ); - }, - ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: Consumer( - builder: (_, ref, __) { - final externalCalls = ref.watch( - prefsChangeNotifierProvider - .select((value) => value.externalCalls), - ); - return RawMaterialButton( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - Navigator.of(context).pushNamed( - StackPrivacyCalls.routeName, - arguments: true, - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), - child: Row( - children: [ - RichText( - textAlign: TextAlign.left, - text: TextSpan( - children: [ - TextSpan( - text: "Stack Experience", - style: STextStyles.titleBold12(context), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.showTestNetCoins), ), - TextSpan( - text: externalCalls - ? "\nEasy crypto" - : "\nIncognito", - style: STextStyles.label(context) - .copyWith(fontSize: 15.0), - ) - ], + onValueChanged: (newValue) { + ref + .read(prefsChangeNotifierProvider) + .showTestNetCoins = newValue; + }, + ), ), - ), - ], + ], + ), ), - ), - ); - }, + ); + }, + ), ), - ), - ], + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: Consumer( + builder: (_, ref, __) { + final externalCalls = ref.watch( + prefsChangeNotifierProvider + .select((value) => value.externalCalls), + ); + return RawMaterialButton( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + Navigator.of(context).pushNamed( + StackPrivacyCalls.routeName, + arguments: true, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + children: [ + RichText( + textAlign: TextAlign.left, + text: TextSpan( + children: [ + TextSpan( + text: "Stack Experience", + style: STextStyles.titleBold12(context), + ), + TextSpan( + text: externalCalls + ? "\nEasy crypto" + : "\nIncognito", + style: STextStyles.label(context) + .copyWith(fontSize: 15.0), + ) + ], + ), + ), + ], + ), + ), + ); + }, + ), + ), + ], + ), ), ), ); diff --git a/lib/pages/settings_views/global_settings_view/advanced_views/debug_view.dart b/lib/pages/settings_views/global_settings_view/advanced_views/debug_view.dart index 055773ef6..33f21759e 100644 --- a/lib/pages/settings_views/global_settings_view/advanced_views/debug_view.dart +++ b/lib/pages/settings_views/global_settings_view/advanced_views/debug_view.dart @@ -26,6 +26,7 @@ import 'package:stackwallet/utilities/stack_file_system.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/custom_loading_overlay.dart'; @@ -99,474 +100,484 @@ class _DebugViewState extends ConsumerState<DebugView> { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Debug", - style: STextStyles.navBarTitle(context), - ), - actions: [ - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("deleteLogsAppBarButtonKey"), - size: 36, - shadows: const [], - color: Theme.of(context).extension<StackColors>()!.background, - icon: SvgPicture.asset( - Assets.svg.trash, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - width: 20, - height: 20, - ), - onPressed: () async { - await showDialog<void>( - context: context, - builder: (_) => StackDialog( - title: "Delete logs?", - message: - "You are about to delete all logs permanently. Are you sure?", - leftButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Cancel", - style: STextStyles.itemSubtitle12(context), + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Debug", + style: STextStyles.navBarTitle(context), + ), + actions: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("deleteLogsAppBarButtonKey"), + size: 36, + shadows: const [], + color: Theme.of(context).extension<StackColors>()!.background, + icon: SvgPicture.asset( + Assets.svg.trash, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + width: 20, + height: 20, + ), + onPressed: () async { + await showDialog<void>( + context: context, + builder: (_) => StackDialog( + title: "Delete logs?", + message: + "You are about to delete all logs permanently. Are you sure?", + leftButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Cancel", + style: STextStyles.itemSubtitle12(context), + ), + onPressed: () { + Navigator.of(context).pop(); + }, ), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - rightButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - child: Text( - "Delete logs", - style: STextStyles.button(context), - ), - onPressed: () async { - Navigator.of(context).pop(); + rightButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + child: Text( + "Delete logs", + style: STextStyles.button(context), + ), + onPressed: () async { + Navigator.of(context).pop(); - bool shouldPop = false; - unawaited(showDialog<dynamic>( - barrierDismissible: false, - context: context, - builder: (_) => WillPopScope( - onWillPop: () async { - return shouldPop; - }, - child: const CustomLoadingOverlay( - message: "Deleting logs...", - eventBus: null, + bool shouldPop = false; + unawaited(showDialog<dynamic>( + barrierDismissible: false, + context: context, + builder: (_) => WillPopScope( + onWillPop: () async { + return shouldPop; + }, + child: const CustomLoadingOverlay( + message: "Deleting logs...", + eventBus: null, + ), ), - ), - )); + )); - await ref - .read(debugServiceProvider) - .deleteAllMessages(); - await ref - .read(debugServiceProvider) - .updateRecentLogs(); + await ref + .read(debugServiceProvider) + .deleteAllMessages(); + await ref + .read(debugServiceProvider) + .updateRecentLogs(); - shouldPop = true; + shouldPop = true; - if (mounted) { - Navigator.pop(context); - unawaited(showFloatingFlushBar( - type: FlushBarType.info, - context: context, - message: 'Logs cleared!')); - } - }, + if (mounted) { + Navigator.pop(context); + unawaited(showFloatingFlushBar( + type: FlushBarType.info, + context: context, + message: 'Logs cleared!')); + } + }, + ), ), - ), - ); - }, + ); + }, + ), ), ), - ), - ], - ), - body: Padding( - padding: const EdgeInsets.only( - top: 12, - left: 16, - right: 16, + ], ), - child: NestedScrollView( - floatHeaderSlivers: true, - headerSliverBuilder: (context, innerBoxIsScrolled) { - return [ - SliverOverlapAbsorber( - handle: - NestedScrollView.sliverOverlapAbsorberHandleFor(context), - sliver: SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Column( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: _searchController, - focusNode: _searchFocusNode, - onChanged: (newString) { - setState(() => _searchTerm = newString); - }, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - _searchFocusNode, - context, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, - ), - ), - suffixIcon: _searchController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchController.text = ""; - _searchTerm = ""; - }); - }, - ), - ], - ), - ), - ) - : null, + body: Padding( + padding: const EdgeInsets.only( + top: 12, + left: 16, + right: 16, + ), + child: NestedScrollView( + floatHeaderSlivers: true, + headerSliverBuilder: (context, innerBoxIsScrolled) { + return [ + SliverOverlapAbsorber( + handle: + NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Column( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - ), - ), - const SizedBox( - height: 12, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - BlueTextButton( - text: "Save Debug Info to clipboard", - onTap: () async { - try { - final packageInfo = - await PackageInfo.fromPlatform(); - final version = packageInfo.version; - final build = packageInfo.buildNumber; - final signature = packageInfo.buildSignature; - final appName = packageInfo.appName; - String firoCommit = - FIRO_VERSIONS.getPluginVersion(); - String epicCashCommit = - EPIC_VERSIONS.getPluginVersion(); - String moneroCommit = - MONERO_VERSIONS.getPluginVersion(); - DeviceInfoPlugin deviceInfoPlugin = - DeviceInfoPlugin(); - final deviceInfo = - await deviceInfoPlugin.deviceInfo; - var deviceInfoMap = deviceInfo.toMap(); - deviceInfoMap.remove("systemFeatures"); - - final logs = filtered( - ref.watch(debugServiceProvider.select( - (value) => value.recentLogs)), - _searchTerm) - .reversed - .toList(growable: false); - List errorLogs = []; - for (var log in logs) { - if (log.logLevel == LogLevel.Error || - log.logLevel == LogLevel.Fatal) { - errorLogs.add( - "${log.logLevel}: ${log.message}"); - } - } - - final finalDebugMap = { - "version": version, - "build": build, - "signature": signature, - "appName": appName, - "firoCommit": firoCommit, - "epicCashCommit": epicCashCommit, - "moneroCommit": moneroCommit, - "deviceInfoMap": deviceInfoMap, - "errorLogs": errorLogs, - }; - Logging.instance.log( - json.encode(finalDebugMap), - level: LogLevel.Info, - printFullLength: true); - const ClipboardInterface clipboard = - ClipboardWrapper(); - await clipboard.setData( - ClipboardData( - text: json.encode(finalDebugMap)), - ); - } catch (e, s) { - Logging.instance - .log("$e $s", level: LogLevel.Error); - } + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: (newString) { + setState(() => _searchTerm = newString); }, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), + ), + suffixIcon: _searchController.text.isNotEmpty + ? Padding( + padding: + const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + _searchTerm = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), ), - const Spacer(), - BlueTextButton( - text: "Save logs to file", - onTap: () async { - final systemfile = SWBFileSystem(); - await systemfile.prepareStorage(); - Directory rootPath = await StackFileSystem - .applicationRootDirectory(); - - if (Platform.isAndroid) { - rootPath = Directory("/storage/emulated/0/"); - } - - Directory dir = - Directory('${rootPath.path}/Documents'); - if (Platform.isIOS) { - dir = Directory(rootPath.path); - } - try { - if (!dir.existsSync()) { - dir.createSync(); - } - } catch (e, s) { - Logging.instance - .log("$e\n$s", level: LogLevel.Error); - } - String? path; - if (Platform.isAndroid) { - path = dir.path; - } else { - path = await FilePicker.platform - .getDirectoryPath( - dialogTitle: "Choose Log Save Location", - initialDirectory: - systemfile.startPath!.path, - lockParentWindow: true, - ); - } - - if (path != null) { - final eventBus = EventBus(); - bool shouldPop = false; - unawaited(showDialog<dynamic>( - barrierDismissible: false, - context: context, - builder: (_) => WillPopScope( - onWillPop: () async { - return shouldPop; - }, - child: CustomLoadingOverlay( - message: "Generating Stack logs file", - eventBus: eventBus, - ), - ), - )); - - bool logssaved = true; - var filename; + ), + const SizedBox( + height: 12, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + BlueTextButton( + text: "Save Debug Info to clipboard", + onTap: () async { try { - filename = await ref - .read(debugServiceProvider) - .exportToFile(path, eventBus); + final packageInfo = + await PackageInfo.fromPlatform(); + final version = packageInfo.version; + final build = packageInfo.buildNumber; + final signature = + packageInfo.buildSignature; + final appName = packageInfo.appName; + String firoCommit = + FIRO_VERSIONS.getPluginVersion(); + String epicCashCommit = + EPIC_VERSIONS.getPluginVersion(); + String moneroCommit = + MONERO_VERSIONS.getPluginVersion(); + DeviceInfoPlugin deviceInfoPlugin = + DeviceInfoPlugin(); + final deviceInfo = + await deviceInfoPlugin.deviceInfo; + var deviceInfoMap = deviceInfo.toMap(); + deviceInfoMap.remove("systemFeatures"); + + final logs = filtered( + ref.watch(debugServiceProvider + .select((value) => + value.recentLogs)), + _searchTerm) + .reversed + .toList(growable: false); + List errorLogs = []; + for (var log in logs) { + if (log.logLevel == LogLevel.Error || + log.logLevel == LogLevel.Fatal) { + errorLogs.add( + "${log.logLevel}: ${log.message}"); + } + } + + final finalDebugMap = { + "version": version, + "build": build, + "signature": signature, + "appName": appName, + "firoCommit": firoCommit, + "epicCashCommit": epicCashCommit, + "moneroCommit": moneroCommit, + "deviceInfoMap": deviceInfoMap, + "errorLogs": errorLogs, + }; + Logging.instance.log( + json.encode(finalDebugMap), + level: LogLevel.Info, + printFullLength: true); + const ClipboardInterface clipboard = + ClipboardWrapper(); + await clipboard.setData( + ClipboardData( + text: json.encode(finalDebugMap)), + ); } catch (e, s) { - logssaved = false; Logging.instance .log("$e $s", level: LogLevel.Error); } + }, + ), + const Spacer(), + BlueTextButton( + text: "Save logs to file", + onTap: () async { + final systemfile = SWBFileSystem(); + await systemfile.prepareStorage(); + Directory rootPath = await StackFileSystem + .applicationRootDirectory(); - shouldPop = true; + if (Platform.isAndroid) { + rootPath = + Directory("/storage/emulated/0/"); + } - if (mounted) { - Navigator.pop(context); + Directory dir = + Directory('${rootPath.path}/Documents'); + if (Platform.isIOS) { + dir = Directory(rootPath.path); + } + try { + if (!dir.existsSync()) { + dir.createSync(); + } + } catch (e, s) { + Logging.instance + .log("$e\n$s", level: LogLevel.Error); + } + String? path; + if (Platform.isAndroid) { + path = dir.path; + } else { + path = await FilePicker.platform + .getDirectoryPath( + dialogTitle: "Choose Log Save Location", + initialDirectory: + systemfile.startPath!.path, + lockParentWindow: true, + ); + } - if (Platform.isAndroid) { - unawaited( - showDialog( - context: context, - builder: (context) => StackOkDialog( - title: logssaved - ? "Logs saved to" - : "Error Saving Logs", - message: "${path!}/$filename", + if (path != null) { + final eventBus = EventBus(); + bool shouldPop = false; + unawaited(showDialog<dynamic>( + barrierDismissible: false, + context: context, + builder: (_) => WillPopScope( + onWillPop: () async { + return shouldPop; + }, + child: CustomLoadingOverlay( + message: "Generating Stack logs file", + eventBus: eventBus, + ), + ), + )); + + bool logssaved = true; + var filename; + try { + filename = await ref + .read(debugServiceProvider) + .exportToFile(path, eventBus); + } catch (e, s) { + logssaved = false; + Logging.instance + .log("$e $s", level: LogLevel.Error); + } + + shouldPop = true; + + if (mounted) { + Navigator.pop(context); + + if (Platform.isAndroid) { + unawaited( + showDialog( + context: context, + builder: (context) => StackOkDialog( + title: logssaved + ? "Logs saved to" + : "Error Saving Logs", + message: "${path!}/$filename", + ), ), - ), - ); - } else { - unawaited( - showFloatingFlushBar( - type: FlushBarType.info, - context: context, - message: logssaved - ? 'Logs file saved' - : "Error Saving Logs", - ), - ); + ); + } else { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + context: context, + message: logssaved + ? 'Logs file saved' + : "Error Saving Logs", + ), + ); + } } } - } - }, - ), - ], - ) - ], + }, + ), + ], + ) + ], + ), ), ), ), - ), - ]; - }, - body: Builder( - builder: (context) { - final logs = filtered( - ref.watch(debugServiceProvider - .select((value) => value.recentLogs)), - _searchTerm) - .reversed - .toList(growable: false); + ]; + }, + body: Builder( + builder: (context) { + final logs = filtered( + ref.watch(debugServiceProvider + .select((value) => value.recentLogs)), + _searchTerm) + .reversed + .toList(growable: false); - return CustomScrollView( - reverse: true, - // shrinkWrap: true, - controller: scrollController, - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor( - context, + return CustomScrollView( + reverse: true, + // shrinkWrap: true, + controller: scrollController, + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor( + context, + ), ), - ), - SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - final log = logs[index]; + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final log = logs[index]; - return Container( - key: Key("log_${log.id}_${log.timestampInMillisUTC}"), - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .popupBG, - borderRadius: _borderRadius(index, logs.length), - ), - child: Padding( - padding: const EdgeInsets.all(4), - child: RoundedContainer( - padding: const EdgeInsets.all(0), + return Container( + key: Key( + "log_${log.id}_${log.timestampInMillisUTC}"), + decoration: BoxDecoration( color: Theme.of(context) .extension<StackColors>()! .popupBG, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Text( - " [${log.logLevel.name}]", - style: STextStyles.baseXS(context) - .copyWith( - fontSize: 8, - color: (log.logLevel == LogLevel.Info - ? Theme.of(context) - .extension<StackColors>()! - .topNavIconGreen - : (log.logLevel == - LogLevel.Warning - ? Theme.of(context) - .extension<StackColors>()! - .topNavIconYellow - : (log.logLevel == - LogLevel.Error - ? Colors.orange - : Theme.of(context) - .extension< - StackColors>()! - .topNavIconRed))), + borderRadius: _borderRadius(index, logs.length), + ), + child: Padding( + padding: const EdgeInsets.all(4), + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: Theme.of(context) + .extension<StackColors>()! + .popupBG, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + " [${log.logLevel.name}]", + style: STextStyles.baseXS(context) + .copyWith( + fontSize: 8, + color: (log.logLevel == + LogLevel.Info + ? Theme.of(context) + .extension<StackColors>()! + .topNavIconGreen + : (log.logLevel == + LogLevel.Warning + ? Theme.of(context) + .extension< + StackColors>()! + .topNavIconYellow + : (log.logLevel == + LogLevel.Error + ? Colors.orange + : Theme.of(context) + .extension< + StackColors>()! + .topNavIconRed))), + ), ), - ), - Text( - "[${DateTime.fromMillisecondsSinceEpoch(log.timestampInMillisUTC, isUtc: true)}]: ", - style: STextStyles.baseXS(context) - .copyWith( - fontSize: 8, - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, + Text( + "[${DateTime.fromMillisecondsSinceEpoch(log.timestampInMillisUTC, isUtc: true)}]: ", + style: STextStyles.baseXS(context) + .copyWith( + fontSize: 8, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + ), ), - ), - ], - ), - Row( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - const SizedBox( - width: 20, - ), - Flexible( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - SelectableText( - log.message, - style: STextStyles.baseXS(context) - .copyWith(fontSize: 8), - ), - ], + ], + ), + Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const SizedBox( + width: 20, ), - ), - ], - ), - ], + Flexible( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + SelectableText( + log.message, + style: + STextStyles.baseXS(context) + .copyWith(fontSize: 8), + ), + ], + ), + ), + ], + ), + ], + ), ), ), - ), - ); - }, - childCount: logs.length, + ); + }, + childCount: logs.length, + ), ), - ), - ], - ); - }, + ], + ); + }, + ), ), ), ), diff --git a/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart b/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart index d1e893802..693f39f02 100644 --- a/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart +++ b/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart @@ -10,6 +10,7 @@ import 'package:stackwallet/utilities/theme/dark_colors.dart'; import 'package:stackwallet/utilities/theme/light_colors.dart'; import 'package:stackwallet/utilities/theme/ocean_breeze_colors.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -32,127 +33,131 @@ class AppearanceSettingsView extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Appearance", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Appearance", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - child: Consumer( - builder: (_, ref, __) { - return RawMaterialButton( - splashColor: Theme.of(context) - .extension<StackColors>()! - .highlight, - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + body: Padding( + padding: const EdgeInsets.all(16), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + child: Consumer( + builder: (_, ref, __) { + return RawMaterialButton( + splashColor: Theme.of(context) + .extension<StackColors>()! + .highlight, + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - ), - onPressed: null, - child: Padding( - padding: - const EdgeInsets.symmetric(vertical: 8), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Display favorite wallets", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - SizedBox( - height: 20, - width: 40, - child: DraggableSwitchButton( - isOn: ref.watch( - prefsChangeNotifierProvider.select( - (value) => - value.showFavoriteWallets), - ), - onValueChanged: (newValue) { - ref - .read(prefsChangeNotifierProvider) - .showFavoriteWallets = newValue; - }, + onPressed: null, + child: Padding( + padding: + const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Display favorite wallets", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, ), - ) - ], + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: ref.watch( + prefsChangeNotifierProvider.select( + (value) => + value.showFavoriteWallets), + ), + onValueChanged: (newValue) { + ref + .read( + prefsChangeNotifierProvider) + .showFavoriteWallets = newValue; + }, + ), + ) + ], + ), ), - ), - ); - }, + ); + }, + ), ), - ), - const SizedBox( - height: 10, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + const SizedBox( + height: 10, + ), + RoundedWhiteContainer( padding: const EdgeInsets.all(0), - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + padding: const EdgeInsets.all(0), + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - ), - onPressed: null, - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Choose Theme", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - const Padding( - padding: EdgeInsets.all(10), - child: ThemeOptionsView(), - ), - ], - ), - ], + onPressed: null, + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Choose Theme", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + const Padding( + padding: EdgeInsets.all(10), + child: ThemeOptionsView(), + ), + ], + ), + ], + ), ), ), ), - ), - ], + ], + ), ), ), - ), - ); - }, + ); + }, + ), ), ), ); diff --git a/lib/pages/settings_views/global_settings_view/currency_view.dart b/lib/pages/settings_views/global_settings_view/currency_view.dart index dccf2d61b..5bcf7fb7f 100644 --- a/lib/pages/settings_views/global_settings_view/currency_view.dart +++ b/lib/pages/settings_views/global_settings_view/currency_view.dart @@ -8,6 +8,7 @@ import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; @@ -130,34 +131,37 @@ class _CurrencyViewState extends ConsumerState<BaseCurrencySettingsView> { return ConditionalParent( condition: !isDesktop, builder: (child) { - return Scaffold( - backgroundColor: - Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, + return Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Currency", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Currency", - style: STextStyles.navBarTitle(context), + body: Padding( + padding: const EdgeInsets.only( + top: 12, + left: 16, + right: 16, + ), + child: child, ), ), - body: Padding( - padding: const EdgeInsets.only( - top: 12, - left: 16, - right: 16, - ), - child: child, - ), ); }, child: ConditionalParent( diff --git a/lib/pages/settings_views/global_settings_view/global_settings_view.dart b/lib/pages/settings_views/global_settings_view/global_settings_view.dart index fe6529c20..49e7ea36c 100644 --- a/lib/pages/settings_views/global_settings_view/global_settings_view.dart +++ b/lib/pages/settings_views/global_settings_view/global_settings_view.dart @@ -20,11 +20,10 @@ import 'package:stackwallet/route_generator.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/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; -import 'package:stackwallet/utilities/delete_everything.dart'; - class GlobalSettingsView extends StatelessWidget { const GlobalSettingsView({ Key? key, @@ -35,254 +34,257 @@ class GlobalSettingsView extends StatelessWidget { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Settings", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Settings", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (builderContext, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - padding: const EdgeInsets.all(4), - child: Column( - children: [ - SettingsListButton( - iconAssetName: Assets.svg.addressBook, - iconSize: 16, - title: "Address book", - onPressed: () { - Navigator.of(context) - .pushNamed(AddressBookView.routeName); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.downloadFolder, - iconSize: 14, - title: "Stack backup & restore", - onPressed: () { - Navigator.push( - context, - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator.useMaterialPageRoute, - builder: (_) => const LockscreenView( - showBackButton: true, - routeOnSuccess: - StackBackupView.routeName, - biometricsCancelButtonString: "CANCEL", - biometricsLocalizedReason: - "Authenticate to access Stack backup & restore settings", - biometricsAuthenticationTitle: - "Stack backup", - ), - settings: const RouteSettings( - name: "/swblockscreen"), - ), - ); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.lock, - iconSize: 16, - title: "Security", - onPressed: () { - Navigator.of(context) - .pushNamed(SecurityView.routeName); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.dollarSign, - iconSize: 18, - title: "Currency", - onPressed: () { - Navigator.of(context).pushNamed( - BaseCurrencySettingsView.routeName); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.language, - iconSize: 18, - title: "Language", - onPressed: () { - Navigator.of(context).pushNamed( - LanguageSettingsView.routeName); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.node, - iconSize: 16, - title: "Manage nodes", - onPressed: () { - Navigator.of(context) - .pushNamed(ManageNodesView.routeName); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.arrowRotate3, - iconSize: 18, - title: "Syncing preferences", - onPressed: () { - Navigator.of(context).pushNamed( - SyncingPreferencesView.routeName); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.arrowUpRight, - iconSize: 16, - title: "Startup", - onPressed: () { - Navigator.of(context).pushNamed( - StartupPreferencesView.routeName); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.sun, - iconSize: 18, - title: "Appearance", - onPressed: () { - Navigator.of(context).pushNamed( - AppearanceSettingsView.routeName); - }, - ), - if (Platform.isIOS) + body: LayoutBuilder( + builder: (builderContext, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(4), + child: Column( + children: [ + SettingsListButton( + iconAssetName: Assets.svg.addressBook, + iconSize: 16, + title: "Address book", + onPressed: () { + Navigator.of(context) + .pushNamed(AddressBookView.routeName); + }, + ), const SizedBox( height: 8, ), - if (Platform.isIOS) SettingsListButton( - iconAssetName: Assets.svg.circleAlert, - iconSize: 16, - title: "Delete account", - onPressed: () async { - await Navigator.of(context) - .pushNamed(DeleteAccountView.routeName); + iconAssetName: Assets.svg.downloadFolder, + iconSize: 14, + title: "Stack backup & restore", + onPressed: () { + Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator.useMaterialPageRoute, + builder: (_) => const LockscreenView( + showBackButton: true, + routeOnSuccess: + StackBackupView.routeName, + biometricsCancelButtonString: + "CANCEL", + biometricsLocalizedReason: + "Authenticate to access Stack backup & restore settings", + biometricsAuthenticationTitle: + "Stack backup", + ), + settings: const RouteSettings( + name: "/swblockscreen"), + ), + ); }, ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.ellipsis, - iconSize: 18, - title: "About", - onPressed: () { - Navigator.of(context) - .pushNamed(AboutView.routeName); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.solidSliders, - iconSize: 16, - title: "Advanced", - onPressed: () { - Navigator.of(context).pushNamed( - AdvancedSettingsView.routeName); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.questionMessage, - iconSize: 16, - title: "Support", - onPressed: () { - Navigator.of(context) - .pushNamed(SupportView.routeName); - }, - ), - // TextButton( - // style: Theme.of(context) - // .textButtonTheme - // .style - // ?.copyWith( - // backgroundColor: - // MaterialStateProperty.all<Color>( - // Theme.of(context).extension<StackColors>()!.accentColorDark - // ), - // ), - // child: Text( - // "fire test notification", - // style: STextStyles.button(context), - // ), - // onPressed: () async { - // NotificationApi.showNotification2( - // title: "Test notification", - // body: "My doggy wallet", - // walletId: - // "3c5e2d70-fcc3-11ec-86a3-31a106a81c3b", - // iconAssetName: - // Assets.svg.iconFor(coin: Coin.dogecoin), - // date: DateTime.now(), - // ); - // }, - // ), - ], + const SizedBox( + height: 8, + ), + SettingsListButton( + iconAssetName: Assets.svg.lock, + iconSize: 16, + title: "Security", + onPressed: () { + Navigator.of(context) + .pushNamed(SecurityView.routeName); + }, + ), + const SizedBox( + height: 8, + ), + SettingsListButton( + iconAssetName: Assets.svg.dollarSign, + iconSize: 18, + title: "Currency", + onPressed: () { + Navigator.of(context).pushNamed( + BaseCurrencySettingsView.routeName); + }, + ), + const SizedBox( + height: 8, + ), + SettingsListButton( + iconAssetName: Assets.svg.language, + iconSize: 18, + title: "Language", + onPressed: () { + Navigator.of(context).pushNamed( + LanguageSettingsView.routeName); + }, + ), + const SizedBox( + height: 8, + ), + SettingsListButton( + iconAssetName: Assets.svg.node, + iconSize: 16, + title: "Manage nodes", + onPressed: () { + Navigator.of(context) + .pushNamed(ManageNodesView.routeName); + }, + ), + const SizedBox( + height: 8, + ), + SettingsListButton( + iconAssetName: Assets.svg.arrowRotate3, + iconSize: 18, + title: "Syncing preferences", + onPressed: () { + Navigator.of(context).pushNamed( + SyncingPreferencesView.routeName); + }, + ), + const SizedBox( + height: 8, + ), + SettingsListButton( + iconAssetName: Assets.svg.arrowUpRight, + iconSize: 16, + title: "Startup", + onPressed: () { + Navigator.of(context).pushNamed( + StartupPreferencesView.routeName); + }, + ), + const SizedBox( + height: 8, + ), + SettingsListButton( + iconAssetName: Assets.svg.sun, + iconSize: 18, + title: "Appearance", + onPressed: () { + Navigator.of(context).pushNamed( + AppearanceSettingsView.routeName); + }, + ), + if (Platform.isIOS) + const SizedBox( + height: 8, + ), + if (Platform.isIOS) + SettingsListButton( + iconAssetName: Assets.svg.circleAlert, + iconSize: 16, + title: "Delete account", + onPressed: () async { + await Navigator.of(context).pushNamed( + DeleteAccountView.routeName); + }, + ), + const SizedBox( + height: 8, + ), + SettingsListButton( + iconAssetName: Assets.svg.ellipsis, + iconSize: 18, + title: "About", + onPressed: () { + Navigator.of(context) + .pushNamed(AboutView.routeName); + }, + ), + const SizedBox( + height: 8, + ), + SettingsListButton( + iconAssetName: Assets.svg.solidSliders, + iconSize: 16, + title: "Advanced", + onPressed: () { + Navigator.of(context).pushNamed( + AdvancedSettingsView.routeName); + }, + ), + const SizedBox( + height: 8, + ), + SettingsListButton( + iconAssetName: Assets.svg.questionMessage, + iconSize: 16, + title: "Support", + onPressed: () { + Navigator.of(context) + .pushNamed(SupportView.routeName); + }, + ), + // TextButton( + // style: Theme.of(context) + // .textButtonTheme + // .style + // ?.copyWith( + // backgroundColor: + // MaterialStateProperty.all<Color>( + // Theme.of(context).extension<StackColors>()!.accentColorDark + // ), + // ), + // child: Text( + // "fire test notification", + // style: STextStyles.button(context), + // ), + // onPressed: () async { + // NotificationApi.showNotification2( + // title: "Test notification", + // body: "My doggy wallet", + // walletId: + // "3c5e2d70-fcc3-11ec-86a3-31a106a81c3b", + // iconAssetName: + // Assets.svg.iconFor(coin: Coin.dogecoin), + // date: DateTime.now(), + // ); + // }, + // ), + ], + ), ), - ), - const SizedBox( - height: 12, - ), - ], + const SizedBox( + height: 12, + ), + ], + ), ), ), ), ), - ), - ); - }, + ); + }, + ), ), ); } diff --git a/lib/pages/settings_views/global_settings_view/hidden_settings.dart b/lib/pages/settings_views/global_settings_view/hidden_settings.dart index 6b6377ab6..7f35fcc86 100644 --- a/lib/pages/settings_views/global_settings_view/hidden_settings.dart +++ b/lib/pages/settings_views/global_settings_view/hidden_settings.dart @@ -5,9 +5,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/providers/global/debug_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; -import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class HiddenSettings extends StatelessWidget { @@ -17,149 +17,151 @@ class HiddenSettings extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: Container(), - title: Text( - "Not so secret anymore", - style: STextStyles.navBarTitle(context), + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: Container(), + title: Text( + "Not so secret anymore", + style: STextStyles.navBarTitle(context), + ), ), - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Consumer(builder: (_, ref, __) { - return GestureDetector( - onTap: () async { - final notifs = - ref.read(notificationsProvider).notifications; + body: Padding( + padding: const EdgeInsets.all(16), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Consumer(builder: (_, ref, __) { + return GestureDetector( + onTap: () async { + final notifs = + ref.read(notificationsProvider).notifications; - for (final n in notifs) { + for (final n in notifs) { + await ref + .read(notificationsProvider) + .delete(n, false); + } await ref .read(notificationsProvider) - .delete(n, false); - } - await ref - .read(notificationsProvider) - .delete(notifs[0], true); + .delete(notifs[0], true); - unawaited(showFloatingFlushBar( - type: FlushBarType.success, - message: "Notification history deleted", - context: context, - )); - }, - child: RoundedWhiteContainer( - child: Text( - "Delete notifications", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), + unawaited(showFloatingFlushBar( + type: FlushBarType.success, + message: "Notification history deleted", + context: context, + )); + }, + child: RoundedWhiteContainer( + child: Text( + "Delete notifications", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), ), - ), - ); - }), - // const SizedBox( - // height: 12, - // ), - // Consumer(builder: (_, ref, __) { - // return GestureDetector( - // onTap: () async { - // final trades = - // ref.read(tradesServiceProvider).trades; - // - // for (final trade in trades) { - // ref.read(tradesServiceProvider).delete( - // trade: trade, shouldNotifyListeners: false); - // } - // ref.read(tradesServiceProvider).delete( - // trade: trades[0], shouldNotifyListeners: true); - // - // // ref.read(notificationsProvider).DELETE_EVERYTHING(); - // }, - // child: RoundedWhiteContainer( - // child: Text( - // "Delete trade history", - // style: STextStyles.button(context).copyWith( - // color: Theme.of(context).extension<StackColors>()!.accentColorDark - // ), - // ), - // ), - // ); - // }), - const SizedBox( - height: 12, - ), - Consumer(builder: (_, ref, __) { - return GestureDetector( - onTap: () async { - await ref - .read(debugServiceProvider) - .deleteAllMessages(); + ); + }), + // const SizedBox( + // height: 12, + // ), + // Consumer(builder: (_, ref, __) { + // return GestureDetector( + // onTap: () async { + // final trades = + // ref.read(tradesServiceProvider).trades; + // + // for (final trade in trades) { + // ref.read(tradesServiceProvider).delete( + // trade: trade, shouldNotifyListeners: false); + // } + // ref.read(tradesServiceProvider).delete( + // trade: trades[0], shouldNotifyListeners: true); + // + // // ref.read(notificationsProvider).DELETE_EVERYTHING(); + // }, + // child: RoundedWhiteContainer( + // child: Text( + // "Delete trade history", + // style: STextStyles.button(context).copyWith( + // color: Theme.of(context).extension<StackColors>()!.accentColorDark + // ), + // ), + // ), + // ); + // }), + const SizedBox( + height: 12, + ), + Consumer(builder: (_, ref, __) { + return GestureDetector( + onTap: () async { + await ref + .read(debugServiceProvider) + .deleteAllMessages(); - unawaited(showFloatingFlushBar( - type: FlushBarType.success, - message: "Debug Logs deleted", - context: context, - )); - }, - child: RoundedWhiteContainer( - child: Text( - "Delete Debug Logs", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), + unawaited(showFloatingFlushBar( + type: FlushBarType.success, + message: "Debug Logs deleted", + context: context, + )); + }, + child: RoundedWhiteContainer( + child: Text( + "Delete Debug Logs", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), ), - ), - ); - }), - // const SizedBox( - // height: 12, - // ), - // GestureDetector( - // onTap: () async { - // showDialog<void>( - // context: context, - // builder: (_) { - // return StackDialogBase( - // child: SizedBox( - // width: 200, - // child: Lottie.asset( - // Assets.lottie.test2, - // ), - // ), - // ); - // }, - // ); - // }, - // child: RoundedWhiteContainer( - // child: Text( - // "Lottie test", - // style: STextStyles.button(context).copyWith( - // color: Theme.of(context).extension<StackColors>()!.accentColorDark - // ), - // ), - // ), - // ), - ], + ); + }), + // const SizedBox( + // height: 12, + // ), + // GestureDetector( + // onTap: () async { + // showDialog<void>( + // context: context, + // builder: (_) { + // return StackDialogBase( + // child: SizedBox( + // width: 200, + // child: Lottie.asset( + // Assets.lottie.test2, + // ), + // ), + // ); + // }, + // ); + // }, + // child: RoundedWhiteContainer( + // child: Text( + // "Lottie test", + // style: STextStyles.button(context).copyWith( + // color: Theme.of(context).extension<StackColors>()!.accentColorDark + // ), + // ), + // ), + // ), + ], + ), ), ), - ), - ); - }, + ); + }, + ), ), ), ); diff --git a/lib/pages/settings_views/global_settings_view/language_view.dart b/lib/pages/settings_views/global_settings_view/language_view.dart index b617546e4..9ba2d6dd2 100644 --- a/lib/pages/settings_views/global_settings_view/language_view.dart +++ b/lib/pages/settings_views/global_settings_view/language_view.dart @@ -7,14 +7,14 @@ import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/languages_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; -import 'package:stackwallet/utilities/util.dart'; - class LanguageSettingsView extends ConsumerStatefulWidget { const LanguageSettingsView({Key? key}) : super(key: key); @@ -100,203 +100,205 @@ class _LanguageViewState extends ConsumerState<LanguageSettingsView> { listWithoutSelected.insert(0, current); } listWithoutSelected = _filtered(); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Language", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Language", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.only( - top: 12, - left: 16, - right: 16, - ), - child: NestedScrollView( - floatHeaderSlivers: true, - headerSliverBuilder: (context, innerBoxIsScrolled) { - return [ - SliverOverlapAbsorber( - handle: - NestedScrollView.sliverOverlapAbsorberHandleFor(context), - sliver: SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only(bottom: 16), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: _searchController, - focusNode: _searchFocusNode, - onChanged: (newString) { - setState(() => filter = newString); - }, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - _searchFocusNode, - context, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, + body: Padding( + padding: const EdgeInsets.only( + top: 12, + left: 16, + right: 16, + ), + child: NestedScrollView( + floatHeaderSlivers: true, + headerSliverBuilder: (context, innerBoxIsScrolled) { + return [ + SliverOverlapAbsorber( + handle: + NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(bottom: 16), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: (newString) { + setState(() => filter = newString); + }, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, - ), - ), - suffixIcon: _searchController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchController.text = ""; - filter = ""; - }); - }, - ), - ], + suffixIcon: _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + filter = ""; + }); + }, + ), + ], + ), ), - ), - ) - : null, + ) + : null, + ), ), ), ), ), ), - ), - ]; - }, - body: Builder( - builder: (context) { - return CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor( - context, + ]; + }, + body: Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor( + context, + ), ), - ), - SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - return Container( - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .popupBG, - borderRadius: _borderRadius(index), - ), - child: Padding( - padding: const EdgeInsets.all(4), - key: Key( - "languageSelect_${listWithoutSelected[index]}"), - child: RoundedContainer( - padding: const EdgeInsets.all(0), - color: index == 0 - ? Theme.of(context) - .extension<StackColors>()! - .currencyListItemBG - : Theme.of(context) - .extension<StackColors>()! - .popupBG, - child: RawMaterialButton( - onPressed: () async { - onTap(index); - }, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .popupBG, + borderRadius: _borderRadius(index), + ), + child: Padding( + padding: const EdgeInsets.all(4), + key: Key( + "languageSelect_${listWithoutSelected[index]}"), + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: index == 0 + ? Theme.of(context) + .extension<StackColors>()! + .currencyListItemBG + : Theme.of(context) + .extension<StackColors>()! + .popupBG, + child: RawMaterialButton( + onPressed: () async { + onTap(index); + }, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - ), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Row( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 20, - child: Radio( - activeColor: Theme.of(context) - .extension<StackColors>()! - .radioButtonIconEnabled, - value: true, - groupValue: index == 0, - onChanged: (_) { - onTap(index); - }, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: true, + groupValue: index == 0, + onChanged: (_) { + onTap(index); + }, + ), ), - ), - const SizedBox( - width: 12, - ), - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - listWithoutSelected[index], - key: (index == 0) - ? const Key( - "selectedLanguageSettingsLanguageText") - : null, - style: STextStyles.largeMedium14( - context), - ), - const SizedBox( - height: 2, - ), - Text( - listWithoutSelected[index], - key: (index == 0) - ? const Key( - "selectedLanguageSettingsLanguageTextDescription") - : null, - style: STextStyles.itemSubtitle( - context), - ), - ], - ), - ], + const SizedBox( + width: 12, + ), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + listWithoutSelected[index], + key: (index == 0) + ? const Key( + "selectedLanguageSettingsLanguageText") + : null, + style: STextStyles.largeMedium14( + context), + ), + const SizedBox( + height: 2, + ), + Text( + listWithoutSelected[index], + key: (index == 0) + ? const Key( + "selectedLanguageSettingsLanguageTextDescription") + : null, + style: STextStyles.itemSubtitle( + context), + ), + ], + ), + ], + ), ), ), ), ), - ), - ); - }, - childCount: listWithoutSelected.length, + ); + }, + childCount: listWithoutSelected.length, + ), ), - ), - ], - ); - }, + ], + ); + }, + ), ), ), ), diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index 9062314f0..bd6e5c6d8 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -12,7 +12,6 @@ import 'package:stackwallet/providers/providers.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/flush_bar_type.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/test_epic_box_connection.dart'; @@ -20,6 +19,7 @@ import 'package:stackwallet/utilities/test_monero_node_connection.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; @@ -238,7 +238,8 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> { Expanded( child: SecondaryButton( label: "Cancel", - buttonHeight: ButtonHeight.l, + buttonHeight: + isDesktop ? ButtonHeight.l : null, onPressed: () => Navigator.of( context, rootNavigator: true, @@ -251,7 +252,8 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> { Expanded( child: PrimaryButton( label: "Save", - buttonHeight: ButtonHeight.l, + buttonHeight: + isDesktop ? ButtonHeight.l : null, onPressed: () => Navigator.of( context, rootNavigator: true, @@ -409,84 +411,90 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> { return ConditionalParent( condition: !isDesktop, - builder: (child) => Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - viewType == AddEditNodeViewType.edit ? "Edit node" : "Add node", - style: STextStyles.navBarTitle(context), - ), - actions: [ - if (viewType == AddEditNodeViewType.edit) - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("deleteNodeAppBarButtonKey"), - size: 36, - shadows: const [], - color: - Theme.of(context).extension<StackColors>()!.background, - icon: SvgPicture.asset( - Assets.svg.trash, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + viewType == AddEditNodeViewType.edit ? "Edit node" : "Add node", + style: STextStyles.navBarTitle(context), + ), + actions: [ + if (viewType == AddEditNodeViewType.edit) + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("deleteNodeAppBarButtonKey"), + size: 36, + shadows: const [], color: Theme.of(context) .extension<StackColors>()! - .accentColorDark, - width: 20, - height: 20, - ), - onPressed: () async { - Navigator.popUntil(context, - ModalRoute.withName(widget.routeOnSuccessOrDelete)); + .background, + icon: SvgPicture.asset( + Assets.svg.trash, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + width: 20, + height: 20, + ), + onPressed: () async { + Navigator.popUntil(context, + ModalRoute.withName(widget.routeOnSuccessOrDelete)); - await ref.read(nodeServiceChangeNotifierProvider).delete( - nodeId!, - true, - ); - }, - ), - ), - ), - ], - ), - body: Padding( - padding: const EdgeInsets.only( - top: 12, - left: 12, - right: 12, - bottom: 12, - ), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(4), - child: ConstrainedBox( - constraints: - BoxConstraints(minHeight: constraints.maxHeight - 8), - child: IntrinsicHeight( - child: child, + await ref + .read(nodeServiceChangeNotifierProvider) + .delete( + nodeId!, + true, + ); + }, ), ), ), - ); - }, + ], + ), + body: Padding( + padding: const EdgeInsets.only( + top: 12, + left: 12, + right: 12, + bottom: 12, + ), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(4), + child: ConstrainedBox( + constraints: + BoxConstraints(minHeight: constraints.maxHeight - 8), + child: IntrinsicHeight( + child: child, + ), + ), + ), + ); + }, + ), ), ), ), @@ -562,7 +570,7 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> { child: SecondaryButton( label: "Test connection", enabled: testConnectionEnabled, - buttonHeight: ButtonHeight.l, + buttonHeight: isDesktop ? ButtonHeight.l : null, onPressed: testConnectionEnabled ? () async { await _testConnection(); diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/coin_nodes_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/coin_nodes_view.dart index a93b64be3..91e6871f3 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/coin_nodes_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/coin_nodes_view.dart @@ -8,6 +8,7 @@ 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/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; @@ -122,66 +123,70 @@ class _CoinNodesViewState extends ConsumerState<CoinNodesView> { ), ); } else { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "${widget.coin.prettyName} nodes", - style: STextStyles.navBarTitle(context), - ), - actions: [ - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("manageNodesAddNewNodeButtonKey"), - size: 36, - shadows: const [], - color: Theme.of(context).extension<StackColors>()!.background, - icon: SvgPicture.asset( - Assets.svg.plus, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - width: 20, - height: 20, + return Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "${widget.coin.prettyName} nodes", + style: STextStyles.navBarTitle(context), + ), + actions: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("manageNodesAddNewNodeButtonKey"), + size: 36, + shadows: const [], + color: + Theme.of(context).extension<StackColors>()!.background, + icon: SvgPicture.asset( + Assets.svg.plus, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + width: 20, + height: 20, + ), + onPressed: () { + Navigator.of(context).pushNamed( + AddEditNodeView.routeName, + arguments: Tuple4( + AddEditNodeViewType.add, + widget.coin, + null, + CoinNodesView.routeName, + ), + ); + }, ), - onPressed: () { - Navigator.of(context).pushNamed( - AddEditNodeView.routeName, - arguments: Tuple4( - AddEditNodeViewType.add, - widget.coin, - null, - CoinNodesView.routeName, - ), - ); - }, ), ), - ), - ], - ), - body: Padding( - padding: const EdgeInsets.only( - top: 12, - left: 12, - right: 12, + ], ), - child: SingleChildScrollView( - child: NodesList( - coin: widget.coin, - popBackToRoute: CoinNodesView.routeName, + body: Padding( + padding: const EdgeInsets.only( + top: 12, + left: 12, + right: 12, + ), + child: SingleChildScrollView( + child: NodesList( + coin: widget.coin, + popBackToRoute: CoinNodesView.routeName, + ), ), ), ), diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/manage_nodes_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/manage_nodes_view.dart index 22f239232..743fc6957 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/manage_nodes_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/manage_nodes_view.dart @@ -8,6 +8,7 @@ import 'package:stackwallet/utilities/constants.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/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -47,88 +48,91 @@ class _ManageNodesViewState extends ConsumerState<ManageNodesView> { ? _coins : _coins.sublist(0, _coins.length - kTestNetCoinCount); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Manage nodes", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Manage nodes", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.only( - top: 12, - left: 12, - right: 12, - ), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ...coins.map( - (coin) { - final count = ref - .watch(nodeServiceChangeNotifierProvider - .select((value) => value.getNodesFor(coin))) - .length; + body: Padding( + padding: const EdgeInsets.only( + top: 12, + left: 12, + right: 12, + ), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ...coins.map( + (coin) { + final count = ref + .watch(nodeServiceChangeNotifierProvider + .select((value) => value.getNodesFor(coin))) + .length; - return Padding( - padding: const EdgeInsets.all(4), - child: RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + return Padding( + padding: const EdgeInsets.all(4), + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - ), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - onPressed: () { - Navigator.of(context).pushNamed( - CoinNodesView.routeName, - arguments: coin, - ); - }, - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.iconFor(coin: coin), - width: 24, - height: 24, - ), - const SizedBox( - width: 12, - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "${coin.prettyName} nodes", - style: STextStyles.titleBold12(context), - ), - Text( - count > 1 ? "$count nodes" : "Default", - style: STextStyles.label(context), - ), - ], - ) - ], + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + onPressed: () { + Navigator.of(context).pushNamed( + CoinNodesView.routeName, + arguments: coin, + ); + }, + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.iconFor(coin: coin), + width: 24, + height: 24, + ), + const SizedBox( + width: 12, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${coin.prettyName} nodes", + style: STextStyles.titleBold12(context), + ), + Text( + count > 1 ? "$count nodes" : "Default", + style: STextStyles.label(context), + ), + ], + ) + ], + ), ), ), ), - ), - ); - }, - ), - ], + ); + }, + ), + ], + ), ), ), ), diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart index 71d764135..2e43b5595 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart @@ -18,6 +18,7 @@ import 'package:stackwallet/utilities/test_monero_node_connection.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/delete_button.dart'; @@ -179,85 +180,89 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> { return ConditionalParent( condition: !isDesktop, - builder: (child) => Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "Node details", - style: STextStyles.navBarTitle(context), - ), - actions: [ - if (!nodeId.startsWith("default")) - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("nodeDetailsEditNodeAppBarButtonKey"), - size: 36, - shadows: const [], - color: - Theme.of(context).extension<StackColors>()!.background, - icon: SvgPicture.asset( - Assets.svg.pencil, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Node details", + style: STextStyles.navBarTitle(context), + ), + actions: [ + if (!nodeId.startsWith("default")) + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("nodeDetailsEditNodeAppBarButtonKey"), + size: 36, + shadows: const [], color: Theme.of(context) .extension<StackColors>()! - .accentColorDark, - width: 20, - height: 20, + .background, + icon: SvgPicture.asset( + Assets.svg.pencil, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + width: 20, + height: 20, + ), + onPressed: () { + Navigator.of(context).pushNamed( + AddEditNodeView.routeName, + arguments: Tuple4( + AddEditNodeViewType.edit, + coin, + nodeId, + popRouteName, + ), + ); + }, ), - onPressed: () { - Navigator.of(context).pushNamed( - AddEditNodeView.routeName, - arguments: Tuple4( - AddEditNodeViewType.edit, - coin, - nodeId, - popRouteName, - ), - ); - }, ), ), - ), - ], - ), - body: Padding( - padding: const EdgeInsets.only( - top: 12, - left: 12, - right: 12, + ], ), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(4), - child: ConstrainedBox( - constraints: - BoxConstraints(minHeight: constraints.maxHeight - 8), - child: IntrinsicHeight( - child: child, + body: Padding( + padding: const EdgeInsets.only( + top: 12, + left: 12, + right: 12, + ), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(4), + child: ConstrainedBox( + constraints: + BoxConstraints(minHeight: constraints.maxHeight - 8), + child: IntrinsicHeight( + child: child, + ), ), ), - ), - ); - }, + ); + }, + ), ), ), ), diff --git a/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart b/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart index c88da0521..fb5722594 100644 --- a/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart +++ b/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart @@ -9,6 +9,7 @@ import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_pin_put/custom_pin_put.dart'; @@ -65,182 +66,186 @@ class _ChangePinViewState extends ConsumerState<ChangePinView> { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 70)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 70)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), ), - ), - body: SafeArea( - child: PageView( - controller: _pageController, - physics: const NeverScrollableScrollPhysics(), - children: [ - // page 1 - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Center( - child: Text( - "Create new PIN", - style: STextStyles.pageTitleH1(context), + body: SafeArea( + child: PageView( + controller: _pageController, + physics: const NeverScrollableScrollPhysics(), + children: [ + // page 1 + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Center( + child: Text( + "Create new PIN", + style: STextStyles.pageTitleH1(context), + ), ), - ), - const SizedBox( - height: 52, - ), - CustomPinPut( - fieldsCount: Constants.pinLength, - eachFieldHeight: 12, - eachFieldWidth: 12, - textStyle: STextStyles.label(context).copyWith( - fontSize: 1, + const SizedBox( + height: 52, ), - focusNode: _pinPutFocusNode1, - controller: _pinPutController1, - useNativeKeyboard: false, - obscureText: "", - inputDecoration: InputDecoration( - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - disabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - focusedErrorBorder: InputBorder.none, - fillColor: - Theme.of(context).extension<StackColors>()!.background, - counterText: "", - ), - submittedFieldDecoration: _pinPutDecoration.copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - border: Border.all( - width: 1, + CustomPinPut( + fieldsCount: Constants.pinLength, + eachFieldHeight: 12, + eachFieldWidth: 12, + textStyle: STextStyles.label(context).copyWith( + fontSize: 1, + ), + focusNode: _pinPutFocusNode1, + controller: _pinPutController1, + useNativeKeyboard: false, + obscureText: "", + inputDecoration: InputDecoration( + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + fillColor: Theme.of(context) + .extension<StackColors>()! + .background, + counterText: "", + ), + submittedFieldDecoration: _pinPutDecoration.copyWith( color: Theme.of(context) .extension<StackColors>()! .infoItemIcons, + border: Border.all( + width: 1, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + ), ), - ), - selectedFieldDecoration: _pinPutDecoration, - followingFieldDecoration: _pinPutDecoration, - onSubmit: (String pin) { - if (pin.length == Constants.pinLength) { - _pageController.nextPage( - duration: const Duration(milliseconds: 300), - curve: Curves.linear, - ); - } - }, - ), - ], - ), - - // page 2 - - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Center( - child: Text( - "Confirm new PIN", - style: STextStyles.pageTitleH1(context), - ), - ), - const SizedBox( - height: 52, - ), - CustomPinPut( - fieldsCount: Constants.pinLength, - eachFieldHeight: 12, - eachFieldWidth: 12, - textStyle: STextStyles.infoSmall(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle3, - fontSize: 1, - ), - focusNode: _pinPutFocusNode2, - controller: _pinPutController2, - useNativeKeyboard: false, - obscureText: "", - inputDecoration: InputDecoration( - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - disabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - focusedErrorBorder: InputBorder.none, - fillColor: - Theme.of(context).extension<StackColors>()!.background, - counterText: "", - ), - submittedFieldDecoration: _pinPutDecoration.copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - border: Border.all( - width: 1, - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - ), - ), - selectedFieldDecoration: _pinPutDecoration, - followingFieldDecoration: _pinPutDecoration, - onSubmit: (String pin) async { - if (_pinPutController1.text == _pinPutController2.text) { - // This should never fail as we are overwriting the existing pin - assert( - (await _secureStore.read(key: "stack_pin")) != null); - await _secureStore.write(key: "stack_pin", value: pin); - - showFloatingFlushBar( - type: FlushBarType.success, - message: "New PIN is set up", - context: context, - iconAsset: Assets.svg.check, - ); - - await Future<void>.delayed( - const Duration(milliseconds: 1200)); - - if (mounted) { - Navigator.of(context).popUntil( - ModalRoute.withName(SecurityView.routeName), + selectedFieldDecoration: _pinPutDecoration, + followingFieldDecoration: _pinPutDecoration, + onSubmit: (String pin) { + if (pin.length == Constants.pinLength) { + _pageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.linear, ); } - } else { - _pageController.animateTo( - 0, - duration: const Duration(milliseconds: 300), - curve: Curves.linear, - ); + }, + ), + ], + ), - showFloatingFlushBar( - type: FlushBarType.warning, - message: "PIN codes do not match. Try again.", - context: context, - iconAsset: Assets.svg.alertCircle, - ); + // page 2 - _pinPutController1.text = ''; - _pinPutController2.text = ''; - } - }, - ), - ], - ), - ], + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Center( + child: Text( + "Confirm new PIN", + style: STextStyles.pageTitleH1(context), + ), + ), + const SizedBox( + height: 52, + ), + CustomPinPut( + fieldsCount: Constants.pinLength, + eachFieldHeight: 12, + eachFieldWidth: 12, + textStyle: STextStyles.infoSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle3, + fontSize: 1, + ), + focusNode: _pinPutFocusNode2, + controller: _pinPutController2, + useNativeKeyboard: false, + obscureText: "", + inputDecoration: InputDecoration( + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + fillColor: Theme.of(context) + .extension<StackColors>()! + .background, + counterText: "", + ), + submittedFieldDecoration: _pinPutDecoration.copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + border: Border.all( + width: 1, + color: Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, + ), + ), + selectedFieldDecoration: _pinPutDecoration, + followingFieldDecoration: _pinPutDecoration, + onSubmit: (String pin) async { + if (_pinPutController1.text == _pinPutController2.text) { + // This should never fail as we are overwriting the existing pin + assert((await _secureStore.read(key: "stack_pin")) != + null); + await _secureStore.write(key: "stack_pin", value: pin); + + showFloatingFlushBar( + type: FlushBarType.success, + message: "New PIN is set up", + context: context, + iconAsset: Assets.svg.check, + ); + + await Future<void>.delayed( + const Duration(milliseconds: 1200)); + + if (mounted) { + Navigator.of(context).popUntil( + ModalRoute.withName(SecurityView.routeName), + ); + } + } else { + _pageController.animateTo( + 0, + duration: const Duration(milliseconds: 300), + curve: Curves.linear, + ); + + showFloatingFlushBar( + type: FlushBarType.warning, + message: "PIN codes do not match. Try again.", + context: context, + iconAsset: Assets.svg.alertCircle, + ); + + _pinPutController1.text = ''; + _pinPutController2.text = ''; + } + }, + ), + ], + ), + ], + ), ), ), ); diff --git a/lib/pages/settings_views/global_settings_view/security_views/security_view.dart b/lib/pages/settings_views/global_settings_view/security_views/security_view.dart index 24fce5cd8..c2a64bb50 100644 --- a/lib/pages/settings_views/global_settings_view/security_views/security_view.dart +++ b/lib/pages/settings_views/global_settings_view/security_views/security_view.dart @@ -7,6 +7,7 @@ import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -22,128 +23,131 @@ class SecurityView extends StatelessWidget { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Security", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Security", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - Navigator.push( - context, - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator.useMaterialPageRoute, - builder: (_) => const LockscreenView( - showBackButton: true, - routeOnSuccess: ChangePinView.routeName, - biometricsCancelButtonString: "CANCEL", - biometricsLocalizedReason: "Authenticate to change PIN", - biometricsAuthenticationTitle: "Change PIN", - ), - settings: - const RouteSettings(name: "/changepinlockscreen"), + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, ), - child: Row( - children: [ - Text( - "Change PIN", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, + onPressed: () { + Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator.useMaterialPageRoute, + builder: (_) => const LockscreenView( + showBackButton: true, + routeOnSuccess: ChangePinView.routeName, + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: + "Authenticate to change PIN", + biometricsAuthenticationTitle: "Change PIN", + ), + settings: + const RouteSettings(name: "/changepinlockscreen"), ), - ], + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + children: [ + Text( + "Change PIN", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + ], + ), ), ), ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - child: Consumer( - builder: (_, ref, __) { - return RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + child: Consumer( + builder: (_, ref, __) { + return RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - ), - onPressed: null, - // () { - // final useBio = - // ref.read(prefsChangeNotifierProvider).useBiometrics; - // - // debugPrint("useBio: $useBio"); - // ref.read(prefsChangeNotifierProvider).useBiometrics = - // !useBio; - // - // debugPrint( - // "useBio set to: ${ref.read(prefsChangeNotifierProvider).useBiometrics}"); - // }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Enable biometric authentication", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - SizedBox( - height: 20, - width: 40, - child: DraggableSwitchButton( - isOn: ref.watch( - prefsChangeNotifierProvider - .select((value) => value.useBiometrics), - ), - onValueChanged: (newValue) { - ref - .read(prefsChangeNotifierProvider) - .useBiometrics = newValue; - }, + onPressed: null, + // () { + // final useBio = + // ref.read(prefsChangeNotifierProvider).useBiometrics; + // + // debugPrint("useBio: $useBio"); + // ref.read(prefsChangeNotifierProvider).useBiometrics = + // !useBio; + // + // debugPrint( + // "useBio set to: ${ref.read(prefsChangeNotifierProvider).useBiometrics}"); + // }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Enable biometric authentication", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, ), - ), - ], + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: ref.watch( + prefsChangeNotifierProvider + .select((value) => value.useBiometrics), + ), + onValueChanged: (newValue) { + ref + .read(prefsChangeNotifierProvider) + .useBiometrics = newValue; + }, + ), + ), + ], + ), ), - ), - ); - }, + ); + }, + ), ), - ), - ], + ], + ), ), ), ); diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.dart index a94375742..15b5a12fa 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.dart @@ -11,6 +11,8 @@ import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart'; @@ -19,8 +21,6 @@ import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'package:stackwallet/utilities/util.dart'; - class AutoBackupView extends ConsumerStatefulWidget { const AutoBackupView({Key? key}) : super(key: key); @@ -225,239 +225,241 @@ class _AutoBackupViewState extends ConsumerState<AutoBackupView> { frequencyController.text = Format.prettyFrequencyType(next); }); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Auto Backup", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Auto Backup", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: null, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Auto Backup", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - SizedBox( - height: 20, - width: 40, - child: DraggableSwitchButton( - key: const Key("autoBackupToggleButtonKey"), - isOn: _toggle, - controller: toggleController, - onValueChanged: (newValue) async { - _toggle = newValue; - - if (_toggle) { - attemptEnable(); - } else { - attemptDisable(); - } - }, - ), - ), - ], - ), - ), - ), - ), - const SizedBox( - height: 8, - ), - if (!isEnabledAutoBackup) + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ RoundedWhiteContainer( - child: RichText( - textAlign: TextAlign.left, - text: TextSpan( - style: STextStyles.label(context), - children: [ - const TextSpan( - text: - "Auto Backup is a custom Stack Wallet feature that offers a convenient backup of your data.\n\nTo ensure maximum security, we recommend using a unique password that you haven't used anywhere else on the internet before. Your password is not stored.\n\nFor more information, please see our website "), - TextSpan( - text: "stackwallet.com.", - style: STextStyles.richLink(context), - recognizer: TapGestureRecognizer() - ..onTap = () { - launchUrl( - Uri.parse("https://stackwallet.com"), - mode: LaunchMode.externalApplication, - ); - }, - ), - ], + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - ), - ), - if (isEnabledAutoBackup) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - RoundedWhiteContainer( + onPressed: null, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - BlueTextButton( - text: "Back up now", - onTap: () { - ref.read(autoSWBServiceProvider).doBackup(); - }, - ), Text( - "Backed up ${prettySinceLastBackupString(ref.watch(prefsChangeNotifierProvider.select((value) => value.lastAutoBackup)))}", - style: STextStyles.itemSubtitle(context), - ) + "Auto Backup", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + key: const Key("autoBackupToggleButtonKey"), + isOn: _toggle, + controller: toggleController, + onValueChanged: (newValue) async { + _toggle = newValue; + + if (_toggle) { + attemptEnable(); + } else { + attemptDisable(); + } + }, + ), + ), ], ), ), - const SizedBox( - height: 32, - ), - Text( - "Auto Backup file", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 10, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("backupSavedToFileLocationTextFieldKey"), - focusNode: fileLocationFocusNode, - controller: fileLocationController, - enabled: false, - style: STextStyles.field(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark - .withOpacity(0.5), - ), - readOnly: true, - enableSuggestions: false, - autocorrect: false, - toolbarOptions: const ToolbarOptions( - copy: true, - cut: false, - paste: false, - selectAll: true, - ), - decoration: standardInputDecoration( - "Saved to", - fileLocationFocusNode, - context, - ), - ), - ), - const SizedBox( - height: 10, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("backupPasswordFieldKey"), - focusNode: passwordFocusNode, - controller: passwordController, - enabled: false, - style: STextStyles.field(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark - .withOpacity(0.5), - ), - obscureText: true, - enableSuggestions: false, - autocorrect: false, - toolbarOptions: const ToolbarOptions( - copy: true, - cut: false, - paste: false, - selectAll: true, - ), - decoration: standardInputDecoration( - "Passphrase", - passwordFocusNode, - context, - ), - ), - ), - const SizedBox( - height: 12, - ), - Text( - "Auto Backup frequency", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 10, - ), - TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - key: const Key("backupFrequencyFieldKey"), - controller: frequencyController, - enabled: false, - style: STextStyles.field(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark - .withOpacity(0.5), - ), - toolbarOptions: const ToolbarOptions( - copy: true, - cut: false, - paste: false, - selectAll: true, - ), - ), - const SizedBox( - height: 20, - ), - Center( - child: BlueTextButton( - text: "Edit Auto Backup", - onTap: () async { - Navigator.of(context) - .pushNamed(EditAutoBackupView.routeName); - }, - ), - ) - ], + ), ), - ], + const SizedBox( + height: 8, + ), + if (!isEnabledAutoBackup) + RoundedWhiteContainer( + child: RichText( + textAlign: TextAlign.left, + text: TextSpan( + style: STextStyles.label(context), + children: [ + const TextSpan( + text: + "Auto Backup is a custom Stack Wallet feature that offers a convenient backup of your data.\n\nTo ensure maximum security, we recommend using a unique password that you haven't used anywhere else on the internet before. Your password is not stored.\n\nFor more information, please see our website "), + TextSpan( + text: "stackwallet.com.", + style: STextStyles.richLink(context), + recognizer: TapGestureRecognizer() + ..onTap = () { + launchUrl( + Uri.parse("https://stackwallet.com"), + mode: LaunchMode.externalApplication, + ); + }, + ), + ], + ), + ), + ), + if (isEnabledAutoBackup) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + BlueTextButton( + text: "Back up now", + onTap: () { + ref.read(autoSWBServiceProvider).doBackup(); + }, + ), + Text( + "Backed up ${prettySinceLastBackupString(ref.watch(prefsChangeNotifierProvider.select((value) => value.lastAutoBackup)))}", + style: STextStyles.itemSubtitle(context), + ) + ], + ), + ), + const SizedBox( + height: 32, + ), + Text( + "Auto Backup file", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 10, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("backupSavedToFileLocationTextFieldKey"), + focusNode: fileLocationFocusNode, + controller: fileLocationController, + enabled: false, + style: STextStyles.field(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark + .withOpacity(0.5), + ), + readOnly: true, + enableSuggestions: false, + autocorrect: false, + toolbarOptions: const ToolbarOptions( + copy: true, + cut: false, + paste: false, + selectAll: true, + ), + decoration: standardInputDecoration( + "Saved to", + fileLocationFocusNode, + context, + ), + ), + ), + const SizedBox( + height: 10, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("backupPasswordFieldKey"), + focusNode: passwordFocusNode, + controller: passwordController, + enabled: false, + style: STextStyles.field(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark + .withOpacity(0.5), + ), + obscureText: true, + enableSuggestions: false, + autocorrect: false, + toolbarOptions: const ToolbarOptions( + copy: true, + cut: false, + paste: false, + selectAll: true, + ), + decoration: standardInputDecoration( + "Passphrase", + passwordFocusNode, + context, + ), + ), + ), + const SizedBox( + height: 12, + ), + Text( + "Auto Backup frequency", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 10, + ), + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + key: const Key("backupFrequencyFieldKey"), + controller: frequencyController, + enabled: false, + style: STextStyles.field(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark + .withOpacity(0.5), + ), + toolbarOptions: const ToolbarOptions( + copy: true, + cut: false, + paste: false, + selectAll: true, + ), + ), + const SizedBox( + height: 20, + ), + Center( + child: BlueTextButton( + text: "Edit Auto Backup", + onTap: () async { + Navigator.of(context) + .pushNamed(EditAutoBackupView.routeName); + }, + ), + ) + ], + ), + ], + ), ), ), ); diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart index bf8bd40e7..8e8731105 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart @@ -22,6 +22,7 @@ import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/progress_bar.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; @@ -108,549 +109,559 @@ class _EnableAutoBackupViewState extends ConsumerState<CreateAutoBackupView> { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Create Auto Backup", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Create Auto Backup", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: LayoutBuilder(builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - "Create your backup file", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 10, - ), - if (!Platform.isAndroid) - TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - onTap: Platform.isAndroid - ? null - : () async { - try { - await stackFileSystem.prepareStorage(); - - if (mounted) { - await stackFileSystem.pickDir(context); - } - - if (mounted) { - setState(() { - fileLocationController.text = - stackFileSystem.dirPath ?? ""; - }); - } - } catch (e, s) { - Logging.instance - .log("$e\n$s", level: LogLevel.Error); - } - }, - controller: fileLocationController, - style: STextStyles.field(context), - decoration: InputDecoration( - hintText: "Save to...", - hintStyle: STextStyles.fieldLabel(context), - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox( - width: 16, - ), - SvgPicture.asset( - Assets.svg.folder, - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, - width: 16, - height: 16, - ), - const SizedBox( - width: 12, - ), - ], - ), - ), - ), - key: const Key( - "createBackupSaveToFileLocationTextFieldKey"), - readOnly: true, - toolbarOptions: const ToolbarOptions( - copy: true, - cut: false, - paste: false, - selectAll: false, - ), - onChanged: (newValue) {}, + body: Padding( + padding: const EdgeInsets.all(16), + child: LayoutBuilder(builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Create your backup file", + style: STextStyles.smallMed12(context), ), - if (!Platform.isAndroid) const SizedBox( height: 10, ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("createBackupPasswordFieldKey1"), - focusNode: passwordFocusNode, - controller: passwordController, - style: STextStyles.field(context), - obscureText: hidePassword, - enableSuggestions: false, - autocorrect: false, - decoration: standardInputDecoration( - "Create passphrase", - passwordFocusNode, - context, - ).copyWith( - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox( - width: 16, - ), - GestureDetector( - key: const Key( - "createBackupPasswordFieldShowPasswordButtonKey"), - onTap: () async { - setState(() { - hidePassword = !hidePassword; - }); - }, - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, - width: 16, - height: 16, - ), - ), - const SizedBox( - width: 12, - ), - ], - ), - ), - ), - onChanged: (newValue) { - if (newValue.isEmpty) { - setState(() { - passwordFeedback = ""; - }); - return; - } - final result = zxcvbn.evaluate(newValue); - String suggestionsAndTips = ""; - for (var sug - in result.feedback.suggestions!.toSet()) { - suggestionsAndTips += "$sug\n"; - } - suggestionsAndTips += result.feedback.warning!; - String feedback = - // "Password Strength: ${((result.score! / 4.0) * 100).toInt()}%\n" - suggestionsAndTips; - - passwordStrength = result.score! / 4; - - // hack fix to format back string returned from zxcvbn - if (feedback.contains("phrasesNo need")) { - feedback = feedback.replaceFirst( - "phrasesNo need", "phrases\nNo need"); - } - - if (feedback.endsWith("\n")) { - feedback = - feedback.substring(0, feedback.length - 2); - } - - setState(() { - passwordFeedback = feedback; - }); - }, - ), - ), - if (passwordFocusNode.hasFocus || - passwordRepeatFocusNode.hasFocus || - passwordController.text.isNotEmpty) - Padding( - padding: EdgeInsets.only( - left: 12, - right: 12, - top: passwordFeedback.isNotEmpty ? 4 : 0, - ), - child: passwordFeedback.isNotEmpty - ? Text( - passwordFeedback, - style: STextStyles.infoSmall(context), - ) - : null, - ), - if (passwordFocusNode.hasFocus || - passwordRepeatFocusNode.hasFocus || - passwordController.text.isNotEmpty) - Padding( - padding: const EdgeInsets.only( - left: 12, - right: 12, - top: 10, - ), - child: ProgressBar( - key: const Key("createStackBackUpProgressBar"), - width: MediaQuery.of(context).size.width - 32 - 24, - height: 5, - fillColor: passwordStrength < 0.51 - ? Theme.of(context) - .extension<StackColors>()! - .accentColorRed - : passwordStrength < 1 - ? Theme.of(context) - .extension<StackColors>()! - .accentColorYellow - : Theme.of(context) - .extension<StackColors>()! - .accentColorGreen, - backgroundColor: Theme.of(context) - .extension<StackColors>()! - .buttonBackSecondary, - percent: - passwordStrength < 0.25 ? 0.03 : passwordStrength, - ), - ), - const SizedBox( - height: 10, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("createBackupPasswordFieldKey2"), - focusNode: passwordRepeatFocusNode, - controller: passwordRepeatController, - style: STextStyles.field(context), - obscureText: hidePassword, - enableSuggestions: false, - autocorrect: false, - decoration: standardInputDecoration( - "Confirm passphrase", - passwordRepeatFocusNode, - context, - ).copyWith( - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox( - width: 16, - ), - GestureDetector( - key: const Key( - "createBackupPasswordFieldShowPasswordButtonKey"), - onTap: () async { - setState(() { - hidePassword = !hidePassword; - }); - }, - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, - width: 16, - height: 16, - ), - ), - const SizedBox( - width: 12, - ), - ], - ), - ), - ), - onChanged: (newValue) { - setState(() {}); - // TODO: ? check if passwords match? - }, - ), - ), - const SizedBox( - height: 32, - ), - Text( - "Auto Backup frequency", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 10, - ), - Stack( - children: [ + if (!Platform.isAndroid) TextField( autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, - readOnly: true, - textInputAction: TextInputAction.none, - ), - Positioned.fill( - child: RawMaterialButton( - splashColor: Theme.of(context) - .extension<StackColors>()! - .highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - showModalBottomSheet<dynamic>( - backgroundColor: Colors.transparent, - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - builder: (_) => - const BackupFrequencyTypeSelectSheet(), - ); - }, - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 12.0), + onTap: Platform.isAndroid + ? null + : () async { + try { + await stackFileSystem.prepareStorage(); + + if (mounted) { + await stackFileSystem.pickDir(context); + } + + if (mounted) { + setState(() { + fileLocationController.text = + stackFileSystem.dirPath ?? ""; + }); + } + } catch (e, s) { + Logging.instance + .log("$e\n$s", level: LogLevel.Error); + } + }, + controller: fileLocationController, + style: STextStyles.field(context), + decoration: InputDecoration( + hintText: "Save to...", + hintStyle: STextStyles.fieldLabel(context), + suffixIcon: UnconstrainedBox( child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, children: [ - Text( - Format.prettyFrequencyType(ref.watch( - prefsChangeNotifierProvider.select( - (value) => - value.backupFrequencyType))), - style: STextStyles.itemSubtitle12(context), + const SizedBox( + width: 16, ), - Padding( - padding: const EdgeInsets.only(right: 4.0), - child: SvgPicture.asset( - Assets.svg.chevronDown, - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle2, - width: 12, - height: 6, - ), + SvgPicture.asset( + Assets.svg.folder, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 16, + height: 16, + ), + const SizedBox( + width: 12, ), ], ), ), ), - ) - ], - ), - const Spacer(), - const SizedBox( - height: 10, - ), - TextButton( - style: shouldEnableCreate - ? Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context) - : Theme.of(context) - .extension<StackColors>()! - .getPrimaryDisabledButtonColor(context), - onPressed: !shouldEnableCreate - ? null - : () async { - final String pathToSave = - fileLocationController.text; - final String passphrase = passwordController.text; - final String repeatPassphrase = - passwordRepeatController.text; + key: const Key( + "createBackupSaveToFileLocationTextFieldKey"), + readOnly: true, + toolbarOptions: const ToolbarOptions( + copy: true, + cut: false, + paste: false, + selectAll: false, + ), + onChanged: (newValue) {}, + ), + if (!Platform.isAndroid) + const SizedBox( + height: 10, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("createBackupPasswordFieldKey1"), + focusNode: passwordFocusNode, + controller: passwordController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Create passphrase", + passwordFocusNode, + context, + ).copyWith( + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox( + width: 16, + ), + GestureDetector( + key: const Key( + "createBackupPasswordFieldShowPasswordButtonKey"), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 16, + height: 16, + ), + ), + const SizedBox( + width: 12, + ), + ], + ), + ), + ), + onChanged: (newValue) { + if (newValue.isEmpty) { + setState(() { + passwordFeedback = ""; + }); + return; + } + final result = zxcvbn.evaluate(newValue); + String suggestionsAndTips = ""; + for (var sug + in result.feedback.suggestions!.toSet()) { + suggestionsAndTips += "$sug\n"; + } + suggestionsAndTips += result.feedback.warning!; + String feedback = + // "Password Strength: ${((result.score! / 4.0) * 100).toInt()}%\n" + suggestionsAndTips; - if (pathToSave.isEmpty) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Directory not chosen", - context: context, - ); - return; - } - if (!(await Directory(pathToSave).exists())) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Directory does not exist", - context: context, - ); - return; - } - if (passphrase.isEmpty) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "A passphrase is required", - context: context, - ); - return; - } - if (passphrase != repeatPassphrase) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Passphrase does not match", - context: context, - ); - return; - } + passwordStrength = result.score! / 4; - showDialog<dynamic>( - context: context, - barrierDismissible: false, - builder: (_) => const StackDialog( - title: "Encrypting initial backup", - message: "This shouldn't take long", - ), - ); + // hack fix to format back string returned from zxcvbn + if (feedback.contains("phrasesNo need")) { + feedback = feedback.replaceFirst( + "phrasesNo need", "phrases\nNo need"); + } - // make sure the dialog is able to be displayed for at least some time - final fut = Future<void>.delayed( - const Duration(milliseconds: 300)); + if (feedback.endsWith("\n")) { + feedback = + feedback.substring(0, feedback.length - 2); + } - String adkString; - int adkVersion; - try { - final adk = - await compute(generateAdk, passphrase); - adkString = Format.uint8listToString(adk.item2); - adkVersion = adk.item1; - } on Exception catch (e, s) { - String err = getErrorMessageFromSWBException(e); - Logging.instance - .log("$err\n$s", level: LogLevel.Error); - // pop encryption progress dialog - Navigator.of(context).pop(); - showFloatingFlushBar( - type: FlushBarType.warning, - message: err, - context: context, - ); - return; - } catch (e, s) { - Logging.instance - .log("$e\n$s", level: LogLevel.Error); - // pop encryption progress dialog - Navigator.of(context).pop(); - showFloatingFlushBar( - type: FlushBarType.warning, - message: "$e", - context: context, - ); - return; - } - - await secureStore.write( - key: "auto_adk_string", value: adkString); - await secureStore.write( - key: "auto_adk_version_string", - value: adkVersion.toString()); - - final DateTime now = DateTime.now(); - final String fileToSave = - createAutoBackupFilename(pathToSave, now); - - final backup = await SWB.createStackWalletJSON( - secureStorage: secureStore, - ); - - bool result = await SWB.encryptStackWalletWithADK( - fileToSave, - adkString, - jsonEncode(backup), - adkVersion: adkVersion, - ); - - // this future should already be complete unless there was an error encrypting - await Future.wait([fut]); - - if (mounted) { - // pop encryption progress dialog - Navigator.of(context).pop(); - - if (result) { - ref - .read(prefsChangeNotifierProvider) - .autoBackupLocation = pathToSave; - ref - .read(prefsChangeNotifierProvider) - .lastAutoBackup = now; - - ref - .read(prefsChangeNotifierProvider) - .isAutoBackupEnabled = true; - - await showDialog<dynamic>( - context: context, - barrierDismissible: false, - builder: (_) => Platform.isAndroid - ? StackOkDialog( - title: - "Stack Auto Backup enabled and saved to:", - message: fileToSave, - ) - : const StackOkDialog( - title: - "Stack Auto Backup enabled!"), - ); - if (mounted) { - passwordController.text = ""; - passwordRepeatController.text = ""; - - Navigator.of(context).popUntil( - ModalRoute.withName( - AutoBackupView.routeName)); - } - } else { - await showDialog<dynamic>( - context: context, - barrierDismissible: false, - builder: (_) => const StackOkDialog( - title: "Failed to enable Auto Backup"), - ); - } - } - }, - child: Text( - "Enable Auto Backup", - style: STextStyles.button(context), + setState(() { + passwordFeedback = feedback; + }); + }, + ), ), - ), - ], + if (passwordFocusNode.hasFocus || + passwordRepeatFocusNode.hasFocus || + passwordController.text.isNotEmpty) + Padding( + padding: EdgeInsets.only( + left: 12, + right: 12, + top: passwordFeedback.isNotEmpty ? 4 : 0, + ), + child: passwordFeedback.isNotEmpty + ? Text( + passwordFeedback, + style: STextStyles.infoSmall(context), + ) + : null, + ), + if (passwordFocusNode.hasFocus || + passwordRepeatFocusNode.hasFocus || + passwordController.text.isNotEmpty) + Padding( + padding: const EdgeInsets.only( + left: 12, + right: 12, + top: 10, + ), + child: ProgressBar( + key: const Key("createStackBackUpProgressBar"), + width: MediaQuery.of(context).size.width - 32 - 24, + height: 5, + fillColor: passwordStrength < 0.51 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorRed + : passwordStrength < 1 + ? Theme.of(context) + .extension<StackColors>()! + .accentColorYellow + : Theme.of(context) + .extension<StackColors>()! + .accentColorGreen, + backgroundColor: Theme.of(context) + .extension<StackColors>()! + .buttonBackSecondary, + percent: passwordStrength < 0.25 + ? 0.03 + : passwordStrength, + ), + ), + const SizedBox( + height: 10, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("createBackupPasswordFieldKey2"), + focusNode: passwordRepeatFocusNode, + controller: passwordRepeatController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Confirm passphrase", + passwordRepeatFocusNode, + context, + ).copyWith( + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox( + width: 16, + ), + GestureDetector( + key: const Key( + "createBackupPasswordFieldShowPasswordButtonKey"), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 16, + height: 16, + ), + ), + const SizedBox( + width: 12, + ), + ], + ), + ), + ), + onChanged: (newValue) { + setState(() {}); + // TODO: ? check if passwords match? + }, + ), + ), + const SizedBox( + height: 32, + ), + Text( + "Auto Backup frequency", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 10, + ), + Stack( + children: [ + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + readOnly: true, + textInputAction: TextInputAction.none, + ), + Positioned.fill( + child: RawMaterialButton( + splashColor: Theme.of(context) + .extension<StackColors>()! + .highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + showModalBottomSheet<dynamic>( + backgroundColor: Colors.transparent, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + builder: (_) => + const BackupFrequencyTypeSelectSheet(), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + Format.prettyFrequencyType(ref.watch( + prefsChangeNotifierProvider.select( + (value) => + value.backupFrequencyType))), + style: + STextStyles.itemSubtitle12(context), + ), + Padding( + padding: + const EdgeInsets.only(right: 4.0), + child: SvgPicture.asset( + Assets.svg.chevronDown, + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle2, + width: 12, + height: 6, + ), + ), + ], + ), + ), + ), + ) + ], + ), + const Spacer(), + const SizedBox( + height: 10, + ), + TextButton( + style: shouldEnableCreate + ? Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context) + : Theme.of(context) + .extension<StackColors>()! + .getPrimaryDisabledButtonColor(context), + onPressed: !shouldEnableCreate + ? null + : () async { + final String pathToSave = + fileLocationController.text; + final String passphrase = + passwordController.text; + final String repeatPassphrase = + passwordRepeatController.text; + + if (pathToSave.isEmpty) { + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Directory not chosen", + context: context, + ); + return; + } + if (!(await Directory(pathToSave).exists())) { + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Directory does not exist", + context: context, + ); + return; + } + if (passphrase.isEmpty) { + showFloatingFlushBar( + type: FlushBarType.warning, + message: "A passphrase is required", + context: context, + ); + return; + } + if (passphrase != repeatPassphrase) { + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Passphrase does not match", + context: context, + ); + return; + } + + showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => const StackDialog( + title: "Encrypting initial backup", + message: "This shouldn't take long", + ), + ); + + // make sure the dialog is able to be displayed for at least some time + final fut = Future<void>.delayed( + const Duration(milliseconds: 300)); + + String adkString; + int adkVersion; + try { + final adk = + await compute(generateAdk, passphrase); + adkString = + Format.uint8listToString(adk.item2); + adkVersion = adk.item1; + } on Exception catch (e, s) { + String err = + getErrorMessageFromSWBException(e); + Logging.instance + .log("$err\n$s", level: LogLevel.Error); + // pop encryption progress dialog + Navigator.of(context).pop(); + showFloatingFlushBar( + type: FlushBarType.warning, + message: err, + context: context, + ); + return; + } catch (e, s) { + Logging.instance + .log("$e\n$s", level: LogLevel.Error); + // pop encryption progress dialog + Navigator.of(context).pop(); + showFloatingFlushBar( + type: FlushBarType.warning, + message: "$e", + context: context, + ); + return; + } + + await secureStore.write( + key: "auto_adk_string", value: adkString); + await secureStore.write( + key: "auto_adk_version_string", + value: adkVersion.toString()); + + final DateTime now = DateTime.now(); + final String fileToSave = + createAutoBackupFilename(pathToSave, now); + + final backup = await SWB.createStackWalletJSON( + secureStorage: secureStore, + ); + + bool result = + await SWB.encryptStackWalletWithADK( + fileToSave, + adkString, + jsonEncode(backup), + adkVersion: adkVersion, + ); + + // this future should already be complete unless there was an error encrypting + await Future.wait([fut]); + + if (mounted) { + // pop encryption progress dialog + Navigator.of(context).pop(); + + if (result) { + ref + .read(prefsChangeNotifierProvider) + .autoBackupLocation = pathToSave; + ref + .read(prefsChangeNotifierProvider) + .lastAutoBackup = now; + + ref + .read(prefsChangeNotifierProvider) + .isAutoBackupEnabled = true; + + await showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => Platform.isAndroid + ? StackOkDialog( + title: + "Stack Auto Backup enabled and saved to:", + message: fileToSave, + ) + : const StackOkDialog( + title: + "Stack Auto Backup enabled!"), + ); + if (mounted) { + passwordController.text = ""; + passwordRepeatController.text = ""; + + Navigator.of(context).popUntil( + ModalRoute.withName( + AutoBackupView.routeName)); + } + } else { + await showDialog<dynamic>( + context: context, + barrierDismissible: false, + builder: (_) => const StackOkDialog( + title: + "Failed to enable Auto Backup"), + ); + } + } + }, + child: Text( + "Enable Auto Backup", + style: STextStyles.button(context), + ), + ), + ], + ), ), ), - ), - ); - }), + ); + }), + ), ), ); } diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_information_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_information_view.dart index 772c446f2..012477a5b 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_information_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_information_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -12,75 +13,77 @@ class CreateBackupInfoView extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 75)); - } - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 75)); + } + Navigator.of(context).pop(); + }, + ), + title: Text( + "Create backup", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Create backup", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Center( - child: Text( - "Info", - style: STextStyles.pageTitleH2(context), + body: Padding( + padding: const EdgeInsets.all(16), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: Text( + "Info", + style: STextStyles.pageTitleH2(context), + ), ), - ), - const SizedBox( - height: 16, - ), - RoundedWhiteContainer( - child: Text( - // TODO: need info - "{lorem ipsum}", - style: STextStyles.baseXS(context), + const SizedBox( + height: 16, ), - ), - const SizedBox( - height: 16, - ), - const Spacer(), - TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () { - Navigator.of(context) - .pushNamed(CreateBackupView.routeName); - }, - child: Text( - "Next", - style: STextStyles.button(context), + RoundedWhiteContainer( + child: Text( + // TODO: need info + "{lorem ipsum}", + style: STextStyles.baseXS(context), + ), ), - ), - ], + const SizedBox( + height: 16, + ), + const Spacer(), + TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + onPressed: () { + Navigator.of(context) + .pushNamed(CreateBackupView.routeName); + }, + child: Text( + "Next", + style: STextStyles.button(context), + ), + ), + ], + ), ), ), - ), - ); - }, + ); + }, + ), ), ), ); diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart index a6241d25a..4d69ce4e9 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart @@ -16,6 +16,7 @@ import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; @@ -103,41 +104,44 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { return ConditionalParent( condition: !isDesktop, builder: (child) { - return Scaffold( - backgroundColor: - Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, + return Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Create backup", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Create backup", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, + body: Padding( + padding: const EdgeInsets.all(16), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: child, + ), ), - child: IntrinsicHeight( - child: child, - ), - ), - ); - }, + ); + }, + ), ), ), ); diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart index 310be9f2b..7187c5311 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart @@ -25,6 +25,7 @@ import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; @@ -282,34 +283,37 @@ class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> { return ConditionalParent( condition: !isDesktop, - builder: (child) => Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Edit Auto Backup", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Edit Auto Backup", - style: STextStyles.navBarTitle(context), + body: Padding( + padding: const EdgeInsets.all(16), + child: LayoutBuilder(builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: child, + ), + ), + ); + }), ), ), - body: Padding( - padding: const EdgeInsets.all(16), - child: LayoutBuilder(builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: child, - ), - ), - ); - }), - ), ), child: Column( crossAxisAlignment: 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 3173bc402..14a262d99 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 @@ -9,9 +9,9 @@ import 'package:stackwallet/pages/settings_views/global_settings_view/stack_back import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; @@ -62,204 +62,209 @@ class _RestoreFromEncryptedStringViewState Widget build(BuildContext context) { return WillPopScope( onWillPop: _onWillPop, - child: Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - _onWillPop(); - } - }, + child: Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 75)); + } + if (mounted) { + _onWillPop(); + } + }, + ), + title: Text( + "Restore from file", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Restore from file", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("restoreFromFilePasswordFieldKey"), - focusNode: passwordFocusNode, - controller: passwordController, - style: STextStyles.field(context), - obscureText: hidePassword, - enableSuggestions: false, - autocorrect: false, - decoration: standardInputDecoration( - "Enter password", - passwordFocusNode, - context, - ).copyWith( - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox( - width: 16, - ), - GestureDetector( - key: const Key( - "restoreFromFilePasswordFieldShowPasswordButtonKey"), - onTap: () async { - setState(() { - hidePassword = !hidePassword; - }); - }, - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, + body: Padding( + padding: const EdgeInsets.all(16), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("restoreFromFilePasswordFieldKey"), + focusNode: passwordFocusNode, + controller: passwordController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Enter password", + passwordFocusNode, + context, + ).copyWith( + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox( width: 16, - height: 16, ), - ), - const SizedBox( - width: 12, - ), - ], + GestureDetector( + key: const Key( + "restoreFromFilePasswordFieldShowPasswordButtonKey"), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + width: 16, + height: 16, + ), + ), + const SizedBox( + width: 12, + ), + ], + ), ), ), + onChanged: (newValue) { + setState(() {}); + }, ), - onChanged: (newValue) { - setState(() {}); - }, ), - ), - const SizedBox( - height: 16, - ), - const Spacer(), - TextButton( - style: passwordController.text.isEmpty - ? Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context) - : Theme.of(context) - .extension<StackColors>()! - .getPrimaryDisabledButtonColor(context), - onPressed: passwordController.text.isEmpty - ? null - : () async { - final String passphrase = - passwordController.text; + const SizedBox( + height: 16, + ), + const Spacer(), + TextButton( + style: passwordController.text.isEmpty + ? Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context) + : Theme.of(context) + .extension<StackColors>()! + .getPrimaryDisabledButtonColor(context), + onPressed: passwordController.text.isEmpty + ? null + : () async { + final String passphrase = + passwordController.text; - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration(milliseconds: 75)); - } + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75)); + } - bool shouldPop = false; - showDialog<dynamic>( - barrierDismissible: false, - context: context, - builder: (_) => WillPopScope( - onWillPop: () async { - return shouldPop; - }, - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Material( - color: Colors.transparent, - child: Center( - child: Text( - "Decrypting Stack backup file", - style: STextStyles.pageTitleH2( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textWhite, + bool shouldPop = false; + showDialog<dynamic>( + barrierDismissible: false, + context: context, + builder: (_) => WillPopScope( + onWillPop: () async { + return shouldPop; + }, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Material( + color: Colors.transparent, + child: Center( + child: Text( + "Decrypting Stack backup file", + style: + STextStyles.pageTitleH2( + context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textWhite, + ), ), ), ), - ), - const SizedBox( - height: 64, - ), - const Center( - child: LoadingIndicator( - width: 100, + const SizedBox( + height: 64, ), - ), - ], - ), - ), - ); - - final String? jsonString = await compute( - SWB.decryptStackWalletStringWithPassphrase, - Tuple2(widget.encrypted, passphrase), - debugLabel: - "stack wallet decryption compute", - ); - - if (mounted) { - // pop LoadingIndicator - shouldPop = true; - Navigator.of(context).pop(); - - passwordController.text = ""; - - if (jsonString == null) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: - "Failed to decrypt backup file", - context: context, - ); - return; - } - - Navigator.of(context).push( - RouteGenerator.getRoute( - builder: (_) => - StackRestoreProgressView( - jsonString: jsonString, - fromFile: true, + const Center( + child: LoadingIndicator( + width: 100, + ), + ), + ], ), ), ); - } - }, - child: Text( - "Restore", - style: STextStyles.button(context), + + final String? jsonString = await compute( + SWB.decryptStackWalletStringWithPassphrase, + Tuple2(widget.encrypted, passphrase), + debugLabel: + "stack wallet decryption compute", + ); + + if (mounted) { + // pop LoadingIndicator + shouldPop = true; + Navigator.of(context).pop(); + + passwordController.text = ""; + + if (jsonString == null) { + showFloatingFlushBar( + type: FlushBarType.warning, + message: + "Failed to decrypt backup file", + context: context, + ); + return; + } + + Navigator.of(context).push( + RouteGenerator.getRoute( + builder: (_) => + StackRestoreProgressView( + jsonString: jsonString, + fromFile: true, + ), + ), + ); + } + }, + child: Text( + "Restore", + style: STextStyles.button(context), + ), ), - ), - ], + ], + ), ), ), - ), - ); - }, + ); + }, + ), ), ), ), 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 d6571967d..6350feb52 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 @@ -17,6 +17,7 @@ import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; @@ -75,42 +76,44 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { return ConditionalParent( condition: !isDesktop, builder: (child) { - return Scaffold( - backgroundColor: - Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, + return Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Restore from file", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Restore from file", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, + body: Padding( + padding: const EdgeInsets.all(16), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: child, + ), ), - child: IntrinsicHeight( - child: child, - ), - ), - ); - }, + ); + }, + ), ), ), ); diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/stack_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/stack_backup_view.dart index 679043fe7..fe163cb66 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/stack_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/stack_backup_view.dart @@ -7,6 +7,7 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -21,147 +22,149 @@ class StackBackupView extends StatelessWidget { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Stack backup", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Stack backup", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - ), - onPressed: () { - Navigator.of(context).pushNamed(AutoBackupView.routeName); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.backupAuto, - height: 28, - width: 28, - ), - const SizedBox( - width: 12, - ), - Text( - "Auto Backup", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - ], + onPressed: () { + Navigator.of(context).pushNamed(AutoBackupView.routeName); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.backupAuto, + height: 28, + width: 28, + ), + const SizedBox( + width: 12, + ), + Text( + "Auto Backup", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + ], + ), ), ), ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - ), - onPressed: () { - Navigator.of(context).pushNamed(CreateBackupView.routeName); - // .pushNamed(CreateBackupInfoView.routeName); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.backupAdd, - height: 28, - width: 28, - ), - const SizedBox( - width: 12, - ), - Text( - "Create manual backup", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - ], + onPressed: () { + Navigator.of(context).pushNamed(CreateBackupView.routeName); + // .pushNamed(CreateBackupInfoView.routeName); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.backupAdd, + height: 28, + width: 28, + ), + const SizedBox( + width: 12, + ), + Text( + "Create manual backup", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + ], + ), ), ), ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - ), - onPressed: () { - Navigator.of(context) - .pushNamed(RestoreFromFileView.routeName); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.backupRestore, - height: 28, - width: 28, - ), - const SizedBox( - width: 12, - ), - Text( - "Restore backup", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - ], + onPressed: () { + Navigator.of(context) + .pushNamed(RestoreFromFileView.routeName); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.backupRestore, + height: 28, + width: 28, + ), + const SizedBox( + width: 12, + ), + Text( + "Restore backup", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + ], + ), ), ), ), - ), - ], + ], + ), ), ), ); diff --git a/lib/pages/settings_views/global_settings_view/startup_preferences/startup_preferences_view.dart b/lib/pages/settings_views/global_settings_view/startup_preferences/startup_preferences_view.dart index baf649ba2..186b9b293 100644 --- a/lib/pages/settings_views/global_settings_view/startup_preferences/startup_preferences_view.dart +++ b/lib/pages/settings_views/global_settings_view/startup_preferences/startup_preferences_view.dart @@ -5,6 +5,7 @@ import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -22,255 +23,263 @@ class _StartupPreferencesViewState extends ConsumerState<StartupPreferencesView> { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Startup preferences", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Startup preferences", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: const EdgeInsets.all(4.0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + body: Padding( + padding: const EdgeInsets.all(16), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.all(4.0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + ref + .read(prefsChangeNotifierProvider) + .gotoWalletOnStartup = false; + }, + child: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: false, + groupValue: ref.watch( + prefsChangeNotifierProvider + .select((value) => value + .gotoWalletOnStartup), + ), + onChanged: (value) { + if (value is bool) { + ref + .read( + prefsChangeNotifierProvider) + .gotoWalletOnStartup = value; + } + }, + ), + ), + const SizedBox( + width: 12, + ), + Flexible( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Home screen", + style: + STextStyles.titleBold12( + context), + textAlign: TextAlign.left, + ), + Text( + "Stack Wallet home screen", + style: + STextStyles.itemSubtitle( + context), + textAlign: TextAlign.left, + ), + ], + ), + ), + ], + ), + ), ), ), - onPressed: () { - ref - .read(prefsChangeNotifierProvider) - .gotoWalletOnStartup = false; - }, - child: Container( + ), + Padding( + padding: const EdgeInsets.all(4), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + ref + .read(prefsChangeNotifierProvider) + .gotoWalletOnStartup = true; + }, + child: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.all(8), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: true, + groupValue: ref.watch( + prefsChangeNotifierProvider + .select((value) => value + .gotoWalletOnStartup), + ), + onChanged: (value) { + if (value is bool) { + ref + .read( + prefsChangeNotifierProvider) + .gotoWalletOnStartup = value; + } + }, + ), + ), + const SizedBox( + width: 12, + ), + Flexible( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Specific wallet", + style: + STextStyles.titleBold12( + context), + textAlign: TextAlign.left, + ), + Text( + "Select a specific wallet to load into on startup", + style: + STextStyles.itemSubtitle( + context), + textAlign: TextAlign.left, + ), + ], + ), + ), + ], + ), + ), + ), + ), + ), + if (!ref.watch(prefsChangeNotifierProvider.select( + (value) => value.gotoWalletOnStartup))) + const SizedBox( + height: 12, + ), + if (ref.watch(prefsChangeNotifierProvider.select( + (value) => value.gotoWalletOnStartup))) + Container( color: Colors.transparent, child: Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.only( + left: 12.0, + right: 12, + bottom: 12, + ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( - width: 20, - height: 20, - child: Radio( - activeColor: Theme.of(context) - .extension<StackColors>()! - .radioButtonIconEnabled, - value: false, - groupValue: ref.watch( - prefsChangeNotifierProvider - .select((value) => value - .gotoWalletOnStartup), - ), - onChanged: (value) { - if (value is bool) { - ref - .read( - prefsChangeNotifierProvider) - .gotoWalletOnStartup = value; - } - }, - ), - ), const SizedBox( - width: 12, + width: 12 + 20, + height: 12, ), Flexible( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Home screen", - style: STextStyles.titleBold12( - context), - textAlign: TextAlign.left, + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: + MaterialTapTargetSize + .shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular( + Constants + .size.circularBorderRadius, ), - Text( - "Stack Wallet home screen", - style: STextStyles.itemSubtitle( - context), - textAlign: TextAlign.left, - ), - ], + ), + onPressed: () { + Navigator.of(context).pushNamed( + StartupWalletSelectionView + .routeName); + }, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Select wallet...", + style: STextStyles.link2( + context), + textAlign: TextAlign.left, + ), + ], + ), ), ), ], ), ), ), - ), - ), - Padding( - padding: const EdgeInsets.all(4), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - ref - .read(prefsChangeNotifierProvider) - .gotoWalletOnStartup = true; - }, - child: Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.all(8), - child: Row( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 20, - child: Radio( - activeColor: Theme.of(context) - .extension<StackColors>()! - .radioButtonIconEnabled, - value: true, - groupValue: ref.watch( - prefsChangeNotifierProvider - .select((value) => value - .gotoWalletOnStartup), - ), - onChanged: (value) { - if (value is bool) { - ref - .read( - prefsChangeNotifierProvider) - .gotoWalletOnStartup = value; - } - }, - ), - ), - const SizedBox( - width: 12, - ), - Flexible( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Specific wallet", - style: STextStyles.titleBold12( - context), - textAlign: TextAlign.left, - ), - Text( - "Select a specific wallet to load into on startup", - style: STextStyles.itemSubtitle( - context), - textAlign: TextAlign.left, - ), - ], - ), - ), - ], - ), - ), - ), - ), - ), - if (!ref.watch(prefsChangeNotifierProvider - .select((value) => value.gotoWalletOnStartup))) - const SizedBox( - height: 12, - ), - if (ref.watch(prefsChangeNotifierProvider - .select((value) => value.gotoWalletOnStartup))) - Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.only( - left: 12.0, - right: 12, - bottom: 12, - ), - child: Row( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - const SizedBox( - width: 12 + 20, - height: 12, - ), - Flexible( - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants - .size.circularBorderRadius, - ), - ), - onPressed: () { - Navigator.of(context).pushNamed( - StartupWalletSelectionView - .routeName); - }, - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Select wallet...", - style: - STextStyles.link2(context), - textAlign: TextAlign.left, - ), - ], - ), - ), - ), - ], - ), - ), - ), - ], + ], + ), ), - ), - ], + ], + ), ), ), - ), - ); - }, + ); + }, + ), ), ), ); diff --git a/lib/pages/settings_views/global_settings_view/startup_preferences/startup_wallet_selection_view.dart b/lib/pages/settings_views/global_settings_view/startup_preferences/startup_wallet_selection_view.dart index 5d9f2edb1..975e8394d 100644 --- a/lib/pages/settings_views/global_settings_view/startup_preferences/startup_wallet_selection_view.dart +++ b/lib/pages/settings_views/global_settings_view/startup_preferences/startup_wallet_selection_view.dart @@ -6,6 +6,7 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -33,170 +34,173 @@ class _StartupWalletSelectionViewState _controllers[manager.walletId] = DSBController(); } - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - Navigator.of(context).pop(); - }, - ), - title: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - "Select startup wallet", - style: STextStyles.navBarTitle(context), + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).pop(); + }, + ), + title: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + "Select startup wallet", + style: STextStyles.navBarTitle(context), + ), ), ), - ), - body: LayoutBuilder(builder: (context, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 4, - ), - Text( - "Select a wallet to load into immediately on startup", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: Column( - children: [ - ...managers.map( - (manager) => Padding( - padding: const EdgeInsets.all(12), - child: Row( - key: Key( - "startupWalletSelectionGroupKey_${manager.walletId}"), - children: [ - Container( - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .colorForCoin(manager.coin) - .withOpacity(0.5), - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - child: Padding( - padding: const EdgeInsets.all(4), - child: SvgPicture.asset( - Assets.svg - .iconFor(coin: manager.coin), - width: 20, - height: 20, - ), - ), - ), - const SizedBox( - width: 12, - ), - Expanded( - child: Column( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - manager.walletName, - style: STextStyles.titleBold12( - context), + body: LayoutBuilder(builder: (context, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 4, + ), + Text( + "Select a wallet to load into immediately on startup", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: Column( + children: [ + ...managers.map( + (manager) => Padding( + padding: const EdgeInsets.all(12), + child: Row( + key: Key( + "startupWalletSelectionGroupKey_${manager.walletId}"), + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .colorForCoin(manager.coin) + .withOpacity(0.5), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: Padding( + padding: const EdgeInsets.all(4), + child: SvgPicture.asset( + Assets.svg + .iconFor(coin: manager.coin), + width: 20, + height: 20, ), - // const SizedBox( - // height: 2, - // ), - // FutureBuilder( - // future: manager.totalBalance, - // builder: (builderContext, - // AsyncSnapshot<Decimal> snapshot) { - // if (snapshot.connectionState == - // ConnectionState.done && - // snapshot.hasData) { - // return Text( - // "${Format.localizedStringAsFixed( - // value: snapshot.data!, - // locale: ref.watch( - // localeServiceChangeNotifierProvider - // .select((value) => - // value.locale)), - // decimalPlaces: 8, - // )} ${manager.coin.ticker}", - // style: STextStyles.itemSubtitle(context), - // ); - // } else { - // return AnimatedText( - // stringsToLoopThrough: const [ - // "Loading balance", - // "Loading balance.", - // "Loading balance..", - // "Loading balance..." - // ], - // style: STextStyles.itemSubtitle(context), - // ); - // } - // }, - // ), - ], - ), - ), - SizedBox( - height: 20, - width: 20, - child: Radio( - activeColor: Theme.of(context) - .extension<StackColors>()! - .radioButtonIconEnabled, - value: manager.walletId, - groupValue: ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.startupWalletId), ), - onChanged: (value) { - if (value is String) { - ref - .read( - prefsChangeNotifierProvider) - .startupWalletId = value; - } - }, ), - ), - ], + const SizedBox( + width: 12, + ), + Expanded( + child: Column( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + manager.walletName, + style: STextStyles.titleBold12( + context), + ), + // const SizedBox( + // height: 2, + // ), + // FutureBuilder( + // future: manager.totalBalance, + // builder: (builderContext, + // AsyncSnapshot<Decimal> snapshot) { + // if (snapshot.connectionState == + // ConnectionState.done && + // snapshot.hasData) { + // return Text( + // "${Format.localizedStringAsFixed( + // value: snapshot.data!, + // locale: ref.watch( + // localeServiceChangeNotifierProvider + // .select((value) => + // value.locale)), + // decimalPlaces: 8, + // )} ${manager.coin.ticker}", + // style: STextStyles.itemSubtitle(context), + // ); + // } else { + // return AnimatedText( + // stringsToLoopThrough: const [ + // "Loading balance", + // "Loading balance.", + // "Loading balance..", + // "Loading balance..." + // ], + // style: STextStyles.itemSubtitle(context), + // ); + // } + // }, + // ), + ], + ), + ), + SizedBox( + height: 20, + width: 20, + child: Radio( + activeColor: Theme.of(context) + .extension<StackColors>()! + .radioButtonIconEnabled, + value: manager.walletId, + groupValue: ref.watch( + prefsChangeNotifierProvider.select( + (value) => + value.startupWalletId), + ), + onChanged: (value) { + if (value is String) { + ref + .read( + prefsChangeNotifierProvider) + .startupWalletId = value; + } + }, + ), + ), + ], + ), ), ), - ), - ], + ], + ), ), - ), - ], + ], + ), ), ), ), ), - ), - ); - }), + ); + }), + ), ); } } diff --git a/lib/pages/settings_views/global_settings_view/support_view.dart b/lib/pages/settings_views/global_settings_view/support_view.dart index ee00bdb00..1cc3d35a1 100644 --- a/lib/pages/settings_views/global_settings_view/support_view.dart +++ b/lib/pages/settings_views/global_settings_view/support_view.dart @@ -5,6 +5,7 @@ import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -26,24 +27,26 @@ class SupportView extends StatelessWidget { return ConditionalParent( condition: !isDesktop, builder: (child) { - return Scaffold( - backgroundColor: - Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Support", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Support", - style: STextStyles.navBarTitle(context), + body: Padding( + padding: const EdgeInsets.all(16), + child: child, ), ), - body: Padding( - padding: const EdgeInsets.all(16), - child: child, - ), ); }, child: Column( diff --git a/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart b/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart index 3681009a9..10a93d7e8 100644 --- a/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart +++ b/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart @@ -7,6 +7,7 @@ import 'package:stackwallet/utilities/enums/sync_type_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; @@ -24,35 +25,37 @@ class SyncingOptionsView extends ConsumerWidget { return ConditionalParent( condition: !isDesktop, builder: (child) { - return Scaffold( - backgroundColor: - Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Syncing", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Syncing", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, + body: Padding( + padding: const EdgeInsets.all(16), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: child, + ), ), - child: IntrinsicHeight( - child: child, - ), - ), - ); - }, + ); + }, + ), ), ), ); diff --git a/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart b/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart index e48ebb342..6ce84627e 100644 --- a/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart +++ b/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart @@ -6,6 +6,7 @@ import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/sync_type_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -28,131 +29,136 @@ class SyncingPreferencesView extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Syncing preferences", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Syncing preferences", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + body: Padding( + padding: const EdgeInsets.all(16), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( padding: const EdgeInsets.all(0), - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - Navigator.of(context) - .pushNamed(SyncingOptionsView.routeName); - }, - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Syncing", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - Text( - _currentTypeDescription(ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.syncType))), - style: STextStyles.itemSubtitle(context), - textAlign: TextAlign.left, - ) - ], - ), - const Spacer(), - ], - ), - ), - ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - child: Consumer( - builder: (_, ref, __) { - return RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + padding: const EdgeInsets.all(0), + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - onPressed: null, - child: Padding( - padding: - const EdgeInsets.symmetric(vertical: 8), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "AutoSync only on Wi-Fi", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - SizedBox( - height: 20, - width: 40, - child: DraggableSwitchButton( - isOn: ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.wifiOnly), - ), - onValueChanged: (newValue) { - ref - .read(prefsChangeNotifierProvider) - .wifiOnly = newValue; - }, + ), + onPressed: () { + Navigator.of(context) + .pushNamed(SyncingOptionsView.routeName); + }, + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Syncing", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, ), - ), - ], - ), + Text( + _currentTypeDescription(ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.syncType))), + style: + STextStyles.itemSubtitle(context), + textAlign: TextAlign.left, + ) + ], + ), + const Spacer(), + ], ), - ); - }, + ), + ), ), - ), - ], + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + child: Consumer( + builder: (_, ref, __) { + return RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: null, + child: Padding( + padding: + const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "AutoSync only on Wi-Fi", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.wifiOnly), + ), + onValueChanged: (newValue) { + ref + .read( + prefsChangeNotifierProvider) + .wifiOnly = newValue; + }, + ), + ), + ], + ), + ), + ); + }, + ), + ), + ], + ), ), ), - ), - ); - }, + ); + }, + ), ), ), ); diff --git a/lib/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart b/lib/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart index 9724356a1..7cbc86b7a 100644 --- a/lib/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart +++ b/lib/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart @@ -12,6 +12,7 @@ import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/animated_text.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart'; @@ -31,30 +32,32 @@ class WalletSyncingOptionsView extends ConsumerWidget { return ConditionalParent( condition: !isDesktop, builder: (child) { - return Scaffold( - backgroundColor: - Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - Navigator.of(context).pop(); - }, - ), - title: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - "Sync only selected wallets at startup", - style: STextStyles.navBarTitle(context), + return Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).pop(); + }, + ), + title: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + "Sync only selected wallets at startup", + style: STextStyles.navBarTitle(context), + ), ), ), - ), - body: Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, + body: Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: child, ), - child: child, ), ); }, diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart index 3d557d245..a9235172f 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart @@ -15,6 +15,7 @@ import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; @@ -35,186 +36,188 @@ class WalletBackupView extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { debugPrint("BUILD: $runtimeType"); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Wallet backup", - style: STextStyles.navBarTitle(context), - ), - actions: [ - Padding( - padding: const EdgeInsets.all(10), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - color: Theme.of(context).extension<StackColors>()!.background, - shadows: const [], - icon: SvgPicture.asset( - Assets.svg.copy, - width: 20, - height: 20, - color: Theme.of(context) - .extension<StackColors>()! - .topNavIconPrimary, - ), - onPressed: () async { - await clipboardInterface - .setData(ClipboardData(text: mnemonic.join(" "))); - unawaited(showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - iconAsset: Assets.svg.copy, - context: context, - )); - }, - ), - ), + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, ), - ], - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox( - height: 4, - ), - Text( - ref - .watch(walletsChangeNotifierProvider - .select((value) => value.getManager(walletId))) - .walletName, - textAlign: TextAlign.center, - style: STextStyles.label(context).copyWith( - fontSize: 12, - ), - ), - const SizedBox( - height: 4, - ), - Text( - "Recovery Phrase", - textAlign: TextAlign.center, - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 16, - ), - Container( - decoration: BoxDecoration( - color: Theme.of(context).extension<StackColors>()!.popupBG, - borderRadius: - BorderRadius.circular(Constants.size.circularBorderRadius), - ), - child: Padding( - padding: const EdgeInsets.all(12), - child: Text( - "Please write down your backup key. Keep it safe and never share it with anyone. Your backup key is the only way you can access your funds if you forget your PIN, lose your phone, etc.\n\nStack Wallet does not keep nor is able to restore your backup key. Only you have access to your wallet.", - style: STextStyles.label(context), + title: Text( + "Wallet backup", + style: STextStyles.navBarTitle(context), + ), + actions: [ + Padding( + padding: const EdgeInsets.all(10), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + color: Theme.of(context).extension<StackColors>()!.background, + shadows: const [], + icon: SvgPicture.asset( + Assets.svg.copy, + width: 20, + height: 20, + color: Theme.of(context) + .extension<StackColors>()! + .topNavIconPrimary, + ), + onPressed: () async { + await clipboardInterface + .setData(ClipboardData(text: mnemonic.join(" "))); + unawaited(showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + )); + }, ), ), ), - const SizedBox( - height: 8, - ), - Expanded( - child: SingleChildScrollView( - child: MnemonicTable( - words: mnemonic, - isDesktop: false, + ], + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox( + height: 4, + ), + Text( + ref + .watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletId))) + .walletName, + textAlign: TextAlign.center, + style: STextStyles.label(context).copyWith( + fontSize: 12, ), ), - ), - const SizedBox( - height: 12, - ), - TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () { - String data = AddressUtils.encodeQRSeedData(mnemonic); + const SizedBox( + height: 4, + ), + Text( + "Recovery Phrase", + textAlign: TextAlign.center, + style: STextStyles.pageTitleH1(context), + ), + const SizedBox( + height: 16, + ), + Container( + decoration: BoxDecoration( + color: Theme.of(context).extension<StackColors>()!.popupBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + "Please write down your backup key. Keep it safe and never share it with anyone. Your backup key is the only way you can access your funds if you forget your PIN, lose your phone, etc.\n\nStack Wallet does not keep nor is able to restore your backup key. Only you have access to your wallet.", + style: STextStyles.label(context), + ), + ), + ), + const SizedBox( + height: 8, + ), + Expanded( + child: SingleChildScrollView( + child: MnemonicTable( + words: mnemonic, + isDesktop: false, + ), + ), + ), + const SizedBox( + height: 12, + ), + TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + onPressed: () { + String data = AddressUtils.encodeQRSeedData(mnemonic); - showDialog<dynamic>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (_) { - final width = MediaQuery.of(context).size.width / 2; - return StackDialogBase( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Center( - child: Text( - "Recovery phrase QR code", - style: STextStyles.pageTitleH2(context), - ), - ), - const SizedBox( - height: 12, - ), - Center( - child: RepaintBoundary( - // key: _qrKey, - child: SizedBox( - width: width + 20, - height: width + 20, - child: QrImage( - data: data, - size: width, - backgroundColor: Theme.of(context) - .extension<StackColors>()! - .popupBG, - foregroundColor: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), + showDialog<dynamic>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (_) { + final width = MediaQuery.of(context).size.width / 2; + return StackDialogBase( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: Text( + "Recovery phrase QR code", + style: STextStyles.pageTitleH2(context), ), ), - ), - const SizedBox( - height: 12, - ), - Center( - child: SizedBox( - width: width, - child: TextButton( - onPressed: () async { - // await _capturePng(true); - Navigator.of(context).pop(); - }, - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) + const SizedBox( + height: 12, + ), + Center( + child: RepaintBoundary( + // key: _qrKey, + child: SizedBox( + width: width + 20, + height: width + 20, + child: QrImage( + data: data, + size: width, + backgroundColor: Theme.of(context) + .extension<StackColors>()! + .popupBG, + foregroundColor: Theme.of(context) .extension<StackColors>()! .accentColorDark), ), ), ), - ), - ], - ), - ); - }, - ); - }, - child: Text( - "Show QR Code", - style: STextStyles.button(context), + const SizedBox( + height: 12, + ), + Center( + child: SizedBox( + width: width, + child: TextButton( + onPressed: () async { + // await _capturePng(true); + Navigator.of(context).pop(); + }, + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Cancel", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + ), + ), + ], + ), + ); + }, + ); + }, + child: Text( + "Show QR Code", + style: STextStyles.button(context), + ), ), - ), - ], + ], + ), ), ), ); diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart index 3044467aa..7e29010b1 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart @@ -26,6 +26,7 @@ import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/animated_text.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; @@ -312,118 +313,122 @@ class _WalletNetworkSettingsViewState return ConditionalParent( condition: !isDesktop, builder: (child) { - return Scaffold( - backgroundColor: - Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Network", - style: STextStyles.navBarTitle(context), - ), - actions: [ - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("walletNetworkSettingsAddNewNodeViewButton"), - size: 36, - shadows: const [], - color: - Theme.of(context).extension<StackColors>()!.background, - icon: SvgPicture.asset( - Assets.svg.verticalEllipsis, + return Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Network", + style: STextStyles.navBarTitle(context), + ), + actions: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key( + "walletNetworkSettingsAddNewNodeViewButton"), + size: 36, + shadows: const [], color: Theme.of(context) .extension<StackColors>()! - .accentColorDark, - width: 20, - height: 20, - ), - onPressed: () { - showDialog<dynamic>( - barrierColor: Colors.transparent, - barrierDismissible: true, - context: context, - builder: (_) { - return Stack( - children: [ - Positioned( - top: 9, - right: 10, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .popupBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius), - // boxShadow: [CFColors.standardBoxShadow], - boxShadow: const [], - ), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - GestureDetector( - onTap: () { - Navigator.of(context).pop(); - showDialog<void>( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return ConfirmFullRescanDialog( - onConfirm: _attemptRescan, - ); - }, - ); - }, - child: RoundedWhiteContainer( - child: Material( - color: Colors.transparent, - child: Text( - "Rescan blockchain", - style: - STextStyles.baseXS(context), + .background, + icon: SvgPicture.asset( + Assets.svg.verticalEllipsis, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + width: 20, + height: 20, + ), + onPressed: () { + showDialog<dynamic>( + barrierColor: Colors.transparent, + barrierDismissible: true, + context: context, + builder: (_) { + return Stack( + children: [ + Positioned( + top: 9, + right: 10, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .popupBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius), + // boxShadow: [CFColors.standardBoxShadow], + boxShadow: const [], + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: () { + Navigator.of(context).pop(); + showDialog<void>( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return ConfirmFullRescanDialog( + onConfirm: _attemptRescan, + ); + }, + ); + }, + child: RoundedWhiteContainer( + child: Material( + color: Colors.transparent, + child: Text( + "Rescan blockchain", + style: + STextStyles.baseXS(context), + ), ), ), ), - ), - ], + ], + ), ), ), - ), - ], - ); - }, - ); - }, + ], + ); + }, + ); + }, + ), ), ), - ), - ], - ), - body: Padding( - padding: EdgeInsets.only( - top: 12, - left: _padding, - right: _padding, + ], ), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - child, - ], + body: Padding( + padding: EdgeInsets.only( + top: 12, + left: _padding, + right: _padding, + ), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + child, + ], + ), ), ), ), diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart index 2d8909245..92b712111 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart @@ -23,15 +23,14 @@ import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_ import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:tuple/tuple.dart'; -import 'package:stackwallet/utilities/util.dart'; - /// [eventBus] should only be set during testing class WalletSettingsView extends StatefulWidget { const WalletSettingsView({ @@ -134,196 +133,199 @@ class _WalletSettingsViewState extends State<WalletSettingsView> { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Settings", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Settings", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (builderContext, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - padding: const EdgeInsets.all(4), - child: Column( - children: [ - SettingsListButton( - iconAssetName: Assets.svg.addressBook, - iconSize: 16, - title: "Address book", - onPressed: () { - Navigator.of(context).pushNamed( - AddressBookView.routeName, - arguments: coin, - ); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.node, - iconSize: 16, - title: "Network", - onPressed: () { - Navigator.of(context).pushNamed( - WalletNetworkSettingsView.routeName, - arguments: Tuple3( - walletId, - _currentSyncStatus, - widget.initialNodeStatus, - ), - ); - }, - ), - const SizedBox( - height: 8, - ), - Consumer( - builder: (_, ref, __) { - return SettingsListButton( - iconAssetName: Assets.svg.lock, - iconSize: 16, - title: "Wallet backup", - onPressed: () async { - final mnemonic = await ref - .read(walletsChangeNotifierProvider) - .getManager(walletId) - .mnemonic; + body: LayoutBuilder( + builder: (builderContext, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(4), + child: Column( + children: [ + SettingsListButton( + iconAssetName: Assets.svg.addressBook, + iconSize: 16, + title: "Address book", + onPressed: () { + Navigator.of(context).pushNamed( + AddressBookView.routeName, + arguments: coin, + ); + }, + ), + const SizedBox( + height: 8, + ), + SettingsListButton( + iconAssetName: Assets.svg.node, + iconSize: 16, + title: "Network", + onPressed: () { + Navigator.of(context).pushNamed( + WalletNetworkSettingsView.routeName, + arguments: Tuple3( + walletId, + _currentSyncStatus, + widget.initialNodeStatus, + ), + ); + }, + ), + const SizedBox( + height: 8, + ), + Consumer( + builder: (_, ref, __) { + return SettingsListButton( + iconAssetName: Assets.svg.lock, + iconSize: 16, + title: "Wallet backup", + onPressed: () async { + final mnemonic = await ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .mnemonic; - if (mounted) { - Navigator.push( - context, - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator - .useMaterialPageRoute, - builder: (_) => LockscreenView( - routeOnSuccessArguments: - Tuple2(walletId, mnemonic), - showBackButton: true, - routeOnSuccess: - WalletBackupView.routeName, - biometricsCancelButtonString: - "CANCEL", - biometricsLocalizedReason: - "Authenticate to view recovery phrase", - biometricsAuthenticationTitle: - "View recovery phrase", + if (mounted) { + Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator + .useMaterialPageRoute, + builder: (_) => LockscreenView( + routeOnSuccessArguments: + Tuple2(walletId, mnemonic), + showBackButton: true, + routeOnSuccess: + WalletBackupView.routeName, + biometricsCancelButtonString: + "CANCEL", + biometricsLocalizedReason: + "Authenticate to view recovery phrase", + biometricsAuthenticationTitle: + "View recovery phrase", + ), + settings: const RouteSettings( + name: + "/viewRecoverPhraseLockscreen"), ), - settings: const RouteSettings( - name: - "/viewRecoverPhraseLockscreen"), - ), - ); - } - }, - ); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.downloadFolder, - title: "Wallet settings", - iconSize: 16, - onPressed: () { - Navigator.of(context).pushNamed( - WalletSettingsWalletSettingsView.routeName, - arguments: walletId, - ); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.arrowRotate3, - title: "Syncing preferences", - onPressed: () { - Navigator.of(context).pushNamed( - SyncingPreferencesView.routeName); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.ellipsis, - title: "Debug Info", - onPressed: () { - Navigator.of(context) - .pushNamed(DebugView.routeName); - }, - ), - ], + ); + } + }, + ); + }, + ), + const SizedBox( + height: 8, + ), + SettingsListButton( + iconAssetName: Assets.svg.downloadFolder, + title: "Wallet settings", + iconSize: 16, + onPressed: () { + Navigator.of(context).pushNamed( + WalletSettingsWalletSettingsView + .routeName, + arguments: walletId, + ); + }, + ), + const SizedBox( + height: 8, + ), + SettingsListButton( + iconAssetName: Assets.svg.arrowRotate3, + title: "Syncing preferences", + onPressed: () { + Navigator.of(context).pushNamed( + SyncingPreferencesView.routeName); + }, + ), + const SizedBox( + height: 8, + ), + SettingsListButton( + iconAssetName: Assets.svg.ellipsis, + title: "Debug Info", + onPressed: () { + Navigator.of(context) + .pushNamed(DebugView.routeName); + }, + ), + ], + ), ), - ), - const SizedBox( - height: 12, - ), - const Spacer(), - Consumer( - builder: (_, ref, __) { - return TextButton( - onPressed: () { - ref - .read(walletsChangeNotifierProvider) - .getManager(walletId) - .isActiveWallet = false; - ref - .read(transactionFilterProvider.state) - .state = null; + const SizedBox( + height: 12, + ), + const Spacer(), + Consumer( + builder: (_, ref, __) { + return TextButton( + onPressed: () { + ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .isActiveWallet = false; + ref + .read(transactionFilterProvider.state) + .state = null; - Navigator.of(context).popUntil( - ModalRoute.withName(HomeView.routeName), - ); - }, - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - child: Text( - "Log out", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - ), - ); - }, - ), - ], + Navigator.of(context).popUntil( + ModalRoute.withName(HomeView.routeName), + ); + }, + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + child: Text( + "Log out", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ); + }, + ), + ], + ), ), ), ), ), - ), - ); - }, + ); + }, + ), ), ); } diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_recovery_phrase_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_recovery_phrase_view.dart index 66d666c20..5543bf1c0 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_recovery_phrase_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_recovery_phrase_view.dart @@ -13,6 +13,7 @@ import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; @@ -54,164 +55,167 @@ class _DeleteWalletRecoveryPhraseViewState Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - actions: [ - Padding( - padding: const EdgeInsets.all(10), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - color: Theme.of(context).extension<StackColors>()!.background, - shadows: const [], - icon: SvgPicture.asset( - Assets.svg.copy, - width: 20, - height: 20, - color: Theme.of(context) - .extension<StackColors>()! - .topNavIconPrimary, - ), - onPressed: () async { - final words = await _manager.mnemonic; - await _clipboardInterface - .setData(ClipboardData(text: words.join(" "))); - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - iconAsset: Assets.svg.copy, - context: context, - ); - }, - ), - ), + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, ), - ], - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox( - height: 4, - ), - Text( - _manager.walletName, - textAlign: TextAlign.center, - style: STextStyles.label(context).copyWith( - fontSize: 12, - ), - ), - const SizedBox( - height: 4, - ), - Text( - "Recovery Phrase", - textAlign: TextAlign.center, - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 16, - ), - Container( - decoration: BoxDecoration( - color: Theme.of(context).extension<StackColors>()!.popupBG, - borderRadius: - BorderRadius.circular(Constants.size.circularBorderRadius), - ), - child: Padding( - padding: const EdgeInsets.all(12), - child: Text( - "Please write down your recovery phrase in the correct order and save it to keep your funds secure. You will also be asked to verify the words on the next screen.", - style: STextStyles.label(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - ), - ), - ), - const SizedBox( - height: 8, - ), - Expanded( - child: SingleChildScrollView( - child: MnemonicTable( - words: _mnemonic, - isDesktop: false, - ), - ), - ), - const SizedBox( - height: 16, - ), - TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () { - showDialog<dynamic>( - barrierDismissible: true, - context: context, - builder: (_) => StackDialog( - title: "Thanks! Your wallet will be deleted.", - leftButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - onPressed: () { - Navigator.pop(context); - }, - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - ), - ), - rightButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () async { - final walletId = _manager.walletId; - final walletsInstance = - ref.read(walletsChangeNotifierProvider); - await ref - .read(walletsServiceChangeNotifierProvider) - .deleteWallet(_manager.walletName, true); - - if (mounted) { - Navigator.of(context).popUntil( - ModalRoute.withName(HomeView.routeName)); - } - - // wait for widget tree to dispose of any widgets watching the manager - await Future<void>.delayed(const Duration(seconds: 1)); - walletsInstance.removeWallet(walletId: walletId); - }, - child: Text( - "Ok", - style: STextStyles.button(context), - ), - ), + actions: [ + Padding( + padding: const EdgeInsets.all(10), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + color: Theme.of(context).extension<StackColors>()!.background, + shadows: const [], + icon: SvgPicture.asset( + Assets.svg.copy, + width: 20, + height: 20, + color: Theme.of(context) + .extension<StackColors>()! + .topNavIconPrimary, ), - ); - }, - child: Text( - "Continue", - style: STextStyles.button(context), + onPressed: () async { + final words = await _manager.mnemonic; + await _clipboardInterface + .setData(ClipboardData(text: words.join(" "))); + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ); + }, + ), ), ), ], ), + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox( + height: 4, + ), + Text( + _manager.walletName, + textAlign: TextAlign.center, + style: STextStyles.label(context).copyWith( + fontSize: 12, + ), + ), + const SizedBox( + height: 4, + ), + Text( + "Recovery Phrase", + textAlign: TextAlign.center, + style: STextStyles.pageTitleH1(context), + ), + const SizedBox( + height: 16, + ), + Container( + decoration: BoxDecoration( + color: Theme.of(context).extension<StackColors>()!.popupBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + "Please write down your recovery phrase in the correct order and save it to keep your funds secure. You will also be asked to verify the words on the next screen.", + style: STextStyles.label(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + ), + const SizedBox( + height: 8, + ), + Expanded( + child: SingleChildScrollView( + child: MnemonicTable( + words: _mnemonic, + isDesktop: false, + ), + ), + ), + const SizedBox( + height: 16, + ), + TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + onPressed: () { + showDialog<dynamic>( + barrierDismissible: true, + context: context, + builder: (_) => StackDialog( + title: "Thanks! Your wallet will be deleted.", + leftButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + onPressed: () { + Navigator.pop(context); + }, + child: Text( + "Cancel", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + rightButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + onPressed: () async { + final walletId = _manager.walletId; + final walletsInstance = + ref.read(walletsChangeNotifierProvider); + await ref + .read(walletsServiceChangeNotifierProvider) + .deleteWallet(_manager.walletName, true); + + if (mounted) { + Navigator.of(context).popUntil( + ModalRoute.withName(HomeView.routeName)); + } + + // wait for widget tree to dispose of any widgets watching the manager + await Future<void>.delayed( + const Duration(seconds: 1)); + walletsInstance.removeWallet(walletId: walletId); + }, + child: Text( + "Ok", + style: STextStyles.button(context), + ), + ), + ), + ); + }, + child: Text( + "Continue", + style: STextStyles.button(context), + ), + ), + ], + ), + ), ), ); } diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart index ec8c5a128..f740eee12 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart @@ -4,6 +4,7 @@ import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_set import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:tuple/tuple.dart'; @@ -20,93 +21,96 @@ class DeleteWalletWarningView extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), ), - ), - body: Padding( - padding: const EdgeInsets.only( - top: 12, - left: 16, - right: 16, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox( - height: 32, - ), - Center( - child: Text( - "Attention!", - style: STextStyles.pageTitleH1(context), + body: Padding( + padding: const EdgeInsets.only( + top: 12, + left: 16, + right: 16, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox( + height: 32, ), - ), - const SizedBox( - height: 16, - ), - RoundedContainer( - color: - Theme.of(context).extension<StackColors>()!.warningBackground, - child: Text( - "You are going to permanently delete you wallet.\n\nIf you delete your wallet, the only way you can have access to your funds is by using your backup key.\n\nStack Wallet does not keep nor is able to restore your backup key or your wallet.\n\nPLEASE SAVE YOUR BACKUP KEY.", - style: STextStyles.baseXS(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .warningForeground, + Center( + child: Text( + "Attention!", + style: STextStyles.pageTitleH1(context), ), ), - ), - const Spacer(), - TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - onPressed: () { - Navigator.pop(context); - }, - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( + const SizedBox( + height: 16, + ), + RoundedContainer( + color: Theme.of(context) + .extension<StackColors>()! + .warningBackground, + child: Text( + "You are going to permanently delete you wallet.\n\nIf you delete your wallet, the only way you can have access to your funds is by using your backup key.\n\nStack Wallet does not keep nor is able to restore your backup key or your wallet.\n\nPLEASE SAVE YOUR BACKUP KEY.", + style: STextStyles.baseXS(context).copyWith( color: Theme.of(context) .extension<StackColors>()! - .accentColorDark), - ), - ), - const SizedBox( - height: 12, - ), - TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () async { - final manager = ref - .read(walletsChangeNotifierProvider) - .getManager(walletId); - final mnemonic = await manager.mnemonic; - Navigator.of(context).pushNamed( - DeleteWalletRecoveryPhraseView.routeName, - arguments: Tuple2( - manager, - mnemonic, + .warningForeground, ), - ); - }, - child: Text( - "View Backup Key", - style: STextStyles.button(context), + ), ), - ), - const SizedBox( - height: 16, - ), - ], + const Spacer(), + TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + onPressed: () { + Navigator.pop(context); + }, + child: Text( + "Cancel", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + const SizedBox( + height: 12, + ), + TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + onPressed: () async { + final manager = ref + .read(walletsChangeNotifierProvider) + .getManager(walletId); + final mnemonic = await manager.mnemonic; + Navigator.of(context).pushNamed( + DeleteWalletRecoveryPhraseView.routeName, + arguments: Tuple2( + manager, + mnemonic, + ), + ); + }, + child: Text( + "View Backup Key", + style: STextStyles.button(context), + ), + ), + const SizedBox( + height: 16, + ), + ], + ), ), ), ); diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rename_wallet_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rename_wallet_view.dart index e9eb14868..f76422750 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rename_wallet_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rename_wallet_view.dart @@ -6,13 +6,13 @@ import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; -import 'package:stackwallet/utilities/util.dart'; - class RenameWalletView extends ConsumerStatefulWidget { const RenameWalletView({ Key? key, @@ -53,102 +53,104 @@ class _RenameWalletViewState extends ConsumerState<RenameWalletView> { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Rename wallet", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Rename wallet", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: _controller, - focusNode: _focusNode, - style: STextStyles.field(context), - onChanged: (_) => setState(() {}), - decoration: standardInputDecoration( - "Wallet name", - _focusNode, - context, - ).copyWith( - suffixIcon: _controller.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _controller.text = ""; - }); - }, - ), - ], + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _controller, + focusNode: _focusNode, + style: STextStyles.field(context), + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Wallet name", + _focusNode, + context, + ).copyWith( + suffixIcon: _controller.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _controller.text = ""; + }); + }, + ), + ], + ), ), - ), - ) - : null, + ) + : null, + ), ), ), - ), - const Spacer(), - TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () async { - final newName = _controller.text; - final success = await ref - .read(walletsServiceChangeNotifierProvider) - .renameWallet( - from: originalName, - to: newName, - shouldNotifyListeners: true, - ); + const Spacer(), + TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + onPressed: () async { + final newName = _controller.text; + final success = await ref + .read(walletsServiceChangeNotifierProvider) + .renameWallet( + from: originalName, + to: newName, + shouldNotifyListeners: true, + ); - if (success) { - ref - .read(walletsChangeNotifierProvider) - .getManager(walletId) - .walletName = newName; - Navigator.of(context).pop(); - showFloatingFlushBar( - type: FlushBarType.success, - message: "Wallet renamed", - context: context, - ); - } else { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Wallet named \"$newName\" already exists", - context: context, - ); - } - }, - child: Text( - "Save", - style: STextStyles.button(context), + if (success) { + ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .walletName = newName; + Navigator.of(context).pop(); + showFloatingFlushBar( + type: FlushBarType.success, + message: "Wallet renamed", + context: context, + ); + } else { + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Wallet named \"$newName\" already exists", + context: context, + ); + } + }, + child: Text( + "Save", + style: STextStyles.button(context), + ), ), - ), - ], + ], + ), ), ), ); diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart index 52aa6027a..a9c0f92af 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart @@ -8,6 +8,7 @@ import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; @@ -24,149 +25,151 @@ class WalletSettingsWalletSettingsView extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Wallet settings", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Wallet settings", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.only( - top: 12, - left: 16, - right: 16, - ), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - onPressed: () { - Navigator.of(context).pushNamed( - RenameWalletView.routeName, - arguments: walletId, - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 20, - ), - child: Row( - children: [ - Text( - "Rename wallet", - style: STextStyles.titleBold12(context), - ), - ], - ), - ), - ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension<StackColors>()!.highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + body: Padding( + padding: const EdgeInsets.only( + top: 12, + left: 16, + right: 16, + ), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( padding: const EdgeInsets.all(0), - onPressed: () { - showDialog( - barrierDismissible: true, - context: context, - builder: (_) => StackDialog( - title: - "Do you want to delete ${ref.read(walletsChangeNotifierProvider).getManager(walletId).walletName}?", - leftButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - onPressed: () { - Navigator.pop(context); - }, - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark), - ), - ), - rightButton: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - onPressed: () { - Navigator.pop(context); - Navigator.push( - context, - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator.useMaterialPageRoute, - builder: (_) => LockscreenView( - routeOnSuccessArguments: walletId, - showBackButton: true, - routeOnSuccess: - DeleteWalletWarningView.routeName, - biometricsCancelButtonString: "CANCEL", - biometricsLocalizedReason: - "Authenticate to delete wallet", - biometricsAuthenticationTitle: - "Delete wallet", - ), - settings: const RouteSettings( - name: "/deleteWalletLockscreen"), - ), - ); - }, - child: Text( - "Delete", - style: STextStyles.button(context), - ), - ), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 20, ), - child: Row( - children: [ - Text( - "Delete wallet", - style: STextStyles.titleBold12(context), - ), - ], + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + onPressed: () { + Navigator.of(context).pushNamed( + RenameWalletView.routeName, + arguments: walletId, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 20, + ), + child: Row( + children: [ + Text( + "Rename wallet", + style: STextStyles.titleBold12(context), + ), + ], + ), ), ), ), - ), - ], + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + padding: const EdgeInsets.all(0), + onPressed: () { + showDialog( + barrierDismissible: true, + context: context, + builder: (_) => StackDialog( + title: + "Do you want to delete ${ref.read(walletsChangeNotifierProvider).getManager(walletId).walletName}?", + leftButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + onPressed: () { + Navigator.pop(context); + }, + child: Text( + "Cancel", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + rightButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + onPressed: () { + Navigator.pop(context); + Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator.useMaterialPageRoute, + builder: (_) => LockscreenView( + routeOnSuccessArguments: walletId, + showBackButton: true, + routeOnSuccess: + DeleteWalletWarningView.routeName, + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: + "Authenticate to delete wallet", + biometricsAuthenticationTitle: + "Delete wallet", + ), + settings: const RouteSettings( + name: "/deleteWalletLockscreen"), + ), + ); + }, + child: Text( + "Delete", + style: STextStyles.button(context), + ), + ), + ), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 20, + ), + child: Row( + children: [ + Text( + "Delete wallet", + style: STextStyles.titleBold12(context), + ), + ], + ), + ), + ), + ), + ], + ), ), ), ), diff --git a/lib/pages/wallet_view/transaction_views/edit_note_view.dart b/lib/pages/wallet_view/transaction_views/edit_note_view.dart index 4baaaffc9..e8d7b05f9 100644 --- a/lib/pages/wallet_view/transaction_views/edit_note_view.dart +++ b/lib/pages/wallet_view/transaction_views/edit_note_view.dart @@ -5,6 +5,8 @@ import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; @@ -53,120 +55,141 @@ class _EditNoteViewState extends ConsumerState<EditNoteView> { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: isDesktop - ? Colors.transparent - : Theme.of(context).extension<StackColors>()!.background, - appBar: isDesktop - ? null - : AppBar( - backgroundColor: - Theme.of(context).extension<StackColors>()!.background, - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed( - const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Background( + child: child, + ), + child: Scaffold( + backgroundColor: isDesktop + ? Colors.transparent + : Theme.of(context).extension<StackColors>()!.background, + appBar: isDesktop + ? null + : AppBar( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed( + const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Edit note", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Edit note", - style: STextStyles.navBarTitle(context), - ), - ), - body: MobileEditNoteScaffold( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (isDesktop) + body: MobileEditNoteScaffold( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (isDesktop) + Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 12, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Edit note", + style: STextStyles.desktopH3(context), + ), + const DesktopDialogCloseButton(), + ], + ), + ), Padding( - padding: const EdgeInsets.only( - left: 32, - bottom: 12, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Edit note", - style: STextStyles.desktopH3(context), - ), - const DesktopDialogCloseButton(), - ], - ), - ), - Padding( - padding: isDesktop - ? const EdgeInsets.symmetric( - horizontal: 32, - ) - : const EdgeInsets.all(0), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: _noteController, - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context), - focusNode: noteFieldFocusNode, - decoration: standardInputDecoration( - "Note", - noteFieldFocusNode, - context, - desktopMed: isDesktop, - ).copyWith( - contentPadding: isDesktop - ? const EdgeInsets.only( - left: 16, - top: 11, - bottom: 12, - right: 5, + padding: isDesktop + ? const EdgeInsets.symmetric( + horizontal: 32, + ) + : const EdgeInsets.all(0), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _noteController, + style: isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveText, + height: 1.8, ) - : null, - suffixIcon: _noteController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _noteController.text = ""; - }); - }, - ), - ], + : STextStyles.field(context), + focusNode: noteFieldFocusNode, + decoration: standardInputDecoration( + "Note", + noteFieldFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + contentPadding: isDesktop + ? const EdgeInsets.only( + left: 16, + top: 11, + bottom: 12, + right: 5, + ) + : null, + suffixIcon: _noteController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _noteController.text = ""; + }); + }, + ), + ], + ), ), - ), - ) - : null, + ) + : null, + ), ), ), ), - ), - // if (!isDesktop) - const Spacer(), - if (isDesktop) - Padding( - padding: const EdgeInsets.all(32), - child: PrimaryButton( - label: "Save", + // if (!isDesktop) + const Spacer(), + if (isDesktop) + Padding( + padding: const EdgeInsets.all(32), + child: PrimaryButton( + label: "Save", + onPressed: () async { + await ref + .read(notesServiceChangeNotifierProvider( + widget.walletId)) + .editOrAddNote( + txid: widget.txid, + note: _noteController.text, + ); + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + ), + if (!isDesktop) + TextButton( onPressed: () async { await ref .read( @@ -179,30 +202,16 @@ class _EditNoteViewState extends ConsumerState<EditNoteView> { Navigator.of(context).pop(); } }, - ), - ), - if (!isDesktop) - TextButton( - onPressed: () async { - await ref - .read(notesServiceChangeNotifierProvider(widget.walletId)) - .editOrAddNote( - txid: widget.txid, - note: _noteController.text, - ); - if (mounted) { - Navigator.of(context).pop(); - } - }, - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - child: Text( - "Save", - style: STextStyles.button(context), - ), - ) - ], + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor(context), + child: Text( + "Save", + style: STextStyles.button(context), + ), + ) + ], + ), ), ), ); diff --git a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart index c2a0590e4..738ef721b 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart @@ -25,6 +25,7 @@ import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; @@ -297,177 +298,290 @@ class _TransactionDetailsViewState @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: isDesktop - ? Colors.transparent - : Theme.of(context).extension<StackColors>()!.background, - appBar: isDesktop - ? null - : AppBar( - backgroundColor: - Theme.of(context).extension<StackColors>()!.background, - leading: AppBarBackButton( - onPressed: () async { - // if (FocusScope.of(context).hasFocus) { - // FocusScope.of(context).unfocus(); - // await Future<void>.delayed(Duration(milliseconds: 50)); - // } - Navigator.of(context).pop(); - }, - ), - title: Text( - "Transaction details", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: isDesktop - ? const EdgeInsets.only(left: 32) - : const EdgeInsets.all(12), - child: Column( - children: [ - if (isDesktop) - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Transaction details", - style: STextStyles.desktopH3(context), - ), - const DesktopDialogCloseButton(), - ], - ), - Expanded( - child: Padding( - padding: isDesktop - ? const EdgeInsets.only( - right: 32, - bottom: 32, - ) - : const EdgeInsets.all(0), - child: ConditionalParent( - condition: isDesktop, - builder: (child) { - return RoundedWhiteContainer( - borderColor: isDesktop - ? Theme.of(context) - .extension<StackColors>()! - .background - : null, - padding: const EdgeInsets.all(0), - child: child, - ); + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Background( + child: child, + ), + child: Scaffold( + backgroundColor: isDesktop + ? Colors.transparent + : Theme.of(context).extension<StackColors>()!.background, + appBar: isDesktop + ? null + : AppBar( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + leading: AppBarBackButton( + onPressed: () async { + // if (FocusScope.of(context).hasFocus) { + // FocusScope.of(context).unfocus(); + // await Future<void>.delayed(Duration(milliseconds: 50)); + // } + Navigator.of(context).pop(); }, - child: SingleChildScrollView( - primary: isDesktop ? false : null, - child: Padding( - padding: isDesktop - ? const EdgeInsets.all(0) - : const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(0) - : const EdgeInsets.all(12), - child: Container( - decoration: isDesktop - ? BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .background, - borderRadius: BorderRadius.vertical( - top: Radius.circular( - Constants.size.circularBorderRadius, + ), + title: Text( + "Transaction details", + style: STextStyles.navBarTitle(context), + ), + ), + body: Padding( + padding: isDesktop + ? const EdgeInsets.only(left: 32) + : const EdgeInsets.all(12), + child: Column( + children: [ + if (isDesktop) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transaction details", + style: STextStyles.desktopH3(context), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: isDesktop + ? const EdgeInsets.only( + right: 32, + bottom: 32, + ) + : const EdgeInsets.all(0), + child: ConditionalParent( + condition: isDesktop, + builder: (child) { + return RoundedWhiteContainer( + borderColor: isDesktop + ? Theme.of(context) + .extension<StackColors>()! + .background + : null, + padding: const EdgeInsets.all(0), + child: child, + ); + }, + child: SingleChildScrollView( + primary: isDesktop ? false : null, + child: Padding( + padding: isDesktop + ? const EdgeInsets.all(0) + : const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(0) + : const EdgeInsets.all(12), + child: Container( + decoration: isDesktop + ? BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .background, + borderRadius: BorderRadius.vertical( + top: Radius.circular( + Constants.size.circularBorderRadius, + ), ), - ), - ) - : null, - child: Padding( - padding: isDesktop - ? const EdgeInsets.all(12) - : const EdgeInsets.all(0), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - if (isDesktop) - Row( - children: [ - TxIcon( - transaction: _transaction, - ), - const SizedBox( - width: 16, - ), - SelectableText( - _transaction.isCancelled - ? "Cancelled" - : whatIsIt(_transaction.txType), - style: - STextStyles.desktopTextMedium( - context), - ), - ], - ), - Column( - crossAxisAlignment: isDesktop - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, - children: [ - SelectableText( - "$amountPrefix${Format.localizedStringAsFixed( - value: amount, - locale: ref.watch( - localeServiceChangeNotifierProvider - .select( - (value) => value.locale), + ) + : null, + child: Padding( + padding: isDesktop + ? const EdgeInsets.all(12) + : const EdgeInsets.all(0), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + if (isDesktop) + Row( + children: [ + TxIcon( + transaction: _transaction, ), - decimalPlaces: - Constants.decimalPlacesForCoin( - coin), - )} ${coin.ticker}", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ) - : STextStyles.titleBold12( - context), + const SizedBox( + width: 16, + ), + SelectableText( + _transaction.isCancelled + ? "Cancelled" + : whatIsIt( + _transaction.txType), + style: + STextStyles.desktopTextMedium( + context), + ), + ], ), - const SizedBox( - height: 2, - ), - if (ref.watch( - prefsChangeNotifierProvider.select( - (value) => - value.externalCalls))) + Column( + crossAxisAlignment: isDesktop + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ SelectableText( "$amountPrefix${Format.localizedStringAsFixed( - value: amount * - ref.watch( - priceAnd24hChangeNotifierProvider - .select((value) => value - .getPrice(coin) - .item1), - ), + value: amount, locale: ref.watch( localeServiceChangeNotifierProvider .select((value) => value.locale), ), - decimalPlaces: 2, - )} ${ref.watch( + decimalPlaces: Constants + .decimalPlacesForCoin(coin), + )} ${coin.ticker}", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textDark, + ) + : STextStyles.titleBold12( + context), + ), + const SizedBox( + height: 2, + ), + if (ref.watch( prefsChangeNotifierProvider - .select( - (value) => value.currency, - ), - )}", + .select((value) => + value.externalCalls))) + SelectableText( + "$amountPrefix${Format.localizedStringAsFixed( + value: amount * + ref.watch( + priceAnd24hChangeNotifierProvider + .select((value) => + value + .getPrice( + coin) + .item1), + ), + locale: ref.watch( + localeServiceChangeNotifierProvider + .select((value) => + value.locale), + ), + decimalPlaces: 2, + )} ${ref.watch( + prefsChangeNotifierProvider + .select( + (value) => value.currency, + ), + )}", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle( + context), + ), + ], + ), + if (!isDesktop) + TxIcon( + transaction: _transaction, + ), + ], + ), + ), + ), + ), + + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Status", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall(context) + : STextStyles.itemSubtitle(context), + ), + // Flexible( + // child: FittedBox( + // fit: BoxFit.scaleDown, + // child: + SelectableText( + _transaction.isCancelled + ? "Cancelled" + : whatIsIt(_transaction.txType), + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: _transaction.txType == "Sent" + ? Theme.of(context) + .extension<StackColors>()! + .accentColorOrange + : Theme.of(context) + .extension<StackColors>()! + .accentColorGreen, + ) + : STextStyles.itemSubtitle12(context), + ), + // ), + // ), + ], + ), + ), + if (!((coin == Coin.monero || + coin == Coin.wownero) && + _transaction.txType.toLowerCase() == + "sent") && + !((coin == Coin.firo || + coin == Coin.firoTestNet) && + _transaction.subType == "mint")) + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + if (!((coin == Coin.monero || + coin == Coin.wownero) && + _transaction.txType.toLowerCase() == + "sent") && + !((coin == Coin.firo || + coin == Coin.firoTestNet) && + _transaction.subType == "mint")) + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + _transaction.txType.toLowerCase() == + "sent" + ? "Sent to" + : "Receiving address", style: isDesktop ? STextStyles .desktopTextExtraExtraSmall( @@ -475,81 +589,192 @@ class _TransactionDetailsViewState : STextStyles.itemSubtitle( context), ), - ], + const SizedBox( + height: 8, + ), + _transaction.txType.toLowerCase() == + "received" + ? FutureBuilder( + future: fetchContactNameFor( + _transaction.address), + builder: (builderContext, + AsyncSnapshot<String> + snapshot) { + String + addressOrContactName = + _transaction.address; + if (snapshot.connectionState == + ConnectionState + .done && + snapshot.hasData) { + addressOrContactName = + snapshot.data!; + } + return SelectableText( + addressOrContactName, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textDark, + ) + : STextStyles + .itemSubtitle12( + context), + ); + }, + ) + : SelectableText( + _transaction.address, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textDark, + ) + : STextStyles + .itemSubtitle12( + context), + ), + ], + ), ), - if (!isDesktop) - TxIcon( - transaction: _transaction, + if (isDesktop) + IconCopyButton( + data: _transaction.address, ), ], ), ), - ), - ), - - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Status", - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall( - context) - : STextStyles.itemSubtitle(context), - ), - // Flexible( - // child: FittedBox( - // fit: BoxFit.scaleDown, - // child: - SelectableText( - _transaction.isCancelled - ? "Cancelled" - : whatIsIt(_transaction.txType), - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall( - context) - .copyWith( - color: _transaction.txType == "Sent" - ? Theme.of(context) - .extension<StackColors>()! - .accentColorOrange - : Theme.of(context) - .extension<StackColors>()! - .accentColorGreen, - ) - : STextStyles.itemSubtitle12(context), - ), - // ), - // ), - ], - ), - ), - if (!((coin == Coin.monero || coin == Coin.wownero) && - _transaction.txType.toLowerCase() == - "sent") && - !((coin == Coin.firo || - coin == Coin.firoTestNet) && - _transaction.subType == "mint")) isDesktop ? const _Divider() : const SizedBox( height: 12, ), - if (!((coin == Coin.monero || coin == Coin.wownero) && - _transaction.txType.toLowerCase() == - "sent") && - !((coin == Coin.firo || - coin == Coin.firoTestNet) && - _transaction.subType == "mint")) + + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Note", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle(context), + ), + isDesktop + ? IconPencilButton( + onPressed: () { + showDialog<void>( + context: context, + builder: (context) { + return DesktopDialog( + maxWidth: 580, + maxHeight: 360, + child: EditNoteView( + txid: _transaction.txid, + walletId: walletId, + note: _note, + ), + ); + }, + ); + }, + ) + : GestureDetector( + onTap: () { + Navigator.of(context).pushNamed( + EditNoteView.routeName, + arguments: Tuple3( + _transaction.txid, + walletId, + _note, + ), + ); + }, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.pencil, + width: 10, + height: 10, + color: Theme.of(context) + .extension< + StackColors>()! + .infoItemIcons, + ), + const SizedBox( + width: 4, + ), + Text( + "Edit", + style: STextStyles.link2( + context), + ), + ], + ), + ), + ], + ), + const SizedBox( + height: 8, + ), + FutureBuilder( + future: ref.watch( + notesServiceChangeNotifierProvider( + walletId) + .select((value) => value.getNoteFor( + txid: _transaction.txid))), + builder: (builderContext, + AsyncSnapshot<String> snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + _note = snapshot.data ?? ""; + } + return SelectableText( + _note, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context), + ); + }, + ), + ], + ), + ), + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), RoundedWhiteContainer( padding: isDesktop ? const EdgeInsets.all(16) @@ -558,6 +783,249 @@ class _TransactionDetailsViewState mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Date", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle(context), + ), + if (isDesktop) + const SizedBox( + height: 2, + ), + if (isDesktop) + SelectableText( + Format.extractDateFrom( + _transaction.timestamp, + ), + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context), + ), + ], + ), + if (!isDesktop) + SelectableText( + Format.extractDateFrom( + _transaction.timestamp, + ), + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12(context), + ), + if (isDesktop) + IconCopyButton( + data: Format.extractDateFrom( + _transaction.timestamp, + ), + ), + ], + ), + ), + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Builder(builder: (context) { + final feeString = showFeePending + ? _transaction.confirmedStatus + ? Format.localizedStringAsFixed( + value: fee, + locale: ref.watch( + localeServiceChangeNotifierProvider + .select((value) => + value.locale)), + decimalPlaces: + Constants.decimalPlacesForCoin( + coin)) + : "Pending" + : Format.localizedStringAsFixed( + value: fee, + locale: ref.watch( + localeServiceChangeNotifierProvider + .select( + (value) => value.locale)), + decimalPlaces: + Constants.decimalPlacesForCoin(coin)); + + return Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Transaction fee", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle( + context), + ), + if (isDesktop) + const SizedBox( + height: 2, + ), + if (isDesktop) + SelectableText( + feeString, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context), + ), + ], + ), + if (!isDesktop) + SelectableText( + feeString, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context), + ), + if (isDesktop) + IconCopyButton(data: feeString) + ], + ); + }), + ), + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Builder(builder: (context) { + final height = widget.coin != Coin.epicCash && + _transaction.confirmedStatus + ? "${_transaction.height == 0 ? "Unknown" : _transaction.height}" + : _transaction.confirmations > 0 + ? "${_transaction.height}" + : "Pending"; + + return Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Block height", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle( + context), + ), + if (isDesktop) + const SizedBox( + height: 2, + ), + if (isDesktop) + SelectableText( + height, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context), + ), + ], + ), + if (!isDesktop) + SelectableText( + height, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context), + ), + if (isDesktop) IconCopyButton(data: height), + ], + ); + }), + ), + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, children: [ Expanded( child: Column( @@ -565,10 +1033,7 @@ class _TransactionDetailsViewState CrossAxisAlignment.start, children: [ Text( - _transaction.txType.toLowerCase() == - "sent" - ? "Sent to" - : "Receiving address", + "Transaction ID", style: isDesktop ? STextStyles .desktopTextExtraExtraSmall( @@ -579,771 +1044,338 @@ class _TransactionDetailsViewState const SizedBox( height: 8, ), - _transaction.txType.toLowerCase() == - "received" - ? FutureBuilder( - future: fetchContactNameFor( - _transaction.address), - builder: (builderContext, - AsyncSnapshot<String> - snapshot) { - String addressOrContactName = - _transaction.address; - if (snapshot.connectionState == - ConnectionState - .done && - snapshot.hasData) { - addressOrContactName = - snapshot.data!; - } - return SelectableText( - addressOrContactName, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of( - context) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles - .itemSubtitle12( - context), - ); - }, - ) - : SelectableText( - _transaction.address, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles - .itemSubtitle12( - context), - ), + // Flexible( + // child: FittedBox( + // fit: BoxFit.scaleDown, + // child: + SelectableText( + _transaction.txid, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context), + ), + if (coin != Coin.epicCash) + const SizedBox( + height: 8, + ), + if (coin != Coin.epicCash) + BlueTextButton( + text: "Open in block explorer", + onTap: () async { + final uri = + getBlockExplorerTransactionUrlFor( + coin: coin, + txid: _transaction.txid, + ); + + if (ref + .read( + prefsChangeNotifierProvider) + .hideBlockExplorerWarning == + false) { + final shouldContinue = + await showExplorerWarning( + "${uri.scheme}://${uri.host}"); + + if (!shouldContinue) { + return; + } + } + + // ref + // .read( + // shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = false; + try { + await launchUrl( + uri, + mode: LaunchMode + .externalApplication, + ); + } catch (_) { + unawaited( + showDialog<void>( + context: context, + builder: (_) => + StackOkDialog( + title: + "Could not open in block explorer", + message: + "Failed to open \"${uri.toString()}\"", + ), + ), + ); + } finally { + // Future<void>.delayed( + // const Duration(seconds: 1), + // () => ref + // .read( + // shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = true, + // ); + } + }, + ), + // ), + // ), ], ), ), - if (isDesktop) - IconCopyButton( - data: _transaction.address, - ), - ], - ), - ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Note", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - : STextStyles.itemSubtitle(context), - ), - isDesktop - ? IconPencilButton( - onPressed: () { - showDialog<void>( - context: context, - builder: (context) { - return DesktopDialog( - maxWidth: 580, - maxHeight: 360, - child: EditNoteView( - txid: _transaction.txid, - walletId: walletId, - note: _note, - ), - ); - }, - ); - }, - ) - : GestureDetector( - onTap: () { - Navigator.of(context).pushNamed( - EditNoteView.routeName, - arguments: Tuple3( - _transaction.txid, - walletId, - _note, - ), - ); - }, - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.pencil, - width: 10, - height: 10, - color: Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, - ), - const SizedBox( - width: 4, - ), - Text( - "Edit", - style: STextStyles.link2( - context), - ), - ], - ), - ), - ], - ), - const SizedBox( - height: 8, - ), - FutureBuilder( - future: ref.watch( - notesServiceChangeNotifierProvider( - walletId) - .select((value) => value.getNoteFor( - txid: _transaction.txid))), - builder: (builderContext, - AsyncSnapshot<String> snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - _note = snapshot.data ?? ""; - } - return SelectableText( - _note, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ) - : STextStyles.itemSubtitle12(context), - ); - }, - ), - ], - ), - ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Date", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - : STextStyles.itemSubtitle(context), - ), - if (isDesktop) - const SizedBox( - height: 2, - ), - if (isDesktop) - SelectableText( - Format.extractDateFrom( - _transaction.timestamp, - ), - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ) - : STextStyles.itemSubtitle12( - context), - ), - ], - ), - if (!isDesktop) - SelectableText( - Format.extractDateFrom( - _transaction.timestamp, - ), - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ) - : STextStyles.itemSubtitle12(context), - ), - if (isDesktop) - IconCopyButton( - data: Format.extractDateFrom( - _transaction.timestamp, - ), - ), - ], - ), - ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Builder(builder: (context) { - final feeString = showFeePending - ? _transaction.confirmedStatus - ? Format.localizedStringAsFixed( - value: fee, - locale: ref.watch( - localeServiceChangeNotifierProvider - .select( - (value) => value.locale)), - decimalPlaces: - Constants.decimalPlacesForCoin( - coin)) - : "Pending" - : Format.localizedStringAsFixed( - value: fee, - locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale)), - decimalPlaces: - Constants.decimalPlacesForCoin(coin)); - - return Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Transaction fee", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - : STextStyles.itemSubtitle(context), - ), - if (isDesktop) - const SizedBox( - height: 2, - ), - if (isDesktop) - SelectableText( - feeString, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ) - : STextStyles.itemSubtitle12( - context), - ), - ], - ), - if (!isDesktop) - SelectableText( - feeString, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ) - : STextStyles.itemSubtitle12(context), - ), - if (isDesktop) IconCopyButton(data: feeString) - ], - ); - }), - ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Builder(builder: (context) { - final height = widget.coin != Coin.epicCash && - _transaction.confirmedStatus - ? "${_transaction.height == 0 ? "Unknown" : _transaction.height}" - : _transaction.confirmations > 0 - ? "${_transaction.height}" - : "Pending"; - - return Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Block height", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - : STextStyles.itemSubtitle(context), - ), - if (isDesktop) - const SizedBox( - height: 2, - ), - if (isDesktop) - SelectableText( - height, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ) - : STextStyles.itemSubtitle12( - context), - ), - ], - ), - if (!isDesktop) - SelectableText( - height, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ) - : STextStyles.itemSubtitle12(context), - ), - if (isDesktop) IconCopyButton(data: height), - ], - ); - }), - ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Transaction ID", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - : STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 8, - ), - // Flexible( - // child: FittedBox( - // fit: BoxFit.scaleDown, - // child: - SelectableText( - _transaction.txid, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ) - : STextStyles.itemSubtitle12( - context), - ), - if (coin != Coin.epicCash) - const SizedBox( - height: 8, - ), - if (coin != Coin.epicCash) - BlueTextButton( - text: "Open in block explorer", - onTap: () async { - final uri = - getBlockExplorerTransactionUrlFor( - coin: coin, - txid: _transaction.txid, - ); - - if (ref - .read( - prefsChangeNotifierProvider) - .hideBlockExplorerWarning == - false) { - final shouldContinue = - await showExplorerWarning( - "${uri.scheme}://${uri.host}"); - - if (!shouldContinue) { - return; - } - } - - // ref - // .read( - // shouldShowLockscreenOnResumeStateProvider - // .state) - // .state = false; - try { - await launchUrl( - uri, - mode: LaunchMode - .externalApplication, - ); - } catch (_) { - unawaited( - showDialog<void>( - context: context, - builder: (_) => StackOkDialog( - title: - "Could not open in block explorer", - message: - "Failed to open \"${uri.toString()}\"", - ), - ), - ); - } finally { - // Future<void>.delayed( - // const Duration(seconds: 1), - // () => ref - // .read( - // shouldShowLockscreenOnResumeStateProvider - // .state) - // .state = true, - // ); - } - }, - ), - // ), - // ), - ], - ), - ), - if (isDesktop) - const SizedBox( - width: 12, - ), - if (isDesktop) - IconCopyButton( - data: _transaction.txid, - ), - ], - ), - ), - // if ((coin == Coin.firoTestNet || coin == Coin.firo) && - // _transaction.subType == "mint") - // const SizedBox( - // height: 12, - // ), - // if ((coin == Coin.firoTestNet || coin == Coin.firo) && - // _transaction.subType == "mint") - // RoundedWhiteContainer( - // child: Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // Text( - // "Mint Transaction ID", - // style: STextStyles.itemSubtitle(context), - // ), - // ], - // ), - // const SizedBox( - // height: 8, - // ), - // // Flexible( - // // child: FittedBox( - // // fit: BoxFit.scaleDown, - // // child: - // SelectableText( - // _transaction.otherData ?? "Unknown", - // style: STextStyles.itemSubtitle12(context), - // ), - // // ), - // // ), - // const SizedBox( - // height: 8, - // ), - // BlueTextButton( - // text: "Open in block explorer", - // onTap: () async { - // final uri = getBlockExplorerTransactionUrlFor( - // coin: coin, - // txid: _transaction.otherData ?? "Unknown", - // ); - // // ref - // // .read( - // // shouldShowLockscreenOnResumeStateProvider - // // .state) - // // .state = false; - // try { - // await launchUrl( - // uri, - // mode: LaunchMode.externalApplication, - // ); - // } catch (_) { - // unawaited(showDialog<void>( - // context: context, - // builder: (_) => StackOkDialog( - // title: "Could not open in block explorer", - // message: - // "Failed to open \"${uri.toString()}\"", - // ), - // )); - // } finally { - // // Future<void>.delayed( - // // const Duration(seconds: 1), - // // () => ref - // // .read( - // // shouldShowLockscreenOnResumeStateProvider - // // .state) - // // .state = true, - // // ); - // } - // }, - // ), - // ], - // ), - // ), - if (coin == Coin.epicCash) - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - if (coin == Coin.epicCash) - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Slate ID", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - : STextStyles.itemSubtitle(context), - ), - // Flexible( - // child: FittedBox( - // fit: BoxFit.scaleDown, - // child: - SelectableText( - _transaction.slateId ?? "Unknown", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ) - : STextStyles.itemSubtitle12( - context), - ), - // ), - // ), - ], - ), if (isDesktop) const SizedBox( width: 12, ), if (isDesktop) IconCopyButton( - data: _transaction.slateId ?? "Unknown", + data: _transaction.txid, ), ], ), ), - if (!isDesktop) - const SizedBox( - height: 12, - ), - ], + // if ((coin == Coin.firoTestNet || coin == Coin.firo) && + // _transaction.subType == "mint") + // const SizedBox( + // height: 12, + // ), + // if ((coin == Coin.firoTestNet || coin == Coin.firo) && + // _transaction.subType == "mint") + // RoundedWhiteContainer( + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // Text( + // "Mint Transaction ID", + // style: STextStyles.itemSubtitle(context), + // ), + // ], + // ), + // const SizedBox( + // height: 8, + // ), + // // Flexible( + // // child: FittedBox( + // // fit: BoxFit.scaleDown, + // // child: + // SelectableText( + // _transaction.otherData ?? "Unknown", + // style: STextStyles.itemSubtitle12(context), + // ), + // // ), + // // ), + // const SizedBox( + // height: 8, + // ), + // BlueTextButton( + // text: "Open in block explorer", + // onTap: () async { + // final uri = getBlockExplorerTransactionUrlFor( + // coin: coin, + // txid: _transaction.otherData ?? "Unknown", + // ); + // // ref + // // .read( + // // shouldShowLockscreenOnResumeStateProvider + // // .state) + // // .state = false; + // try { + // await launchUrl( + // uri, + // mode: LaunchMode.externalApplication, + // ); + // } catch (_) { + // unawaited(showDialog<void>( + // context: context, + // builder: (_) => StackOkDialog( + // title: "Could not open in block explorer", + // message: + // "Failed to open \"${uri.toString()}\"", + // ), + // )); + // } finally { + // // Future<void>.delayed( + // // const Duration(seconds: 1), + // // () => ref + // // .read( + // // shouldShowLockscreenOnResumeStateProvider + // // .state) + // // .state = true, + // // ); + // } + // }, + // ), + // ], + // ), + // ), + if (coin == Coin.epicCash) + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + if (coin == Coin.epicCash) + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Slate ID", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle( + context), + ), + // Flexible( + // child: FittedBox( + // fit: BoxFit.scaleDown, + // child: + SelectableText( + _transaction.slateId ?? "Unknown", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context), + ), + // ), + // ), + ], + ), + if (isDesktop) + const SizedBox( + width: 12, + ), + if (isDesktop) + IconCopyButton( + data: _transaction.slateId ?? "Unknown", + ), + ], + ), + ), + if (!isDesktop) + const SizedBox( + height: 12, + ), + ], + ), ), ), ), ), ), - ), - ], + ], + ), ), - ), - floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, - floatingActionButton: (coin == Coin.epicCash && - _transaction.confirmedStatus == false && - _transaction.isCancelled == false && - _transaction.txType == "Sent") - ? SizedBox( - width: MediaQuery.of(context).size.width - 32, - child: TextButton( - style: ButtonStyle( - backgroundColor: MaterialStateProperty.all<Color>( - Theme.of(context).extension<StackColors>()!.textError, + floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, + floatingActionButton: (coin == Coin.epicCash && + _transaction.confirmedStatus == false && + _transaction.isCancelled == false && + _transaction.txType == "Sent") + ? SizedBox( + width: MediaQuery.of(context).size.width - 32, + child: TextButton( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all<Color>( + Theme.of(context).extension<StackColors>()!.textError, + ), ), - ), - onPressed: () async { - final Manager manager = ref - .read(walletsChangeNotifierProvider) - .getManager(walletId); + onPressed: () async { + final Manager manager = ref + .read(walletsChangeNotifierProvider) + .getManager(walletId); - if (manager.wallet is EpicCashWallet) { - final String? id = _transaction.slateId; - if (id == null) { + if (manager.wallet is EpicCashWallet) { + final String? id = _transaction.slateId; + if (id == null) { + unawaited(showFloatingFlushBar( + type: FlushBarType.warning, + message: "Could not find Epic transaction ID", + context: context, + )); + return; + } + + unawaited(showDialog<dynamic>( + barrierDismissible: false, + context: context, + builder: (_) => + const CancellingTransactionProgressDialog(), + )); + + final result = await (manager.wallet as EpicCashWallet) + .cancelPendingTransactionAndPost(id); + if (mounted) { + // pop progress dialog + Navigator.of(context).pop(); + + if (result.isEmpty) { + await showDialog<dynamic>( + context: context, + builder: (_) => StackOkDialog( + title: "Transaction cancelled", + onOkPressed: (_) { + manager.refresh(); + Navigator.of(context).popUntil( + ModalRoute.withName(WalletView.routeName)); + }, + ), + ); + } else { + await showDialog<dynamic>( + context: context, + builder: (_) => StackOkDialog( + title: "Failed to cancel transaction", + message: result, + ), + ); + } + } + } else { unawaited(showFloatingFlushBar( type: FlushBarType.warning, - message: "Could not find Epic transaction ID", + message: "ERROR: Wallet type is not Epic Cash", context: context, )); return; } - - unawaited(showDialog<dynamic>( - barrierDismissible: false, - context: context, - builder: (_) => - const CancellingTransactionProgressDialog(), - )); - - final result = await (manager.wallet as EpicCashWallet) - .cancelPendingTransactionAndPost(id); - if (mounted) { - // pop progress dialog - Navigator.of(context).pop(); - - if (result.isEmpty) { - await showDialog<dynamic>( - context: context, - builder: (_) => StackOkDialog( - title: "Transaction cancelled", - onOkPressed: (_) { - manager.refresh(); - Navigator.of(context).popUntil( - ModalRoute.withName(WalletView.routeName)); - }, - ), - ); - } else { - await showDialog<dynamic>( - context: context, - builder: (_) => StackOkDialog( - title: "Failed to cancel transaction", - message: result, - ), - ); - } - } - } else { - unawaited(showFloatingFlushBar( - type: FlushBarType.warning, - message: "ERROR: Wallet type is not Epic Cash", - context: context, - )); - return; - } - }, - child: Text( - "Cancel Transaction", - style: STextStyles.button(context), + }, + child: Text( + "Cancel Transaction", + style: STextStyles.button(context), + ), ), - ), - ) - : null, + ) + : null, + ), ); } } diff --git a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart index 0b3fd3acb..7e1b53cbb 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart @@ -15,6 +15,7 @@ import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; @@ -428,42 +429,46 @@ class _TransactionSearchViewState ), ); } else { - return Scaffold( - backgroundColor: Theme.of(context).extension<StackColors>()!.background, - appBar: AppBar( + return Background( + child: Scaffold( backgroundColor: Theme.of(context).extension<StackColors>()!.background, - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future<void>.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future<void>.delayed(const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Transactions filter", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Transactions filter", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: EdgeInsets.symmetric( - horizontal: Constants.size.standardPadding, - ), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints(minHeight: constraints.maxHeight), - child: IntrinsicHeight( - child: _buildContent(context), + body: Padding( + padding: EdgeInsets.symmetric( + horizontal: Constants.size.standardPadding, + ), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: + BoxConstraints(minHeight: constraints.maxHeight), + child: IntrinsicHeight( + child: _buildContent(context), + ), ), - ), - ); - }, + ); + }, + ), ), ), ); @@ -869,7 +874,7 @@ class _TransactionSearchViewState Expanded( child: SecondaryButton( label: "Cancel", - buttonHeight: ButtonHeight.l, + buttonHeight: isDesktop ? ButtonHeight.l : null, onPressed: () async { if (!isDesktop) { if (FocusScope.of(context).hasFocus) { @@ -919,7 +924,7 @@ class _TransactionSearchViewState ), Expanded( child: PrimaryButton( - buttonHeight: ButtonHeight.l, + buttonHeight: isDesktop ? ButtonHeight.l : null, onPressed: () async { await _onApplyPressed(); }, diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index c84ddf2b9..61a1da0ac 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -40,6 +40,7 @@ import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.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/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/custom_loading_overlay.dart'; @@ -378,401 +379,415 @@ class _WalletViewState extends ConsumerState<WalletView> { return WillPopScope( onWillPop: _onWillPop, - child: Scaffold( - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - _logout(); - Navigator.of(context).pop(); - }, - ), - titleSpacing: 0, - title: Row( - children: [ - SvgPicture.asset( - Assets.svg.iconFor(coin: coin), - // color: Theme.of(context).extension<StackColors>()!.accentColorDark - width: 24, - height: 24, - ), - const SizedBox( - width: 16, - ), - Expanded( - child: Text( - ref.watch( - managerProvider.select((value) => value.walletName)), - style: STextStyles.navBarTitle(context), - overflow: TextOverflow.ellipsis, + child: Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + _logout(); + Navigator.of(context).pop(); + }, + ), + titleSpacing: 0, + title: Row( + children: [ + SvgPicture.asset( + Assets.svg.iconFor(coin: coin), + // color: Theme.of(context).extension<StackColors>()!.accentColorDark + width: 24, + height: 24, ), - ) + const SizedBox( + width: 16, + ), + Expanded( + child: Text( + ref.watch( + managerProvider.select((value) => value.walletName)), + style: STextStyles.navBarTitle(context), + overflow: TextOverflow.ellipsis, + ), + ) + ], + ), + actions: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("walletViewRadioButton"), + size: 36, + shadows: const [], + color: + Theme.of(context).extension<StackColors>()!.background, + icon: _buildNetworkIcon(_currentSyncStatus), + onPressed: () { + Navigator.of(context).pushNamed( + WalletNetworkSettingsView.routeName, + arguments: Tuple3( + walletId, + _currentSyncStatus, + _currentNodeStatus, + ), + ); + }, + ), + ), + ), + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("walletViewAlertsButton"), + size: 36, + shadows: const [], + color: + Theme.of(context).extension<StackColors>()!.background, + icon: SvgPicture.asset( + ref.watch(notificationsProvider.select((value) => + value.hasUnreadNotificationsFor(walletId))) + ? Assets.svg.bellNew(context) + : Assets.svg.bell, + width: 20, + height: 20, + color: ref.watch(notificationsProvider.select((value) => + value.hasUnreadNotificationsFor(walletId))) + ? null + : Theme.of(context) + .extension<StackColors>()! + .topNavIconPrimary, + ), + onPressed: () { + // reset unread state + ref.refresh(unreadNotificationsStateProvider); + + Navigator.of(context) + .pushNamed( + NotificationsView.routeName, + arguments: walletId, + ) + .then((_) { + final Set<int> unreadNotificationIds = ref + .read(unreadNotificationsStateProvider.state) + .state; + if (unreadNotificationIds.isEmpty) return; + + List<Future<dynamic>> futures = []; + for (int i = 0; + i < unreadNotificationIds.length - 1; + i++) { + futures.add(ref + .read(notificationsProvider) + .markAsRead( + unreadNotificationIds.elementAt(i), false)); + } + + // wait for multiple to update if any + Future.wait(futures).then((_) { + // only notify listeners once + ref + .read(notificationsProvider) + .markAsRead(unreadNotificationIds.last, true); + }); + }); + }, + ), + ), + ), + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("walletViewSettingsButton"), + size: 36, + shadows: const [], + color: + Theme.of(context).extension<StackColors>()!.background, + icon: SvgPicture.asset( + Assets.svg.bars, + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + width: 20, + height: 20, + ), + onPressed: () { + debugPrint("wallet view settings tapped"); + Navigator.of(context).pushNamed( + WalletSettingsView.routeName, + arguments: Tuple4( + walletId, + ref.read(managerProvider).coin, + _currentSyncStatus, + _currentNodeStatus, + ), + ); + }, + ), + ), + ), ], ), - actions: [ - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("walletViewRadioButton"), - size: 36, - shadows: const [], - color: Theme.of(context).extension<StackColors>()!.background, - icon: _buildNetworkIcon(_currentSyncStatus), - onPressed: () { - Navigator.of(context).pushNamed( - WalletNetworkSettingsView.routeName, - arguments: Tuple3( - walletId, - _currentSyncStatus, - _currentNodeStatus, - ), - ); - }, - ), - ), - ), - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("walletViewAlertsButton"), - size: 36, - shadows: const [], - color: Theme.of(context).extension<StackColors>()!.background, - icon: SvgPicture.asset( - ref.watch(notificationsProvider.select((value) => - value.hasUnreadNotificationsFor(walletId))) - ? Assets.svg.bellNew(context) - : Assets.svg.bell, - width: 20, - height: 20, - color: ref.watch(notificationsProvider.select((value) => - value.hasUnreadNotificationsFor(walletId))) - ? null - : Theme.of(context) - .extension<StackColors>()! - .topNavIconPrimary, - ), - onPressed: () { - // reset unread state - ref.refresh(unreadNotificationsStateProvider); - - Navigator.of(context) - .pushNamed( - NotificationsView.routeName, - arguments: walletId, - ) - .then((_) { - final Set<int> unreadNotificationIds = ref - .read(unreadNotificationsStateProvider.state) - .state; - if (unreadNotificationIds.isEmpty) return; - - List<Future<dynamic>> futures = []; - for (int i = 0; - i < unreadNotificationIds.length - 1; - i++) { - futures.add(ref.read(notificationsProvider).markAsRead( - unreadNotificationIds.elementAt(i), false)); - } - - // wait for multiple to update if any - Future.wait(futures).then((_) { - // only notify listeners once - ref - .read(notificationsProvider) - .markAsRead(unreadNotificationIds.last, true); - }); - }); - }, - ), - ), - ), - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("walletViewSettingsButton"), - size: 36, - shadows: const [], - color: Theme.of(context).extension<StackColors>()!.background, - icon: SvgPicture.asset( - Assets.svg.bars, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - width: 20, - height: 20, - ), - onPressed: () { - debugPrint("wallet view settings tapped"); - Navigator.of(context).pushNamed( - WalletSettingsView.routeName, - arguments: Tuple4( - walletId, - ref.read(managerProvider).coin, - _currentSyncStatus, - _currentNodeStatus, - ), - ); - }, - ), - ), - ), - ], - ), - body: SafeArea( - child: Container( - color: Theme.of(context).extension<StackColors>()!.background, - child: Column( - children: [ - const SizedBox( - height: 10, - ), - Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: WalletSummary( - walletId: walletId, - managerProvider: managerProvider, - initialSyncStatus: ref.watch(managerProvider - .select((value) => value.isRefreshing)) - ? WalletSyncStatus.syncing - : WalletSyncStatus.synced, - ), - ), - ), - if (coin == Coin.firo) + body: SafeArea( + child: Container( + color: Theme.of(context).extension<StackColors>()!.background, + child: Column( + children: [ const SizedBox( height: 10, ), - if (coin == Coin.firo) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - children: [ - Expanded( - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonColor(context), - onPressed: () async { - await showDialog<void>( - context: context, - builder: (context) => StackDialog( - title: "Attention!", - message: - "You're about to anonymize all of your public funds.", - leftButton: TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text( - "Cancel", - style: - STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, + Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: WalletSummary( + walletId: walletId, + managerProvider: managerProvider, + initialSyncStatus: ref.watch(managerProvider + .select((value) => value.isRefreshing)) + ? WalletSyncStatus.syncing + : WalletSyncStatus.synced, + ), + ), + ), + if (coin == Coin.firo) + const SizedBox( + height: 10, + ), + if (coin == Coin.firo) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + Expanded( + child: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonColor(context), + onPressed: () async { + await showDialog<void>( + context: context, + builder: (context) => StackDialog( + title: "Attention!", + message: + "You're about to anonymize all of your public funds.", + leftButton: TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + "Cancel", + style: STextStyles.button(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark, + ), + ), + ), + rightButton: TextButton( + onPressed: () async { + Navigator.of(context).pop(); + + unawaited(attemptAnonymize()); + }, + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonColor( + context), + child: Text( + "Continue", + style: STextStyles.button(context), ), ), ), - rightButton: TextButton( - onPressed: () async { - Navigator.of(context).pop(); - - unawaited(attemptAnonymize()); - }, - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonColor(context), - child: Text( - "Continue", - style: STextStyles.button(context), - ), - ), + ); + }, + child: Text( + "Anonymize funds", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, ), - ); - }, - child: Text( - "Anonymize funds", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .buttonTextSecondary, ), ), ), + ], + ), + ), + const SizedBox( + height: 20, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transactions", + style: STextStyles.itemSubtitle(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark3, + ), + ), + BlueTextButton( + text: "See all", + onTap: () { + Navigator.of(context).pushNamed( + AllTransactionsView.routeName, + arguments: walletId, + ); + }, ), ], ), ), - const SizedBox( - height: 20, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Transactions", - style: STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark3, - ), - ), - BlueTextButton( - text: "See all", - onTap: () { - Navigator.of(context).pushNamed( - AllTransactionsView.routeName, - arguments: walletId, - ); - }, - ), - ], + const SizedBox( + height: 12, ), - ), - const SizedBox( - height: 12, - ), - Expanded( - child: Stack( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Padding( - padding: const EdgeInsets.only(bottom: 14), - child: ClipRRect( - borderRadius: BorderRadius.vertical( - top: Radius.circular( - Constants.size.circularBorderRadius, - ), - bottom: Radius.circular( - // WalletView.navBarHeight / 2.0, - Constants.size.circularBorderRadius, - ), - ), - child: Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular( + Expanded( + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Padding( + padding: const EdgeInsets.only(bottom: 14), + child: ClipRRect( + borderRadius: BorderRadius.vertical( + top: Radius.circular( + Constants.size.circularBorderRadius, + ), + bottom: Radius.circular( + // WalletView.navBarHeight / 2.0, Constants.size.circularBorderRadius, ), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: TransactionsList( - managerProvider: managerProvider, - walletId: walletId, - ), + child: Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - ], + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + Expanded( + child: TransactionsList( + managerProvider: managerProvider, + walletId: walletId, + ), + ), + ], + ), ), ), ), ), - ), - Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - const Spacer(), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only( - bottom: 14, - left: 16, - right: 16, - ), - child: SizedBox( - height: WalletView.navBarHeight, - child: WalletNavigationBar( - enableExchange: Constants.enableExchange && - ref.watch(managerProvider.select( - (value) => value.coin)) != - Coin.epicCash, + Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const Spacer(), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only( + bottom: 14, + left: 16, + right: 16, + ), + child: SizedBox( height: WalletView.navBarHeight, - onExchangePressed: () => - _onExchangePressed(context), - onReceivePressed: () async { - final coin = - ref.read(managerProvider).coin; - if (mounted) { - unawaited( - Navigator.of(context).pushNamed( - ReceiveView.routeName, + child: WalletNavigationBar( + enableExchange: + Constants.enableExchange && + ref.watch(managerProvider.select( + (value) => value.coin)) != + Coin.epicCash, + height: WalletView.navBarHeight, + onExchangePressed: () => + _onExchangePressed(context), + onReceivePressed: () async { + final coin = + ref.read(managerProvider).coin; + if (mounted) { + unawaited( + Navigator.of(context).pushNamed( + ReceiveView.routeName, + arguments: Tuple2( + walletId, + coin, + ), + )); + } + }, + onSendPressed: () { + final walletId = + ref.read(managerProvider).walletId; + final coin = + ref.read(managerProvider).coin; + switch (ref + .read( + walletBalanceToggleStateProvider + .state) + .state) { + case WalletBalanceToggleState.full: + ref + .read( + publicPrivateBalanceStateProvider + .state) + .state = "Public"; + break; + case WalletBalanceToggleState + .available: + ref + .read( + publicPrivateBalanceStateProvider + .state) + .state = "Private"; + break; + } + Navigator.of(context).pushNamed( + SendView.routeName, arguments: Tuple2( walletId, coin, ), - )); - } - }, - onSendPressed: () { - final walletId = - ref.read(managerProvider).walletId; - final coin = - ref.read(managerProvider).coin; - switch (ref - .read(walletBalanceToggleStateProvider - .state) - .state) { - case WalletBalanceToggleState.full: - ref - .read( - publicPrivateBalanceStateProvider - .state) - .state = "Public"; - break; - case WalletBalanceToggleState.available: - ref - .read( - publicPrivateBalanceStateProvider - .state) - .state = "Private"; - break; - } - Navigator.of(context).pushNamed( - SendView.routeName, - arguments: Tuple2( - walletId, - coin, - ), - ); - }, - onBuyPressed: () {}, + ); + }, + onBuyPressed: () {}, + ), ), ), - ), - ], - ), - ], - ) - ], + ], + ), + ], + ) + ], + ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/pages_desktop_specific/desktop_login_view.dart b/lib/pages_desktop_specific/desktop_login_view.dart index eb5dec18a..9bddda3da 100644 --- a/lib/pages_desktop_specific/desktop_login_view.dart +++ b/lib/pages_desktop_specific/desktop_login_view.dart @@ -10,7 +10,6 @@ import 'package:stackwallet/providers/desktop/storage_crypto_handler_provider.da import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; diff --git a/lib/pages_desktop_specific/home/desktop_home_view.dart b/lib/pages_desktop_specific/home/desktop_home_view.dart index 9791cd867..54c74fe88 100644 --- a/lib/pages_desktop_specific/home/desktop_home_view.dart +++ b/lib/pages_desktop_specific/home/desktop_home_view.dart @@ -13,6 +13,7 @@ import 'package:stackwallet/providers/global/notifications_provider.dart'; import 'package:stackwallet/providers/ui/unread_notifications_provider.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; class DesktopHomeView extends ConsumerStatefulWidget { const DesktopHomeView({Key? key}) : super(key: key); @@ -98,21 +99,23 @@ class _DesktopHomeViewState extends ConsumerState<DesktopHomeView> { Widget build(BuildContext context) { return Material( color: Theme.of(context).extension<StackColors>()!.background, - child: Row( - children: [ - DesktopMenu( - // onSelectionChanged: onMenuSelectionChanged, - onSelectionWillChange: onMenuSelectionWillChange, - ), - Container( - width: 1, - color: Theme.of(context).extension<StackColors>()!.background, - ), - Expanded( - child: contentViews[ - ref.watch(currentDesktopMenuItemProvider.state).state]!, - ), - ], + child: Background( + child: Row( + children: [ + DesktopMenu( + // onSelectionChanged: onMenuSelectionChanged, + onSelectionWillChange: onMenuSelectionWillChange, + ), + Container( + width: 1, + color: Theme.of(context).extension<StackColors>()!.background, + ), + Expanded( + child: contentViews[ + ref.watch(currentDesktopMenuItemProvider.state).state]!, + ), + ], + ), ), ); } diff --git a/lib/pages_desktop_specific/home/my_stack_view/my_stack_view.dart b/lib/pages_desktop_specific/home/my_stack_view/my_stack_view.dart index 6b60902c4..6710c23a4 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/my_stack_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/my_stack_view.dart @@ -6,6 +6,7 @@ import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/my_wallets import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; class MyStackView extends ConsumerStatefulWidget { @@ -23,36 +24,38 @@ class _MyStackViewState extends ConsumerState<MyStackView> { debugPrint("BUILD: $runtimeType"); final hasWallets = ref.watch(walletsChangeNotifierProvider).hasWallets; - return Column( - children: [ - DesktopAppBar( - isCompactHeight: true, - leading: Row( - children: [ - const SizedBox( - width: 24, - ), - SizedBox( - width: 32, - height: 32, - child: SvgPicture.asset( - Assets.svg.stackIcon(context), + return Background( + child: Column( + children: [ + DesktopAppBar( + isCompactHeight: true, + leading: Row( + children: [ + const SizedBox( + width: 24, ), - ), - const SizedBox( - width: 12, - ), - Text( - "My Stack", - style: STextStyles.desktopH3(context), - ) - ], + SizedBox( + width: 32, + height: 32, + child: SvgPicture.asset( + Assets.svg.stackIcon(context), + ), + ), + const SizedBox( + width: 12, + ), + Text( + "My Stack", + style: STextStyles.desktopH3(context), + ) + ], + ), ), - ), - Expanded( - child: hasWallets ? const MyWallets() : const EmptyWallets(), - ), - ], + Expanded( + child: hasWallets ? const MyWallets() : const EmptyWallets(), + ), + ], + ), ); } } diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 149d46b3c..27c8fe3b4 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/theme/color_theme.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; abstract class Assets { @@ -28,6 +29,16 @@ class _EXCHANGE { class _SVG { const _SVG(); + String? background(BuildContext context) { + switch (Theme.of(context).extension<StackColors>()!.themeType) { + case ThemeType.light: + case ThemeType.dark: + return null; + + case ThemeType.oceanBreeze: + return "assets/svg/${Theme.of(context).extension<StackColors>()!.themeType.name}/bg.svg"; + } + } String bellNew(BuildContext context) => "assets/svg/${Theme.of(context).extension<StackColors>()!.themeType.name}/bell-new.svg"; diff --git a/lib/utilities/theme/color_theme.dart b/lib/utilities/theme/color_theme.dart index 852e2f586..4a480491c 100644 --- a/lib/utilities/theme/color_theme.dart +++ b/lib/utilities/theme/color_theme.dart @@ -1,5 +1,4 @@ -import 'dart:ui'; - +import 'package:flutter/material.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; enum ThemeType { @@ -12,6 +11,10 @@ abstract class StackColorTheme { ThemeType get themeType; Color get background; + Color get backgroundAppBar; + + Gradient? get gradientBackground; + Color get overlay; Color get accentColorBlue; diff --git a/lib/utilities/theme/dark_colors.dart b/lib/utilities/theme/dark_colors.dart index e7c4e51db..d55581921 100644 --- a/lib/utilities/theme/dark_colors.dart +++ b/lib/utilities/theme/dark_colors.dart @@ -7,6 +7,11 @@ class DarkColors extends StackColorTheme { @override Color get background => const Color(0xFF2A2D34); + @override + Color get backgroundAppBar => background; + @override + Gradient? get gradientBackground => null; + @override Color get overlay => const Color(0xFF111215); diff --git a/lib/utilities/theme/light_colors.dart b/lib/utilities/theme/light_colors.dart index 896ae4e5e..1303d0b75 100644 --- a/lib/utilities/theme/light_colors.dart +++ b/lib/utilities/theme/light_colors.dart @@ -7,6 +7,11 @@ class LightColors extends StackColorTheme { @override Color get background => const Color(0xFFF7F7F7); + @override + Color get backgroundAppBar => background; + @override + Gradient? get gradientBackground => null; + @override Color get overlay => const Color(0xFF111215); diff --git a/lib/utilities/theme/ocean_breeze_colors.dart b/lib/utilities/theme/ocean_breeze_colors.dart index 665eaa0c3..8c4259bb9 100644 --- a/lib/utilities/theme/ocean_breeze_colors.dart +++ b/lib/utilities/theme/ocean_breeze_colors.dart @@ -6,7 +6,19 @@ class OceanBreezeColors extends StackColorTheme { ThemeType get themeType => ThemeType.oceanBreeze; @override - Color get background => const Color(0xFFF3F7FA); + Color get background => Colors.transparent; + @override + Color get backgroundAppBar => const Color(0xFFF3F7FA); + @override + Gradient? get gradientBackground => const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFFF3F7FA), + Color(0xFFE8F2F9), + ], + ); + @override Color get overlay => const Color(0xFF111215); diff --git a/lib/utilities/theme/stack_colors.dart b/lib/utilities/theme/stack_colors.dart index 8249dccf4..935fa03ae 100644 --- a/lib/utilities/theme/stack_colors.dart +++ b/lib/utilities/theme/stack_colors.dart @@ -6,6 +6,9 @@ class StackColors extends ThemeExtension<StackColors> { final ThemeType themeType; final Color background; + final Color backgroundAppBar; + final Gradient? gradientBackground; + final Color overlay; final Color accentColorBlue; @@ -173,6 +176,8 @@ class StackColors extends ThemeExtension<StackColors> { StackColors({ required this.themeType, required this.background, + required this.backgroundAppBar, + required this.gradientBackground, required this.overlay, required this.accentColorBlue, required this.accentColorGreen, @@ -307,6 +312,8 @@ class StackColors extends ThemeExtension<StackColors> { return StackColors( themeType: colorTheme.themeType, background: colorTheme.background, + backgroundAppBar: colorTheme.backgroundAppBar, + gradientBackground: colorTheme.gradientBackground, overlay: colorTheme.overlay, accentColorBlue: colorTheme.accentColorBlue, accentColorGreen: colorTheme.accentColorGreen, @@ -444,6 +451,8 @@ class StackColors extends ThemeExtension<StackColors> { ThemeExtension<StackColors> copyWith({ ThemeType? themeType, Color? background, + Color? backgroundAppBar, + Gradient? gradientBackground, Color? overlay, Color? accentColorBlue, Color? accentColorGreen, @@ -576,6 +585,8 @@ class StackColors extends ThemeExtension<StackColors> { return StackColors( themeType: themeType ?? this.themeType, background: background ?? this.background, + backgroundAppBar: backgroundAppBar ?? this.backgroundAppBar, + gradientBackground: gradientBackground ?? this.gradientBackground, overlay: overlay ?? this.overlay, accentColorBlue: accentColorBlue ?? this.accentColorBlue, accentColorGreen: accentColorGreen ?? this.accentColorGreen, @@ -755,11 +766,17 @@ class StackColors extends ThemeExtension<StackColors> { return StackColors( themeType: other.themeType, + gradientBackground: other.gradientBackground, background: Color.lerp( background, other.background, t, )!, + backgroundAppBar: Color.lerp( + backgroundAppBar, + other.backgroundAppBar, + t, + )!, overlay: Color.lerp( overlay, other.overlay, diff --git a/lib/widgets/background.dart b/lib/widgets/background.dart new file mode 100644 index 000000000..4f70f5252 --- /dev/null +++ b/lib/widgets/background.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/providers/ui/color_theme_provider.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/theme/color_theme.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; + +class Background extends ConsumerWidget { + const Background({ + Key? key, + required this.child, + }) : super(key: key); + + final Widget child; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final colorTheme = ref.watch(colorThemeProvider.state).state; + + Color? color; + + switch (colorTheme.themeType) { + case ThemeType.light: + case ThemeType.dark: + color = Theme.of(context).extension<StackColors>()!.background; + break; + case ThemeType.oceanBreeze: + color = null; + break; + } + + final bgAsset = Assets.svg.background(context); + + return Container( + decoration: BoxDecoration( + color: color, + gradient: + Theme.of(context).extension<StackColors>()!.gradientBackground, + ), + child: ConditionalParent( + condition: bgAsset != null, + builder: (child) => Stack( + children: [ + Positioned.fill( + child: Padding( + padding: EdgeInsets.only( + top: MediaQuery.of(context).size.height * (1 / 8), + bottom: MediaQuery.of(context).size.height * (1 / 12), + ), + child: SvgPicture.asset( + bgAsset!, + fit: BoxFit.fill, + ), + ), + ), + Positioned.fill( + child: child, + ), + ], + ), + child: child, + ), + ); + } +} diff --git a/lib/widgets/desktop/desktop_scaffold.dart b/lib/widgets/desktop/desktop_scaffold.dart index 439289518..51fa9f3a5 100644 --- a/lib/widgets/desktop/desktop_scaffold.dart +++ b/lib/widgets/desktop/desktop_scaffold.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; class DesktopScaffold extends StatelessWidget { const DesktopScaffold({ @@ -18,15 +19,17 @@ class DesktopScaffold extends StatelessWidget { return Material( color: background ?? Theme.of(context).extension<StackColors>()!.background, - child: Column( - // crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (appBar != null) appBar!, - if (body != null) - Expanded( - child: body!, - ), - ], + child: Background( + child: Column( + // crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (appBar != null) appBar!, + if (body != null) + Expanded( + child: body!, + ), + ], + ), ), ); } @@ -50,17 +53,18 @@ class MasterScaffold extends StatelessWidget { Widget build(BuildContext context) { if (isDesktop) { return DesktopScaffold( - background: background ?? - Theme.of(context).extension<StackColors>()!.background, + background: background, appBar: appBar, body: body, ); } else { - return Scaffold( - backgroundColor: background ?? - Theme.of(context).extension<StackColors>()!.background, - appBar: appBar as PreferredSizeWidget?, - body: body, + return Background( + child: Scaffold( + backgroundColor: background ?? + Theme.of(context).extension<StackColors>()!.background, + appBar: appBar as PreferredSizeWidget?, + body: body, + ), ); } } diff --git a/pubspec.yaml b/pubspec.yaml index 7ca368d29..2397fbfbc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -380,6 +380,7 @@ flutter: - assets/svg/oceanBreeze/bell-new.svg - assets/svg/oceanBreeze/stack-icon1.svg - assets/svg/oceanBreeze/buy-coins-icon.svg + - assets/svg/oceanBreeze/bg.svg # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. From 05bdc8c52fe53d1ff3d07dc1f7899480fa20a4d2 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 25 Nov 2022 13:50:13 -0600 Subject: [PATCH 405/426] fix node loading on initial start for desktop, only add default node back if there are no nodes exist for a certain coin --- .../create_password/create_password_view.dart | 6 +++++- lib/services/node_service.dart | 15 ++++++++++----- lib/utilities/default_nodes.dart | 3 ++- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/lib/pages_desktop_specific/create_password/create_password_view.dart b/lib/pages_desktop_specific/create_password/create_password_view.dart index 8e752f508..a8c9e7758 100644 --- a/lib/pages_desktop_specific/create_password/create_password_view.dart +++ b/lib/pages_desktop_specific/create_password/create_password_view.dart @@ -7,9 +7,9 @@ import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; import 'package:stackwallet/providers/desktop/storage_crypto_handler_provider.dart'; import 'package:stackwallet/providers/global/secure_store_provider.dart'; +import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -81,6 +81,10 @@ class _CreatePasswordViewState extends ConsumerState<CreatePasswordView> { await ref.read(storageCryptoHandlerProvider).initFromNew(passphrase); await (ref.read(secureStoreProvider).store as DesktopSecureStore).init(); + + // load default nodes now as node service requires storage handler to exist + + await ref.read(nodeServiceChangeNotifierProvider).updateDefaults(); } catch (e) { unawaited(showFloatingFlushBar( type: FlushBarType.warning, diff --git a/lib/services/node_service.dart b/lib/services/node_service.dart index aa8d5a6d9..ca1aae082 100644 --- a/lib/services/node_service.dart +++ b/lib/services/node_service.dart @@ -24,11 +24,14 @@ class NodeService extends ChangeNotifier { final savedNode = DB.instance .get<NodeModel>(boxName: DB.boxNameNodeModels, key: defaultNode.id); if (savedNode == null) { - // save the default node to hive - await DB.instance.put<NodeModel>( + // save the default node to hive only if no other nodes for the specific coin exist + if (getNodesFor(coinFromPrettyName(defaultNode.coinName)).isEmpty) { + await DB.instance.put<NodeModel>( boxName: DB.boxNameNodeModels, key: defaultNode.id, - value: defaultNode); + value: defaultNode, + ); + } } else { // update all fields but copy over previously set enabled state await DB.instance.put<NodeModel>( @@ -81,14 +84,16 @@ class NodeService extends ChangeNotifier { final list = DB.instance .values<NodeModel>(boxName: DB.boxNameNodeModels) .where((e) => - e.coinName == coin.name && e.name != DefaultNodes.defaultName) + e.coinName == coin.name && + !e.id.startsWith(DefaultNodes.defaultNodeIdPrefix)) .toList(); // add default to end of list list.addAll(DB.instance .values<NodeModel>(boxName: DB.boxNameNodeModels) .where((e) => - e.coinName == coin.name && e.name == DefaultNodes.defaultName) + e.coinName == coin.name && + e.id.startsWith(DefaultNodes.defaultNodeIdPrefix)) .toList()); // return reversed list so default node appears at beginning diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index abe702b78..c9e96fbac 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -4,7 +4,8 @@ import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; abstract class DefaultNodes { - static String _nodeId(Coin coin) => "default_${coin.name}"; + static const String defaultNodeIdPrefix = "default_"; + static String _nodeId(Coin coin) => "$defaultNodeIdPrefix${coin.name}"; static const String defaultName = "Stack Default"; static List<NodeModel> get all => [ From 276d08d22f6c174ea2db9204c07f52b24f592826 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 25 Nov 2022 14:28:53 -0600 Subject: [PATCH 406/426] allow default node deletion if other nodes exist --- lib/models/node_model.dart | 3 +- .../add_edit_node_view.dart | 7 +- .../manage_nodes_views/node_details_view.dart | 124 +++++++++++------- lib/widgets/node_card.dart | 17 +-- lib/widgets/node_options_sheet.dart | 6 +- 5 files changed, 94 insertions(+), 63 deletions(-) diff --git a/lib/models/node_model.dart b/lib/models/node_model.dart index 2628c5dd9..af5f8cbc1 100644 --- a/lib/models/node_model.dart +++ b/lib/models/node_model.dart @@ -1,4 +1,5 @@ import 'package:hive/hive.dart'; +import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; part 'type_adaptors/node_model.g.dart'; @@ -84,7 +85,7 @@ class NodeModel { return map; } - bool get isDefault => id.startsWith("default_"); + bool get isDefault => id.startsWith(DefaultNodes.defaultNodeIdPrefix); @override String toString() { diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index bd6e5c6d8..32fa3974a 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -432,7 +432,12 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> { style: STextStyles.navBarTitle(context), ), actions: [ - if (viewType == AddEditNodeViewType.edit) + if (viewType == AddEditNodeViewType.edit && + ref + .watch(nodeServiceChangeNotifierProvider + .select((value) => value.getNodesFor(coin))) + .length > + 1) Padding( padding: const EdgeInsets.only( top: 10, diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart index 2e43b5595..24af0e78e 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart @@ -10,7 +10,6 @@ import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/test_epic_box_connection.dart'; @@ -178,6 +177,11 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> { final node = ref.watch(nodeServiceChangeNotifierProvider .select((value) => value.getNodeById(id: nodeId))); + final nodesForCoin = ref.watch(nodeServiceChangeNotifierProvider + .select((value) => value.getNodesFor(coin))); + + final canDelete = nodesForCoin.length > 1; + return ConditionalParent( condition: !isDesktop, builder: (child) => Background( @@ -201,44 +205,43 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> { style: STextStyles.navBarTitle(context), ), actions: [ - if (!nodeId.startsWith("default")) - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("nodeDetailsEditNodeAppBarButtonKey"), - size: 36, - shadows: const [], + // if (!nodeId.startsWith(DefaultNodes.defaultNodeIdPrefix)) + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("nodeDetailsEditNodeAppBarButtonKey"), + size: 36, + shadows: const [], + color: + Theme.of(context).extension<StackColors>()!.background, + icon: SvgPicture.asset( + Assets.svg.pencil, color: Theme.of(context) .extension<StackColors>()! - .background, - icon: SvgPicture.asset( - Assets.svg.pencil, - color: Theme.of(context) - .extension<StackColors>()! - .accentColorDark, - width: 20, - height: 20, - ), - onPressed: () { - Navigator.of(context).pushNamed( - AddEditNodeView.routeName, - arguments: Tuple4( - AddEditNodeViewType.edit, - coin, - nodeId, - popRouteName, - ), - ); - }, + .accentColorDark, + width: 20, + height: 20, ), + onPressed: () { + Navigator.of(context).pushNamed( + AddEditNodeView.routeName, + arguments: Tuple4( + AddEditNodeViewType.edit, + coin, + nodeId, + popRouteName, + ), + ); + }, ), ), + ), ], ), body: Padding( @@ -315,7 +318,7 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> { const SizedBox( height: 22, ), - if (isDesktop) + if (isDesktop && canDelete) SizedBox( height: 56, child: _desktopReadOnly @@ -345,7 +348,7 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> { ], ), ), - if (isDesktop && !_desktopReadOnly) + if (isDesktop && !_desktopReadOnly && canDelete) const SizedBox( height: 45, ), @@ -366,22 +369,41 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> { ), if (isDesktop) Expanded( - child: !nodeId.startsWith("default") - ? PrimaryButton( - label: _desktopReadOnly ? "Edit" : "Save", - buttonHeight: ButtonHeight.l, - onPressed: () async { - final shouldSave = _desktopReadOnly == false; - setState(() { - _desktopReadOnly = !_desktopReadOnly; - }); + child: + // !nodeId.startsWith(DefaultNodes.defaultNodeIdPrefix) + // ? + PrimaryButton( + label: _desktopReadOnly ? "Edit" : "Save", + buttonHeight: ButtonHeight.l, + onPressed: () async { + final shouldSave = _desktopReadOnly == false; + setState(() { + _desktopReadOnly = !_desktopReadOnly; + }); - if (shouldSave) { - // todo save node - } - }, - ) - : Container(), + if (shouldSave) { + final editedNode = node!.copyWith( + host: ref.read(nodeFormDataProvider).host, + port: ref.read(nodeFormDataProvider).port, + name: ref.read(nodeFormDataProvider).name, + useSSL: ref.read(nodeFormDataProvider).useSSL, + loginName: ref.read(nodeFormDataProvider).login, + isFailover: + ref.read(nodeFormDataProvider).isFailover, + ); + + await ref + .read(nodeServiceChangeNotifierProvider) + .edit( + editedNode, + ref.read(nodeFormDataProvider).password, + true, + ); + } + }, + ) + // : Container() + , ), ], ), diff --git a/lib/widgets/node_card.dart b/lib/widgets/node_card.dart index c3fb36c70..fb8260b24 100644 --- a/lib/widgets/node_card.dart +++ b/lib/widgets/node_card.dart @@ -306,7 +306,7 @@ class _NodeCardState extends ConsumerState<NodeCard> { width: isDesktop ? 40 : 24, height: isDesktop ? 40 : 24, decoration: BoxDecoration( - color: _node.name == DefaultNodes.defaultName + color: _node.id.startsWith(DefaultNodes.defaultNodeIdPrefix) ? Theme.of(context) .extension<StackColors>()! .buttonBackSecondary @@ -321,13 +321,14 @@ class _NodeCardState extends ConsumerState<NodeCard> { Assets.svg.node, height: isDesktop ? 18 : 11, width: isDesktop ? 20 : 14, - color: _node.name == DefaultNodes.defaultName - ? Theme.of(context) - .extension<StackColors>()! - .accentColorDark - : Theme.of(context) - .extension<StackColors>()! - .infoItemIcons, + color: + _node.id.startsWith(DefaultNodes.defaultNodeIdPrefix) + ? Theme.of(context) + .extension<StackColors>()! + .accentColorDark + : Theme.of(context) + .extension<StackColors>()! + .infoItemIcons, ), ), ), diff --git a/lib/widgets/node_options_sheet.dart b/lib/widgets/node_options_sheet.dart index 7ffd290f3..1ef9b07fe 100644 --- a/lib/widgets/node_options_sheet.dart +++ b/lib/widgets/node_options_sheet.dart @@ -234,7 +234,8 @@ class NodeOptionsSheet extends ConsumerWidget { width: 32, height: 32, decoration: BoxDecoration( - color: node.name == DefaultNodes.defaultName + color: node.id + .startsWith(DefaultNodes.defaultNodeIdPrefix) ? Theme.of(context) .extension<StackColors>()! .textSubtitle4 @@ -249,7 +250,8 @@ class NodeOptionsSheet extends ConsumerWidget { Assets.svg.node, height: 15, width: 19, - color: node.name == DefaultNodes.defaultName + color: node.id.startsWith( + DefaultNodes.defaultNodeIdPrefix) ? Theme.of(context) .extension<StackColors>()! .accentColorDark From 4b0d44a239d5e94248b30eb07fca21a45d4e86b3 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 25 Nov 2022 16:49:43 -0600 Subject: [PATCH 407/426] emoji search --- lib/widgets/emoji_select_sheet.dart | 178 ++++++++++++++++++++++------ 1 file changed, 143 insertions(+), 35 deletions(-) diff --git a/lib/widgets/emoji_select_sheet.dart b/lib/widgets/emoji_select_sheet.dart index 7bf02e967..d5a37d142 100644 --- a/lib/widgets/emoji_select_sheet.dart +++ b/lib/widgets/emoji_select_sheet.dart @@ -1,14 +1,19 @@ import 'package:emojis/emoji.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; -class EmojiSelectSheet extends ConsumerWidget { +class EmojiSelectSheet extends ConsumerStatefulWidget { const EmojiSelectSheet({ Key? key, }) : super(key: key); @@ -18,17 +23,59 @@ class EmojiSelectSheet extends ConsumerWidget { final double minimumEmojiSpacing = 25; @override - Widget build(BuildContext context, WidgetRef ref) { - final isDesktop = Util.isDesktop; + ConsumerState<EmojiSelectSheet> createState() => _EmojiSelectSheetState(); +} +class _EmojiSelectSheetState extends ConsumerState<EmojiSelectSheet> { + final isDesktop = Util.isDesktop; + + late final TextEditingController _searchController; + late final FocusNode _searchFocusNode; + late final double horizontalPadding = 24; + late final double emojiSize = 24; + late final double minimumEmojiSpacing = 25; + + String _searchTerm = ""; + + List<Emoji> filtered(String text) { + if (text.isEmpty) { + return Emoji.all(); + } + + text = text.toLowerCase(); + + return Emoji.all() + .where((e) => e.keywords + .where( + (e) => e.contains(text), + ) + .isNotEmpty) + .toList(growable: false); + } + + @override + void initState() { + _searchController = TextEditingController(); + _searchFocusNode = FocusNode(); + + super.initState(); + } + + @override + void dispose() { + _searchController.dispose(); + _searchFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { final size = isDesktop ? const Size(600, 700) : MediaQuery.of(context).size; - final double maxHeight = size.height * 0.60; - final double availableWidth = size.width - (2 * horizontalPadding); - final int emojisPerRow = + final maxHeight = size.height * (isDesktop ? 0.6 : 0.9); + final availableWidth = size.width - (2 * horizontalPadding); + final emojisPerRow = ((availableWidth - emojiSize) ~/ (emojiSize + minimumEmojiSpacing)) + 1; - final itemCount = Emoji.all().length; - return ConditionalParent( condition: !isDesktop, builder: (child) => Container( @@ -81,40 +128,101 @@ class EmojiSelectSheet extends ConsumerWidget { : STextStyles.pageTitleH2(context), textAlign: TextAlign.left, ), + SizedBox( + height: isDesktop ? 16 : 12, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: (newString) { + setState(() => _searchTerm = newString); + }, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), + ), + suffixIcon: _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + _searchTerm = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), SizedBox( height: isDesktop ? 28 : 16, ), - Flexible( + Expanded( child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Flexible( - child: GridView.builder( - itemCount: itemCount, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: emojisPerRow, - ), - itemBuilder: (context, index) { - final emoji = Emoji.all()[index]; - return GestureDetector( - onTap: () { - Navigator.of(context).pop(emoji); - }, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(100), - color: Colors.transparent, - ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - emoji.char, - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : null, - ), - ), + Expanded( + child: Builder( + builder: (context) { + final emojis = filtered(_searchTerm); + final itemCount = emojis.length; + return GridView.builder( + itemCount: itemCount, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: emojisPerRow, ), + itemBuilder: (context, index) { + final emoji = emojis[index]; + return GestureDetector( + onTap: () { + Navigator.of(context).pop(emoji); + }, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100), + color: Colors.transparent, + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + emoji.char, + style: isDesktop + ? STextStyles.desktopTextSmall(context) + : null, + ), + ), + ), + ); + }, ); }, ), From 9fce8ca107b1ce1dae9d96184b4f5affd2576840 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 25 Nov 2022 17:14:06 -0600 Subject: [PATCH 408/426] familiarity fix --- lib/hive/db.dart | 12 ------ lib/main.dart | 5 +++ .../global_settings_view/hidden_settings.dart | 37 +++++++++++++++++++ lib/utilities/prefs.dart | 22 +++++++++++ 4 files changed, 64 insertions(+), 12 deletions(-) diff --git a/lib/hive/db.dart b/lib/hive/db.dart index 48e6ba154..1a52d64df 100644 --- a/lib/hive/db.dart +++ b/lib/hive/db.dart @@ -9,7 +9,6 @@ import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/models/notification_model.dart'; import 'package:stackwallet/models/trade_wallet_lookup.dart'; import 'package:stackwallet/services/wallets_service.dart'; -import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; @@ -151,17 +150,6 @@ class DB { _loadSharedCoinCacheBoxes(), ]); _initialized = true; - - try { - if (_boxPrefs!.get("familiarity") == null) { - await _boxPrefs!.put("familiarity", 0); - } - int count = _boxPrefs!.get("familiarity") as int; - await _boxPrefs!.put("familiarity", count + 1); - Constants.exchangeForExperiencedUsers(count + 1); - } catch (e, s) { - print("$e $s"); - } } } diff --git a/lib/main.dart b/lib/main.dart index d5980409f..2086d351e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -235,6 +235,11 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme> await DB.instance.init(); await ref.read(prefsChangeNotifierProvider).init(); + final familiarity = ref.read(prefsChangeNotifierProvider).familiarity + 1; + ref.read(prefsChangeNotifierProvider).familiarity = familiarity; + + Constants.exchangeForExperiencedUsers(familiarity); + if (Util.isDesktop) { _desktopHasPassword = await ref.read(storageCryptoHandlerProvider).hasPassword(); diff --git a/lib/pages/settings_views/global_settings_view/hidden_settings.dart b/lib/pages/settings_views/global_settings_view/hidden_settings.dart index 7f35fcc86..d92b166d7 100644 --- a/lib/pages/settings_views/global_settings_view/hidden_settings.dart +++ b/lib/pages/settings_views/global_settings_view/hidden_settings.dart @@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/providers/global/debug_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/background.dart'; @@ -127,6 +128,42 @@ class HiddenSettings extends StatelessWidget { ), ); }), + const SizedBox( + height: 12, + ), + Consumer( + builder: (_, ref, __) { + if (ref.watch(prefsChangeNotifierProvider + .select((value) => value.familiarity)) < + 6) { + return GestureDetector( + onTap: () async { + final familiarity = ref + .read(prefsChangeNotifierProvider) + .familiarity; + if (familiarity < 6) { + ref + .read(prefsChangeNotifierProvider) + .familiarity = 6; + + Constants.exchangeForExperiencedUsers(6); + } + }, + child: RoundedWhiteContainer( + child: Text( + "Enable exchange", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .accentColorDark), + ), + ), + ); + } else { + return Container(); + } + }, + ), // const SizedBox( // height: 12, // ), diff --git a/lib/utilities/prefs.dart b/lib/utilities/prefs.dart index 246291053..6b4b9821a 100644 --- a/lib/utilities/prefs.dart +++ b/lib/utilities/prefs.dart @@ -37,6 +37,7 @@ class Prefs extends ChangeNotifier { _gotoWalletOnStartup = await _getGotoWalletOnStartup(); _startupWalletId = await _getStartupWalletId(); _externalCalls = await _getHasExternalCalls(); + _familiarity = await _getHasFamiliarity(); _initialized = true; } @@ -328,6 +329,27 @@ class Prefs extends ChangeNotifier { false; } + // familiarity + + int _familiarity = 0; + + int get familiarity => _familiarity; + + set familiarity(int familiarity) { + if (_familiarity != familiarity) { + DB.instance.put<dynamic>( + boxName: DB.boxNamePrefs, key: "familiarity", value: familiarity); + _familiarity = familiarity; + notifyListeners(); + } + } + + Future<int> _getHasFamiliarity() async { + return await DB.instance.get<dynamic>( + boxName: DB.boxNamePrefs, key: "familiarity") as int? ?? + 0; + } + // show testnet coins bool _showTestNetCoins = false; From 56f54ac4872e8f063e20ab01cac53f6844f6e54d Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Fri, 25 Nov 2022 17:49:47 -0600 Subject: [PATCH 409/426] clean up and test fixes --- lib/widgets/background.dart | 10 +- lib/widgets/emoji_select_sheet.dart | 97 ++++++++++--------- .../widget_tests/emoji_select_sheet_test.dart | 14 ++- 3 files changed, 63 insertions(+), 58 deletions(-) diff --git a/lib/widgets/background.dart b/lib/widgets/background.dart index 4f70f5252..67ff44f55 100644 --- a/lib/widgets/background.dart +++ b/lib/widgets/background.dart @@ -1,13 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/providers/ui/color_theme_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/theme/color_theme.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; -class Background extends ConsumerWidget { +class Background extends StatelessWidget { const Background({ Key? key, required this.child, @@ -16,12 +14,10 @@ class Background extends ConsumerWidget { final Widget child; @override - Widget build(BuildContext context, WidgetRef ref) { - final colorTheme = ref.watch(colorThemeProvider.state).state; - + Widget build(BuildContext context) { Color? color; - switch (colorTheme.themeType) { + switch (Theme.of(context).extension<StackColors>()!.themeType) { case ThemeType.light: case ThemeType.dark: color = Theme.of(context).extension<StackColors>()!.background; diff --git a/lib/widgets/emoji_select_sheet.dart b/lib/widgets/emoji_select_sheet.dart index d5a37d142..ecb6d1a1b 100644 --- a/lib/widgets/emoji_select_sheet.dart +++ b/lib/widgets/emoji_select_sheet.dart @@ -131,55 +131,58 @@ class _EmojiSelectSheetState extends ConsumerState<EmojiSelectSheet> { SizedBox( height: isDesktop ? 16 : 12, ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: _searchController, - focusNode: _searchFocusNode, - onChanged: (newString) { - setState(() => _searchTerm = newString); - }, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - _searchFocusNode, - context, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, + Material( + color: Colors.transparent, + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: (newString) { + setState(() => _searchTerm = newString); + }, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, - ), - ), - suffixIcon: _searchController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchController.text = ""; - _searchTerm = ""; - }); - }, - ), - ], + suffixIcon: _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + _searchTerm = ""; + }); + }, + ), + ], + ), ), - ), - ) - : null, + ) + : null, + ), ), ), ), diff --git a/test/widget_tests/emoji_select_sheet_test.dart b/test/widget_tests/emoji_select_sheet_test.dart index 368a1d99b..aec05d580 100644 --- a/test/widget_tests/emoji_select_sheet_test.dart +++ b/test/widget_tests/emoji_select_sheet_test.dart @@ -1,8 +1,8 @@ import 'package:emojis/emoji.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:mockingjay/mockingjay.dart' as mockingjay; import 'package:flutter_test/flutter_test.dart'; +import 'package:mockingjay/mockingjay.dart' as mockingjay; import 'package:stackwallet/utilities/theme/light_colors.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/emoji_select_sheet.dart'; @@ -43,15 +43,21 @@ void main() { ], ), home: mockingjay.MockNavigatorProvider( - navigator: navigator, child: emojiSelectSheet), + navigator: navigator, + child: Column( + children: const [ + Expanded(child: emojiSelectSheet), + ], + ), + ), ), ), ); - final gestureDetector = find.byType(GestureDetector).first; + final gestureDetector = find.byType(GestureDetector).at(5); expect(gestureDetector, findsOneWidget); - final emoji = Emoji.all()[0]; + final emoji = Emoji.byChar("😅"); await tester.tap(gestureDetector); await tester.pumpAndSettle(); From 8f157ccfc4dcc5e4ed3b1313b70e61b40746aa93 Mon Sep 17 00:00:00 2001 From: Diego Salazar <diego@cypherstack.com> Date: Sat, 26 Nov 2022 13:12:38 -0700 Subject: [PATCH 410/426] Bump version. Mm! --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 2397fbfbc..ef1b495a5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.5.19+91 +version: 1.5.20+92 environment: sdk: ">=2.17.0 <3.0.0" From d0b2a5a3fed1a41382ce109c85d9e55b45c37e01 Mon Sep 17 00:00:00 2001 From: Dan Miller <dan@cypherstack.com> Date: Mon, 28 Nov 2022 08:59:23 -0800 Subject: [PATCH 411/426] Add tomli python lib to build script comment. --- scripts/linux/build_secure_storage_deps.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/linux/build_secure_storage_deps.sh b/scripts/linux/build_secure_storage_deps.sh index 7a725d65c..69b452e2d 100755 --- a/scripts/linux/build_secure_storage_deps.sh +++ b/scripts/linux/build_secure_storage_deps.sh @@ -20,7 +20,7 @@ cd "$LINUX_DIRECTORY" || exit # Build libSecret # sudo apt install meson libgirepository1.0-dev valac xsltproc gi-docgen docbook-xsl # sudo apt install python3-pip -#pip3 install --user meson markdown --upgrade +#pip3 install --user meson markdown tomli --upgrade # pip3 install --user gi-docgen cd build || exit git -C libsecret pull || git clone https://gitlab.gnome.org/GNOME/libsecret.git libsecret From 76e0616ade79a9ef56012e80a4f4e0ede7eb825d Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 28 Nov 2022 10:40:45 -0700 Subject: [PATCH 412/426] view backup keys text changed for wallet deletion --- .../wallet_view/sub_widgets/delete_wallet_keys_popup.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart index 70f4a3e13..a2d58465b 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart @@ -95,7 +95,8 @@ class _DeleteWalletKeysPopup extends ConsumerState<DeleteWalletKeysPopup> { horizontal: 32, ), child: Text( - "Please write down your recovery phrase in the correct order and save it to keep your funds secure. You will also be asked to verify the words on the next screen.", + "Please write down your recovery phrase in the correct order and " + "save it to keep your funds secure. You will be shown your recovery phrase on the next screen.", style: STextStyles.desktopTextExtraExtraSmall(context), textAlign: TextAlign.center, ), From 8960bb576464ecd5ac415cfa3494a53077d10677 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 28 Nov 2022 12:46:35 -0600 Subject: [PATCH 413/426] linux small screen width check --- lib/main.dart | 8 +++++++- lib/utilities/util.dart | 14 ++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 2086d351e..a49bcab82 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -76,11 +76,17 @@ void main() async { Util.libraryPath = await getLibraryDirectory(); } + Screen? screen; + if (Platform.isLinux || Util.isDesktop) { + screen = await getCurrentScreen(); + Util.screenWidth = screen?.frame.width; + } + if (Util.isDesktop) { setWindowTitle('Stack Wallet'); setWindowMinSize(const Size(1220, 100)); setWindowMaxSize(Size.infinite); - final screen = await getCurrentScreen(); + final screenHeight = screen?.frame.height; if (screenHeight != null) { // starting to height be 3/4 screen height or 900, whichever is smaller diff --git a/lib/utilities/util.dart b/lib/utilities/util.dart index 5963bfee9..2940b6d40 100644 --- a/lib/utilities/util.dart +++ b/lib/utilities/util.dart @@ -1,14 +1,24 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:path_provider/path_provider.dart'; abstract class Util { static Directory? libraryPath; + static double? screenWidth; + static bool get isDesktop { - if(Platform.isIOS && libraryPath != null && !libraryPath!.path.contains("/var/mobile/")){ + // special check for running on linux based phones + if (Platform.isLinux && screenWidth != null && screenWidth! < 800) { + return false; + } + + // special check for running under ipad mode in macos + if (Platform.isIOS && + libraryPath != null && + !libraryPath!.path.contains("/var/mobile/")) { return true; } + return Platform.isLinux || Platform.isMacOS || Platform.isWindows; } From 66ff5a437dd8a00bb55687aacbfc4fc5139f91e5 Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 28 Nov 2022 11:51:13 -0700 Subject: [PATCH 414/426] reverted mobile restore calendar height --- .../restore_options_view/restore_options_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart index ac84964ca..1ce5d713a 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart @@ -155,7 +155,7 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> { final date = await showRoundedDatePicker( context: context, initialDate: DateTime.now(), - height: height / 3.2, + height: height * 0.5, theme: ThemeData( primarySwatch: Util.createMaterialColor(fetchedColor), ), From 221e654dd669b24e5e4426131947098b47774b21 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 28 Nov 2022 13:50:55 -0600 Subject: [PATCH 415/426] animated main menu --- .../home/desktop_menu.dart | 92 +++++++++++--- .../home/desktop_menu_item.dart | 119 +++++++++++++++--- 2 files changed, 174 insertions(+), 37 deletions(-) diff --git a/lib/pages_desktop_specific/home/desktop_menu.dart b/lib/pages_desktop_specific/home/desktop_menu.dart index d82d62883..6ae4b91a1 100644 --- a/lib/pages_desktop_specific/home/desktop_menu.dart +++ b/lib/pages_desktop_specific/home/desktop_menu.dart @@ -38,6 +38,9 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { static const expandedWidth = 225.0; static const minimizedWidth = 72.0; + final Duration duration = const Duration(milliseconds: 250); + late final List<DMIController> controllers; + double _width = expandedWidth; void updateSelectedMenuItem(DesktopMenuItemId idKey) { @@ -49,26 +52,58 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { } void toggleMinimize() { + final expanded = _width == expandedWidth; + + for (var e in controllers) { + e.toggle?.call(); + } + setState(() { - _width = _width == expandedWidth ? minimizedWidth : expandedWidth; + _width = expanded ? minimizedWidth : expandedWidth; }); } + @override + void initState() { + controllers = [ + DMIController(), + DMIController(), + DMIController(), + DMIController(), + DMIController(), + DMIController(), + DMIController(), + DMIController(), + ]; + + super.initState(); + } + + @override + void dispose() { + for (var e in controllers) { + e.dispose(); + } + super.dispose(); + } + @override Widget build(BuildContext context) { return Material( color: Theme.of(context).extension<StackColors>()!.popupBG, - child: SizedBox( + child: AnimatedContainer( width: _width, + duration: duration, child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - SizedBox( - height: _width == expandedWidth ? 22 : 25, + const SizedBox( + height: 25, ), - SizedBox( + AnimatedContainer( + duration: duration, width: _width == expandedWidth ? 70 : 32, - height: _width == expandedWidth ? 70 : 32, + height: 70, //_width == expandedWidth ? 70 : 32, child: SvgPicture.asset( Assets.svg.stackIcon(context), ), @@ -76,18 +111,26 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { const SizedBox( height: 10, ), - Text( - _width == expandedWidth ? "Stack Wallet" : "", - style: STextStyles.desktopH2(context).copyWith( - fontSize: 18, - height: 23.4 / 18, + AnimatedOpacity( + duration: duration, + opacity: _width == expandedWidth ? 1 : 0, + child: SizedBox( + height: 28, + child: Text( + "Stack Wallet", + style: STextStyles.desktopH2(context).copyWith( + fontSize: 18, + height: 23.4 / 18, + ), + ), ), ), const SizedBox( height: 60, ), Expanded( - child: SizedBox( + child: AnimatedContainer( + duration: duration, width: _width == expandedWidth ? _width - 32 // 16 padding on either side : _width - 16, // 8 padding on either side @@ -95,6 +138,7 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ DesktopMenuItem( + duration: duration, icon: SvgPicture.asset( Assets.svg.walletDesktop, width: 20, @@ -116,12 +160,13 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { group: ref.watch(currentDesktopMenuItemProvider.state).state, onChanged: updateSelectedMenuItem, - iconOnly: _width == minimizedWidth, + controller: controllers[0], ), const SizedBox( height: 2, ), DesktopMenuItem( + duration: duration, icon: SvgPicture.asset( Assets.svg.exchangeDesktop, width: 20, @@ -143,12 +188,13 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { group: ref.watch(currentDesktopMenuItemProvider.state).state, onChanged: updateSelectedMenuItem, - iconOnly: _width == minimizedWidth, + controller: controllers[1], ), const SizedBox( height: 2, ), DesktopMenuItem( + duration: duration, icon: SvgPicture.asset( ref.watch(notificationsProvider.select( (value) => value.hasUnreadNotifications)) @@ -177,12 +223,13 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { group: ref.watch(currentDesktopMenuItemProvider.state).state, onChanged: updateSelectedMenuItem, - iconOnly: _width == minimizedWidth, + controller: controllers[2], ), const SizedBox( height: 2, ), DesktopMenuItem( + duration: duration, icon: SvgPicture.asset( Assets.svg.addressBookDesktop, width: 20, @@ -204,12 +251,13 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { group: ref.watch(currentDesktopMenuItemProvider.state).state, onChanged: updateSelectedMenuItem, - iconOnly: _width == minimizedWidth, + controller: controllers[3], ), const SizedBox( height: 2, ), DesktopMenuItem( + duration: duration, icon: SvgPicture.asset( Assets.svg.gear, width: 20, @@ -231,12 +279,13 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { group: ref.watch(currentDesktopMenuItemProvider.state).state, onChanged: updateSelectedMenuItem, - iconOnly: _width == minimizedWidth, + controller: controllers[4], ), const SizedBox( height: 2, ), DesktopMenuItem( + duration: duration, icon: SvgPicture.asset( Assets.svg.messageQuestion, width: 20, @@ -258,12 +307,13 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { group: ref.watch(currentDesktopMenuItemProvider.state).state, onChanged: updateSelectedMenuItem, - iconOnly: _width == minimizedWidth, + controller: controllers[5], ), const SizedBox( height: 2, ), DesktopMenuItem( + duration: duration, icon: SvgPicture.asset( Assets.svg.aboutDesktop, width: 20, @@ -285,10 +335,12 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { group: ref.watch(currentDesktopMenuItemProvider.state).state, onChanged: updateSelectedMenuItem, - iconOnly: _width == minimizedWidth, + controller: controllers[6], ), const Spacer(), DesktopMenuItem( + duration: duration, + labelLength: 123, icon: SvgPicture.asset( Assets.svg.exitDesktop, width: 20, @@ -306,7 +358,7 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { // todo: save stuff/ notify before exit? exit(0); }, - iconOnly: _width == minimizedWidth, + controller: controllers[7], ), ], ), diff --git a/lib/pages_desktop_specific/home/desktop_menu_item.dart b/lib/pages_desktop_specific/home/desktop_menu_item.dart index 76d945e2d..e73a4a477 100644 --- a/lib/pages_desktop_specific/home/desktop_menu_item.dart +++ b/lib/pages_desktop_specific/home/desktop_menu_item.dart @@ -2,7 +2,14 @@ import 'package:flutter/material.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; -class DesktopMenuItem<T> extends StatelessWidget { +class DMIController { + VoidCallback? toggle; + void dispose() { + toggle = null; + } +} + +class DesktopMenuItem<T> extends StatefulWidget { const DesktopMenuItem({ Key? key, required this.icon, @@ -10,7 +17,9 @@ class DesktopMenuItem<T> extends StatelessWidget { required this.value, required this.group, required this.onChanged, - required this.iconOnly, + required this.duration, + this.labelLength = 125, + this.controller, }) : super(key: key); final Widget icon; @@ -18,7 +27,67 @@ class DesktopMenuItem<T> extends StatelessWidget { final T value; final T group; final void Function(T) onChanged; - final bool iconOnly; + final Duration duration; + final double labelLength; + final DMIController? controller; + + @override + State<DesktopMenuItem<T>> createState() => _DesktopMenuItemState<T>(); +} + +class _DesktopMenuItemState<T> extends State<DesktopMenuItem<T>> + with SingleTickerProviderStateMixin { + late final Widget icon; + late final String label; + late final T value; + late final T group; + late final void Function(T) onChanged; + late final Duration duration; + late final double labelLength; + + late final DMIController? controller; + + late final AnimationController animationController; + + bool _iconOnly = false; + + void toggle() { + setState(() { + _iconOnly = !_iconOnly; + }); + if (_iconOnly) { + animationController.reverse(); + } else { + animationController.forward(); + } + } + + @override + void initState() { + icon = widget.icon; + label = widget.label; + value = widget.value; + group = widget.group; + onChanged = widget.onChanged; + duration = widget.duration; + labelLength = widget.labelLength; + controller = widget.controller; + + controller?.toggle = toggle; + animationController = AnimationController( + vsync: this, + duration: duration, + ); + + super.initState(); + } + + @override + void dispose() { + controller?.dispose(); + animationController.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { @@ -34,26 +103,42 @@ class DesktopMenuItem<T> extends StatelessWidget { onChanged(value); }, child: Padding( - padding: EdgeInsets.symmetric( + padding: const EdgeInsets.symmetric( vertical: 16, - horizontal: iconOnly ? 0 : 16, ), child: Row( - mainAxisAlignment: - iconOnly ? MainAxisAlignment.center : MainAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, children: [ + AnimatedContainer( + duration: duration, + width: _iconOnly ? 0 : 16, + ), icon, - if (!iconOnly) - const SizedBox( - width: 12, - ), - if (!iconOnly) - Text( - label, - style: value == group - ? STextStyles.desktopMenuItemSelected(context) - : STextStyles.desktopMenuItem(context), + AnimatedOpacity( + duration: duration, + opacity: _iconOnly ? 0 : 1.0, + child: SizeTransition( + sizeFactor: animationController, + axis: Axis.horizontal, + axisAlignment: -1, + child: SizedBox( + width: labelLength, + child: Row( + children: [ + const SizedBox( + width: 12, + ), + Text( + label, + style: value == group + ? STextStyles.desktopMenuItemSelected(context) + : STextStyles.desktopMenuItem(context), + ), + ], + ), + ), ), + ) ], ), ), From 6bbabcd729aa50d74e9351a3bb2948d740437b3a Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 28 Nov 2022 13:06:02 -0700 Subject: [PATCH 416/426] MyStackView tab after a restore backup --- .../sub_views/stack_restore_progress_view.dart | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) 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 c7f53378d..92e7742e1 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 @@ -25,6 +25,9 @@ import 'package:stackwallet/widgets/icon_widgets/addressbook_icon.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; +import '../../../../../pages_desktop_specific/home/desktop_home_view.dart'; +import '../../../../../pages_desktop_specific/home/desktop_menu.dart'; +import '../../../../../providers/desktop/current_desktop_menu_item.dart'; import '../../../../../widgets/desktop/primary_button.dart'; class StackRestoreProgressView extends ConsumerStatefulWidget { @@ -685,7 +688,19 @@ class _StackRestoreProgressViewState enabled: true, label: "Done", onPressed: () async { - Navigator.of(context).pop(); + DesktopMenuItemId keyID = + DesktopMenuItemId.myStack; + + ref + .read(currentDesktopMenuItemProvider + .state) + .state = keyID; + + Navigator.of(context, rootNavigator: true) + .popUntil( + ModalRoute.withName( + DesktopHomeView.routeName), + ); }, ) : SecondaryButton( From c3921b01de78978a13874b8c3457a4837bc8846a Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 28 Nov 2022 14:25:49 -0600 Subject: [PATCH 417/426] animated desktop stack icon --- .../home/desktop_menu.dart | 6 +-- .../home/desktop_menu_item.dart | 2 +- lib/widgets/desktop/living_stack_icon.dart | 54 +++++++++++++++++++ 3 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 lib/widgets/desktop/living_stack_icon.dart diff --git a/lib/pages_desktop_specific/home/desktop_menu.dart b/lib/pages_desktop_specific/home/desktop_menu.dart index 6ae4b91a1..8af307e7b 100644 --- a/lib/pages_desktop_specific/home/desktop_menu.dart +++ b/lib/pages_desktop_specific/home/desktop_menu.dart @@ -9,6 +9,7 @@ import 'package:stackwallet/providers/providers.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/desktop/living_stack_icon.dart'; enum DesktopMenuItemId { myStack, @@ -103,9 +104,8 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { AnimatedContainer( duration: duration, width: _width == expandedWidth ? 70 : 32, - height: 70, //_width == expandedWidth ? 70 : 32, - child: SvgPicture.asset( - Assets.svg.stackIcon(context), + child: LivingStackIcon( + onPressed: toggleMinimize, ), ), const SizedBox( diff --git a/lib/pages_desktop_specific/home/desktop_menu_item.dart b/lib/pages_desktop_specific/home/desktop_menu_item.dart index e73a4a477..1fb39213b 100644 --- a/lib/pages_desktop_specific/home/desktop_menu_item.dart +++ b/lib/pages_desktop_specific/home/desktop_menu_item.dart @@ -77,7 +77,7 @@ class _DesktopMenuItemState<T> extends State<DesktopMenuItem<T>> animationController = AnimationController( vsync: this, duration: duration, - ); + )..forward(); super.initState(); } diff --git a/lib/widgets/desktop/living_stack_icon.dart b/lib/widgets/desktop/living_stack_icon.dart new file mode 100644 index 000000000..7afc8f8d2 --- /dev/null +++ b/lib/widgets/desktop/living_stack_icon.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/utilities/assets.dart'; + +class LivingStackIcon extends StatefulWidget { + const LivingStackIcon({Key? key, this.onPressed,}) : super(key: key); + + final VoidCallback? onPressed; + + @override + State<LivingStackIcon> createState() => _LivingStackIconState(); +} + +class _LivingStackIconState extends State<LivingStackIcon> { + bool _hovering = false; + + late final VoidCallback? onPressed; + + @override + void initState() { + onPressed = widget.onPressed; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 76, + child: MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (_) { + setState(() { + _hovering = true; + }); + }, + onExit: (_) { + setState(() { + _hovering = false; + }); + }, + child: GestureDetector( + onTap: () => onPressed?.call(), + child: AnimatedScale( + duration: const Duration(milliseconds: 200), + scale: _hovering ? 1.2 : 1, + child: SvgPicture.asset( + Assets.svg.stackIcon(context), + ), + ), + ), + ), + ); + } +} From b4cbf078c7a1a6105fc76bd92715b97447802d9e Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 28 Nov 2022 13:50:55 -0700 Subject: [PATCH 418/426] flutter_rounded_date_picker files from picker lib --- .../flutter_rounded_date_picker_dialog.dart | 332 ++++++++++++++++++ .../flutter_rounded_date_picker_widget.dart | 216 ++++++++++++ 2 files changed, 548 insertions(+) create mode 100644 lib/widgets/rounded_date_picker/flutter_rounded_date_picker_dialog.dart create mode 100644 lib/widgets/rounded_date_picker/flutter_rounded_date_picker_widget.dart diff --git a/lib/widgets/rounded_date_picker/flutter_rounded_date_picker_dialog.dart b/lib/widgets/rounded_date_picker/flutter_rounded_date_picker_dialog.dart new file mode 100644 index 000000000..6d7f775cd --- /dev/null +++ b/lib/widgets/rounded_date_picker/flutter_rounded_date_picker_dialog.dart @@ -0,0 +1,332 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/semantics.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_rounded_date_picker/flutter_rounded_date_picker.dart'; +import 'package:flutter_rounded_date_picker/src/flutter_rounded_button_action.dart'; +import 'package:flutter_rounded_date_picker/src/material_rounded_date_picker_style.dart'; +import 'package:flutter_rounded_date_picker/src/material_rounded_year_picker_style.dart'; +import 'package:flutter_rounded_date_picker/src/widgets/flutter_rounded_date_picker_header.dart'; +import 'package:flutter_rounded_date_picker/src/widgets/flutter_rounded_day_picker.dart'; +import 'package:flutter_rounded_date_picker/src/widgets/flutter_rounded_month_picker.dart'; +import 'package:flutter_rounded_date_picker/src/widgets/flutter_rounded_year_picker.dart'; +import 'package:stackwallet/utilities/util.dart'; + +/// +/// This file uses code taken from https://github.com/benznest/flutter_rounded_date_picker +/// + +class FlutterRoundedDatePickerDialog extends StatefulWidget { + const FlutterRoundedDatePickerDialog( + {Key? key, + this.height, + required this.initialDate, + required this.firstDate, + required this.lastDate, + this.selectableDayPredicate, + required this.initialDatePickerMode, + required this.era, + this.locale, + required this.borderRadius, + this.imageHeader, + this.description = "", + this.fontFamily, + this.textNegativeButton, + this.textPositiveButton, + this.textActionButton, + this.onTapActionButton, + this.styleDatePicker, + this.styleYearPicker, + this.customWeekDays, + this.builderDay, + this.listDateDisabled, + this.onTapDay, + this.onMonthChange}) + : super(key: key); + + final DateTime initialDate; + final DateTime firstDate; + final DateTime lastDate; + final SelectableDayPredicate? selectableDayPredicate; + final DatePickerMode initialDatePickerMode; + + /// double height. + final double? height; + + /// Custom era year. + final EraMode era; + final Locale? locale; + + /// Border + final double borderRadius; + + /// Header; + final ImageProvider? imageHeader; + final String description; + + /// Font + final String? fontFamily; + + /// Button + final String? textNegativeButton; + final String? textPositiveButton; + final String? textActionButton; + + final VoidCallback? onTapActionButton; + + /// Style + final MaterialRoundedDatePickerStyle? styleDatePicker; + final MaterialRoundedYearPickerStyle? styleYearPicker; + + /// Custom Weekday + final List<String>? customWeekDays; + + final BuilderDayOfDatePicker? builderDay; + + final List<DateTime>? listDateDisabled; + final OnTapDay? onTapDay; + + final Function? onMonthChange; + + @override + _FlutterRoundedDatePickerDialogState createState() => + _FlutterRoundedDatePickerDialogState(); +} + +class _FlutterRoundedDatePickerDialogState + extends State<FlutterRoundedDatePickerDialog> { + @override + void initState() { + super.initState(); + _selectedDate = widget.initialDate; + _mode = widget.initialDatePickerMode; + } + + bool _announcedInitialDate = false; + + late MaterialLocalizations localizations; + late TextDirection textDirection; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + localizations = MaterialLocalizations.of(context); + textDirection = Directionality.of(context); + if (!_announcedInitialDate) { + _announcedInitialDate = true; + SemanticsService.announce( + localizations.formatFullDate(_selectedDate), + textDirection, + ); + } + } + + late DateTime _selectedDate; + late DatePickerMode _mode; + final GlobalKey _pickerKey = GlobalKey(); + + void _vibrate() { + switch (Theme.of(context).platform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + HapticFeedback.vibrate(); + break; + case TargetPlatform.iOS: + default: + break; + } + } + + void _handleModeChanged(DatePickerMode mode) { + _vibrate(); + setState(() { + _mode = mode; + if (_mode == DatePickerMode.day) { + SemanticsService.announce( + localizations.formatMonthYear(_selectedDate), + textDirection, + ); + } else { + SemanticsService.announce( + localizations.formatYear(_selectedDate), + textDirection, + ); + } + }); + } + + Future<void> _handleYearChanged(DateTime value) async { + if (value.isBefore(widget.firstDate)) { + value = widget.firstDate; + } else if (value.isAfter(widget.lastDate)) { + value = widget.lastDate; + } + if (value == _selectedDate) return; + + if (widget.onMonthChange != null) await widget.onMonthChange!(value); + + _vibrate(); + setState(() { + _mode = DatePickerMode.day; + _selectedDate = value; + }); + } + + void _handleDayChanged(DateTime value) { + _vibrate(); + setState(() { + _selectedDate = value; + }); + } + + void _handleCancel() { + Navigator.of(context).pop(); + } + + void _handleOk() { + Navigator.of(context).pop(_selectedDate); + } + + Widget _buildPicker() { + switch (_mode) { + case DatePickerMode.year: + return FlutterRoundedYearPicker( + key: _pickerKey, + selectedDate: _selectedDate, + onChanged: (DateTime date) async => await _handleYearChanged(date), + firstDate: widget.firstDate, + lastDate: widget.lastDate, + era: widget.era, + fontFamily: widget.fontFamily, + style: widget.styleYearPicker, + ); + case DatePickerMode.day: + default: + return FlutterRoundedMonthPicker( + key: _pickerKey, + selectedDate: _selectedDate, + onChanged: _handleDayChanged, + firstDate: widget.firstDate, + lastDate: widget.lastDate, + era: widget.era, + locale: widget.locale, + selectableDayPredicate: widget.selectableDayPredicate, + fontFamily: widget.fontFamily, + style: widget.styleDatePicker, + borderRadius: widget.borderRadius, + customWeekDays: widget.customWeekDays, + builderDay: widget.builderDay, + listDateDisabled: widget.listDateDisabled, + onTapDay: widget.onTapDay, + onMonthChange: widget.onMonthChange); + } + } + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final Widget picker = _buildPicker(); + final isDesktop = Util.isDesktop; + + final Widget actions = FlutterRoundedButtonAction( + textButtonNegative: widget.textNegativeButton, + textButtonPositive: widget.textPositiveButton, + onTapButtonNegative: _handleCancel, + onTapButtonPositive: _handleOk, + textActionButton: widget.textActionButton, + onTapButtonAction: widget.onTapActionButton, + localizations: localizations, + textStyleButtonNegative: widget.styleDatePicker?.textStyleButtonNegative, + textStyleButtonPositive: widget.styleDatePicker?.textStyleButtonPositive, + textStyleButtonAction: widget.styleDatePicker?.textStyleButtonAction, + borderRadius: widget.borderRadius, + paddingActionBar: widget.styleDatePicker?.paddingActionBar, + background: widget.styleDatePicker?.backgroundActionBar, + ); + + Color backgroundPicker = theme.dialogBackgroundColor; + if (_mode == DatePickerMode.day) { + backgroundPicker = widget.styleDatePicker?.backgroundPicker ?? + theme.dialogBackgroundColor; + } else { + backgroundPicker = widget.styleYearPicker?.backgroundPicker ?? + theme.dialogBackgroundColor; + } + + final Dialog dialog = Dialog( + child: OrientationBuilder( + builder: (BuildContext context, Orientation orientation) { + final Widget header = FlutterRoundedDatePickerHeader( + selectedDate: _selectedDate, + mode: _mode, + onModeChanged: _handleModeChanged, + orientation: orientation, + era: widget.era, + borderRadius: widget.borderRadius, + imageHeader: widget.imageHeader, + description: widget.description, + fontFamily: widget.fontFamily, + style: widget.styleDatePicker); + switch (orientation) { + case Orientation.landscape: + return Container( + height: isDesktop ? 600 : null, + width: isDesktop ? 700 : null, + decoration: BoxDecoration( + color: backgroundPicker, + borderRadius: BorderRadius.circular(widget.borderRadius), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: <Widget>[ + Flexible(flex: 1, child: header), + Flexible( + flex: 2, // have the picker take up 2/3 of the dialog width + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: <Widget>[ + SizedBox( + height: isDesktop ? 530 : null, + width: isDesktop ? 700 : null, + child: picker), + actions, + ], + ), + ), + ], + ), + ); + case Orientation.portrait: + default: + return Container( + decoration: BoxDecoration( + color: backgroundPicker, + borderRadius: BorderRadius.circular(widget.borderRadius), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: <Widget>[ + header, + if (widget.height == null) + Flexible(child: picker) + else + SizedBox( + height: widget.height, + child: picker, + ), + actions, + ], + ), + ); + } + }), + ); + + return Theme( + data: theme.copyWith(dialogBackgroundColor: Colors.transparent), + child: dialog, + ); + } +} diff --git a/lib/widgets/rounded_date_picker/flutter_rounded_date_picker_widget.dart b/lib/widgets/rounded_date_picker/flutter_rounded_date_picker_widget.dart new file mode 100644 index 000000000..5f576f480 --- /dev/null +++ b/lib/widgets/rounded_date_picker/flutter_rounded_date_picker_widget.dart @@ -0,0 +1,216 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; +// import 'package:flutter_rounded_date_picker/src/dialogs/flutter_rounded_date_picker_dialog.dart'; +import 'package:flutter_rounded_date_picker/src/era_mode.dart'; +import 'package:flutter_rounded_date_picker/src/material_rounded_date_picker_style.dart'; +import 'package:flutter_rounded_date_picker/src/material_rounded_year_picker_style.dart'; +import 'package:flutter_rounded_date_picker/src/widgets/flutter_rounded_day_picker.dart'; +import 'package:stackwallet/widgets/rounded_date_picker/flutter_rounded_date_picker_dialog.dart'; + +/// +/// This file uses code taken from https://github.com/benznest/flutter_rounded_date_picker +/// + +// Examples can assume: +// BuildContext context; + +/// Initial display mode of the date picker dialog. +/// +/// Date picker UI mode for either showing a list of available years or a +/// monthly calendar initially in the dialog shown by calling [showDatePicker]. +/// + +// Shows the selected date in large font and toggles between year and day mode + +/// Signature for predicating dates for enabled date selections. +/// +/// See [showDatePicker]. +typedef SelectableDayPredicate = bool Function(DateTime day); + +/// Shows a dialog containing a material design date picker. +/// +/// The returned [Future] resolves to the date selected by the user when the +/// user closes the dialog. If the user cancels the dialog, null is returned. +/// +/// An optional [selectableDayPredicate] function can be passed in to customize +/// the days to enable for selection. If provided, only the days that +/// [selectableDayPredicate] returned true for will be selectable. +/// +/// An optional [initialDatePickerMode] argument can be used to display the +/// date picker initially in the year or month+day picker mode. It defaults +/// to month+day, and must not be null. +/// +/// An optional [locale] argument can be used to set the locale for the date +/// picker. It defaults to the ambient locale provided by [Localizations]. +/// +/// An optional [textDirection] argument can be used to set the text direction +/// (RTL or LTR) for the date picker. It defaults to the ambient text direction +/// provided by [Directionality]. If both [locale] and [textDirection] are not +/// null, [textDirection] overrides the direction chosen for the [locale]. +/// +/// The [context] argument is passed to [showDialog], the documentation for +/// which discusses how it is used. +/// +/// The [builder] parameter can be used to wrap the dialog widget +/// to add inherited widgets like [Theme]. +/// +/// {@tool sample} +/// Show a date picker with the dark theme. +/// +/// ```dart +/// Future<DateTime> selectedDate = showDatePicker( +/// context: context, +/// initialDate: DateTime.now(), +/// firstDate: DateTime(2018), +/// lastDate: DateTime(2030), +/// builder: (BuildContext context, Widget child) { +/// return Theme( +/// data: ThemeData.dark(), +/// child: child, +/// ); +/// }, +/// ); +/// ``` +/// {@end-tool} +/// +/// The [context], [initialDate], [firstDate], and [lastDate] parameters must +/// not be null. +/// +/// See also: +/// +/// * [showTimePicker], which shows a dialog that contains a material design +/// time picker. +/// * [DayPicker], which displays the days of a given month and allows +/// choosing a day. +/// * [MonthPicker], which displays a scrollable list of months to allow +/// picking a month. +/// * [YearPicker], which displays a scrollable list of years to allow picking +/// a year. +/// + +Future<DateTime?> showRoundedDatePicker( + {required BuildContext context, + double? height, + DateTime? initialDate, + DateTime? firstDate, + DateTime? lastDate, + SelectableDayPredicate? selectableDayPredicate, + DatePickerMode initialDatePickerMode = DatePickerMode.day, + Locale? locale, + TextDirection? textDirection, + ThemeData? theme, + double borderRadius = 16, + EraMode era = EraMode.CHRIST_YEAR, + ImageProvider? imageHeader, + String description = "", + String? fontFamily, + bool barrierDismissible = false, + Color background = Colors.transparent, + String? textNegativeButton, + String? textPositiveButton, + String? textActionButton, + VoidCallback? onTapActionButton, + MaterialRoundedDatePickerStyle? styleDatePicker, + MaterialRoundedYearPickerStyle? styleYearPicker, + List<String>? customWeekDays, + BuilderDayOfDatePicker? builderDay, + List<DateTime>? listDateDisabled, + OnTapDay? onTapDay, + Function? onMonthChange}) async { + initialDate ??= DateTime.now(); + firstDate ??= DateTime(initialDate.year - 1); + lastDate ??= DateTime(initialDate.year + 1); + theme ??= ThemeData(); + + assert( + !initialDate.isBefore(firstDate), + 'initialDate must be on or after firstDate', + ); + assert( + !initialDate.isAfter(lastDate), + 'initialDate must be on or before lastDate', + ); + assert( + !firstDate.isAfter(lastDate), + 'lastDate must be on or after firstDate', + ); + assert( + selectableDayPredicate == null || selectableDayPredicate(initialDate), + 'Provided initialDate must satisfy provided selectableDayPredicate', + ); + assert( + (onTapActionButton != null && textActionButton != null) || + onTapActionButton == null, + "If you provide onLeftBtn, you must provide leftBtn", + ); + assert(debugCheckHasMaterialLocalizations(context)); + + Widget child = GestureDetector( + onTap: () { + if (!barrierDismissible) { + Navigator.pop(context); + } + }, + child: Container( + color: background, + child: GestureDetector( + onTap: () { + // + }, + child: FlutterRoundedDatePickerDialog( + height: height, + initialDate: initialDate, + firstDate: firstDate, + lastDate: lastDate, + selectableDayPredicate: selectableDayPredicate, + initialDatePickerMode: initialDatePickerMode, + era: era, + locale: locale, + borderRadius: borderRadius, + imageHeader: imageHeader, + description: description, + fontFamily: fontFamily, + textNegativeButton: textNegativeButton, + textPositiveButton: textPositiveButton, + textActionButton: textActionButton, + onTapActionButton: onTapActionButton, + styleDatePicker: styleDatePicker, + styleYearPicker: styleYearPicker, + customWeekDays: customWeekDays, + builderDay: builderDay, + listDateDisabled: listDateDisabled, + onTapDay: onTapDay, + onMonthChange: onMonthChange, + ), + ), + ), + ); + + if (textDirection != null) { + child = Directionality( + textDirection: textDirection, + child: child, + ); + } + + if (locale != null) { + child = Localizations.override( + context: context, + locale: locale, + child: child, + ); + } + + return await showDialog<DateTime>( + context: context, + barrierDismissible: barrierDismissible, + builder: (_) => Theme(data: theme!, child: child), + ); +} From 3fef1ee67461e4ca3fd9078f3908679f9522953d Mon Sep 17 00:00:00 2001 From: ryleedavis <rylee@cypherstack.com> Date: Mon, 28 Nov 2022 14:10:44 -0700 Subject: [PATCH 419/426] desktop restore calendar resize --- .../restore_options_view/restore_options_view.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart index 1ce5d713a..a66af63fc 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart @@ -23,6 +23,8 @@ import 'package:stackwallet/utilities/util.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/rounded_date_picker/flutter_rounded_date_picker_widget.dart' + as datePicker; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:tuple/tuple.dart'; @@ -152,7 +154,7 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> { await Future<void>.delayed(const Duration(milliseconds: 125)); } - final date = await showRoundedDatePicker( + final date = await datePicker.showRoundedDatePicker( context: context, initialDate: DateTime.now(), height: height * 0.5, From d7cd5cb8a9c556a29f6ef8e273a3f72503b523e3 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 28 Nov 2022 15:21:18 -0600 Subject: [PATCH 420/426] desktop wallets table hover effects --- .../my_stack_view/coin_wallets_table.dart | 87 ++++++++++++-- lib/widgets/table_view/table_view_row.dart | 106 ++++++++++++++---- .../wallet_info_row/wallet_info_row.dart | 95 ++++++++-------- 3 files changed, 209 insertions(+), 79 deletions(-) diff --git a/lib/pages_desktop_specific/home/my_stack_view/coin_wallets_table.dart b/lib/pages_desktop_specific/home/my_stack_view/coin_wallets_table.dart index b16a9bc58..4ed8765ae 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/coin_wallets_table.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/coin_wallets_table.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/wallet_info_row/wallet_info_row.dart'; class CoinWalletsTable extends ConsumerWidget { @@ -24,8 +25,10 @@ class CoinWalletsTable extends ConsumerWidget { ), child: Padding( padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 16, + // horizontal: 20, + // vertical: 16, + horizontal: 6, + vertical: 6, ), child: Column( children: [ @@ -36,14 +39,26 @@ class CoinWalletsTable extends ConsumerWidget { const SizedBox( height: 32, ), - WalletInfoRow( - walletId: walletIds[i], - onPressed: () async { - await Navigator.of(context).pushNamed( - DesktopWalletView.routeName, - arguments: walletIds[i], - ); - }, + Stack( + children: [ + WalletInfoRow( + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 10, + ), + walletId: walletIds[i], + ), + Positioned.fill( + child: WalletRowHoverOverlay( + onPressed: () async { + await Navigator.of(context).pushNamed( + DesktopWalletView.routeName, + arguments: walletIds[i], + ); + }, + ), + ), + ], ), ], ), @@ -53,3 +68,55 @@ class CoinWalletsTable extends ConsumerWidget { ); } } + +class WalletRowHoverOverlay extends StatefulWidget { + const WalletRowHoverOverlay({ + Key? key, + required this.onPressed, + }) : super(key: key); + + final VoidCallback onPressed; + + @override + State<WalletRowHoverOverlay> createState() => _WalletRowHoverOverlayState(); +} + +class _WalletRowHoverOverlayState extends State<WalletRowHoverOverlay> { + late final VoidCallback onPressed; + + bool _hovering = false; + + @override + void initState() { + onPressed = widget.onPressed; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (_) { + setState(() { + _hovering = true; + }); + }, + onExit: (_) { + setState(() { + _hovering = false; + }); + }, + child: GestureDetector( + onTap: onPressed, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 100), + opacity: _hovering ? 0.1 : 0, + child: RoundedContainer( + color: + Theme.of(context).extension<StackColors>()!.buttonBackSecondary, + ), + ), + ), + ); + } +} diff --git a/lib/widgets/table_view/table_view_row.dart b/lib/widgets/table_view/table_view_row.dart index e95eb68bd..9c3175efe 100644 --- a/lib/widgets/table_view/table_view_row.dart +++ b/lib/widgets/table_view/table_view_row.dart @@ -3,7 +3,7 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/expandable.dart'; import 'package:stackwallet/widgets/table_view/table_view_cell.dart'; -class TableViewRow extends StatelessWidget { +class TableViewRow extends StatefulWidget { const TableViewRow({ Key? key, required this.cells, @@ -17,40 +17,66 @@ class TableViewRow extends StatelessWidget { final List<TableViewCell> cells; final Widget? expandingChild; - final Decoration? decoration; + final BoxDecoration? decoration; final void Function(ExpandableState)? onExpandChanged; final EdgeInsetsGeometry padding; final double spacing; final CrossAxisAlignment crossAxisAlignment; + @override + State<TableViewRow> createState() => _TableViewRowState(); +} + +class _TableViewRowState extends State<TableViewRow> { + late final List<TableViewCell> cells; + late final Widget? expandingChild; + late final BoxDecoration? decoration; + late final void Function(ExpandableState)? onExpandChanged; + late final EdgeInsetsGeometry padding; + late final double spacing; + late final CrossAxisAlignment crossAxisAlignment; + + bool _hovering = false; + + @override + void initState() { + cells = widget.cells; + expandingChild = widget.expandingChild; + decoration = widget.decoration; + onExpandChanged = widget.onExpandChanged; + padding = widget.padding; + spacing = widget.spacing; + crossAxisAlignment = widget.crossAxisAlignment; + super.initState(); + } + @override Widget build(BuildContext context) { return Container( - decoration: decoration, + decoration: !_hovering + ? decoration + : decoration?.copyWith( + boxShadow: [ + Theme.of(context).extension<StackColors>()!.standardBoxShadow, + Theme.of(context).extension<StackColors>()!.standardBoxShadow, + ], + ), child: expandingChild == null - ? Padding( - padding: padding, - child: Row( - crossAxisAlignment: crossAxisAlignment, - children: [ - for (int i = 0; i < cells.length; i++) ...[ - if (i != 0 && i != cells.length) - SizedBox( - width: spacing, - ), - Expanded( - flex: cells[i].flex, - child: cells[i], - ), - ], - ], - ), - ) - : Expandable( - onExpandChanged: onExpandChanged, - header: Padding( + ? MouseRegion( + onEnter: (_) { + setState(() { + _hovering = true; + }); + }, + onExit: (_) { + setState(() { + _hovering = false; + }); + }, + child: Padding( padding: padding, child: Row( + crossAxisAlignment: crossAxisAlignment, children: [ for (int i = 0; i < cells.length; i++) ...[ if (i != 0 && i != cells.length) @@ -65,6 +91,38 @@ class TableViewRow extends StatelessWidget { ], ), ), + ) + : Expandable( + onExpandChanged: onExpandChanged, + header: MouseRegion( + onEnter: (_) { + setState(() { + _hovering = true; + }); + }, + onExit: (_) { + setState(() { + _hovering = false; + }); + }, + child: Padding( + padding: padding, + child: Row( + children: [ + for (int i = 0; i < cells.length; i++) ...[ + if (i != 0 && i != cells.length) + SizedBox( + width: spacing, + ), + Expanded( + flex: cells[i].flex, + child: cells[i], + ), + ], + ], + ), + ), + ), body: Column( children: [ Container( diff --git a/lib/widgets/wallet_info_row/wallet_info_row.dart b/lib/widgets/wallet_info_row/wallet_info_row.dart index fe006a67b..5bb51e2e6 100644 --- a/lib/widgets/wallet_info_row/wallet_info_row.dart +++ b/lib/widgets/wallet_info_row/wallet_info_row.dart @@ -14,10 +14,12 @@ class WalletInfoRow extends ConsumerWidget { Key? key, required this.walletId, this.onPressed, + this.padding = const EdgeInsets.all(0), }) : super(key: key); final String walletId; final VoidCallback? onPressed; + final EdgeInsets padding; @override Widget build(BuildContext context, WidgetRef ref) { @@ -30,53 +32,56 @@ class WalletInfoRow extends ConsumerWidget { cursor: SystemMouseCursors.click, child: GestureDetector( onTap: onPressed, - child: Container( - color: Colors.transparent, - child: Row( - children: [ - Expanded( - flex: 4, - child: Row( - children: [ - WalletInfoCoinIcon(coin: manager.coin), - const SizedBox( - width: 12, - ), - Text( - manager.walletName, - style: - STextStyles.desktopTextExtraSmall(context).copyWith( + child: Padding( + padding: padding, + child: Container( + color: Colors.transparent, + child: Row( + children: [ + Expanded( + flex: 4, + child: Row( + children: [ + WalletInfoCoinIcon(coin: manager.coin), + const SizedBox( + width: 12, + ), + Text( + manager.walletName, + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + ), + ], + ), + ), + Expanded( + flex: 4, + child: WalletInfoRowBalanceFuture( + walletId: walletId, + ), + ), + Expanded( + flex: 6, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + SvgPicture.asset( + Assets.svg.chevronRight, + width: 20, + height: 20, color: Theme.of(context) .extension<StackColors>()! - .textDark, - ), - ), - ], - ), - ), - Expanded( - flex: 4, - child: WalletInfoRowBalanceFuture( - walletId: walletId, - ), - ), - Expanded( - flex: 6, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - SvgPicture.asset( - Assets.svg.chevronRight, - width: 20, - height: 20, - color: Theme.of(context) - .extension<StackColors>()! - .textSubtitle1, - ) - ], - ), - ) - ], + .textSubtitle1, + ) + ], + ), + ) + ], + ), ), ), ), From 345a077e0611d3e294325bfd74529938f0c93e14 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 28 Nov 2022 15:37:18 -0600 Subject: [PATCH 421/426] desktop fav card hover effect --- .../sub_widgets/favorite_card.dart | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/lib/pages/wallets_view/sub_widgets/favorite_card.dart b/lib/pages/wallets_view/sub_widgets/favorite_card.dart index 7749f264d..24b1e021a 100644 --- a/lib/pages/wallets_view/sub_widgets/favorite_card.dart +++ b/lib/pages/wallets_view/sub_widgets/favorite_card.dart @@ -49,6 +49,8 @@ class _FavoriteCardState extends ConsumerState<FavoriteCard> { super.initState(); } + bool _hovering = false; + @override Widget build(BuildContext context) { final coin = ref.watch(managerProvider.select((value) => value.coin)); @@ -59,7 +61,48 @@ class _FavoriteCardState extends ConsumerState<FavoriteCard> { condition: Util.isDesktop, builder: (child) => MouseRegion( cursor: SystemMouseCursors.click, - child: child, + onEnter: (_) { + setState(() { + _hovering = true; + }); + }, + onExit: (_) { + setState(() { + _hovering = false; + }); + }, + child: AnimatedScale( + duration: const Duration(milliseconds: 200), + scale: _hovering ? 1.05 : 1, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + decoration: _hovering + ? BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + boxShadow: [ + Theme.of(context) + .extension<StackColors>()! + .standardBoxShadow, + Theme.of(context) + .extension<StackColors>()! + .standardBoxShadow, + Theme.of(context) + .extension<StackColors>()! + .standardBoxShadow, + ], + ) + : BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: child, + ), + ), ), child: GestureDetector( onTap: () { From 178565a19025d0348e04beded423db91110ba5b4 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 28 Nov 2022 15:43:35 -0600 Subject: [PATCH 422/426] date picker file license added --- lib/widgets/rounded_date_picker/LICENSE | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 lib/widgets/rounded_date_picker/LICENSE diff --git a/lib/widgets/rounded_date_picker/LICENSE b/lib/widgets/rounded_date_picker/LICENSE new file mode 100644 index 000000000..58665fbd2 --- /dev/null +++ b/lib/widgets/rounded_date_picker/LICENSE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in compliance with the License. You may obtain a copy of the License in the LICENSE file, or at: + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. \ No newline at end of file From 18da658a652863c7599e2602ba4b8472bfaab6f4 Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Sat, 26 Nov 2022 13:39:52 -0600 Subject: [PATCH 423/426] persist active wallet on desktop --- .../home/desktop_home_view.dart | 71 +++++++++++++++++-- .../my_stack_view/coin_wallets_table.dart | 4 ++ .../wallet_view/desktop_wallet_view.dart | 2 +- 3 files changed, 70 insertions(+), 7 deletions(-) diff --git a/lib/pages_desktop_specific/home/desktop_home_view.dart b/lib/pages_desktop_specific/home/desktop_home_view.dart index 54c74fe88..3e0b9311b 100644 --- a/lib/pages_desktop_specific/home/desktop_home_view.dart +++ b/lib/pages_desktop_specific/home/desktop_home_view.dart @@ -9,12 +9,19 @@ import 'package:stackwallet/pages_desktop_specific/home/notifications/desktop_no import 'package:stackwallet/pages_desktop_specific/home/support_and_about_view/desktop_about_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/support_and_about_view/desktop_support_view.dart'; import 'package:stackwallet/providers/desktop/current_desktop_menu_item.dart'; +import 'package:stackwallet/providers/global/auto_swb_service_provider.dart'; import 'package:stackwallet/providers/global/notifications_provider.dart'; +import 'package:stackwallet/providers/global/prefs_provider.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/providers/ui/transaction_filter_provider.dart'; import 'package:stackwallet/providers/ui/unread_notifications_provider.dart'; import 'package:stackwallet/route_generator.dart'; +import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/background.dart'; +final currentWalletIdProvider = StateProvider<String?>((_) => null); + class DesktopHomeView extends ConsumerStatefulWidget { const DesktopHomeView({Key? key}) : super(key: key); @@ -25,12 +32,25 @@ class DesktopHomeView extends ConsumerStatefulWidget { } class _DesktopHomeViewState extends ConsumerState<DesktopHomeView> { - final Map<DesktopMenuItemId, Widget> contentViews = { - DesktopMenuItemId.myStack: const Navigator( - key: Key("desktopStackHomeKey"), + final GlobalKey key = GlobalKey<NavigatorState>(); + late final Navigator myStackViewNav; + + @override + void initState() { + myStackViewNav = Navigator( + key: key, onGenerateRoute: RouteGenerator.generateRoute, initialRoute: MyStackView.routeName, - ), + ); + super.initState(); + } + + final Map<DesktopMenuItemId, Widget> contentViews = { + DesktopMenuItemId.myStack: Container( + // key: Key("desktopStackHomeKey"), + // onGenerateRoute: RouteGenerator.generateRoute, + // initialRoute: MyStackView.routeName, + ), DesktopMenuItemId.exchange: const Navigator( key: Key("desktopExchangeHomeKey"), onGenerateRoute: RouteGenerator.generateRoute, @@ -63,7 +83,30 @@ class _DesktopHomeViewState extends ConsumerState<DesktopHomeView> { ), }; + DesktopMenuItemId prev = DesktopMenuItemId.myStack; + void onMenuSelectionWillChange(DesktopMenuItemId newKey) { + if (prev == DesktopMenuItemId.myStack && prev == newKey) { + Navigator.of(key.currentContext!) + .popUntil(ModalRoute.withName(MyStackView.routeName)); + if (ref.read(currentWalletIdProvider.state).state != null) { + final managerProvider = ref + .read(walletsChangeNotifierProvider) + .getManagerProvider(ref.read(currentWalletIdProvider.state).state!); + if (ref.read(managerProvider).shouldAutoSync) { + ref.read(managerProvider).shouldAutoSync = false; + } + ref.read(transactionFilterProvider.state).state = null; + if (ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled && + ref.read(prefsChangeNotifierProvider).backupFrequencyType == + BackupFrequencyType.afterClosingAWallet) { + ref.read(autoSWBServiceProvider).doBackup(); + } + ref.read(managerProvider.notifier).isActiveWallet = false; + } + } + prev = newKey; + // check for unread notifications and refresh provider before // showing notifications view if (newKey == DesktopMenuItemId.notifications) { @@ -111,9 +154,25 @@ class _DesktopHomeViewState extends ConsumerState<DesktopHomeView> { color: Theme.of(context).extension<StackColors>()!.background, ), Expanded( - child: contentViews[ - ref.watch(currentDesktopMenuItemProvider.state).state]!, + child: IndexedStack( + index: ref + .watch(currentDesktopMenuItemProvider.state) + .state + .index > + 0 + ? 1 + : 0, + children: [ + myStackViewNav, + contentViews[ + ref.watch(currentDesktopMenuItemProvider.state).state]!, + ], + ), ), + // Expanded( + // child: contentViews[ + // ref.watch(currentDesktopMenuItemProvider.state).state]!, + // ), ], ), ), diff --git a/lib/pages_desktop_specific/home/my_stack_view/coin_wallets_table.dart b/lib/pages_desktop_specific/home/my_stack_view/coin_wallets_table.dart index 4ed8765ae..1edb93e06 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/coin_wallets_table.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/coin_wallets_table.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -51,6 +52,9 @@ class CoinWalletsTable extends ConsumerWidget { Positioned.fill( child: WalletRowHoverOverlay( onPressed: () async { + ref.read(currentWalletIdProvider.state).state = + walletIds[i]; + await Navigator.of(context).pushNamed( DesktopWalletView.routeName, arguments: walletIds[i], diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart index 5996597b5..d870835f1 100644 --- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -81,13 +81,13 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> { // disable auto sync if it was enabled only when loading wallet ref.read(managerProvider).shouldAutoSync = false; } - ref.read(managerProvider.notifier).isActiveWallet = false; ref.read(transactionFilterProvider.state).state = null; if (ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled && ref.read(prefsChangeNotifierProvider).backupFrequencyType == BackupFrequencyType.afterClosingAWallet) { unawaited(ref.read(autoSWBServiceProvider).doBackup()); } + ref.read(managerProvider.notifier).isActiveWallet = false; } void _loadCNData() { From c9a91e10ac3d260a14714394c154bca7f7df591e Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 28 Nov 2022 16:11:02 -0600 Subject: [PATCH 424/426] clean up theme init --- lib/main.dart | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index a49bcab82..8136965db 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -318,17 +318,17 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme> final colorScheme = DB.instance .get<dynamic>(boxName: DB.boxNameTheme, key: "colorScheme") as String?; - ThemeType themeType; + StackColorTheme colorTheme; switch (colorScheme) { case "dark": - themeType = ThemeType.dark; + colorTheme = DarkColors(); break; case "oceanBreeze": - themeType = ThemeType.oceanBreeze; + colorTheme = OceanBreezeColors(); break; case "light": default: - themeType = ThemeType.light; + colorTheme = LightColors(); } loadingCompleter = Completer(); WidgetsBinding.instance.addObserver(this); @@ -339,11 +339,7 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme> WidgetsBinding.instance.addPostFrameCallback((_) async { ref.read(colorThemeProvider.state).state = - StackColors.fromStackColorTheme(themeType == ThemeType.dark - ? DarkColors() - : (themeType == ThemeType.light - ? LightColors() - : OceanBreezeColors())); + StackColors.fromStackColorTheme(colorTheme); if (Platform.isAndroid) { // fetch open file if it exists From 1aca715397a4146059c53ca446eea229a868d73e Mon Sep 17 00:00:00 2001 From: julian <julian@cypherstack.com> Date: Mon, 28 Nov 2022 16:17:33 -0600 Subject: [PATCH 425/426] animated desktop menu fix --- .../home/desktop_menu.dart | 16 ---------------- .../home/desktop_menu_item.dart | 14 +++++++------- 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/lib/pages_desktop_specific/home/desktop_menu.dart b/lib/pages_desktop_specific/home/desktop_menu.dart index 8af307e7b..fd404db94 100644 --- a/lib/pages_desktop_specific/home/desktop_menu.dart +++ b/lib/pages_desktop_specific/home/desktop_menu.dart @@ -157,8 +157,6 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { ), label: "My Stack", value: DesktopMenuItemId.myStack, - group: - ref.watch(currentDesktopMenuItemProvider.state).state, onChanged: updateSelectedMenuItem, controller: controllers[0], ), @@ -185,8 +183,6 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { ), label: "Exchange", value: DesktopMenuItemId.exchange, - group: - ref.watch(currentDesktopMenuItemProvider.state).state, onChanged: updateSelectedMenuItem, controller: controllers[1], ), @@ -220,8 +216,6 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { ), label: "Notifications", value: DesktopMenuItemId.notifications, - group: - ref.watch(currentDesktopMenuItemProvider.state).state, onChanged: updateSelectedMenuItem, controller: controllers[2], ), @@ -248,8 +242,6 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { ), label: "Address Book", value: DesktopMenuItemId.addressBook, - group: - ref.watch(currentDesktopMenuItemProvider.state).state, onChanged: updateSelectedMenuItem, controller: controllers[3], ), @@ -276,8 +268,6 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { ), label: "Settings", value: DesktopMenuItemId.settings, - group: - ref.watch(currentDesktopMenuItemProvider.state).state, onChanged: updateSelectedMenuItem, controller: controllers[4], ), @@ -304,8 +294,6 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { ), label: "Support", value: DesktopMenuItemId.support, - group: - ref.watch(currentDesktopMenuItemProvider.state).state, onChanged: updateSelectedMenuItem, controller: controllers[5], ), @@ -332,8 +320,6 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { ), label: "About", value: DesktopMenuItemId.about, - group: - ref.watch(currentDesktopMenuItemProvider.state).state, onChanged: updateSelectedMenuItem, controller: controllers[6], ), @@ -352,8 +338,6 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { ), label: "Exit", value: 7, - group: - ref.watch(currentDesktopMenuItemProvider.state).state, onChanged: (_) { // todo: save stuff/ notify before exit? exit(0); diff --git a/lib/pages_desktop_specific/home/desktop_menu_item.dart b/lib/pages_desktop_specific/home/desktop_menu_item.dart index 1fb39213b..78dcde79b 100644 --- a/lib/pages_desktop_specific/home/desktop_menu_item.dart +++ b/lib/pages_desktop_specific/home/desktop_menu_item.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/providers/desktop/current_desktop_menu_item.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -9,13 +11,12 @@ class DMIController { } } -class DesktopMenuItem<T> extends StatefulWidget { +class DesktopMenuItem<T> extends ConsumerStatefulWidget { const DesktopMenuItem({ Key? key, required this.icon, required this.label, required this.value, - required this.group, required this.onChanged, required this.duration, this.labelLength = 125, @@ -25,22 +26,20 @@ class DesktopMenuItem<T> extends StatefulWidget { final Widget icon; final String label; final T value; - final T group; final void Function(T) onChanged; final Duration duration; final double labelLength; final DMIController? controller; @override - State<DesktopMenuItem<T>> createState() => _DesktopMenuItemState<T>(); + ConsumerState<DesktopMenuItem<T>> createState() => _DesktopMenuItemState<T>(); } -class _DesktopMenuItemState<T> extends State<DesktopMenuItem<T>> +class _DesktopMenuItemState<T> extends ConsumerState<DesktopMenuItem<T>> with SingleTickerProviderStateMixin { late final Widget icon; late final String label; late final T value; - late final T group; late final void Function(T) onChanged; late final Duration duration; late final double labelLength; @@ -67,7 +66,6 @@ class _DesktopMenuItemState<T> extends State<DesktopMenuItem<T>> icon = widget.icon; label = widget.label; value = widget.value; - group = widget.group; onChanged = widget.onChanged; duration = widget.duration; labelLength = widget.labelLength; @@ -91,6 +89,8 @@ class _DesktopMenuItemState<T> extends State<DesktopMenuItem<T>> @override Widget build(BuildContext context) { + final group = ref.watch(currentDesktopMenuItemProvider.state).state; + debugPrint("============ value:$value ============ group:$group"); return TextButton( style: value == group ? Theme.of(context) From df4592214930849b033e921fb8e6bf275e3ce475 Mon Sep 17 00:00:00 2001 From: Diego Salazar <diego@cypherstack.com> Date: Mon, 28 Nov 2022 17:02:26 -0700 Subject: [PATCH 426/426] Bump version. 1.5.21, build 93. --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index ef1b495a5..8cfce39cb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.5.20+92 +version: 1.5.21+93 environment: sdk: ">=2.17.0 <3.0.0"