diff --git a/assets/svg/swap2.svg b/assets/svg/swap2.svg new file mode 100644 index 000000000..1c9ce8191 --- /dev/null +++ b/assets/svg/swap2.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/lib/electrumx_rpc/client_manager.dart b/lib/electrumx_rpc/client_manager.dart index 8e580ec78..26db04b4b 100644 --- a/lib/electrumx_rpc/client_manager.dart +++ b/lib/electrumx_rpc/client_manager.dart @@ -51,12 +51,12 @@ class ClientManager { if (_map[key] == null) { throw Exception( - "No managed ElectrumClient for $cryptoCurrency found.", + "No managed ElectrumClient for $key found.", ); } if (_heightCompleters[key] == null) { throw Exception( - "No managed _heightCompleters for $cryptoCurrency found.", + "No managed _heightCompleters for $key found.", ); } diff --git a/lib/frost_route_generator.dart b/lib/frost_route_generator.dart new file mode 100644 index 000000000..a6905a243 --- /dev/null +++ b/lib/frost_route_generator.dart @@ -0,0 +1,274 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1a.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1b.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_4.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1a.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1b.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2abd.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2c.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3c.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_5.dart'; +import 'package:stackwallet/pages/send_view/frost_ms/send_steps/frost_send_step_1a.dart'; +import 'package:stackwallet/pages/send_view/frost_ms/send_steps/frost_send_step_1b.dart'; +import 'package:stackwallet/pages/send_view/frost_ms/send_steps/frost_send_step_2.dart'; +import 'package:stackwallet/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart'; +import 'package:stackwallet/pages/send_view/frost_ms/send_steps/frost_send_step_4.dart'; +import 'package:stackwallet/route_generator.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/frost_currency.dart'; + +typedef FrostStepRoute = ({String routeName, String title}); + +enum FrostInterruptionDialogType { + walletCreation, + resharing, + transactionCreation; +} + +final pFrostCreateCurrentStep = StateProvider.autoDispose((ref) => 1); +final pFrostScaffoldCanPopDesktop = StateProvider.autoDispose((_) => false); +final pFrostScaffoldArgs = StateProvider< + ({ + ({String walletName, FrostCurrency frostCurrency}) info, + String? walletId, + List stepRoutes, + FrostInterruptionDialogType frostInterruptionDialogType, + NavigatorState parentNav, + })?>((ref) => null); + +abstract class FrostRouteGenerator { + static const bool useMaterialPageRoute = true; + + static const List createNewConfigStepRoutes = [ + (routeName: FrostCreateStep1a.routeName, title: FrostCreateStep1a.title), + (routeName: FrostCreateStep2.routeName, title: FrostCreateStep2.title), + (routeName: FrostCreateStep3.routeName, title: FrostCreateStep3.title), + (routeName: FrostCreateStep4.routeName, title: FrostCreateStep4.title), + (routeName: FrostCreateStep5.routeName, title: FrostCreateStep5.title), + ]; + + static const List importNewConfigStepRoutes = [ + (routeName: FrostCreateStep1b.routeName, title: FrostCreateStep1b.title), + (routeName: FrostCreateStep2.routeName, title: FrostCreateStep2.title), + (routeName: FrostCreateStep3.routeName, title: FrostCreateStep3.title), + (routeName: FrostCreateStep4.routeName, title: FrostCreateStep4.title), + (routeName: FrostCreateStep5.routeName, title: FrostCreateStep5.title), + ]; + + static const List initiateReshareStepRoutes = [ + (routeName: FrostReshareStep1a.routeName, title: FrostReshareStep1a.title), + ( + routeName: FrostReshareStep2abd.routeName, + title: FrostReshareStep2abd.title + ), + ( + routeName: FrostReshareStep3abd.routeName, + title: FrostReshareStep3abd.title + ), + (routeName: FrostReshareStep4.routeName, title: FrostReshareStep4.title), + (routeName: FrostReshareStep5.routeName, title: FrostReshareStep5.title), + ]; + + static const List importReshareStepRoutes = [ + (routeName: FrostReshareStep1b.routeName, title: FrostReshareStep1b.title), + ( + routeName: FrostReshareStep2abd.routeName, + title: FrostReshareStep2abd.title + ), + ( + routeName: FrostReshareStep3abd.routeName, + title: FrostReshareStep3abd.title + ), + (routeName: FrostReshareStep4.routeName, title: FrostReshareStep4.title), + (routeName: FrostReshareStep5.routeName, title: FrostReshareStep5.title), + ]; + + static const List joinReshareStepRoutes = [ + (routeName: FrostReshareStep1c.routeName, title: FrostReshareStep1c.title), + (routeName: FrostReshareStep2c.routeName, title: FrostReshareStep2c.title), + (routeName: FrostReshareStep3c.routeName, title: FrostReshareStep3c.title), + (routeName: FrostReshareStep4.routeName, title: FrostReshareStep4.title), + (routeName: FrostReshareStep5.routeName, title: FrostReshareStep5.title), + ]; + + static const List sendFrostTxStepRoutes = [ + (routeName: FrostSendStep1a.routeName, title: FrostSendStep1a.title), + (routeName: FrostSendStep2.routeName, title: FrostSendStep2.title), + (routeName: FrostSendStep3.routeName, title: FrostSendStep3.title), + (routeName: FrostSendStep4.routeName, title: FrostSendStep4.title), + ]; + + static const List signFrostTxStepRoutes = [ + (routeName: FrostSendStep1b.routeName, title: FrostSendStep1b.title), + (routeName: FrostSendStep2.routeName, title: FrostSendStep2.title), + (routeName: FrostSendStep3.routeName, title: FrostSendStep3.title), + (routeName: FrostSendStep4.routeName, title: FrostSendStep4.title), + ]; + + static Route generateRoute(RouteSettings settings) { + final args = settings.arguments; + + switch (settings.name) { + case FrostCreateStep1a.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostCreateStep1a(), + settings: settings, + ); + + case FrostCreateStep1b.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostCreateStep1b(), + settings: settings, + ); + + case FrostCreateStep2.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostCreateStep2(), + settings: settings, + ); + + case FrostCreateStep3.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostCreateStep3(), + settings: settings, + ); + + case FrostCreateStep4.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostCreateStep4(), + settings: settings, + ); + + case FrostCreateStep5.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostCreateStep5(), + settings: settings, + ); + + case FrostReshareStep1a.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostReshareStep1a(), + settings: settings, + ); + + case FrostReshareStep1b.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostReshareStep1b(), + settings: settings, + ); + + case FrostReshareStep1c.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostReshareStep1c(), + settings: settings, + ); + + case FrostReshareStep2abd.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostReshareStep2abd(), + settings: settings, + ); + + case FrostReshareStep2c.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostReshareStep2c(), + settings: settings, + ); + + case FrostReshareStep3abd.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostReshareStep3abd(), + settings: settings, + ); + + case FrostReshareStep3c.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostReshareStep3c(), + settings: settings, + ); + + case FrostReshareStep4.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostReshareStep4(), + settings: settings, + ); + + case FrostReshareStep5.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostReshareStep5(), + settings: settings, + ); + + case FrostSendStep1a.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostSendStep1a(), + settings: settings, + ); + + case FrostSendStep1b.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostSendStep1b(), + settings: settings, + ); + + case FrostSendStep2.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostSendStep2(), + settings: settings, + ); + + case FrostSendStep3.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostSendStep3(), + settings: settings, + ); + + case FrostSendStep4.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostSendStep4(), + settings: settings, + ); + + default: + return _routeError(""); + } + } + + static Route _routeError(String message) { + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => Placeholder( + child: Center( + child: Text(message), + ), + ), + ); + } +} 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 d84bfc708..6d0582c5c 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 @@ -46,7 +46,7 @@ import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; class AddWalletView extends ConsumerStatefulWidget { - const AddWalletView({Key? key}) : super(key: key); + const AddWalletView({super.key}); static const routeName = "/addWallet"; @@ -134,11 +134,6 @@ class _AddWalletViewState extends ConsumerState { _coins.remove(Coin.wownero); } - // Remove FROST from the list of coins based on our frostEnabled preference. - if (!ref.read(prefsChangeNotifierProvider).frostEnabled) { - _coins.remove(Coin.bitcoinFrost); - } - // Remove Solana from the list of coins based on our frostEnabled preference. if (!ref.read(prefsChangeNotifierProvider).solanaEnabled) { _coins.remove(Coin.solana); @@ -147,10 +142,6 @@ class _AddWalletViewState extends ConsumerState { coinEntities.addAll(_coins.map((e) => CoinEntity(e))); if (ref.read(prefsChangeNotifierProvider).showTestNetCoins) { - if (!ref.read(prefsChangeNotifierProvider).frostEnabled) { - _coinsTestnet.remove(Coin.bitcoinFrostTestNet); - } - coinEntities.addAll(_coinsTestnet.map((e) => CoinEntity(e))); } diff --git a/lib/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart b/lib/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart deleted file mode 100644 index ffd1127b6..000000000 --- a/lib/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart +++ /dev/null @@ -1,336 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/notifications/show_flush_bar.dart'; -import 'package:stackwallet/pages/frost_mascot.dart'; -import 'package:stackwallet/pages/home_view/home_view.dart'; -import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; -import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; -import 'package:stackwallet/providers/db/main_db_provider.dart'; -import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; -import 'package:stackwallet/providers/global/node_service_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/services/frost.dart'; -import 'package:stackwallet/themes/stack_colors.dart'; -import 'package:stackwallet/utilities/assets.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/util.dart'; -import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; -import 'package:stackwallet/wallets/wallet/wallet.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/simple_copy_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/detail_item.dart'; -import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart'; -import 'package:stackwallet/widgets/loading_indicator.dart'; - -import 'package:stackwallet/wallets/isar/models/wallet_info.dart'; - -class ConfirmNewFrostMSWalletCreationView extends ConsumerStatefulWidget { - const ConfirmNewFrostMSWalletCreationView({ - super.key, - required this.walletName, - required this.coin, - }); - - static const String routeName = "/confirmNewFrostMSWalletCreationView"; - - final String walletName; - final Coin coin; - - @override - ConsumerState createState() => - _ConfirmNewFrostMSWalletCreationViewState(); -} - -class _ConfirmNewFrostMSWalletCreationViewState - extends ConsumerState { - late final String seed, recoveryString, serializedKeys, multisigConfig; - late final Uint8List multisigId; - - @override - void initState() { - seed = ref.read(pFrostStartKeyGenData.state).state!.seed; - serializedKeys = - ref.read(pFrostCompletedKeyGenData.state).state!.serializedKeys; - recoveryString = - ref.read(pFrostCompletedKeyGenData.state).state!.recoveryString; - multisigId = ref.read(pFrostCompletedKeyGenData.state).state!.multisigId; - multisigConfig = ref.read(pFrostMultisigConfig.state).state!; - - super.initState(); - } - - @override - Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { - await showDialog( - context: context, - builder: (_) => FrostInterruptionDialog( - type: FrostInterruptionDialogType.walletCreation, - popUntilOnYesRouteName: - Util.isDesktop ? DesktopHomeView.routeName : HomeView.routeName, - ), - ); - - return false; - }, - child: ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.walletCreation, - popUntilOnYesRouteName: DesktopHomeView.routeName, - ), - ); - }, - ), - trailing: FrostMascot( - title: 'Lorem ipsum', - body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', - ), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.walletCreation, - popUntilOnYesRouteName: HomeView.routeName, - ), - ); - }, - ), - title: Text( - "Finalize FROST multisig wallet", - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(16), - child: child, - ), - ), - ), - ); - }, - ), - ), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Ensure your multisig ID matches that of each other participant", - style: STextStyles.pageTitleH2(context), - ), - const _Div(), - DetailItem( - title: "ID", - detail: multisigId.toString(), - button: Util.isDesktop - ? IconCopyButton( - data: multisigId.toString(), - ) - : SimpleCopyButton( - data: multisigId.toString(), - ), - ), - const _Div(), - const _Div(), - Text( - "Back up your keys and config", - style: STextStyles.pageTitleH2(context), - ), - const _Div(), - DetailItem( - title: "Multisig Config", - detail: multisigConfig, - button: Util.isDesktop - ? IconCopyButton( - data: multisigConfig, - ) - : SimpleCopyButton( - data: multisigConfig, - ), - ), - const _Div(), - DetailItem( - title: "Keys", - detail: serializedKeys, - button: Util.isDesktop - ? IconCopyButton( - data: serializedKeys, - ) - : SimpleCopyButton( - data: serializedKeys, - ), - ), - if (!Util.isDesktop) const Spacer(), - const _Div(), - PrimaryButton( - label: "Confirm", - onPressed: () async { - bool progressPopped = false; - try { - unawaited( - showDialog( - context: context, - barrierDismissible: false, - useSafeArea: true, - builder: (ctx) { - return const Center( - child: LoadingIndicator( - width: 50, - height: 50, - ), - ); - }, - ), - ); - - final info = WalletInfo.createNew( - coin: widget.coin, - name: widget.walletName, - ); - - final wallet = await Wallet.create( - walletInfo: info, - mainDB: ref.read(mainDBProvider), - secureStorageInterface: ref.read(secureStoreProvider), - nodeService: ref.read(nodeServiceChangeNotifierProvider), - prefs: ref.read(prefsChangeNotifierProvider), - ); - - await (wallet as BitcoinFrostWallet).initializeNewFrost( - mnemonic: seed, - multisigConfig: multisigConfig, - recoveryString: recoveryString, - serializedKeys: serializedKeys, - multisigId: multisigId, - myName: ref.read(pFrostMyName.state).state!, - participants: Frost.getParticipants( - multisigConfig: - ref.read(pFrostMultisigConfig.state).state!, - ), - threshold: Frost.getThreshold( - multisigConfig: - ref.read(pFrostMultisigConfig.state).state!, - ), - ); - - await info.setMnemonicVerified( - isar: ref.read(mainDBProvider).isar, - ); - - ref.read(pWallets).addWallet(wallet); - - // pop progress dialog - if (mounted) { - Navigator.pop(context); - progressPopped = true; - } - - if (mounted) { - if (Util.isDesktop) { - Navigator.of(context).popUntil( - ModalRoute.withName( - DesktopHomeView.routeName, - ), - ); - } else { - unawaited( - Navigator.of(context).pushNamedAndRemoveUntil( - HomeView.routeName, - (route) => false, - ), - ); - } - - ref.read(pFrostMultisigConfig.state).state = null; - ref.read(pFrostStartKeyGenData.state).state = null; - ref.read(pFrostSecretSharesData.state).state = null; - - unawaited( - showFloatingFlushBar( - type: FlushBarType.success, - message: "Your wallet is set up.", - iconAsset: Assets.svg.check, - context: context, - ), - ); - } - } catch (e, s) { - Logging.instance.log( - "$e\n$s", - level: LogLevel.Fatal, - ); - - // pop progress dialog - if (mounted && !progressPopped) { - Navigator.pop(context); - progressPopped = true; - } - // TODO: handle gracefully - rethrow; - } - }, - ), - ], - ), - ), - ), - ); - } -} - -class _Div extends StatelessWidget { - const _Div({super.key}); - - @override - Widget build(BuildContext context) { - return const SizedBox( - height: 12, - ); - } -} diff --git a/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart index 9f42a3fa6..620d9df06 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart @@ -1,33 +1,37 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/pages/frost_mascot.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart'; +import 'package:stackwallet/frost_route_generator.dart'; import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; import 'package:stackwallet/services/frost.dart'; import 'package:stackwallet/themes/stack_colors.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/frost_currency.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'; 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/dialogs/simple_mobile_dialog.dart'; +import 'package:stackwallet/widgets/frost_mascot.dart'; +import 'package:stackwallet/widgets/frost_scaffold.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; class CreateNewFrostMsWalletView extends ConsumerStatefulWidget { const CreateNewFrostMsWalletView({ super.key, required this.walletName, - required this.coin, + required this.frostCurrency, }); static const String routeName = "/createNewFrostMsWalletView"; final String walletName; - final Coin coin; + final FrostCurrency frostCurrency; @override ConsumerState createState() => @@ -67,13 +71,14 @@ class _NewFrostMsWalletViewState } final hasEmptyParticipants = controllers - .map((e) => e.text.isEmpty) + .map((e) => e.text.trim().isEmpty) .reduce((value, element) => value |= element); if (hasEmptyParticipants) { return "Participants must not be empty"; } - if (controllers.length != controllers.map((e) => e.text).toSet().length) { + if (controllers.length != + controllers.map((e) => e.text.trim()).toSet().length) { return "Duplicate participant name found"; } @@ -102,6 +107,31 @@ class _NewFrostMsWalletViewState } } + void _showWhatIsThresholdDialog() { + showDialog( + context: context, + builder: (_) => SimpleMobileDialog( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // TODO: [prio=high] need text from designers! + Text( + "What is a threshold?", + style: STextStyles.w600_20(context), + ), + const SizedBox( + height: 12, + ), + Text( + "Text here", + style: STextStyles.w400_16(context), + ), + ], + ), + ), + ); + } + @override void dispose() { _thresholdController.dispose(); @@ -118,12 +148,14 @@ class _NewFrostMsWalletViewState condition: Util.isDesktop, builder: (child) => DesktopScaffold( background: Theme.of(context).extension()!.background, - appBar: DesktopAppBar( + appBar: const DesktopAppBar( isCompactHeight: false, leading: AppBarBackButton(), + // TODO: [prio=high] get rid of placeholder text?? trailing: FrostMascot( title: 'Lorem ipsum', - body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', + body: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', ), ), body: SizedBox( @@ -144,7 +176,7 @@ class _NewFrostMsWalletViewState }, ), title: Text( - "New FROST multisig config", + "Create new group", style: STextStyles.navBarTitle(context), ), ), @@ -172,9 +204,21 @@ class _NewFrostMsWalletViewState child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Threshold", - style: STextStyles.label(context), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Threshold", + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of(context).extension()!.textDark3, + ), + ), + CustomTextButton( + text: "What is a threshold?", + onTap: _showWhatIsThresholdDialog, + ), + ], ), const SizedBox( height: 10, @@ -183,22 +227,53 @@ class _NewFrostMsWalletViewState keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], controller: _thresholdController, + decoration: InputDecoration( + hintText: "Enter number of signatures", + hintStyle: STextStyles.fieldLabel(context), + ), ), const SizedBox( height: 16, ), Text( "Number of participants", - style: STextStyles.label(context), + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context).extension()!.textDark3, + ), ), const SizedBox( height: 10, ), - TextField( - keyboardType: TextInputType.number, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - controller: _participantsController, - onChanged: _participantsCountChanged, + Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + TextField( + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + controller: _participantsController, + onChanged: _participantsCountChanged, + decoration: InputDecoration( + hintText: "Enter number of participants", + hintStyle: STextStyles.fieldLabel(context), + ), + ), + const SizedBox( + height: 6, + ), + Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + child: Text( + "Enter number of signatures required for fund management", + style: STextStyles.label(context), + ), + ), + ), + ], + ), + ], ), const SizedBox( height: 16, @@ -206,24 +281,75 @@ class _NewFrostMsWalletViewState if (controllers.isNotEmpty) Text( "My name", - style: STextStyles.label(context), + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context).extension()!.textDark3, + ), ), if (controllers.isNotEmpty) const SizedBox( height: 10, ), if (controllers.isNotEmpty) - TextField( - controller: controllers.first, + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + controller: controllers.first, + decoration: InputDecoration( + hintText: "Enter your name", + hintStyle: STextStyles.fieldLabel(context), + ), + ), + const SizedBox( + height: 6, + ), + Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + child: Text( + "Type your name in one word without spaces", + style: STextStyles.label(context), + ), + ), + ), + ], + ), + ], ), if (controllers.length > 1) const SizedBox( height: 16, ), if (controllers.length > 1) - Text( - "Remaining participants", - style: STextStyles.label(context), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Remaining participants", + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of(context).extension()!.textDark3, + ), + ), + const SizedBox( + height: 6, + ), + Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + child: Text( + "Type each name in one word without spaces", + style: STextStyles.label(context), + ), + ), + ), + ], + ), + ], ), if (controllers.length > 1) Column( @@ -235,6 +361,10 @@ class _NewFrostMsWalletViewState ), child: TextField( controller: controllers[i], + decoration: InputDecoration( + hintText: "Enter name", + hintStyle: STextStyles.fieldLabel(context), + ), ), ), ], @@ -244,7 +374,7 @@ class _NewFrostMsWalletViewState height: 16, ), PrimaryButton( - label: "Generate", + label: "Create new group", onPressed: () async { if (FocusScope.of(context).hasFocus) { FocusScope.of(context).unfocus(); @@ -263,20 +393,29 @@ class _NewFrostMsWalletViewState } final config = Frost.createMultisigConfig( - name: controllers.first.text, + name: controllers.first.text.trim(), threshold: int.parse(_thresholdController.text), - participants: controllers.map((e) => e.text).toList(), + participants: controllers.map((e) => e.text.trim()).toList(), ); - ref.read(pFrostMyName.notifier).state = controllers.first.text; + ref.read(pFrostMyName.notifier).state = + controllers.first.text.trim(); ref.read(pFrostMultisigConfig.notifier).state = config; - await Navigator.of(context).pushNamed( - ShareNewMultisigConfigView.routeName, - arguments: ( + ref.read(pFrostScaffoldArgs.state).state = ( + info: ( walletName: widget.walletName, - coin: widget.coin, + frostCurrency: widget.frostCurrency, ), + walletId: null, + stepRoutes: FrostRouteGenerator.createNewConfigStepRoutes, + frostInterruptionDialogType: + FrostInterruptionDialogType.walletCreation, + parentNav: Navigator.of(context), + ); + + await Navigator.of(context).pushNamed( + FrostStepScaffold.routeName, ); }, ), diff --git a/lib/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart b/lib/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart deleted file mode 100644 index 1234dbf8a..000000000 --- a/lib/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart +++ /dev/null @@ -1,436 +0,0 @@ -import 'package:barcode_scan2/barcode_scan2.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:qr_flutter/qr_flutter.dart'; -import 'package:stackwallet/pages/frost_mascot.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart'; -import 'package:stackwallet/pages/home_view/home_view.dart'; -import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; -import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; -import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; -import 'package:stackwallet/services/frost.dart'; -import 'package:stackwallet/themes/stack_colors.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/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/simple_copy_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/detail_item.dart'; -import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.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'; - -class FrostShareCommitmentsView extends ConsumerStatefulWidget { - const FrostShareCommitmentsView({ - super.key, - required this.walletName, - required this.coin, - }); - - static const String routeName = "/frostShareCommitmentsView"; - - final String walletName; - final Coin coin; - - @override - ConsumerState createState() => - _FrostShareCommitmentsViewState(); -} - -class _FrostShareCommitmentsViewState - extends ConsumerState { - final List controllers = []; - final List focusNodes = []; - - late final List participants; - late final String myCommitment; - late final int myIndex; - - final List fieldIsEmptyFlags = []; - - @override - void initState() { - participants = Frost.getParticipants( - multisigConfig: ref.read(pFrostMultisigConfig.state).state!, - ); - myIndex = participants.indexOf(ref.read(pFrostMyName.state).state!); - myCommitment = ref.read(pFrostStartKeyGenData.state).state!.commitments; - - // temporarily remove my name - participants.removeAt(myIndex); - - for (int i = 0; i < participants.length; i++) { - controllers.add(TextEditingController()); - focusNodes.add(FocusNode()); - fieldIsEmptyFlags.add(true); - } - super.initState(); - } - - @override - void dispose() { - for (int i = 0; i < controllers.length; i++) { - controllers[i].dispose(); - } - for (int i = 0; i < focusNodes.length; i++) { - focusNodes[i].dispose(); - } - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { - await showDialog( - context: context, - builder: (_) => FrostInterruptionDialog( - type: FrostInterruptionDialogType.walletCreation, - popUntilOnYesRouteName: - Util.isDesktop ? DesktopHomeView.routeName : HomeView.routeName, - ), - ); - return false; - }, - child: ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.walletCreation, - popUntilOnYesRouteName: DesktopHomeView.routeName, - ), - ); - }, - ), - trailing: FrostMascot( - title: 'Lorem ipsum', - body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', - ), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.walletCreation, - popUntilOnYesRouteName: HomeView.routeName, - ), - ); - }, - ), - title: Text( - "Commitments", - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(16), - child: child, - ), - ), - ), - ); - }, - ), - ), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: 220, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QrImageView( - data: myCommitment, - size: 220, - backgroundColor: Theme.of(context) - .extension()! - .background, - foregroundColor: Theme.of(context) - .extension()! - .accentColorDark, - ), - ], - ), - ), - const _Div(), - DetailItem( - title: "My name", - detail: ref.watch(pFrostMyName.state).state!, - ), - const _Div(), - DetailItem( - title: "My commitment", - detail: myCommitment, - button: Util.isDesktop - ? IconCopyButton( - data: myCommitment, - ) - : SimpleCopyButton( - data: myCommitment, - ), - ), - const _Div(), - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - for (int i = 0; i < participants.length; i++) - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: Key("frostCommitmentsTextFieldKey_$i"), - controller: controllers[i], - focusNode: focusNodes[i], - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - onChanged: (_) { - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].text.isEmpty; - }); - }, - decoration: standardInputDecoration( - "Enter ${participants[i]}'s commitment", - focusNodes[i], - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: fieldIsEmptyFlags[i] - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - !fieldIsEmptyFlags[i] - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Commitment Field Input.", - key: Key( - "frostCommitmentsClearButtonKey_$i"), - onTap: () { - controllers[i].text = ""; - - setState(() { - fieldIsEmptyFlags[i] = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Commitment Field Input.", - key: Key( - "frostCommitmentsPasteButtonKey_$i"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - controllers[i].text = - data.text!.trim(); - } - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i] - .text - .isEmpty; - }); - }, - child: fieldIsEmptyFlags[i] - ? const ClipboardIcon() - : const XIcon(), - ), - if (fieldIsEmptyFlags[i]) - TextFieldIconButton( - semanticsLabel: - "Scan QR Button. Opens Camera For Scanning QR Code.", - key: Key( - "frostCommitmentsScanQrButtonKey_$i"), - onTap: () async { - try { - if (FocusScope.of(context) - .hasFocus) { - FocusScope.of(context) - .unfocus(); - await Future.delayed( - const Duration( - milliseconds: 75)); - } - - final qrResult = - await BarcodeScanner.scan(); - - controllers[i].text = - qrResult.rawContent; - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i] - .text - .isEmpty; - }); - } on PlatformException catch (e, s) { - Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code: $e\n$s", - level: LogLevel.Warning, - ); - } - }, - child: const QrCodeIcon(), - ) - ], - ), - ), - ), - ), - ), - ), - ), - ], - ), - ], - ), - if (!Util.isDesktop) const Spacer(), - const _Div(), - PrimaryButton( - label: "Generate shares", - enabled: !fieldIsEmptyFlags.reduce((v, e) => v |= e), - onPressed: () async { - // check for empty commitments - if (controllers - .map((e) => e.text.isEmpty) - .reduce((value, element) => value |= element)) { - return await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Missing commitments", - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } - - // collect commitment strings and insert my own at the correct index - final commitments = controllers.map((e) => e.text).toList(); - commitments.insert(myIndex, myCommitment); - - try { - ref.read(pFrostSecretSharesData.notifier).state = - Frost.generateSecretShares( - multisigConfigWithNamePtr: ref - .read(pFrostStartKeyGenData.state) - .state! - .multisigConfigWithNamePtr, - mySeed: ref.read(pFrostStartKeyGenData.state).state!.seed, - secretShareMachineWrapperPtr: ref - .read(pFrostStartKeyGenData.state) - .state! - .secretShareMachineWrapperPtr, - commitments: commitments, - ); - - await Navigator.of(context).pushNamed( - FrostShareSharesView.routeName, - arguments: ( - walletName: widget.walletName, - coin: widget.coin, - ), - ); - } catch (e, s) { - Logging.instance.log( - "$e\n$s", - level: LogLevel.Fatal, - ); - - return await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Failed to generate shares", - message: e.toString(), - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } - }, - ), - ], - ), - ), - ), - ); - } -} - -class _Div extends StatelessWidget { - const _Div({super.key}); - - @override - Widget build(BuildContext context) { - return const SizedBox( - height: 12, - ); - } -} diff --git a/lib/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart b/lib/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart deleted file mode 100644 index 20ac39c03..000000000 --- a/lib/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart +++ /dev/null @@ -1,402 +0,0 @@ -import 'package:barcode_scan2/barcode_scan2.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:qr_flutter/qr_flutter.dart'; -import 'package:stackwallet/pages/frost_mascot.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart'; -import 'package:stackwallet/pages/home_view/home_view.dart'; -import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; -import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; -import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; -import 'package:stackwallet/services/frost.dart'; -import 'package:stackwallet/themes/stack_colors.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/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/simple_copy_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/detail_item.dart'; -import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.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'; - -class FrostShareSharesView extends ConsumerStatefulWidget { - const FrostShareSharesView({ - super.key, - required this.walletName, - required this.coin, - }); - - static const String routeName = "/frostShareSharesView"; - - final String walletName; - final Coin coin; - - @override - ConsumerState createState() => - _FrostShareSharesViewState(); -} - -class _FrostShareSharesViewState extends ConsumerState { - final List controllers = []; - final List focusNodes = []; - - late final List participants; - late final String myShare; - late final int myIndex; - - final List fieldIsEmptyFlags = []; - - @override - void initState() { - participants = Frost.getParticipants( - multisigConfig: ref.read(pFrostMultisigConfig.state).state!, - ); - myIndex = participants.indexOf(ref.read(pFrostMyName.state).state!); - myShare = ref.read(pFrostSecretSharesData.state).state!.share; - - // temporarily remove my name. Added back later - participants.removeAt(myIndex); - - for (int i = 0; i < participants.length; i++) { - controllers.add(TextEditingController()); - focusNodes.add(FocusNode()); - fieldIsEmptyFlags.add(true); - } - super.initState(); - } - - @override - void dispose() { - for (int i = 0; i < controllers.length; i++) { - controllers[i].dispose(); - } - for (int i = 0; i < focusNodes.length; i++) { - focusNodes[i].dispose(); - } - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { - await showDialog( - context: context, - builder: (_) => FrostInterruptionDialog( - type: FrostInterruptionDialogType.walletCreation, - popUntilOnYesRouteName: - Util.isDesktop ? DesktopHomeView.routeName : HomeView.routeName, - ), - ); - return false; - }, - child: ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.walletCreation, - popUntilOnYesRouteName: DesktopHomeView.routeName, - ), - ); - }, - ), - trailing: FrostMascot( - title: 'Lorem ipsum', - body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', - ), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.walletCreation, - popUntilOnYesRouteName: HomeView.routeName, - ), - ); - }, - ), - title: Text( - "Generate shares", - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(16), - child: child, - ), - ), - ), - ); - }, - ), - ), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: 220, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QrImageView( - data: myShare, - size: 220, - backgroundColor: Theme.of(context) - .extension()! - .background, - foregroundColor: Theme.of(context) - .extension()! - .accentColorDark, - ), - ], - ), - ), - const _Div(), - DetailItem( - title: "My name", - detail: ref.watch(pFrostMyName.state).state!, - ), - const _Div(), - DetailItem( - title: "My share", - detail: myShare, - button: Util.isDesktop - ? IconCopyButton( - data: myShare, - ) - : SimpleCopyButton( - data: myShare, - ), - ), - const _Div(), - for (int i = 0; i < participants.length; i++) - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: Key("frSharesTextFieldKey_$i"), - controller: controllers[i], - focusNode: focusNodes[i], - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter ${participants[i]}'s share", - focusNodes[i], - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: fieldIsEmptyFlags[i] - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - !fieldIsEmptyFlags[i] - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Share Field Input.", - key: Key("frSharesClearButtonKey_$i"), - onTap: () { - controllers[i].text = ""; - - setState(() { - fieldIsEmptyFlags[i] = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Share Field Input.", - key: Key("frSharesPasteButtonKey_$i"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - controllers[i].text = - data.text!.trim(); - } - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].text.isEmpty; - }); - }, - child: fieldIsEmptyFlags[i] - ? const ClipboardIcon() - : const XIcon(), - ), - if (fieldIsEmptyFlags[i]) - TextFieldIconButton( - semanticsLabel: - "Scan QR Button. Opens Camera For Scanning QR Code.", - key: Key("frSharesScanQrButtonKey_$i"), - onTap: () async { - try { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 75)); - } - - final qrResult = - await BarcodeScanner.scan(); - - controllers[i].text = - qrResult.rawContent; - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].text.isEmpty; - }); - } on PlatformException catch (e, s) { - Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code: $e\n$s", - level: LogLevel.Warning, - ); - } - }, - child: const QrCodeIcon(), - ) - ], - ), - ), - ), - ), - ), - ), - ), - if (!Util.isDesktop) const Spacer(), - const _Div(), - PrimaryButton( - label: "Generate", - onPressed: () async { - // check for empty commitments - if (controllers - .map((e) => e.text.isEmpty) - .reduce((value, element) => value |= element)) { - return await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Missing shares", - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } - - // collect commitment strings and insert my own at the correct index - final shares = controllers.map((e) => e.text).toList(); - shares.insert(myIndex, myShare); - - try { - ref.read(pFrostCompletedKeyGenData.notifier).state = - Frost.completeKeyGeneration( - multisigConfigWithNamePtr: ref - .read(pFrostStartKeyGenData.state) - .state! - .multisigConfigWithNamePtr, - secretSharesResPtr: ref - .read(pFrostSecretSharesData.state) - .state! - .secretSharesResPtr, - shares: shares, - ); - await Navigator.of(context).pushNamed( - ConfirmNewFrostMSWalletCreationView.routeName, - arguments: ( - walletName: widget.walletName, - coin: widget.coin, - ), - ); - } catch (e, s) { - Logging.instance.log( - "$e\n$s", - level: LogLevel.Fatal, - ); - - return await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Failed to complete key generation", - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } - }, - ), - ], - ), - ), - ), - ); - } -} - -class _Div extends StatelessWidget { - const _Div({super.key}); - - @override - Widget build(BuildContext context) { - return const SizedBox( - height: 12, - ); - } -} diff --git a/lib/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart deleted file mode 100644 index 4eeb3a045..000000000 --- a/lib/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart +++ /dev/null @@ -1,390 +0,0 @@ -import 'package:barcode_scan2/barcode_scan2.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart'; -import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; -import 'package:stackwallet/services/frost.dart'; -import 'package:stackwallet/themes/stack_colors.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/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_app_bar.dart'; -import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; -import 'package:stackwallet/widgets/desktop/primary_button.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:stackwallet/pages/frost_mascot.dart'; - -class ImportNewFrostMsWalletView extends ConsumerStatefulWidget { - const ImportNewFrostMsWalletView({ - super.key, - required this.walletName, - required this.coin, - }); - - static const String routeName = "/importNewFrostMsWalletView"; - - final String walletName; - final Coin coin; - - @override - ConsumerState createState() => - _ImportNewFrostMsWalletViewState(); -} - -class _ImportNewFrostMsWalletViewState - extends ConsumerState { - late final TextEditingController myNameFieldController, configFieldController; - late final FocusNode myNameFocusNode, configFocusNode; - - bool _nameEmpty = true, _configEmpty = true; - - @override - void initState() { - myNameFieldController = TextEditingController(); - configFieldController = TextEditingController(); - myNameFocusNode = FocusNode(); - configFocusNode = FocusNode(); - super.initState(); - } - - @override - void dispose() { - myNameFieldController.dispose(); - configFieldController.dispose(); - myNameFocusNode.dispose(); - configFocusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton(), - trailing: FrostMascot( - title: 'Lorem ipsum', - body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', - ), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Import FROST multisig config", - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(16), - child: child, - ), - ), - ), - ); - }, - ), - ), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 16, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("frMyNameTextFieldKey"), - controller: myNameFieldController, - onChanged: (_) { - setState(() { - _nameEmpty = myNameFieldController.text.isEmpty; - }); - }, - focusNode: myNameFocusNode, - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "My name", - myNameFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: _nameEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - !_nameEmpty - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Config Field.", - key: const Key("frMyNameClearButtonKey"), - onTap: () { - myNameFieldController.text = ""; - - setState(() { - _nameEmpty = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Name Field.", - key: const Key("frMyNamePasteButtonKey"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - myNameFieldController.text = - data.text!.trim(); - } - - setState(() { - _nameEmpty = - myNameFieldController.text.isEmpty; - }); - }, - child: _nameEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - ], - ), - ), - ), - ), - ), - ), - const SizedBox( - height: 16, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("frConfigTextFieldKey"), - controller: configFieldController, - onChanged: (_) { - setState(() { - _configEmpty = configFieldController.text.isEmpty; - }); - }, - focusNode: configFocusNode, - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter config", - configFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: _configEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - !_configEmpty - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Config Field.", - key: const Key("frConfigClearButtonKey"), - onTap: () { - configFieldController.text = ""; - - setState(() { - _configEmpty = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Config Field Input.", - key: const Key("frConfigPasteButtonKey"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - configFieldController.text = - data.text!.trim(); - } - - setState(() { - _configEmpty = - configFieldController.text.isEmpty; - }); - }, - child: _configEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - if (_configEmpty) - TextFieldIconButton( - semanticsLabel: - "Scan QR Button. Opens Camera For Scanning QR Code.", - key: const Key("frConfigScanQrButtonKey"), - onTap: () async { - try { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 75)); - } - - final qrResult = await BarcodeScanner.scan(); - - configFieldController.text = - qrResult.rawContent; - - setState(() { - _configEmpty = - configFieldController.text.isEmpty; - }); - } on PlatformException catch (e, s) { - Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code: $e\n$s", - level: LogLevel.Warning, - ); - } - }, - child: const QrCodeIcon(), - ) - ], - ), - ), - ), - ), - ), - ), - const SizedBox( - height: 16, - ), - if (!Util.isDesktop) const Spacer(), - const SizedBox( - height: 16, - ), - PrimaryButton( - label: "Start key generation", - enabled: !_nameEmpty && !_configEmpty, - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - } - - final config = configFieldController.text; - - if (!Frost.validateEncodedMultisigConfig( - encodedConfig: config)) { - return await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Invalid config", - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } - - if (!Frost.getParticipants(multisigConfig: config) - .contains(myNameFieldController.text)) { - return await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "My name not found in config participants", - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } - - ref.read(pFrostMyName.state).state = myNameFieldController.text; - ref.read(pFrostMultisigConfig.notifier).state = config; - - ref.read(pFrostStartKeyGenData.state).state = - Frost.startKeyGeneration( - multisigConfig: ref.read(pFrostMultisigConfig.state).state!, - myName: ref.read(pFrostMyName.state).state!, - ); - - await Navigator.of(context).pushNamed( - FrostShareCommitmentsView.routeName, - arguments: ( - walletName: widget.walletName, - coin: widget.coin, - ), - ); - }, - ), - ], - ), - ), - ); - } -} diff --git a/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart b/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart new file mode 100644 index 000000000..5596534a3 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart @@ -0,0 +1,360 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/frost_currency.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_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/dialogs/simple_mobile_dialog.dart'; +import 'package:stackwallet/widgets/frost_scaffold.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class SelectNewFrostImportTypeView extends ConsumerStatefulWidget { + const SelectNewFrostImportTypeView({ + super.key, + required this.walletName, + required this.frostCurrency, + }); + + static const String routeName = "/selectNewFrostImportTypeView"; + + final String walletName; + final FrostCurrency frostCurrency; + + @override + ConsumerState createState() => + _SelectNewFrostImportTypeViewState(); +} + +class _SelectNewFrostImportTypeViewState + extends ConsumerState { + _ImportOption _selectedOption = _ImportOption.multisigNew; + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: Util.isDesktop, + builder: (content) => DesktopScaffold( + appBar: const DesktopAppBar( + leading: AppBarBackButton(), + trailing: ExitToMyStackButton(), + isCompactHeight: false, + ), + body: SizedBox( + width: 480, + child: content, + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (content) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + actions: [ + AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + size: 36, + icon: SvgPicture.asset( + Assets.svg.circleQuestion, + width: 20, + height: 20, + colorFilter: ColorFilter.mode( + Theme.of(context) + .extension()! + .topNavIconPrimary, + BlendMode.srcIn, + ), + ), + onPressed: () async { + await showDialog( + context: context, + builder: (_) => const _FrostJoinInfoDialog(), + ); + }, + ), + ), + ], + ), + body: Container( + color: Theme.of(context).extension()!.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, + ), + ), + ); + }, + ), + ), + ), + ), + ), + child: Column( + children: [ + ..._ImportOption.values.map( + (e) => Padding( + padding: const EdgeInsets.only(bottom: 16), + child: _ImportOptionCard( + onPressed: () => setState(() => _selectedOption = e), + title: e.info, + description: e.description, + value: e, + groupValue: _selectedOption, + ), + ), + ), + const Spacer(), + PrimaryButton( + label: "Continue", + onPressed: () async { + final String route; + switch (_selectedOption) { + case _ImportOption.multisigNew: + ref.read(pFrostScaffoldArgs.state).state = ( + info: ( + walletName: widget.walletName, + frostCurrency: widget.frostCurrency, + ), + walletId: null, // no wallet id yet + stepRoutes: FrostRouteGenerator.importNewConfigStepRoutes, + parentNav: Navigator.of(context), + frostInterruptionDialogType: + FrostInterruptionDialogType.walletCreation, + ); + break; + + case _ImportOption.resharerExisting: + ref.read(pFrostScaffoldArgs.state).state = ( + info: ( + walletName: widget.walletName, + frostCurrency: widget.frostCurrency, + ), + walletId: null, // no wallet id yet + stepRoutes: FrostRouteGenerator.joinReshareStepRoutes, + parentNav: Navigator.of(context), + frostInterruptionDialogType: + FrostInterruptionDialogType.resharing, + ); + break; + } + + await Navigator.of(context).pushNamed( + FrostStepScaffold.routeName, + ); + }, + ) + ], + ), + ), + ); + } +} + +enum _ImportOption { + multisigNew, + resharerExisting; + + String get info { + switch (this) { + case _ImportOption.multisigNew: + return "I want to join a new group"; + case _ImportOption.resharerExisting: + return "I want to join an existing group"; + } + } + + String get description { + switch (this) { + case _ImportOption.multisigNew: + return "You are currently participating in the process of creating a new group"; + case _ImportOption.resharerExisting: + return "You are joining an existing group through the process of resharing"; + } + } +} + +class _ImportOptionCard extends StatefulWidget { + const _ImportOptionCard({ + super.key, + required this.onPressed, + required this.title, + required this.description, + required this.value, + required this.groupValue, + }); + + final VoidCallback onPressed; + final String title; + final String description; + final _ImportOption value; + final _ImportOption groupValue; + + @override + State<_ImportOptionCard> createState() => _ImportOptionCardState(); +} + +class _ImportOptionCardState extends State<_ImportOptionCard> { + @override + Widget build(BuildContext context) { + return RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + onPressed: widget.onPressed, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(6.0), + child: Radio( + value: widget.value, + groupValue: widget.groupValue, + activeColor: Theme.of(context) + .extension()! + .radioButtonIconEnabled, + onChanged: (_) => widget.onPressed(), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + top: 12.0, + right: 12.0, + bottom: 12.0, + ), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Text( + widget.title, + style: STextStyles.w600_16(context), + ), + ), + ], + ), + const SizedBox( + height: 2, + ), + Row( + children: [ + Expanded( + child: Text( + widget.description, + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ); + } +} + +class _FrostJoinInfoDialog extends StatelessWidget { + const _FrostJoinInfoDialog({super.key}); + + @override + Widget build(BuildContext context) { + return SimpleMobileDialog( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // TODO: [prio=high] need text from designers! + Text( + "Join a group", + style: STextStyles.w600_20(context), + ), + const SizedBox( + height: 12, + ), + Text( + "Text here", + style: STextStyles.w400_16(context), + ), + const SizedBox( + height: 8, + ), + Text( + "What is resharing?", + style: STextStyles.w600_16(context), + ), + const SizedBox( + height: 8, + ), + Text( + "In cryptocurrency, you are your own bank." + " Imagine keeping cash at home. If that cash" + " burns down or gets stolen, you lose it and" + " nobody will help you get your money back.", + style: STextStyles.w400_16(context), + ), + const SizedBox( + height: 8, + ), + Text( + "Since cryptocurrency is digital money, your " + "wallet key is like that “cash” you keep at " + "home. If you lose your phone or if you " + "forget your wallet PIN, but you have your " + "wallet key, your crypto money will be safe. " + "That is why you should keep your wallet key " + "safe.", + style: STextStyles.w400_16(context), + ), + const SizedBox( + height: 12, + ), + Text( + "Why write it down?", + style: STextStyles.w600_16(context), + ), + const SizedBox( + height: 8, + ), + Text( + "You do not put your cash on display, do you?" + " Keeping your wallet key on a digital device" + " is like having it on display for thieves - " + "malicious software and hackers. Write your " + "wallet key down on paper in multiple copies " + "and keep them in a real, physical safe.", + style: STextStyles.w400_16(context), + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart b/lib/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart deleted file mode 100644 index 7d463c4ca..000000000 --- a/lib/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart +++ /dev/null @@ -1,165 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:qr_flutter/qr_flutter.dart'; -import 'package:stackwallet/pages/frost_mascot.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart'; -import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; -import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; -import 'package:stackwallet/services/frost.dart'; -import 'package:stackwallet/themes/stack_colors.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/text_styles.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/simple_copy_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/detail_item.dart'; - -class ShareNewMultisigConfigView extends ConsumerStatefulWidget { - const ShareNewMultisigConfigView({ - super.key, - required this.walletName, - required this.coin, - }); - - static const String routeName = "/shareNewMultisigConfigView"; - - final String walletName; - final Coin coin; - - @override - ConsumerState createState() => - _ShareNewMultisigConfigViewState(); -} - -class _ShareNewMultisigConfigViewState - extends ConsumerState { - @override - Widget build(BuildContext context) { - return ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton(), - trailing: FrostMascot( - title: 'Lorem ipsum', - body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', - ), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Multisig config", - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(16), - child: child, - ), - ), - ), - ); - }, - ), - ), - ), - ), - child: Column( - children: [ - if (!Util.isDesktop) const Spacer(), - SizedBox( - height: 220, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QrImageView( - data: - ref.watch(pFrostMultisigConfig.state).state ?? "Error", - size: 220, - backgroundColor: - Theme.of(context).extension()!.background, - foregroundColor: Theme.of(context) - .extension()! - .accentColorDark, - ), - ], - ), - ), - const SizedBox( - height: 32, - ), - DetailItem( - title: "Encoded config", - detail: ref.watch(pFrostMultisigConfig.state).state ?? "Error", - button: Util.isDesktop - ? IconCopyButton( - data: ref.watch(pFrostMultisigConfig.state).state ?? - "Error", - ) - : SimpleCopyButton( - data: ref.watch(pFrostMultisigConfig.state).state ?? - "Error", - ), - ), - SizedBox( - height: Util.isDesktop ? 64 : 16, - ), - if (!Util.isDesktop) - const Spacer( - flex: 2, - ), - PrimaryButton( - label: "Start key generation", - onPressed: () async { - ref.read(pFrostStartKeyGenData.notifier).state = - Frost.startKeyGeneration( - multisigConfig: ref.watch(pFrostMultisigConfig.state).state!, - myName: ref.read(pFrostMyName.state).state!, - ); - - await Navigator.of(context).pushNamed( - FrostShareCommitmentsView.routeName, - arguments: ( - walletName: widget.walletName, - coin: widget.coin, - ), - ); - }, - ), - ], - ), - ), - ); - } -} diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1a.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1a.dart new file mode 100644 index 000000000..0f1cd84aa --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1a.dart @@ -0,0 +1,248 @@ +import 'package:flutter/material.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/frost_route_generator.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/custom_buttons/checkbox_text_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/detail_item.dart'; +import 'package:stackwallet/widgets/dialogs/simple_mobile_dialog.dart'; +import 'package:stackwallet/widgets/frost_step_user_steps.dart'; + +class FrostCreateStep1a extends ConsumerStatefulWidget { + const FrostCreateStep1a({super.key}); + + static const String routeName = "/frostCreateStep1a"; + static const String title = "Multisig group info"; + + @override + ConsumerState createState() => _FrostCreateStep1aState(); +} + +class _FrostCreateStep1aState extends ConsumerState { + static const info = [ + "Share this config with the group participants.", + "Wait for them to join the group.", + "Verify that everyone has filled out their forms before continuing. If you " + "try to continue before everyone is ready, the process will be canceled.", + "Check the box and press “Generate keys”.", + ]; + + bool _userVerifyContinue = false; + + void _showParticipantsDialog() { + final participants = Frost.getParticipants( + multisigConfig: ref.read(pFrostMultisigConfig.state).state!, + ); + + showDialog( + context: context, + builder: (_) => SimpleMobileDialog( + showCloseButton: false, + padding: EdgeInsets.zero, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 24, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + "Group participants", + style: STextStyles.w600_20(context), + ), + ), + const SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + "The names are case-sensitive and must be entered exactly.", + style: STextStyles.w400_16(context).copyWith( + color: Theme.of(context).extension()!.textDark3, + ), + ), + ), + const SizedBox( + height: 12, + ), + for (final participant in participants) + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: double.infinity, + height: 1.5, + color: + Theme.of(context).extension()!.background, + ), + const SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Row( + children: [ + Container( + width: 26, + height: 26, + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .textFieldActiveBG, + borderRadius: BorderRadius.circular( + 200, + ), + ), + child: Center( + child: SvgPicture.asset( + Assets.svg.user, + width: 16, + height: 16, + ), + ), + ), + const SizedBox( + width: 8, + ), + Expanded( + child: Text( + participant, + style: STextStyles.w500_14(context), + ), + ), + const SizedBox( + width: 8, + ), + IconCopyButton( + data: participant, + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + ], + ), + const SizedBox( + height: 24, + ), + ], + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + const FrostStepUserSteps( + userSteps: info, + ), + const SizedBox( + height: 20, + ), + SizedBox( + height: 220, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QrImageView( + data: ref.watch(pFrostMultisigConfig.state).state ?? "Error", + size: 220, + backgroundColor: + Theme.of(context).extension()!.background, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark, + ), + ], + ), + ), + const SizedBox( + height: 20, + ), + DetailItem( + title: "Encoded config", + detail: ref.watch(pFrostMultisigConfig.state).state ?? "Error", + button: Util.isDesktop + ? IconCopyButton( + data: + ref.watch(pFrostMultisigConfig.state).state ?? "Error", + ) + : SimpleCopyButton( + data: + ref.watch(pFrostMultisigConfig.state).state ?? "Error", + ), + ), + SizedBox( + height: Util.isDesktop ? 64 : 16, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Show group participants", + onPressed: _showParticipantsDialog, + ), + ), + ], + ), + if (!Util.isDesktop) + const Spacer( + flex: 2, + ), + const SizedBox( + height: 16, + ), + CheckboxTextButton( + label: "I have verified that everyone has joined the group", + onChanged: (value) { + setState(() { + _userVerifyContinue = value; + }); + }, + ), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Start key generation", + enabled: _userVerifyContinue, + onPressed: () async { + ref.read(pFrostStartKeyGenData.notifier).state = + Frost.startKeyGeneration( + multisigConfig: ref.watch(pFrostMultisigConfig.state).state!, + myName: ref.read(pFrostMyName.state).state!, + ); + + ref.read(pFrostCreateCurrentStep.state).state = 2; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + // FrostShareCommitmentsView.routeName, + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1b.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1b.dart new file mode 100644 index 000000000..21ed92a7d --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1b.dart @@ -0,0 +1,184 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/custom_buttons/checkbox_text_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/frost_step_user_steps.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; +import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; + +class FrostCreateStep1b extends ConsumerStatefulWidget { + const FrostCreateStep1b({super.key}); + + static const String routeName = "/frostCreateStep1b"; + static const String title = "Import group info"; + + @override + ConsumerState createState() => _FrostCreateStep1bState(); +} + +class _FrostCreateStep1bState extends ConsumerState { + static const info = [ + "Scan the config QR code or paste the code provided by the group creator.", + "Enter your name EXACTLY as the group creator entered it. When in doubt, " + "double check with them. The names are case-sensitive.", + "Wait for other participants to finish entering their information.", + "Verify that everyone has filled out their forms before continuing. If you " + "try to continue before everyone is ready, the process will be canceled.", + "Check the box and press “Generate keys”.", + ]; + + late final TextEditingController myNameFieldController, configFieldController; + late final FocusNode myNameFocusNode, configFocusNode; + + bool _nameEmpty = true, _configEmpty = true, _userVerifyContinue = false; + + @override + void initState() { + myNameFieldController = TextEditingController(); + configFieldController = TextEditingController(); + myNameFocusNode = FocusNode(); + configFocusNode = FocusNode(); + super.initState(); + } + + @override + void dispose() { + myNameFieldController.dispose(); + configFieldController.dispose(); + myNameFocusNode.dispose(); + configFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + const FrostStepUserSteps( + userSteps: info, + ), + const SizedBox( + height: 16, + ), + FrostStepField( + controller: configFieldController, + focusNode: configFocusNode, + showQrScanOption: true, + label: "Enter config", + hint: "Enter config", + onChanged: (_) { + setState(() { + _configEmpty = configFieldController.text.isEmpty; + }); + }, + ), + const SizedBox( + height: 16, + ), + FrostStepField( + controller: myNameFieldController, + focusNode: myNameFocusNode, + showQrScanOption: false, + label: "My name", + hint: "Enter your name", + onChanged: (_) { + setState(() { + _nameEmpty = myNameFieldController.text.isEmpty; + }); + }, + ), + const SizedBox( + height: 6, + ), + Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + child: Text( + "Enter your name EXACTLY as the group creator entered it. " + "The names are case-sensitive.", + style: STextStyles.label(context), + ), + ), + ), + ], + ), + const SizedBox( + height: 16, + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 16, + ), + CheckboxTextButton( + label: "I have verified that everyone has joined the group", + onChanged: (value) { + setState(() { + _userVerifyContinue = value; + }); + }, + ), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Start key generation", + enabled: _userVerifyContinue && !_nameEmpty && !_configEmpty, + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + } + + final config = configFieldController.text; + + if (!Frost.validateEncodedMultisigConfig(encodedConfig: config)) { + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Invalid config", + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + + if (!Frost.getParticipants(multisigConfig: config) + .contains(myNameFieldController.text)) { + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "My name not found in config participants", + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + + ref.read(pFrostMyName.state).state = myNameFieldController.text; + ref.read(pFrostMultisigConfig.notifier).state = config; + + ref.read(pFrostStartKeyGenData.state).state = + Frost.startKeyGeneration( + multisigConfig: ref.read(pFrostMultisigConfig.state).state!, + myName: ref.read(pFrostMyName.state).state!, + ); + ref.read(pFrostCreateCurrentStep.state).state = 2; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + }, + ) + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart new file mode 100644 index 000000000..181a7c3d6 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart @@ -0,0 +1,200 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/custom_buttons/checkbox_text_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/frost_qr_dialog_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/detail_item.dart'; +import 'package:stackwallet/widgets/frost_step_user_steps.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; +import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; + +class FrostCreateStep2 extends ConsumerStatefulWidget { + const FrostCreateStep2({ + super.key, + }); + + static const String routeName = "/frostCreateStep2"; + static const String title = "Commitments"; + + @override + ConsumerState createState() => _FrostCreateStep2State(); +} + +class _FrostCreateStep2State extends ConsumerState { + static const info = [ + "Share your commitment with other group members.", + "Enter their commitments into the corresponding fields.", + ]; + + final List controllers = []; + final List focusNodes = []; + + late final List participants; + late final String myCommitment; + late final int myIndex; + + final List fieldIsEmptyFlags = []; + bool _userVerifyContinue = false; + + @override + void initState() { + participants = Frost.getParticipants( + multisigConfig: ref.read(pFrostMultisigConfig.state).state!, + ); + myIndex = participants.indexOf(ref.read(pFrostMyName.state).state!); + myCommitment = ref.read(pFrostStartKeyGenData.state).state!.commitments; + + // temporarily remove my name + participants.removeAt(myIndex); + + for (int i = 0; i < participants.length; i++) { + controllers.add(TextEditingController()); + focusNodes.add(FocusNode()); + fieldIsEmptyFlags.add(true); + } + super.initState(); + } + + @override + void dispose() { + for (int i = 0; i < controllers.length; i++) { + controllers[i].dispose(); + } + for (int i = 0; i < focusNodes.length; i++) { + focusNodes[i].dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + const FrostStepUserSteps( + userSteps: info, + ), + const SizedBox(height: 12), + DetailItem( + title: "My name", + detail: ref.watch(pFrostMyName.state).state!, + ), + const SizedBox(height: 12), + DetailItem( + title: "My commitment", + detail: myCommitment, + button: Util.isDesktop + ? IconCopyButton( + data: myCommitment, + ) + : SimpleCopyButton( + data: myCommitment, + ), + ), + const SizedBox(height: 12), + FrostQrDialogPopupButton( + data: myCommitment, + ), + const SizedBox(height: 12), + for (int i = 0; i < participants.length; i++) + Padding( + padding: const EdgeInsets.only(top: 12), + child: FrostStepField( + controller: controllers[i], + focusNode: focusNodes[i], + showQrScanOption: true, + label: participants[i], + hint: "Enter ${participants[i]}'s commitment", + onChanged: (_) { + setState(() { + fieldIsEmptyFlags[i] = controllers[i].text.isEmpty; + }); + }, + ), + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox(height: 12), + CheckboxTextButton( + label: "I have verified that everyone has my commitment", + onChanged: (value) { + setState(() { + _userVerifyContinue = value; + }); + }, + ), + const SizedBox(height: 12), + PrimaryButton( + label: "Generate shares", + enabled: _userVerifyContinue && + !fieldIsEmptyFlags.reduce((v, e) => v |= e), + onPressed: () async { + // check for empty commitments + if (controllers + .map((e) => e.text.isEmpty) + .reduce((value, element) => value |= element)) { + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Missing commitments", + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + + // collect commitment strings and insert my own at the correct index + final commitments = controllers.map((e) => e.text).toList(); + commitments.insert(myIndex, myCommitment); + + try { + ref.read(pFrostSecretSharesData.notifier).state = + Frost.generateSecretShares( + multisigConfigWithNamePtr: ref + .read(pFrostStartKeyGenData.state) + .state! + .multisigConfigWithNamePtr, + mySeed: ref.read(pFrostStartKeyGenData.state).state!.seed, + secretShareMachineWrapperPtr: ref + .read(pFrostStartKeyGenData.state) + .state! + .secretShareMachineWrapperPtr, + commitments: commitments, + ); + + ref.read(pFrostCreateCurrentStep.state).state = 3; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + if (context.mounted) { + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Failed to generate shares", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart new file mode 100644 index 000000000..782d86135 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart @@ -0,0 +1,200 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/custom_buttons/checkbox_text_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/frost_qr_dialog_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/detail_item.dart'; +import 'package:stackwallet/widgets/frost_step_user_steps.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; +import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; + +class FrostCreateStep3 extends ConsumerStatefulWidget { + const FrostCreateStep3({super.key}); + + static const String routeName = "/frostCreateStep3"; + static const String title = "Shares"; + + @override + ConsumerState createState() => _FrostCreateStep3State(); +} + +class _FrostCreateStep3State extends ConsumerState { + static const info = [ + "Send your share to other group members.", + "Enter their shares into the corresponding fields.", + ]; + + bool _userVerifyContinue = false; + + final List controllers = []; + final List focusNodes = []; + + late final List participants; + late final String myShare; + late final int myIndex; + + final List fieldIsEmptyFlags = []; + + @override + void initState() { + participants = Frost.getParticipants( + multisigConfig: ref.read(pFrostMultisigConfig.state).state!, + ); + myIndex = participants.indexOf(ref.read(pFrostMyName.state).state!); + myShare = ref.read(pFrostSecretSharesData.state).state!.share; + + // temporarily remove my name. Added back later + participants.removeAt(myIndex); + + for (int i = 0; i < participants.length; i++) { + controllers.add(TextEditingController()); + focusNodes.add(FocusNode()); + fieldIsEmptyFlags.add(true); + } + super.initState(); + } + + @override + void dispose() { + for (int i = 0; i < controllers.length; i++) { + controllers[i].dispose(); + } + for (int i = 0; i < focusNodes.length; i++) { + focusNodes[i].dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + const FrostStepUserSteps( + userSteps: info, + ), + const SizedBox(height: 12), + DetailItem( + title: "My name", + detail: ref.watch(pFrostMyName.state).state!, + ), + const SizedBox(height: 12), + DetailItem( + title: "My share", + detail: myShare, + button: Util.isDesktop + ? IconCopyButton( + data: myShare, + ) + : SimpleCopyButton( + data: myShare, + ), + ), + const SizedBox(height: 12), + FrostQrDialogPopupButton( + data: myShare, + ), + const SizedBox(height: 12), + for (int i = 0; i < participants.length; i++) + Padding( + padding: const EdgeInsets.only(top: 12), + child: FrostStepField( + controller: controllers[i], + focusNode: focusNodes[i], + showQrScanOption: true, + label: participants[i], + hint: "Enter ${participants[i]}'s share", + onChanged: (_) { + setState(() { + fieldIsEmptyFlags[i] = controllers[i].text.isEmpty; + }); + }, + ), + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox(height: 12), + CheckboxTextButton( + label: "I have verified that everyone has my share", + onChanged: (value) { + setState(() { + _userVerifyContinue = value; + }); + }, + ), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Generate", + enabled: _userVerifyContinue && + !fieldIsEmptyFlags.reduce((v, e) => v |= e), + onPressed: () async { + // check for empty commitments + if (controllers + .map((e) => e.text.isEmpty) + .reduce((value, element) => value |= element)) { + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Missing shares", + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + + // collect commitment strings and insert my own at the correct index + final shares = controllers.map((e) => e.text).toList(); + shares.insert(myIndex, myShare); + + try { + ref.read(pFrostCompletedKeyGenData.notifier).state = + Frost.completeKeyGeneration( + multisigConfigWithNamePtr: ref + .read(pFrostStartKeyGenData.state) + .state! + .multisigConfigWithNamePtr, + secretSharesResPtr: ref + .read(pFrostSecretSharesData.state) + .state! + .secretSharesResPtr, + shares: shares, + ); + + ref.read(pFrostCreateCurrentStep.state).state = 4; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + if (context.mounted) { + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Failed to complete key generation", + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_4.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_4.dart new file mode 100644 index 000000000..864e905bf --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_4.dart @@ -0,0 +1,77 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/detail_item.dart'; +import 'package:stackwallet/widgets/frost_step_user_steps.dart'; + +class FrostCreateStep4 extends ConsumerStatefulWidget { + const FrostCreateStep4({super.key}); + + static const String routeName = "/frostCreateStep4"; + static const String title = "Verify multisig ID"; + + @override + ConsumerState createState() => _FrostCreateStep4State(); +} + +class _FrostCreateStep4State extends ConsumerState { + static const info = [ + "Ensure your multisig ID matches that of each other participant.", + ]; + + late final Uint8List multisigId; + + @override + void initState() { + multisigId = ref.read(pFrostCompletedKeyGenData.state).state!.multisigId; + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + const FrostStepUserSteps( + userSteps: info, + ), + const SizedBox(height: 12), + DetailItem( + title: "Multisig ID", + detail: multisigId.toString(), + button: Util.isDesktop + ? IconCopyButton( + data: multisigId.toString(), + ) + : SimpleCopyButton( + data: multisigId.toString(), + ), + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox(height: 12), + PrimaryButton( + label: "Confirm", + onPressed: () { + ref.read(pFrostCreateCurrentStep.state).state = 5; + Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + }, + ) + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart new file mode 100644 index 000000000..586a1c189 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart @@ -0,0 +1,239 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/home_view/home_view.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/providers/global/node_service_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/services/frost.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/isar/models/wallet_info.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; +import 'package:stackwallet/wallets/wallet/wallet.dart'; +import 'package:stackwallet/widgets/custom_buttons/checkbox_text_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/detail_item.dart'; +import 'package:stackwallet/widgets/loading_indicator.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; + +class FrostCreateStep5 extends ConsumerStatefulWidget { + const FrostCreateStep5({super.key}); + + static const String routeName = "/frostCreateStep5"; + static const String title = "Back up your keys"; + + @override + ConsumerState createState() => _FrostCreateStep5State(); +} + +class _FrostCreateStep5State extends ConsumerState { + static const _warning = "These are your private keys. Please back them up, " + "keep them safe and never share it with anyone. Your private keys are the" + " only way you can access your funds if you forget PIN, lose your phone, " + "etc. Stack Wallet does not keep nor is able to restore your private keys" + "."; + + late final String seed, recoveryString, serializedKeys, multisigConfig; + late final Uint8List multisigId; + + bool _userVerifyContinue = false; + + @override + void initState() { + seed = ref.read(pFrostStartKeyGenData.state).state!.seed; + serializedKeys = + ref.read(pFrostCompletedKeyGenData.state).state!.serializedKeys; + recoveryString = + ref.read(pFrostCompletedKeyGenData.state).state!.recoveryString; + multisigConfig = ref.read(pFrostMultisigConfig.state).state!; + multisigId = ref.read(pFrostCompletedKeyGenData.state).state!.multisigId; + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + RoundedContainer( + color: + Theme.of(context).extension()!.warningBackground, + child: Text( + _warning, + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .warningForeground, + ), + ), + ), + const SizedBox(height: 12), + DetailItem( + title: "Multisig Config", + detail: multisigConfig, + button: Util.isDesktop + ? IconCopyButton( + data: multisigConfig, + ) + : SimpleCopyButton( + data: multisigConfig, + ), + ), + const SizedBox(height: 12), + DetailItem( + title: "Keys", + detail: serializedKeys, + button: Util.isDesktop + ? IconCopyButton( + data: serializedKeys, + ) + : SimpleCopyButton( + data: serializedKeys, + ), + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox(height: 12), + CheckboxTextButton( + label: "I have backed up my keys and the config", + onChanged: (value) { + setState(() { + _userVerifyContinue = value; + }); + }, + ), + const SizedBox(height: 12), + PrimaryButton( + label: "Continue", + enabled: _userVerifyContinue, + onPressed: () async { + bool progressPopped = false; + try { + unawaited( + showDialog( + context: context, + barrierDismissible: false, + useSafeArea: true, + builder: (ctx) { + return const Center( + child: LoadingIndicator( + width: 50, + height: 50, + ), + ); + }, + ), + ); + + final data = ref.read(pFrostScaffoldArgs)!; + + final info = WalletInfo.createNew( + coin: data.info.frostCurrency.coin, + name: data.info.walletName, + ); + + final wallet = await Wallet.create( + walletInfo: info, + mainDB: ref.read(mainDBProvider), + secureStorageInterface: ref.read(secureStoreProvider), + nodeService: ref.read(nodeServiceChangeNotifierProvider), + prefs: ref.read(prefsChangeNotifierProvider), + mnemonic: seed, + mnemonicPassphrase: "", + ); + + await (wallet as BitcoinFrostWallet).initializeNewFrost( + multisigConfig: multisigConfig, + recoveryString: recoveryString, + serializedKeys: serializedKeys, + multisigId: multisigId, + myName: ref.read(pFrostMyName.state).state!, + participants: Frost.getParticipants( + multisigConfig: ref.read(pFrostMultisigConfig.state).state!, + ), + threshold: Frost.getThreshold( + multisigConfig: ref.read(pFrostMultisigConfig.state).state!, + ), + ); + + await info.setMnemonicVerified( + isar: ref.read(mainDBProvider).isar, + ); + + ref.read(pWallets).addWallet(wallet); + + // pop progress dialog + if (context.mounted) { + Navigator.pop(context); + progressPopped = true; + } + + if (mounted) { + ref.read(pFrostScaffoldCanPopDesktop.notifier).state = true; + final nav = ref.read(pFrostScaffoldArgs)!.parentNav; + + if (Util.isDesktop) { + nav.popUntil( + ModalRoute.withName( + DesktopHomeView.routeName, + ), + ); + } else { + unawaited( + nav.pushNamedAndRemoveUntil( + HomeView.routeName, + (route) => false, + ), + ); + } + + ref.read(pFrostMultisigConfig.state).state = null; + ref.read(pFrostStartKeyGenData.state).state = null; + ref.read(pFrostSecretSharesData.state).state = null; + ref.read(pFrostScaffoldArgs.state).state = null; + + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "Your wallet is set up.", + iconAsset: Assets.svg.check, + context: nav.context, + ), + ); + } + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + // pop progress dialog + if (context.mounted && !progressPopped) { + Navigator.pop(context); + progressPopped = true; + } + // TODO: handle gracefully + rethrow; + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1a.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1a.dart new file mode 100644 index 000000000..46f272daf --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1a.dart @@ -0,0 +1,309 @@ +import 'package:flutter/material.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/frost_route_generator.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; +import 'package:stackwallet/widgets/custom_buttons/checkbox_text_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/detail_item.dart'; +import 'package:stackwallet/widgets/dialogs/simple_mobile_dialog.dart'; +import 'package:stackwallet/widgets/frost_step_user_steps.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; + +class FrostReshareStep1a extends ConsumerStatefulWidget { + const FrostReshareStep1a({super.key}); + + static const String routeName = "/frostReshareStep1a"; + static const String title = "Resharer config"; + + @override + ConsumerState createState() => _FrostReshareStep1aState(); +} + +class _FrostReshareStep1aState extends ConsumerState { + static const info = [ + "Share this config with the signing group participants as well as any new " + "participant.", + "Wait for them to import the config.", + "Verify that everyone has imported the config. If you try to continue " + "before everyone is ready, the process will be canceled.", + "Check the box and press “Start resharing”.", + ]; + + late final bool iAmInvolved; + + bool _buttonLock = false; + bool _userVerifyContinue = false; + + Future _onPressed() async { + if (_buttonLock) { + return; + } + _buttonLock = true; + + try { + final wallet = + ref.read(pWallets).getWallet(ref.read(pFrostScaffoldArgs)!.walletId!) + as BitcoinFrostWallet; + + final serializedKeys = await wallet.getSerializedKeys(); + if (mounted) { + final result = Frost.beginResharer( + serializedKeys: serializedKeys!, + config: Frost.decodeRConfig( + ref.read(pFrostResharingData).resharerRConfig!, + ), + ); + + ref.read(pFrostResharingData).startResharerData = result; + + ref.read(pFrostCreateCurrentStep.state).state = 2; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + } + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + if (mounted) { + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + } finally { + _buttonLock = false; + } + } + + void _showParticipantsDialog() { + final participants = + ref.read(pFrostResharingData).configData!.newParticipants; + + showDialog( + context: context, + builder: (_) => SimpleMobileDialog( + showCloseButton: false, + padding: EdgeInsets.zero, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 24, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + "Group participants", + style: STextStyles.w600_20(context), + ), + ), + const SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + "The names are case-sensitive and must be entered exactly.", + style: STextStyles.w400_16(context).copyWith( + color: Theme.of(context).extension()!.textDark3, + ), + ), + ), + const SizedBox( + height: 12, + ), + for (final participant in participants) + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: double.infinity, + height: 1.5, + color: + Theme.of(context).extension()!.background, + ), + const SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Row( + children: [ + Container( + width: 26, + height: 26, + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .textFieldActiveBG, + borderRadius: BorderRadius.circular( + 200, + ), + ), + child: Center( + child: SvgPicture.asset( + Assets.svg.user, + width: 16, + height: 16, + ), + ), + ), + const SizedBox( + width: 8, + ), + Expanded( + child: Text( + participant, + style: STextStyles.w500_14(context), + ), + ), + const SizedBox( + width: 8, + ), + IconCopyButton( + data: participant, + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + ], + ), + const SizedBox( + height: 24, + ), + ], + ), + ), + ); + } + + @override + void initState() { + // TODO: optimize this by creating watcher providers (similar to normal WalletInfo) + final frostInfo = ref + .read(mainDBProvider) + .isar + .frostWalletInfo + .getByWalletIdSync(ref.read(pFrostScaffoldArgs)!.walletId!)!; + + final myOldIndex = frostInfo.participants.indexOf(frostInfo.myName); + + iAmInvolved = ref + .read(pFrostResharingData) + .configData! + .resharers + .values + .contains(myOldIndex); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + const FrostStepUserSteps( + userSteps: info, + ), + const SizedBox(height: 20), + SizedBox( + height: 220, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QrImageView( + data: ref.watch(pFrostResharingData).resharerRConfig!, + size: 220, + backgroundColor: + Theme.of(context).extension()!.background, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark, + ), + ], + ), + ), + const SizedBox( + height: 32, + ), + DetailItem( + title: "Config", + detail: ref.watch(pFrostResharingData).resharerRConfig!, + button: Util.isDesktop + ? IconCopyButton( + data: ref.watch(pFrostResharingData).resharerRConfig!, + ) + : SimpleCopyButton( + data: ref.watch(pFrostResharingData).resharerRConfig!, + ), + ), + SizedBox( + height: Util.isDesktop ? 64 : 16, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Show group participants", + onPressed: _showParticipantsDialog, + ), + ), + ], + ), + if (iAmInvolved && !Util.isDesktop) const Spacer(), + if (iAmInvolved) + const SizedBox( + height: 16, + ), + if (iAmInvolved) + CheckboxTextButton( + label: "I have verified that everyone has imported the config", + onChanged: (value) { + setState(() { + _userVerifyContinue = value; + }); + }, + ), + if (iAmInvolved) + const SizedBox( + height: 16, + ), + if (iAmInvolved) + PrimaryButton( + label: "Start resharing", + enabled: _userVerifyContinue, + onPressed: _onPressed, + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1b.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1b.dart new file mode 100644 index 000000000..71fe0e737 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1b.dart @@ -0,0 +1,211 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frostdart/frostdart.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/utilities/format.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart'; +import 'package:stackwallet/widgets/custom_buttons/checkbox_text_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/frost_step_user_steps.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; +import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; + +class FrostReshareStep1b extends ConsumerStatefulWidget { + const FrostReshareStep1b({ + super.key, + }); + + static const String routeName = "/frostReshareStep1b"; + static const String title = "Import reshare config"; + + @override + ConsumerState createState() => _FrostReshareStep1bState(); +} + +class _FrostReshareStep1bState extends ConsumerState { + static const info = [ + "Scan the config QR code or paste the code provided by the group member who" + " is initiating resharing.", + "Wait for other participants to finish importing the config.", + "Verify that everyone has filled out their forms before continuing. If you " + "try to continue before everyone is ready, the process will be canceled.", + "Check the box and press “Start resharing”.", + ]; + + late final TextEditingController configFieldController; + late final FocusNode configFocusNode; + + bool _configEmpty = true; + + bool _buttonLock = false; + bool _userVerifyContinue = false; + + Future _onPressed() async { + if (_buttonLock) { + return; + } + _buttonLock = true; + + try { + final walletId = ref.read(pFrostScaffoldArgs)!.walletId!; + // TODO: optimize this by creating watcher providers (similar to normal WalletInfo) + final frostInfo = ref + .read(mainDBProvider) + .isar + .frostWalletInfo + .getByWalletIdSync(walletId)!; + + ref.read(pFrostResharingData).reset(); + ref.read(pFrostResharingData).myName = frostInfo.myName; + ref.read(pFrostResharingData).resharerRConfig = + configFieldController.text; + + String? salt; + try { + salt = Format.uint8listToString( + resharerSalt( + resharerConfig: Frost.decodeRConfig( + ref.read(pFrostResharingData).resharerRConfig!, + ), + ), + ); + } catch (_) { + throw Exception("Bad resharer config"); + } + + if (frostInfo.knownSalts.contains(salt)) { + throw Exception("Duplicate config salt"); + } else { + final salts = frostInfo.knownSalts.toList(); + salts.add(salt); + final mainDB = ref.read(mainDBProvider); + await mainDB.isar.writeTxn(() async { + final id = frostInfo.id; + await mainDB.isar.frostWalletInfo.delete(id); + await mainDB.isar.frostWalletInfo.put( + frostInfo.copyWith(knownSalts: salts), + ); + }); + } + + final serializedKeys = await ref.read(secureStoreProvider).read( + key: "{$walletId}_serializedFROSTKeys", + ); + if (mounted) { + final result = Frost.beginResharer( + serializedKeys: serializedKeys!, + config: Frost.decodeRConfig( + ref.read(pFrostResharingData).resharerRConfig!, + ), + ); + + ref.read(pFrostResharingData).startResharerData = result; + + ref.read(pFrostCreateCurrentStep.state).state = 2; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + } + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + if (mounted) { + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + } finally { + _buttonLock = false; + } + } + + @override + void initState() { + configFieldController = TextEditingController(); + configFocusNode = FocusNode(); + super.initState(); + } + + @override + void dispose() { + configFieldController.dispose(); + configFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 16, + ), + const FrostStepUserSteps( + userSteps: info, + ), + const SizedBox(height: 20), + FrostStepField( + controller: configFieldController, + focusNode: configFocusNode, + showQrScanOption: true, + label: "Enter config", + hint: "Enter config", + onChanged: (_) { + setState(() { + _configEmpty = configFieldController.text.isEmpty; + }); + }, + ), + const SizedBox( + height: 16, + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 16, + ), + CheckboxTextButton( + label: "I have verified that everyone has imported the config", + onChanged: (value) { + setState(() { + _userVerifyContinue = value; + }); + }, + ), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Start resharing", + enabled: !_configEmpty && _userVerifyContinue, + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + } + + await _onPressed(); + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart new file mode 100644 index 000000000..adc1c7f5e --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart @@ -0,0 +1,228 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/show_loading.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/isar/models/wallet_info.dart'; +import 'package:stackwallet/wallets/models/incomplete_frost_wallet.dart'; +import 'package:stackwallet/widgets/custom_buttons/checkbox_text_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/frost_step_user_steps.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; +import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; + +class FrostReshareStep1c extends ConsumerStatefulWidget { + const FrostReshareStep1c({super.key}); + + static const String routeName = "/frostReshareStep1c"; + static const String title = "Import reshare config"; + + @override + ConsumerState createState() => _FrostReshareStep1cState(); +} + +class _FrostReshareStep1cState extends ConsumerState { + static const info = [ + "Scan the config QR code or paste the code provided by the group creator.", + "Enter your name EXACTLY as the group creator entered it. When in doubt, " + "double check with them. The names are case-sensitive.", + "Wait for other participants to finish entering their information.", + "Verify that everyone has filled out their forms before continuing. If you " + "try to continue before everyone is ready, the process could be canceled.", + "Check the box and press “Join group”.", + ]; + + late final TextEditingController myNameFieldController, configFieldController; + late final FocusNode myNameFocusNode, configFocusNode; + + bool _nameEmpty = true, + _configEmpty = true, + _userVerifyContinue = false, + _buttonLock = false; + + Future _createWallet() async { + final data = ref.read(pFrostScaffoldArgs)!; + + final info = WalletInfo.createNew( + name: data.info.walletName, + coin: data.info.frostCurrency.coin, + ); + + final wallet = IncompleteFrostWallet(); + wallet.info = info; + + return wallet; + } + + @override + void initState() { + myNameFieldController = TextEditingController(); + configFieldController = TextEditingController(); + myNameFocusNode = FocusNode(); + configFocusNode = FocusNode(); + super.initState(); + } + + @override + void dispose() { + myNameFieldController.dispose(); + configFieldController.dispose(); + myNameFocusNode.dispose(); + configFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + const FrostStepUserSteps( + userSteps: info, + ), + const SizedBox( + height: 16, + ), + FrostStepField( + controller: myNameFieldController, + focusNode: myNameFocusNode, + showQrScanOption: false, + label: "My name", + hint: "Enter your name", + onChanged: (_) { + setState(() { + _nameEmpty = myNameFieldController.text.isEmpty; + }); + }, + ), + const SizedBox( + height: 16, + ), + FrostStepField( + controller: configFieldController, + focusNode: configFocusNode, + showQrScanOption: true, + label: "Enter config", + hint: "Enter config", + onChanged: (_) { + setState(() { + _configEmpty = configFieldController.text.isEmpty; + }); + }, + ), + const SizedBox( + height: 16, + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 16, + ), + CheckboxTextButton( + label: "I have verified that everyone has joined the group", + onChanged: (value) { + setState(() { + _userVerifyContinue = value; + }); + }, + ), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Join group", + enabled: _userVerifyContinue && !_nameEmpty && !_configEmpty, + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + } + if (_buttonLock) { + return; + } + _buttonLock = true; + + try { + ref.read(pFrostResharingData).reset(); + ref.read(pFrostResharingData).myName = + myNameFieldController.text; + ref.read(pFrostResharingData).resharerRConfig = + configFieldController.text; + + if (!ref + .read(pFrostResharingData) + .configData! + .newParticipants + .contains(ref.read(pFrostResharingData).myName!)) { + ref.read(pFrostResharingData).reset(); + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "My name not found in config participants", + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + + Exception? ex; + final wallet = await showLoading( + whileFuture: _createWallet(), + context: context, + message: "Setting up wallet", + rootNavigator: true, + onException: (e) => ex = e, + ); + + if (ex != null) { + throw ex!; + } + + if (context.mounted) { + ref.read(pFrostResharingData).incompleteWallet = wallet!; + final data = ref.read(pFrostScaffoldArgs)!; + ref.read(pFrostScaffoldArgs.state).state = ( + info: data.info, + walletId: wallet.walletId, + stepRoutes: data.stepRoutes, + parentNav: data.parentNav, + frostInterruptionDialogType: + FrostInterruptionDialogType.resharing, + ); + ref.read(pFrostMyName.state).state = + ref.read(pFrostResharingData).myName!; + ref.read(pFrostCreateCurrentStep.state).state = 2; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + } + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + if (context.mounted) { + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + } finally { + _buttonLock = false; + } + }, + ) + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2abd.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2abd.dart new file mode 100644 index 000000000..63edc03cf --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2abd.dart @@ -0,0 +1,218 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart'; +import 'package:stackwallet/widgets/custom_buttons/checkbox_text_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/frost_qr_dialog_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/detail_item.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; +import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; + +class FrostReshareStep2abd extends ConsumerStatefulWidget { + const FrostReshareStep2abd({super.key}); + + static const String routeName = "/FrostReshareStep2abd"; + static const String title = "Resharers"; + + @override + ConsumerState createState() => + _FrostReshareStep2abdState(); +} + +class _FrostReshareStep2abdState extends ConsumerState { + final List controllers = []; + final List focusNodes = []; + + late final Map resharers; + late final int myResharerIndexIndex; + late final String myResharerStart; + late final bool amOutgoingParticipant; + + final List fieldIsEmptyFlags = []; + + bool _buttonLock = false; + + bool _userVerifyContinue = false; + + Future _onPressed() async { + if (_buttonLock) { + return; + } + _buttonLock = true; + + try { + if (!amOutgoingParticipant) { + // collect resharer strings + final resharerStarts = controllers.map((e) => e.text).toList(); + if (myResharerIndexIndex >= 0) { + // only insert my own at the correct index if I am a resharer + resharerStarts.insert(myResharerIndexIndex, myResharerStart); + } + + final result = Frost.beginReshared( + myName: ref.read(pFrostResharingData).myName!, + resharerConfig: Frost.decodeRConfig( + ref.read(pFrostResharingData).resharerRConfig!, + ), + resharerStarts: resharerStarts, + ); + + ref.read(pFrostResharingData).startResharedData = result; + } + + ref.read(pFrostCreateCurrentStep.state).state = 3; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + if (mounted) { + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Error", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + } finally { + _buttonLock = false; + } + } + + @override + void initState() { + // TODO: optimize this by creating watcher providers (similar to normal WalletInfo) + final frostInfo = ref + .read(mainDBProvider) + .isar + .frostWalletInfo + .getByWalletIdSync(ref.read(pFrostScaffoldArgs)!.walletId!)!; + final myOldIndex = + frostInfo.participants.indexOf(ref.read(pFrostResharingData).myName!); + + myResharerStart = + ref.read(pFrostResharingData).startResharerData!.resharerStart; + + resharers = ref.read(pFrostResharingData).configData!.resharers; + myResharerIndexIndex = resharers.values.toList().indexOf(myOldIndex); + if (myResharerIndexIndex >= 0) { + // remove my name for now as we don't need a text field for it + resharers.remove(ref.read(pFrostResharingData).myName!); + } + + amOutgoingParticipant = !ref + .read(pFrostResharingData) + .configData! + .newParticipants + .contains(ref.read(pFrostResharingData).myName!); + + for (int i = 0; i < resharers.length; i++) { + controllers.add(TextEditingController()); + focusNodes.add(FocusNode()); + fieldIsEmptyFlags.add(true); + } + super.initState(); + } + + @override + void dispose() { + for (int i = 0; i < controllers.length; i++) { + controllers[i].dispose(); + } + for (int i = 0; i < focusNodes.length; i++) { + focusNodes[i].dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + DetailItem( + title: "My resharer", + detail: myResharerStart, + button: Util.isDesktop + ? IconCopyButton( + data: myResharerStart, + ) + : SimpleCopyButton( + data: myResharerStart, + ), + ), + const SizedBox(height: 12), + FrostQrDialogPopupButton( + data: myResharerStart, + ), + const SizedBox( + height: 12, + ), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < resharers.length; i++) + FrostStepField( + controller: controllers[i], + focusNode: focusNodes[i], + showQrScanOption: true, + label: resharers.keys.elementAt(i), + hint: "Enter " + "${resharers.keys.elementAt(i)}" + "'s resharer", + onChanged: (_) { + setState(() { + fieldIsEmptyFlags[i] = controllers[i].text.isEmpty; + }); + }, + ), + ], + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 12, + ), + CheckboxTextButton( + label: "I have verified that everyone has my resharer", + onChanged: (value) { + setState(() { + _userVerifyContinue = value; + }); + }, + ), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Continue", + enabled: _userVerifyContinue && + (amOutgoingParticipant || + !fieldIsEmptyFlags.reduce((v, e) => v |= e)), + onPressed: _onPressed, + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2c.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2c.dart new file mode 100644 index 000000000..0c811c635 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2c.dart @@ -0,0 +1,145 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; +import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; + +class FrostReshareStep2c extends ConsumerStatefulWidget { + const FrostReshareStep2c({super.key}); + + static const String routeName = "/FrostReshareStep2c"; + static const String title = "Resharers"; + + @override + ConsumerState createState() => _FrostReshareStep2cState(); +} + +class _FrostReshareStep2cState extends ConsumerState { + final List controllers = []; + final List focusNodes = []; + + late final Map resharers; + + final List fieldIsEmptyFlags = []; + + bool _buttonLock = false; + Future _onPressed() async { + if (_buttonLock) { + return; + } + _buttonLock = true; + + try { + // collect resharer strings + final resharerStarts = controllers.map((e) => e.text).toList(); + + final result = Frost.beginReshared( + myName: ref.read(pFrostResharingData).myName!, + resharerConfig: Frost.decodeRConfig( + ref.read(pFrostResharingData).resharerRConfig!, + ), + resharerStarts: resharerStarts, + ); + + ref.read(pFrostResharingData).startResharedData = result; + + ref.read(pFrostCreateCurrentStep.state).state = 3; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Error", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } finally { + _buttonLock = false; + } + } + + @override + void initState() { + resharers = ref.read(pFrostResharingData).configData!.resharers; + + for (int i = 0; i < resharers.length; i++) { + controllers.add(TextEditingController()); + focusNodes.add(FocusNode()); + fieldIsEmptyFlags.add(true); + } + super.initState(); + } + + @override + void dispose() { + for (int i = 0; i < controllers.length; i++) { + controllers[i].dispose(); + } + for (int i = 0; i < focusNodes.length; i++) { + focusNodes[i].dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < resharers.length; i++) + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: FrostStepField( + controller: controllers[i], + focusNode: focusNodes[i], + showQrScanOption: true, + label: resharers.keys.elementAt(i), + hint: "Enter " + "${resharers.keys.elementAt(i)}" + "'s resharer", + onChanged: (_) { + setState(() { + fieldIsEmptyFlags[i] = controllers[i].text.isEmpty; + }); + }, + ), + ), + ], + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Continue", + enabled: !fieldIsEmptyFlags.reduce((v, e) => v |= e), + onPressed: _onPressed, + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart new file mode 100644 index 000000000..5bccf7756 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart @@ -0,0 +1,208 @@ +import 'dart:ffi'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/custom_buttons/checkbox_text_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/frost_qr_dialog_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/detail_item.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; +import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; + +class FrostReshareStep3abd extends ConsumerStatefulWidget { + const FrostReshareStep3abd({super.key}); + + static const String routeName = "/frostReshareStep3abd"; + static const String title = "Encryption keys"; + + @override + ConsumerState createState() => + _FrostReshareStep3abdState(); +} + +class _FrostReshareStep3abdState extends ConsumerState { + final List controllers = []; + final List focusNodes = []; + + late final List newParticipants; + late final int myIndex; + late final String? myEncryptionKey; + late final bool amOutgoingParticipant; + + final List fieldIsEmptyFlags = []; + + bool _userVerifyContinue = false; + + bool _buttonLock = false; + Future _onPressed() async { + if (_buttonLock) { + return; + } + _buttonLock = true; + + try { + // collect encryptionKeys strings and insert my own at the correct index + final encryptionKeys = controllers.map((e) => e.text).toList(); + if (!amOutgoingParticipant) { + encryptionKeys.insert(myIndex, myEncryptionKey!); + } + + final result = Frost.finishResharer( + machine: ref.read(pFrostResharingData).startResharerData!.machine.ref, + encryptionKeysOfResharedTo: encryptionKeys, + ); + + ref.read(pFrostResharingData).resharerComplete = result; + + ref.read(pFrostCreateCurrentStep.state).state = 4; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Error", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } finally { + _buttonLock = false; + } + } + + @override + void initState() { + myEncryptionKey = + ref.read(pFrostResharingData).startResharedData?.resharedStart; + + newParticipants = ref.read(pFrostResharingData).configData!.newParticipants; + myIndex = newParticipants.indexOf(ref.read(pFrostResharingData).myName!); + + if (myIndex >= 0) { + // remove my name for now as we don't need a text field for it + newParticipants.removeAt(myIndex); + } + + if (myEncryptionKey == null && myIndex == -1) { + amOutgoingParticipant = true; + } else if (myEncryptionKey != null && myIndex >= 0) { + amOutgoingParticipant = false; + } else { + throw Exception("Invalid resharing state"); + } + + for (int i = 0; i < newParticipants.length; i++) { + controllers.add(TextEditingController()); + focusNodes.add(FocusNode()); + fieldIsEmptyFlags.add(true); + } + super.initState(); + } + + @override + void dispose() { + for (int i = 0; i < controllers.length; i++) { + controllers[i].dispose(); + } + for (int i = 0; i < focusNodes.length; i++) { + focusNodes[i].dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + if (!amOutgoingParticipant) + DetailItem( + title: "My encryption key", + detail: myEncryptionKey!, + button: Util.isDesktop + ? IconCopyButton( + data: myEncryptionKey!, + ) + : SimpleCopyButton( + data: myEncryptionKey!, + ), + ), + if (!amOutgoingParticipant) const SizedBox(height: 12), + if (!amOutgoingParticipant) + FrostQrDialogPopupButton( + data: myEncryptionKey!, + ), + if (!amOutgoingParticipant) + const SizedBox( + height: 12, + ), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < newParticipants.length; i++) + Padding( + padding: const EdgeInsets.only(top: 12), + child: FrostStepField( + controller: controllers[i], + focusNode: focusNodes[i], + showQrScanOption: true, + label: newParticipants[i], + hint: "Enter " + "${newParticipants[i]}" + "'s encryption key", + onChanged: (_) { + setState(() { + fieldIsEmptyFlags[i] = controllers[i].text.isEmpty; + }); + }, + ), + ), + ], + ), + if (!Util.isDesktop) const Spacer(), + if (!amOutgoingParticipant) + const SizedBox( + height: 12, + ), + if (!amOutgoingParticipant) + CheckboxTextButton( + label: "I have verified that everyone has my encryption key", + onChanged: (value) { + setState(() { + _userVerifyContinue = value; + }); + }, + ), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Continue", + enabled: (amOutgoingParticipant || _userVerifyContinue) && + !fieldIsEmptyFlags.reduce((v, e) => v |= e), + onPressed: _onPressed, + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3c.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3c.dart new file mode 100644 index 000000000..3bde7bc76 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3c.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/custom_buttons/checkbox_text_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/frost_qr_dialog_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/detail_item.dart'; + +class FrostReshareStep3c extends ConsumerStatefulWidget { + const FrostReshareStep3c({super.key}); + + static const String routeName = "/frostReshareStep3c"; + static const String title = "Encryption keys"; + + @override + ConsumerState createState() => _FrostReshareStep3cState(); +} + +class _FrostReshareStep3cState extends ConsumerState { + bool _userVerifyContinue = false; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + DetailItem( + title: "My encryption key", + detail: + ref.watch(pFrostResharingData).startResharedData!.resharedStart, + button: Util.isDesktop + ? IconCopyButton( + data: ref + .watch(pFrostResharingData) + .startResharedData! + .resharedStart, + ) + : SimpleCopyButton( + data: ref + .watch(pFrostResharingData) + .startResharedData! + .resharedStart, + ), + ), + const SizedBox(height: 12), + FrostQrDialogPopupButton( + data: + ref.watch(pFrostResharingData).startResharedData!.resharedStart, + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 16, + ), + CheckboxTextButton( + label: "I have verified that everyone has my encryption key", + onChanged: (value) { + setState(() { + _userVerifyContinue = value; + }); + }, + ), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Continue", + enabled: _userVerifyContinue, + onPressed: () { + ref.read(pFrostCreateCurrentStep.state).state = 4; + Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart new file mode 100644 index 000000000..2eb7c8f65 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart @@ -0,0 +1,248 @@ +import 'dart:ffi'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; +import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart'; +import 'package:stackwallet/widgets/custom_buttons/checkbox_text_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/frost_qr_dialog_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/detail_item.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; +import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; + +// was FinishResharingView +class FrostReshareStep4 extends ConsumerStatefulWidget { + const FrostReshareStep4({super.key}); + + static const String routeName = "/frostReshareStep4"; + static const String title = "Resharer completes"; + + @override + ConsumerState createState() => _FrostReshareStep4State(); +} + +class _FrostReshareStep4State extends ConsumerState { + final List controllers = []; + final List focusNodes = []; + + late final Map resharers; + late final String myName; + late final int? myResharerIndexIndex; + late final String? myResharerComplete; + late final bool amOutgoingParticipant; + late final bool amNewParticipant; + + final List fieldIsEmptyFlags = []; + + bool _userVerifyContinue = false; + + bool _buttonLock = false; + Future _onPressed() async { + if (_buttonLock) { + return; + } + _buttonLock = true; + + try { + if (amOutgoingParticipant) { + ref.read(pFrostResharingData).reset(); + ref.read(pFrostScaffoldCanPopDesktop.notifier).state = true; + ref.read(pFrostScaffoldArgs)?.parentNav.popUntil( + ModalRoute.withName( + Util.isDesktop + ? DesktopWalletView.routeName + : WalletView.routeName, + ), + ); + } else { + // collect resharer completes strings and insert my own at the correct index + final resharerCompletes = controllers.map((e) => e.text).toList(); + if (myResharerIndexIndex != null && myResharerComplete != null) { + resharerCompletes.insert(myResharerIndexIndex!, myResharerComplete!); + } + + final data = Frost.finishReshared( + prior: ref.read(pFrostResharingData).startResharedData!.prior.ref, + resharerCompletes: resharerCompletes, + ); + + ref.read(pFrostResharingData).newWalletData = data; + + ref.read(pFrostCreateCurrentStep.state).state = 5; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + } + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + if (mounted) { + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Error", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + } finally { + _buttonLock = false; + } + } + + @override + void initState() { + amNewParticipant = + ref.read(pFrostResharingData).startResharerData == null && + ref.read(pFrostResharingData).incompleteWallet != null && + ref.read(pFrostResharingData).incompleteWallet?.walletId == + ref.read(pFrostScaffoldArgs)!.walletId!; + + myName = ref.read(pFrostResharingData).myName!; + + resharers = ref.read(pFrostResharingData).configData!.resharers; + + if (amNewParticipant) { + myResharerComplete = null; + myResharerIndexIndex = null; + amOutgoingParticipant = false; + } else { + myResharerComplete = ref.read(pFrostResharingData).resharerComplete!; + + final frostInfo = ref + .read(mainDBProvider) + .isar + .frostWalletInfo + .getByWalletIdSync(ref.read(pFrostScaffoldArgs)!.walletId!)!; + final myOldIndex = + frostInfo.participants.indexOf(ref.read(pFrostResharingData).myName!); + + myResharerIndexIndex = resharers.values.toList().indexOf(myOldIndex); + if (myResharerIndexIndex! >= 0) { + // remove my name for now as we don't need a text field for it + resharers.remove(ref.read(pFrostResharingData).myName!); + } + + amOutgoingParticipant = !ref + .read(pFrostResharingData) + .configData! + .newParticipants + .contains(ref.read(pFrostResharingData).myName!); + } + + for (int i = 0; i < resharers.length; i++) { + controllers.add(TextEditingController()); + focusNodes.add(FocusNode()); + fieldIsEmptyFlags.add(true); + } + super.initState(); + } + + @override + void dispose() { + for (int i = 0; i < controllers.length; i++) { + controllers[i].dispose(); + } + for (int i = 0; i < focusNodes.length; i++) { + focusNodes[i].dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + if (myResharerComplete != null) + DetailItem( + title: "My resharer complete", + detail: myResharerComplete!, + button: Util.isDesktop + ? IconCopyButton( + data: myResharerComplete!, + ) + : SimpleCopyButton( + data: myResharerComplete!, + ), + ), + if (myResharerComplete != null) const SizedBox(height: 12), + if (myResharerComplete != null) + FrostQrDialogPopupButton( + data: myResharerComplete!, + ), + if (!amOutgoingParticipant) + const SizedBox( + height: 16, + ), + if (!amOutgoingParticipant) + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < resharers.length; i++) + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: FrostStepField( + controller: controllers[i], + focusNode: focusNodes[i], + showQrScanOption: true, + label: resharers.keys.elementAt(i), + hint: "Enter " + "${resharers.keys.elementAt(i)}" + "'s resharer", + onChanged: (_) { + setState(() { + fieldIsEmptyFlags[i] = controllers[i].text.isEmpty; + }); + }, + ), + ), + ], + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 16, + ), + if (!amNewParticipant) + CheckboxTextButton( + label: "I have verified that everyone has my resharer complete", + onChanged: (value) { + setState(() { + _userVerifyContinue = value; + }); + }, + ), + if (!amNewParticipant) + const SizedBox( + height: 16, + ), + PrimaryButton( + label: amOutgoingParticipant ? "Done" : "Complete", + enabled: (amNewParticipant || _userVerifyContinue) && + (amOutgoingParticipant || + !fieldIsEmptyFlags.reduce((v, e) => v |= e)), + onPressed: _onPressed, + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_5.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_5.dart new file mode 100644 index 000000000..2171f5ad8 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_5.dart @@ -0,0 +1,221 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/pages/home_view/home_view.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; +import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/providers/global/node_service_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/utilities/logger.dart'; +import 'package:stackwallet/utilities/show_loading.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/detail_item.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; + +// was VerifyUpdatedWalletView +class FrostReshareStep5 extends ConsumerStatefulWidget { + const FrostReshareStep5({super.key}); + + static const String routeName = "/frostReshareStep5"; + static const String title = "Verify"; + + @override + ConsumerState createState() => _FrostReshareStep5State(); +} + +class _FrostReshareStep5State extends ConsumerState { + late final String config; + late final String serializedKeys; + late final String reshareId; + + late final bool isNew; + + bool _buttonLock = false; + Future _onPressed() async { + if (_buttonLock) { + return; + } + _buttonLock = true; + + try { + Exception? ex; + + final BitcoinFrostWallet wallet; + + if (isNew) { + wallet = await ref + .read(pFrostResharingData) + .incompleteWallet! + .toBitcoinFrostWallet( + mainDB: ref.read(mainDBProvider), + secureStorageInterface: ref.read(secureStoreProvider), + nodeService: ref.read(nodeServiceChangeNotifierProvider), + prefs: ref.read(prefsChangeNotifierProvider), + ); + + await wallet.info.setMnemonicVerified( + isar: ref.read(mainDBProvider).isar, + ); + + ref.read(pWallets).addWallet(wallet); + } else { + wallet = ref + .read(pWallets) + .getWallet(ref.read(pFrostScaffoldArgs)!.walletId!) + as BitcoinFrostWallet; + } + + if (mounted) { + await showLoading( + whileFuture: wallet.updateWithResharedData( + serializedKeys: serializedKeys, + multisigConfig: config, + isNewWallet: isNew, + ), + context: context, + message: isNew ? "Creating wallet" : "Updating wallet data", + rootNavigator: true, + onException: (e) => ex = e, + ); + + if (ex != null) { + throw ex!; + } + + if (mounted) { + ref.read(pFrostResharingData).reset(); + ref.read(pFrostScaffoldCanPopDesktop.notifier).state = true; + ref.read(pFrostScaffoldArgs)?.parentNav.popUntil( + ModalRoute.withName( + _popUntilPath, + ), + ); + } + } + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + if (mounted) { + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Error", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + } finally { + _buttonLock = false; + } + } + + String get _popUntilPath => isNew + ? Util.isDesktop + ? DesktopHomeView.routeName + : HomeView.routeName + : Util.isDesktop + ? DesktopWalletView.routeName + : WalletView.routeName; + + @override + void initState() { + config = ref.read(pFrostResharingData).newWalletData!.multisigConfig; + serializedKeys = + ref.read(pFrostResharingData).newWalletData!.serializedKeys; + reshareId = ref.read(pFrostResharingData).newWalletData!.resharedId; + + isNew = ref.read(pFrostResharingData).incompleteWallet != null && + ref.read(pFrostResharingData).incompleteWallet!.walletId == + ref.read(pFrostScaffoldArgs)!.walletId!; + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Text( + "Ensure your reshare ID matches that of each other participant", + style: STextStyles.pageTitleH2(context), + ), + const SizedBox( + height: 12, + ), + DetailItem( + title: "ID", + detail: reshareId, + button: Util.isDesktop + ? IconCopyButton( + data: reshareId, + ) + : SimpleCopyButton( + data: reshareId, + ), + ), + const SizedBox( + height: 12, + ), + const SizedBox( + height: 12, + ), + Text( + "Back up your keys and config", + style: STextStyles.pageTitleH2(context), + ), + const SizedBox( + height: 12, + ), + DetailItem( + title: "Config", + detail: config, + button: Util.isDesktop + ? IconCopyButton( + data: config, + ) + : SimpleCopyButton( + data: config, + ), + ), + const SizedBox( + height: 12, + ), + DetailItem( + title: "Keys", + detail: serializedKeys, + button: Util.isDesktop + ? IconCopyButton( + data: serializedKeys, + ) + : SimpleCopyButton( + data: serializedKeys, + ), + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 12, + ), + PrimaryButton( + label: "Confirm", + onPressed: _onPressed, + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart index 08f36ebde..68c220c1f 100644 --- a/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart @@ -8,7 +8,6 @@ import 'package:frostdart/frostdart.dart' as frost; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; -import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/global/node_service_provider.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; @@ -18,11 +17,11 @@ import 'package:stackwallet/services/frost.dart'; import 'package:stackwallet/themes/stack_colors.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/show_loading.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/frost_currency.dart'; import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart'; import 'package:stackwallet/wallets/isar/models/wallet_info.dart'; import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; @@ -33,6 +32,7 @@ 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/frost_mascot.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'; @@ -40,19 +40,17 @@ 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/pages/frost_mascot.dart'; - class RestoreFrostMsWalletView extends ConsumerStatefulWidget { const RestoreFrostMsWalletView({ super.key, required this.walletName, - required this.coin, + required this.frostCurrency, }); static const String routeName = "/restoreFrostMsWalletView"; final String walletName; - final Coin coin; + final FrostCurrency frostCurrency; @override ConsumerState createState() => @@ -77,7 +75,7 @@ class _RestoreFrostMsWalletViewState final myName = participants[myNameIndex]; final info = WalletInfo.createNew( - coin: widget.coin, + coin: widget.frostCurrency.coin, name: widget.walletName, ); @@ -132,7 +130,7 @@ class _RestoreFrostMsWalletViewState whileFuture: _createWalletAndRecover(), context: context, message: "Restoring wallet...", - isDesktop: Util.isDesktop, + rootNavigator: Util.isDesktop, onException: (e) { ex = e; }, @@ -214,13 +212,15 @@ class _RestoreFrostMsWalletViewState condition: Util.isDesktop, builder: (child) => DesktopScaffold( background: Theme.of(context).extension()!.background, - appBar: DesktopAppBar( + appBar: const DesktopAppBar( isCompactHeight: false, leading: AppBarBackButton(), + // TODO: [prio=high] get rid of placeholder text?? trailing: FrostMascot( title: 'Lorem ipsum', - body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', - ) + body: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', + ), ), body: SizedBox( width: 480, 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 7ddaaba3a..3cfeb6bbd 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 @@ -15,12 +15,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/sub_widgets/coin_image.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/new_wallet_options/new_wallet_options_view.dart'; import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart'; import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/ui/verify_recovery_phrase/mnemonic_word_count_state_provider.dart'; @@ -31,6 +30,8 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/name_generator.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin_frost.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/isar/models/wallet_info.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; @@ -243,7 +244,7 @@ class _NameYourWalletViewState extends ConsumerState { height: isDesktop ? 0 : 16, ), Text( - "Name your ${coin.prettyName} wallet", + "Name your ${coin.prettyName} ${coin.isFrost ? "multisig " : ""}wallet", textAlign: TextAlign.center, style: isDesktop ? STextStyles.desktopH2(context) @@ -253,7 +254,7 @@ class _NameYourWalletViewState extends ConsumerState { height: isDesktop ? 16 : 8, ), Text( - "Enter a label for your wallet (e.g. Savings)", + "Enter a label for your wallet (e.g. ${coin.isFrost ? "Multisig" : "Savings"})", textAlign: TextAlign.center, style: isDesktop ? STextStyles.desktopSubtitleH2(context) @@ -394,7 +395,10 @@ class _NameYourWalletViewState extends ConsumerState { RestoreFrostMsWalletView.routeName, arguments: ( walletName: name, - coin: coin, + // TODO: [prio=med] this will cause issues if frost is ever applied to other coins + frostCurrency: coin.isTestNet + ? BitcoinFrost(CryptoCurrencyNetwork.test) + : BitcoinFrost(CryptoCurrencyNetwork.main), ), ); }, @@ -403,7 +407,7 @@ class _NameYourWalletViewState extends ConsumerState { Column( children: [ PrimaryButton( - label: "Create config", + label: "Create new group", enabled: _nextEnabled, onPressed: () async { final name = textEditingController.text; @@ -412,7 +416,10 @@ class _NameYourWalletViewState extends ConsumerState { CreateNewFrostMsWalletView.routeName, arguments: ( walletName: name, - coin: coin, + // TODO: [prio=med] this will cause issues if frost is ever applied to other coins + frostCurrency: coin.isTestNet + ? BitcoinFrost(CryptoCurrencyNetwork.test) + : BitcoinFrost(CryptoCurrencyNetwork.main), ), ); }, @@ -421,38 +428,56 @@ class _NameYourWalletViewState extends ConsumerState { height: 12, ), SecondaryButton( - label: "Import multisig config", + label: "Join group", enabled: _nextEnabled, onPressed: () async { final name = textEditingController.text; await Navigator.of(context).pushNamed( - ImportNewFrostMsWalletView.routeName, + SelectNewFrostImportTypeView.routeName, arguments: ( walletName: name, - coin: coin, - ), - ); - }, - ), - const SizedBox( - height: 12, - ), - SecondaryButton( - label: "Import resharer config", - enabled: _nextEnabled, - onPressed: () async { - final name = textEditingController.text; - - await Navigator.of(context).pushNamed( - NewImportResharerConfigView.routeName, - arguments: ( - walletName: name, - coin: coin, + // TODO: [prio=med] this will cause issues if frost is ever applied to other coins + frostCurrency: coin.isTestNet + ? BitcoinFrost(CryptoCurrencyNetwork.test) + : BitcoinFrost(CryptoCurrencyNetwork.main), ), ); }, ), + // SecondaryButton( + // label: "Import multisig config", + // enabled: _nextEnabled, + // onPressed: () async { + // final name = textEditingController.text; + // + // await Navigator.of(context).pushNamed( + // ImportNewFrostMsWalletView.routeName, + // arguments: ( + // walletName: name, + // coin: coin, + // ), + // ); + // }, + // ), + // const SizedBox( + // height: 12, + // ), + // SecondaryButton( + // label: "Import resharer config", + // enabled: _nextEnabled, + // onPressed: () async { + // final name = textEditingController.text; + // + // await Navigator.of(context).pushNamed( + // NewImportResharerConfigView.routeName, + // arguments: ( + // walletName: name, + // coin: coin, + // ), + // ); + // }, + // ), ], ), if (!widget.coin.isFrost) diff --git a/lib/pages/cashfusion/fusion_progress_view.dart b/lib/pages/cashfusion/fusion_progress_view.dart index 746825d36..78bc9ccc7 100644 --- a/lib/pages/cashfusion/fusion_progress_view.dart +++ b/lib/pages/cashfusion/fusion_progress_view.dart @@ -79,7 +79,7 @@ class _FusionProgressViewState extends ConsumerState { Future.delayed(const Duration(seconds: 2)), ]), context: context, - isDesktop: Util.isDesktop, + rootNavigator: Util.isDesktop, message: "Stopping fusion", ); diff --git a/lib/pages/monkey/monkey_view.dart b/lib/pages/monkey/monkey_view.dart index a747011ee..9e8bdb090 100644 --- a/lib/pages/monkey/monkey_view.dart +++ b/lib/pages/monkey/monkey_view.dart @@ -349,7 +349,7 @@ class _MonkeyViewState extends ConsumerState { ), ]), context: context, - isDesktop: Util.isDesktop, + rootNavigator: Util.isDesktop, message: "Saving MonKey svg", onException: (e) { didError = true; @@ -402,7 +402,7 @@ class _MonkeyViewState extends ConsumerState { const Duration(seconds: 2)), ]), context: context, - isDesktop: Util.isDesktop, + rootNavigator: Util.isDesktop, message: "Downloading MonKey png", onException: (e) { didError = true; @@ -500,7 +500,7 @@ class _MonkeyViewState extends ConsumerState { Future.delayed(const Duration(seconds: 2)), ]), context: context, - isDesktop: Util.isDesktop, + rootNavigator: Util.isDesktop, message: "Fetching MonKey", subMessage: "We are fetching your MonKey", onException: (e) { diff --git a/lib/pages/ordinals/ordinal_details_view.dart b/lib/pages/ordinals/ordinal_details_view.dart index d15523e10..590bca266 100644 --- a/lib/pages/ordinals/ordinal_details_view.dart +++ b/lib/pages/ordinals/ordinal_details_view.dart @@ -321,7 +321,7 @@ class _OrdinalImageGroup extends ConsumerWidget { final filePath = await showLoading( whileFuture: _savePngToFile(ref), context: context, - isDesktop: true, + rootNavigator: true, message: "Saving ordinal image", onException: (e) { didError = true; diff --git a/lib/pages/send_view/frost_ms/frost_attempt_sign_config_view.dart b/lib/pages/send_view/frost_ms/frost_attempt_sign_config_view.dart deleted file mode 100644 index 149eb61d3..000000000 --- a/lib/pages/send_view/frost_ms/frost_attempt_sign_config_view.dart +++ /dev/null @@ -1,404 +0,0 @@ -import 'package:barcode_scan2/barcode_scan2.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:qr_flutter/qr_flutter.dart'; -import 'package:stackwallet/pages/send_view/frost_ms/frost_continue_sign_config_view.dart'; -import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; -import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; -import 'package:stackwallet/providers/global/wallets_provider.dart'; -import 'package:stackwallet/services/frost.dart'; -import 'package:stackwallet/themes/stack_colors.dart'; -import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/utilities/util.dart'; -import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.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/simple_copy_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/detail_item.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'; - -class FrostAttemptSignConfigView extends ConsumerStatefulWidget { - const FrostAttemptSignConfigView({ - super.key, - required this.walletId, - }); - - static const String routeName = "/frostAttemptSignConfigView"; - - final String walletId; - - @override - ConsumerState createState() => - _FrostAttemptSignConfigViewState(); -} - -class _FrostAttemptSignConfigViewState - extends ConsumerState { - final List controllers = []; - final List focusNodes = []; - - late final String myName; - late final List participantsWithoutMe; - late final String myPreprocess; - late final int myIndex; - late final int threshold; - - final List fieldIsEmptyFlags = []; - - bool hasEnoughPreprocesses() { - // own preprocess is not included in controllers and must be set here - int count = 1; - - for (final controller in controllers) { - if (controller.text.isNotEmpty) { - count++; - } - } - - return count >= threshold; - } - - @override - void initState() { - final wallet = - ref.read(pWallets).getWallet(widget.walletId) as BitcoinFrostWallet; - final frostInfo = wallet.frostInfo; - - myName = frostInfo.myName; - threshold = frostInfo.threshold; - participantsWithoutMe = List.from(frostInfo.participants); // Copy so it isn't fixed-length. - myIndex = participantsWithoutMe.indexOf(frostInfo.myName); - myPreprocess = ref.read(pFrostAttemptSignData.state).state!.preprocess; - - participantsWithoutMe.removeAt(myIndex); - - for (int i = 0; i < participantsWithoutMe.length; i++) { - controllers.add(TextEditingController()); - focusNodes.add(FocusNode()); - fieldIsEmptyFlags.add(true); - } - super.initState(); - } - - @override - void dispose() { - for (int i = 0; i < controllers.length; i++) { - controllers[i].dispose(); - } - for (int i = 0; i < focusNodes.length; i++) { - focusNodes[i].dispose(); - } - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: const DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton(), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Preprocesses", - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(16), - child: child, - ), - ), - ), - ); - }, - ), - ), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: 220, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QrImageView( - data: myPreprocess, - size: 220, - backgroundColor: - Theme.of(context).extension()!.background, - foregroundColor: Theme.of(context) - .extension()! - .accentColorDark, - ), - ], - ), - ), - const _Div(), - DetailItem( - title: "My name", - detail: myName, - ), - const _Div(), - DetailItem( - title: "My preprocess", - detail: myPreprocess, - button: Util.isDesktop - ? IconCopyButton( - data: myPreprocess, - ) - : SimpleCopyButton( - data: myPreprocess, - ), - ), - const _Div(), - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - for (int i = 0; i < participantsWithoutMe.length; i++) - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: Key("frostPreprocessesTextFieldKey_$i"), - controller: controllers[i], - focusNode: focusNodes[i], - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - onChanged: (_) { - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].text.isEmpty; - }); - }, - decoration: standardInputDecoration( - "Enter ${participantsWithoutMe[i]}'s preprocess", - focusNodes[i], - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: fieldIsEmptyFlags[i] - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - !fieldIsEmptyFlags[i] - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Preprocess Field Input.", - key: Key( - "frostPreprocessesClearButtonKey_$i", - ), - onTap: () { - controllers[i].text = ""; - - setState(() { - fieldIsEmptyFlags[i] = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Preprocess Field Input.", - key: Key( - "frostPreprocessesPasteButtonKey_$i", - ), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - controllers[i].text = - data.text!.trim(); - } - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i] - .text - .isEmpty; - }); - }, - child: fieldIsEmptyFlags[i] - ? const ClipboardIcon() - : const XIcon(), - ), - if (fieldIsEmptyFlags[i]) - TextFieldIconButton( - semanticsLabel: - "Scan QR Button. Opens Camera For Scanning QR Code.", - key: Key( - "frostPreprocessesScanQrButtonKey_$i", - ), - onTap: () async { - try { - if (FocusScope.of(context) - .hasFocus) { - FocusScope.of(context) - .unfocus(); - await Future.delayed( - const Duration( - milliseconds: 75)); - } - - final qrResult = - await BarcodeScanner.scan(); - - controllers[i].text = - qrResult.rawContent; - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].text.isEmpty; - }); - } on PlatformException catch (e, s) { - Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code: $e\n$s", - level: LogLevel.Warning, - ); - } - }, - child: const QrCodeIcon(), - ) - ], - ), - ), - ), - ), - ), - ), - ), - ], - ), - ], - ), - if (!Util.isDesktop) const Spacer(), - const _Div(), - PrimaryButton( - label: "Continue signing", - enabled: hasEnoughPreprocesses(), - onPressed: () async { - // collect Preprocess strings (not including my own) - final preprocesses = controllers.map((e) => e.text).toList(); - - // collect participants who are involved in this transaction - final List requiredParticipantsUnordered = []; - for (int i = 0; i < participantsWithoutMe.length; i++) { - if (preprocesses[i].isNotEmpty) { - requiredParticipantsUnordered.add(participantsWithoutMe[i]); - } - } - ref.read(pFrostSelectParticipantsUnordered.notifier).state = - requiredParticipantsUnordered; - - // insert an empty string at my index - preprocesses.insert(myIndex, ""); - - try { - ref.read(pFrostContinueSignData.notifier).state = - Frost.continueSigning( - machinePtr: - ref.read(pFrostAttemptSignData.state).state!.machinePtr, - preprocesses: preprocesses, - ); - - await Navigator.of(context).pushNamed( - FrostContinueSignView.routeName, - arguments: widget.walletId, - ); - } catch (e, s) { - Logging.instance.log( - "$e\n$s", - level: LogLevel.Fatal, - ); - - return await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Failed to continue signing", - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } - }, - ), - ], - ), - ), - ); - } -} - -class _Div extends StatelessWidget { - const _Div({super.key}); - - @override - Widget build(BuildContext context) { - return const SizedBox( - height: 12, - ); - } -} diff --git a/lib/pages/send_view/frost_ms/frost_complete_sign_view.dart b/lib/pages/send_view/frost_ms/frost_complete_sign_view.dart deleted file mode 100644 index 6478495c0..000000000 --- a/lib/pages/send_view/frost_ms/frost_complete_sign_view.dart +++ /dev/null @@ -1,206 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:qr_flutter/qr_flutter.dart'; -import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; -import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; -import 'package:stackwallet/pages_desktop_specific/my_stack_view/my_stack_view.dart'; -import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; -import 'package:stackwallet/providers/global/wallets_provider.dart'; -import 'package:stackwallet/themes/stack_colors.dart'; -import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/show_loading.dart'; -import 'package:stackwallet/utilities/text_styles.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/simple_copy_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/detail_item.dart'; -import 'package:stackwallet/widgets/stack_dialog.dart'; - -class FrostCompleteSignView extends ConsumerStatefulWidget { - const FrostCompleteSignView({ - super.key, - required this.walletId, - }); - - static const String routeName = "/frostCompleteSignView"; - - final String walletId; - - @override - ConsumerState createState() => - _FrostCompleteSignViewState(); -} - -class _FrostCompleteSignViewState extends ConsumerState { - bool _broadcastLock = false; - - @override - Widget build(BuildContext context) { - return ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: const DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton(), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Preview transaction", - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(16), - child: child, - ), - ), - ), - ); - }, - ), - ), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: 220, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QrImageView( - data: ref.watch(pFrostTxData.state).state!.raw!, - size: 220, - backgroundColor: - Theme.of(context).extension()!.background, - foregroundColor: Theme.of(context) - .extension()! - .accentColorDark, - ), - ], - ), - ), - const _Div(), - DetailItem( - title: "Raw transaction hex", - detail: ref.watch(pFrostTxData.state).state!.raw!, - button: Util.isDesktop - ? IconCopyButton( - data: ref.watch(pFrostTxData.state).state!.raw!, - ) - : SimpleCopyButton( - data: ref.watch(pFrostTxData.state).state!.raw!, - ), - ), - const _Div(), - if (!Util.isDesktop) const Spacer(), - const _Div(), - PrimaryButton( - label: "Broadcast Transaction", - onPressed: () async { - if (_broadcastLock) { - return; - } - _broadcastLock = true; - - try { - Exception? ex; - final txData = await showLoading( - whileFuture: ref - .read(pWallets) - .getWallet(widget.walletId) - .confirmSend( - txData: ref.read(pFrostTxData.state).state!, - ), - context: context, - message: "Broadcasting transaction to network", - isDesktop: Util.isDesktop, - onException: (e) { - ex = e; - }, - ); - - if (ex != null) { - throw ex!; - } - - if (mounted) { - if (txData != null) { - ref.read(pFrostTxData.state).state = txData; - Navigator.of(context).popUntil( - ModalRoute.withName( - Util.isDesktop - ? MyStackView.routeName - : WalletView.routeName, - ), - ); - } - } - } catch (e, s) { - Logging.instance.log( - "$e\n$s", - level: LogLevel.Fatal, - ); - - return await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Broadcast error", - message: e.toString(), - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } finally { - _broadcastLock = false; - } - }, - ), - ], - ), - ), - ); - } -} - -class _Div extends StatelessWidget { - const _Div({super.key}); - - @override - Widget build(BuildContext context) { - return const SizedBox( - height: 12, - ); - } -} diff --git a/lib/pages/send_view/frost_ms/frost_continue_sign_config_view.dart b/lib/pages/send_view/frost_ms/frost_continue_sign_config_view.dart deleted file mode 100644 index 732fb2f82..000000000 --- a/lib/pages/send_view/frost_ms/frost_continue_sign_config_view.dart +++ /dev/null @@ -1,445 +0,0 @@ -import 'package:barcode_scan2/barcode_scan2.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:qr_flutter/qr_flutter.dart'; -import 'package:stackwallet/pages/send_view/frost_ms/frost_complete_sign_view.dart'; -import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; -import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; -import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart'; -import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; -import 'package:stackwallet/providers/global/wallets_provider.dart'; -import 'package:stackwallet/services/frost.dart'; -import 'package:stackwallet/themes/stack_colors.dart'; -import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/utilities/util.dart'; -import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.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/simple_copy_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/detail_item.dart'; -import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.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'; - -class FrostContinueSignView extends ConsumerStatefulWidget { - const FrostContinueSignView({ - super.key, - required this.walletId, - }); - - static const String routeName = "/frostContinueSignView"; - - final String walletId; - - @override - ConsumerState createState() => - _FrostContinueSignViewState(); -} - -class _FrostContinueSignViewState extends ConsumerState { - final List controllers = []; - final List focusNodes = []; - - late final String myName; - late final List participantsWithoutMe; - late final List participantsAll; - late final String myShare; - late final int myIndex; - - final List fieldIsEmptyFlags = []; - - @override - void initState() { - final wallet = - ref.read(pWallets).getWallet(widget.walletId) as BitcoinFrostWallet; - - final frostInfo = wallet.frostInfo; - - myName = frostInfo.myName; - participantsAll = frostInfo.participants; - myIndex = frostInfo.participants.indexOf(frostInfo.myName); - myShare = ref.read(pFrostContinueSignData.state).state!.share; - - participantsWithoutMe = frostInfo.participants - .toSet() - .intersection( - ref.read(pFrostSelectParticipantsUnordered.state).state!.toSet()) - .toList(); - - participantsWithoutMe.remove(myName); - - for (int i = 0; i < participantsWithoutMe.length; i++) { - controllers.add(TextEditingController()); - focusNodes.add(FocusNode()); - fieldIsEmptyFlags.add(true); - } - super.initState(); - } - - @override - void dispose() { - for (int i = 0; i < controllers.length; i++) { - controllers[i].dispose(); - } - for (int i = 0; i < focusNodes.length; i++) { - focusNodes[i].dispose(); - } - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { - await showDialog( - context: context, - builder: (_) => FrostInterruptionDialog( - type: FrostInterruptionDialogType.transactionCreation, - popUntilOnYesRouteName: Util.isDesktop - ? DesktopWalletView.routeName - : WalletView.routeName, - ), - ); - return false; - }, - child: ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.transactionCreation, - popUntilOnYesRouteName: DesktopWalletView.routeName, - ), - ); - }, - ), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.transactionCreation, - popUntilOnYesRouteName: WalletView.routeName, - ), - ); - }, - ), - title: Text( - "Shares", - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(16), - child: child, - ), - ), - ), - ); - }, - ), - ), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: 220, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QrImageView( - data: myShare, - size: 220, - backgroundColor: Theme.of(context) - .extension()! - .background, - foregroundColor: Theme.of(context) - .extension()! - .accentColorDark, - ), - ], - ), - ), - const _Div(), - DetailItem( - title: "My name", - detail: myName, - ), - const _Div(), - DetailItem( - title: "My shares", - detail: myShare, - button: Util.isDesktop - ? IconCopyButton( - data: myShare, - ) - : SimpleCopyButton( - data: myShare, - ), - ), - const _Div(), - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - for (int i = 0; i < participantsWithoutMe.length; i++) - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: Key("frostSharesTextFieldKey_$i"), - controller: controllers[i], - focusNode: focusNodes[i], - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter ${participantsWithoutMe[i]}'s share", - focusNodes[i], - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: fieldIsEmptyFlags[i] - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - !fieldIsEmptyFlags[i] - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears " - "The Share Field Input.", - key: Key( - "frostSharesClearButtonKey_$i", - ), - onTap: () { - controllers[i].text = ""; - - setState(() { - fieldIsEmptyFlags[i] = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From " - "Clipboard To Share Field Input.", - key: Key( - "frostSharesPasteButtonKey_$i"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - controllers[i].text = - data.text!.trim(); - } - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i] - .text - .isEmpty; - }); - }, - child: fieldIsEmptyFlags[i] - ? const ClipboardIcon() - : const XIcon(), - ), - if (fieldIsEmptyFlags[i]) - TextFieldIconButton( - semanticsLabel: - "Scan QR Button. Opens Camera " - "For Scanning QR Code.", - key: Key( - "frostSharesScanQrButtonKey_$i", - ), - onTap: () async { - try { - if (FocusScope.of(context) - .hasFocus) { - FocusScope.of(context) - .unfocus(); - await Future.delayed( - const Duration( - milliseconds: 75)); - } - - final qrResult = - await BarcodeScanner.scan(); - - controllers[i].text = - qrResult.rawContent; - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i] - .text - .isEmpty; - }); - } on PlatformException catch (e, s) { - Logging.instance.log( - "Failed to get camera permissions " - "while trying to scan qr code: $e\n$s", - level: LogLevel.Warning, - ); - } - }, - child: const QrCodeIcon(), - ) - ], - ), - ), - ), - ), - ), - ), - ), - ], - ), - ], - ), - if (!Util.isDesktop) const Spacer(), - const _Div(), - PrimaryButton( - label: "Complete signing", - onPressed: () async { - // check for empty shares - if (controllers - .map((e) => e.text.isEmpty) - .reduce((value, element) => value |= element)) { - return await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Missing Shares", - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } - - // collect Share strings - final sharesCollected = - controllers.map((e) => e.text).toList(); - - final List shares = []; - for (final participant in participantsAll) { - if (participantsWithoutMe.contains(participant)) { - shares.add(sharesCollected[ - participantsWithoutMe.indexOf(participant)]); - } else { - shares.add(""); - } - } - - try { - final rawTx = Frost.completeSigning( - machinePtr: ref - .read(pFrostContinueSignData.state) - .state! - .machinePtr, - shares: shares, - ); - - ref.read(pFrostTxData.state).state = - ref.read(pFrostTxData.state).state!.copyWith( - raw: rawTx, - ); - - await Navigator.of(context).pushNamed( - FrostCompleteSignView.routeName, - arguments: widget.walletId, - ); - } catch (e, s) { - Logging.instance.log( - "$e\n$s", - level: LogLevel.Fatal, - ); - - return await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Failed to complete signing process", - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } - }, - ), - ], - ), - ), - ), - ); - } -} - -class _Div extends StatelessWidget { - const _Div({super.key}); - - @override - Widget build(BuildContext context) { - return const SizedBox( - height: 12, - ); - } -} diff --git a/lib/pages/send_view/frost_ms/frost_create_sign_config_view.dart b/lib/pages/send_view/frost_ms/frost_create_sign_config_view.dart deleted file mode 100644 index bb2c129ca..000000000 --- a/lib/pages/send_view/frost_ms/frost_create_sign_config_view.dart +++ /dev/null @@ -1,185 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:qr_flutter/qr_flutter.dart'; -import 'package:stackwallet/pages/send_view/frost_ms/frost_attempt_sign_config_view.dart'; -import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; -import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; -import 'package:stackwallet/providers/global/wallets_provider.dart'; -import 'package:stackwallet/themes/stack_colors.dart'; -import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/utilities/util.dart'; -import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.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/simple_copy_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/detail_item.dart'; - -class FrostCreateSignConfigView extends ConsumerStatefulWidget { - const FrostCreateSignConfigView({ - super.key, - required this.walletId, - }); - - static const String routeName = "/frostCreateSignConfigView"; - - final String walletId; - - @override - ConsumerState createState() => - _FrostCreateSignConfigViewState(); -} - -class _FrostCreateSignConfigViewState - extends ConsumerState { - bool _attemptSignLock = false; - - Future _attemptSign() async { - if (_attemptSignLock) { - return; - } - - _attemptSignLock = true; - - try { - final wallet = - ref.read(pWallets).getWallet(widget.walletId) as BitcoinFrostWallet; - - final attemptSignRes = await wallet.frostAttemptSignConfig( - config: ref.read(pFrostTxData.state).state!.frostMSConfig!, - ); - - ref.read(pFrostAttemptSignData.notifier).state = attemptSignRes; - - await Navigator.of(context).pushNamed( - FrostAttemptSignConfigView.routeName, - arguments: widget.walletId, - ); - } catch (e, s) { - Logging.instance.log( - "$e\n$s", - level: LogLevel.Error, - ); - } finally { - _attemptSignLock = false; - } - } - - @override - Widget build(BuildContext context) { - double qrImageSize = - Util.isDesktop ? 360 : MediaQuery.of(context).size.width - 32; - return ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: const DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton(), - ), - body: SingleChildScrollView( - child: SizedBox( - width: 600, // Was 480, may look better but overflows the bottom. - child: child, - ), - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Sign config", - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: child, - ), - ), - ), - ); - }, - ), - ), - ), - ), - child: Column( - children: [ - if (!Util.isDesktop) const Spacer(), - SizedBox( - height: qrImageSize, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QrImageView( - data: ref.watch(pFrostTxData.state).state!.frostMSConfig!, - size: qrImageSize, - backgroundColor: - Theme.of(context).extension()!.background, - foregroundColor: Theme.of(context) - .extension()! - .accentColorDark, - ), - ], - ), - ), - if (!Util.isDesktop) - const SizedBox( - height: 32, - ), - DetailItem( - title: "Encoded config", - detail: ref.watch(pFrostTxData.state).state!.frostMSConfig!, - button: Util.isDesktop - ? IconCopyButton( - data: ref.watch(pFrostTxData.state).state!.frostMSConfig!, - ) - : SimpleCopyButton( - data: ref.watch(pFrostTxData.state).state!.frostMSConfig!, - ), - ), - SizedBox( - height: Util.isDesktop ? 20 : 16, - ), - if (!Util.isDesktop) - const Spacer( - flex: 2, - ), - PrimaryButton( - label: "Attempt sign", - onPressed: () { - _attemptSign(); - }, - ), - const SizedBox( - height: 16, - ), - ], - ), - ), - ); - } -} diff --git a/lib/pages/send_view/frost_ms/frost_import_sign_config_view.dart b/lib/pages/send_view/frost_ms/frost_import_sign_config_view.dart deleted file mode 100644 index c89b6846a..000000000 --- a/lib/pages/send_view/frost_ms/frost_import_sign_config_view.dart +++ /dev/null @@ -1,332 +0,0 @@ -import 'package:barcode_scan2/barcode_scan2.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:isar/isar.dart'; -import 'package:stackwallet/models/isar/models/isar_models.dart'; -import 'package:stackwallet/pages/send_view/frost_ms/frost_attempt_sign_config_view.dart'; -import 'package:stackwallet/providers/db/main_db_provider.dart'; -import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; -import 'package:stackwallet/providers/global/wallets_provider.dart'; -import 'package:stackwallet/services/frost.dart'; -import 'package:stackwallet/themes/stack_colors.dart'; -import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/format.dart'; -import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/utilities/util.dart'; -import 'package:stackwallet/wallets/models/tx_data.dart'; -import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.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_app_bar.dart'; -import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; -import 'package:stackwallet/widgets/desktop/primary_button.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'; - -class FrostImportSignConfigView extends ConsumerStatefulWidget { - const FrostImportSignConfigView({ - super.key, - required this.walletId, - }); - - static const String routeName = "/frostImportSignConfigView"; - - final String walletId; - - @override - ConsumerState createState() => - _FrostImportSignConfigViewState(); -} - -class _FrostImportSignConfigViewState - extends ConsumerState { - late final TextEditingController configFieldController; - late final FocusNode configFocusNode; - - bool _configEmpty = true; - - bool _attemptSignLock = false; - - Future _attemptSign() async { - if (_attemptSignLock) { - return; - } - - _attemptSignLock = true; - - try { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - } - - final config = configFieldController.text; - final wallet = - ref.read(pWallets).getWallet(widget.walletId) as BitcoinFrostWallet; - - final data = Frost.extractDataFromSignConfig( - signConfig: config, - coin: wallet.cryptoCurrency, - ); - - final utxos = await ref - .read(mainDBProvider) - .getUTXOs(wallet.walletId) - .filter() - .anyOf( - data.inputs, - (q, e) => q - .txidEqualTo(Format.uint8listToString(e.hash)) - .and() - .valueEqualTo(e.value) - .and() - .voutEqualTo(e.vout)) - .findAll(); - - // TODO add more data from 'data' and display to user ? - ref.read(pFrostTxData.notifier).state = TxData( - frostMSConfig: config, - recipients: data.recipients - .map((e) => (address: e.address, amount: e.amount, isChange: false)) - .toList(), - utxos: utxos.toSet(), - ); - - final attemptSignRes = await wallet.frostAttemptSignConfig( - config: ref.read(pFrostTxData.state).state!.frostMSConfig!, - ); - - ref.read(pFrostAttemptSignData.notifier).state = attemptSignRes; - - await Navigator.of(context).pushNamed( - FrostAttemptSignConfigView.routeName, - arguments: widget.walletId, - ); - } catch (e, s) { - Logging.instance.log( - "$e\n$s", - level: LogLevel.Error, - ); - await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Import and attempt sign config failed", - message: e.toString(), - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } finally { - _attemptSignLock = false; - } - } - - @override - void initState() { - configFieldController = TextEditingController(); - configFocusNode = FocusNode(); - super.initState(); - } - - @override - void dispose() { - configFieldController.dispose(); - configFocusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: const DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton(), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Import FROST sign config", - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(16), - child: child, - ), - ), - ), - ); - }, - ), - ), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 16, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("frConfigTextFieldKey"), - controller: configFieldController, - onChanged: (_) { - setState(() { - _configEmpty = configFieldController.text.isEmpty; - }); - }, - focusNode: configFocusNode, - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter config", - configFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: _configEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - !_configEmpty - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Config Field.", - key: const Key("frConfigClearButtonKey"), - onTap: () { - configFieldController.text = ""; - - setState(() { - _configEmpty = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Config Field Input.", - key: const Key("frConfigPasteButtonKey"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - configFieldController.text = - data.text!.trim(); - } - - setState(() { - _configEmpty = - configFieldController.text.isEmpty; - }); - }, - child: _configEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - if (_configEmpty) - TextFieldIconButton( - semanticsLabel: - "Scan QR Button. Opens Camera For Scanning QR Code.", - key: const Key("frConfigScanQrButtonKey"), - onTap: () async { - try { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 75)); - } - - final qrResult = await BarcodeScanner.scan(); - - configFieldController.text = - qrResult.rawContent; - - setState(() { - _configEmpty = - configFieldController.text.isEmpty; - }); - } on PlatformException catch (e, s) { - Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code: $e\n$s", - level: LogLevel.Warning, - ); - } - }, - child: const QrCodeIcon(), - ) - ], - ), - ), - ), - ), - ), - ), - const SizedBox( - height: 16, - ), - if (!Util.isDesktop) const Spacer(), - const SizedBox( - height: 16, - ), - PrimaryButton( - label: "Start signing", - enabled: !_configEmpty, - onPressed: () { - _attemptSign(); - }, - ), - ], - ), - ), - ); - } -} diff --git a/lib/pages/send_view/frost_ms/frost_send_view.dart b/lib/pages/send_view/frost_ms/frost_send_view.dart index 5f04bb346..133d0afd4 100644 --- a/lib/pages/send_view/frost_ms/frost_send_view.dart +++ b/lib/pages/send_view/frost_ms/frost_send_view.dart @@ -14,10 +14,9 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/frost_route_generator.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/pages/coin_control/coin_control_view.dart'; -import 'package:stackwallet/pages/send_view/frost_ms/frost_create_sign_config_view.dart'; -import 'package:stackwallet/pages/send_view/frost_ms/frost_import_sign_config_view.dart'; import 'package:stackwallet/pages/send_view/frost_ms/recipient.dart'; import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; import 'package:stackwallet/providers/providers.dart'; @@ -25,7 +24,6 @@ import 'package:stackwallet/themes/coin_icon_provider.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/amount/amount_formatter.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/show_loading.dart'; @@ -39,7 +37,10 @@ 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'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/fee_slider.dart'; +import 'package:stackwallet/widgets/frost_scaffold.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'; @@ -49,10 +50,10 @@ import 'package:tuple/tuple.dart'; class FrostSendView extends ConsumerStatefulWidget { const FrostSendView({ - Key? key, + super.key, required this.walletId, required this.coin, - }) : super(key: key); + }); static const String routeName = "/frostSendView"; @@ -115,19 +116,33 @@ class _FrostSendViewState extends ConsumerState { whileFuture: _loadingFuture(), context: context, message: "Generating sign config", - isDesktop: Util.isDesktop, + rootNavigator: Util.isDesktop, onException: (e) { throw e; }, ); } + final wallet = + ref.read(pWallets).getWallet(walletId) as BitcoinFrostWallet; + if (mounted && txData != null) { ref.read(pFrostTxData.notifier).state = txData; + ref.read(pFrostScaffoldArgs.state).state = ( + info: ( + walletName: wallet.info.name, + frostCurrency: wallet.cryptoCurrency, + ), + walletId: walletId, + stepRoutes: FrostRouteGenerator.sendFrostTxStepRoutes, + parentNav: Navigator.of(context), + frostInterruptionDialogType: + FrostInterruptionDialogType.transactionCreation, + ); + await Navigator.of(context).pushNamed( - FrostCreateSignConfigView.routeName, - arguments: widget.walletId, + FrostStepScaffold.routeName, ); } } catch (e) { @@ -168,16 +183,24 @@ class _FrostSendViewState extends ConsumerState { int customFeeRate = 1; - void _validateRecipientFormStates() { + bool _buttonEnabled = false; + + bool _validateRecipientFormStatesHelper() { for (final i in recipientWidgetIndexes) { - final state = ref.read(pRecipient(i).state).state; - if (state?.amount == null || state?.address == null) { - ref.read(previewTxButtonStateProvider.notifier).state = false; - return; + final state = ref.read(pRecipient(i)); + if (state?.amount == null || + state?.address == null || + state!.address.isEmpty) { + return false; } } - ref.read(previewTxButtonStateProvider.notifier).state = true; - return; + return true; + } + + void _validateRecipientFormStates() { + setState(() { + _buttonEnabled = _validateRecipientFormStatesHelper(); + }); } @override @@ -223,7 +246,7 @@ class _FrostSendViewState extends ConsumerState { FocusScope.of(context).unfocus(); await Future.delayed(const Duration(milliseconds: 50)); } - if (mounted) { + if (context.mounted) { Navigator.of(context).pop(); } }, @@ -232,40 +255,6 @@ class _FrostSendViewState extends ConsumerState { "Send ${coin.ticker}", style: STextStyles.navBarTitle(context), ), - actions: [ - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - semanticsLabel: "Import sign config Button.", - key: const Key("importSignConfigButtonKey"), - size: 36, - shadows: const [], - color: - Theme.of(context).extension()!.background, - icon: SvgPicture.asset( - Assets.svg.circlePlus, - color: Theme.of(context) - .extension()! - .accentColorDark, - width: 20, - height: 20, - ), - onPressed: () { - Navigator.of(context).pushNamed( - FrostImportSignConfigView.routeName, - arguments: walletId, - ); - }, - ), - ), - ), - ], ), body: LayoutBuilder( builder: (builderContext, constraints) { @@ -349,14 +338,7 @@ class _FrostSendViewState extends ConsumerState { ) : const Spacer(), GestureDetector( - onTap: () { - // cryptoAmountController.text = ref - // .read(pAmountFormatter(coin)) - // .format( - // _cachedBalance!, - // withUnitName: false, - // ); - }, + onTap: () {}, child: Container( color: Colors.transparent, child: Column( @@ -372,24 +354,6 @@ class _FrostSendViewState extends ConsumerState { ), textAlign: TextAlign.right, ), - // Text( - // "${(manager.balance.spendable.decimal * ref.watch( - // priceAnd24hChangeNotifierProvider.select( - // (value) => value.getPrice(coin).item1, - // ), - // )).toAmount( - // fractionDigits: 2, - // ).fiatString( - // locale: locale, - // )} ${ref.watch( - // prefsChangeNotifierProvider - // .select((value) => value.currency), - // )}", - // style: STextStyles.subtitle(context).copyWith( - // fontSize: 8, - // ), - // textAlign: TextAlign.right, - // ) ], ), ), @@ -398,30 +362,8 @@ class _FrostSendViewState extends ConsumerState { ), ), ), - const SizedBox( - height: 16, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Recipients", - style: STextStyles.smallMed12(context), - textAlign: TextAlign.left, - ), - CustomTextButton( - text: "Add", - onTap: () { - // used for tracking recipient forms - _greatestWidgetIndex++; - recipientWidgetIndexes.add(_greatestWidgetIndex); - setState(() {}); - }, - ), - ], - ), - const SizedBox( - height: 8, + SizedBox( + height: recipientWidgetIndexes.length > 1 ? 8 : 16, ), Column( children: [ @@ -437,6 +379,7 @@ class _FrostSendViewState extends ConsumerState { "recipientKey_${recipientWidgetIndexes[i]}", ), index: recipientWidgetIndexes[i], + displayNumber: i + 1, coin: coin, onChanged: () { _validateRecipientFormStates(); @@ -444,13 +387,46 @@ class _FrostSendViewState extends ConsumerState { remove: i == 0 && recipientWidgetIndexes.length == 1 ? null : () { + ref + .read(pRecipient(recipientWidgetIndexes[i]) + .notifier) + .state = null; recipientWidgetIndexes.removeAt(i); setState(() {}); + _validateRecipientFormStates(); }, + addAnotherRecipientTapped: () { + // used for tracking recipient forms + _greatestWidgetIndex++; + recipientWidgetIndexes.add(_greatestWidgetIndex); + setState(() {}); + _validateRecipientFormStates(); + }, + sendAllTapped: () { + return ref.read(pAmountFormatter(coin)).format( + ref.read(pWalletBalance(walletId)).spendable, + withUnitName: false, + ); + }, ), ), ], ), + if (recipientWidgetIndexes.length > 1) + const SizedBox( + height: 12, + ), + if (recipientWidgetIndexes.length > 1) + SecondaryButton( + width: double.infinity, + label: "Add recipient", + onPressed: () { + // used for tracking recipient forms + _greatestWidgetIndex++; + recipientWidgetIndexes.add(_greatestWidgetIndex); + setState(() {}); + }, + ), if (showCoinControl) const SizedBox( height: 8, @@ -481,12 +457,6 @@ class _FrostSendViewState extends ConsumerState { } if (mounted) { - // finally spendable = ref - // .read(walletsChangeNotifierProvider) - // .getManager(widget.walletId) - // .balance - // .spendable; - // TODO: [prio=high] make sure this coincontrol works correctly Amount? amount; @@ -571,6 +541,7 @@ class _FrostSendViewState extends ConsumerState { ), child: FeeSlider( coin: coin, + showWU: true, onSatVByteChanged: (rate) { customFeeRate = rate; }, @@ -584,21 +555,10 @@ class _FrostSendViewState extends ConsumerState { const SizedBox( height: 12, ), - TextButton( - onPressed: ref.watch(previewTxButtonStateProvider.state).state - ? _createSignConfig - : null, - style: ref.watch(previewTxButtonStateProvider.state).state - ? Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context) - : Theme.of(context) - .extension()! - .getPrimaryDisabledButtonStyle(context), - child: Text( - "Create config", - style: STextStyles.button(context), - ), + PrimaryButton( + label: "Create multisig transaction", + enabled: _buttonEnabled, + onPressed: _createSignConfig, ), const SizedBox( height: 16, @@ -609,5 +569,3 @@ class _FrostSendViewState extends ConsumerState { ); } } - -final previewTxButtonStateProvider = StateProvider((_) => false); diff --git a/lib/pages/send_view/frost_ms/recipient.dart b/lib/pages/send_view/frost_ms/recipient.dart index 89121d065..043f080f6 100644 --- a/lib/pages/send_view/frost_ms/recipient.dart +++ b/lib/pages/send_view/frost_ms/recipient.dart @@ -20,7 +20,7 @@ import 'package:stackwallet/widgets/custom_buttons/blue_text_button.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/rounded_white_container.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; @@ -45,16 +45,22 @@ class Recipient extends ConsumerStatefulWidget { const Recipient({ super.key, required this.index, + required this.displayNumber, required this.coin, this.remove, this.onChanged, + required this.addAnotherRecipientTapped, + required this.sendAllTapped, }); final int index; + final int displayNumber; final Coin coin; final VoidCallback? remove; final VoidCallback? onChanged; + final VoidCallback addAnotherRecipientTapped; + final String Function() sendAllTapped; @override ConsumerState createState() => _RecipientState(); @@ -65,7 +71,9 @@ class _RecipientState extends ConsumerState { late final FocusNode addressFocusNode, amountFocusNode; bool _addressIsEmpty = true; - bool _cryptoAmountChangeLock = false; + final bool _cryptoAmountChangeLock = false; + + bool get isSingle => widget.remove == null; void _updateRecipientData() { final address = addressController.text; @@ -116,6 +124,16 @@ class _RecipientState extends ConsumerState { amountController = TextEditingController(); // baseController = TextEditingController(); + final amount = ref.read(pRecipient(widget.index))?.amount; + if (amount != null) { + amountController.text = ref + .read(pAmountFormatter(widget.coin)) + .format(amount, withUnitName: false); + } + addressController.text = ref.read(pRecipient(widget.index))?.address ?? ""; + + _addressIsEmpty = addressController.text.isEmpty; + addressFocusNode = FocusNode(); amountFocusNode = FocusNode(); // baseFocusNode = FocusNode(); @@ -148,12 +166,31 @@ class _RecipientState extends ConsumerState { ), ); - return RoundedWhiteContainer( + return RoundedContainer( + color: Colors.transparent, padding: const EdgeInsets.all(0), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + isSingle ? "Send to" : "Recipient ${widget.displayNumber}", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + CustomTextButton( + text: isSingle ? "Add another recipient" : "Remove", + onTap: + isSingle ? widget.addAnotherRecipientTapped : widget.remove, + ), + ], + ), + const SizedBox( + height: 8, + ), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -167,6 +204,7 @@ class _RecipientState extends ConsumerState { focusNode: addressFocusNode, style: STextStyles.field(context), onChanged: (_) { + _updateRecipientData(); setState(() { _addressIsEmpty = addressController.text.isEmpty; }); @@ -323,9 +361,33 @@ class _RecipientState extends ConsumerState { ), ), ), - const SizedBox( - height: 12, + SizedBox( + height: isSingle ? 12 : 8, ), + if (isSingle) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Amount", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + // disable send all since the frost tx creation logic isn't there (yet?) + const Spacer(), + // CustomTextButton( + // text: "Send all ${widget.coin.ticker}", + // onTap: () { + // amountController.text = widget.sendAllTapped(); + // _cryptoAmountChanged(); + // }, + // ), + ], + ), + if (isSingle) + const SizedBox( + height: 8, + ), TextField( autocorrect: false, enableSuggestions: false, @@ -335,6 +397,9 @@ class _RecipientState extends ConsumerState { key: const Key("amountInputFieldCryptoTextFieldKey"), controller: amountController, focusNode: amountFocusNode, + onChanged: (_) { + _updateRecipientData(); + }, keyboardType: Util.isDesktop ? null : const TextInputType.numberWithOptions( @@ -375,126 +440,6 @@ class _RecipientState extends ConsumerState { ), ), ), - // if (ref.watch(prefsChangeNotifierProvider - // .select((value) => value.externalCalls))) - // const SizedBox( - // height: 8, - // ), - // if (ref.watch(prefsChangeNotifierProvider - // .select((value) => value.externalCalls))) - // TextField( - // autocorrect: Util.isDesktop ? false : true, - // enableSuggestions: Util.isDesktop ? false : true, - // style: STextStyles.smallMed14(context).copyWith( - // color: Theme.of(context).extension()!.textDark, - // ), - // key: const Key("amountInputFieldFiatTextFieldKey"), - // controller: baseController, - // focusNode: baseFocusNode, - // keyboardType: Util.isDesktop - // ? null - // : const TextInputType.numberWithOptions( - // signed: false, - // decimal: true, - // ), - // textAlign: TextAlign.right, - // inputFormatters: [ - // AmountInputFormatter( - // decimals: 2, - // locale: locale, - // ), - // ], - // onChanged: (baseAmountString) { - // final baseAmount = Amount.tryParseFiatString( - // baseAmountString, - // locale: locale, - // ); - // Amount? cryptoAmount; - // final int decimals = widget.coin.decimals; - // if (baseAmount != null) { - // final _price = ref.read(_pPrice(widget.coin)); - // - // if (_price == Decimal.zero) { - // cryptoAmount = 0.toAmountAsRaw( - // fractionDigits: decimals, - // ); - // } else { - // cryptoAmount = baseAmount <= Amount.zero - // ? 0.toAmountAsRaw(fractionDigits: decimals) - // : (baseAmount.decimal / _price) - // .toDecimal( - // scaleOnInfinitePrecision: decimals, - // ) - // .toAmount(fractionDigits: decimals); - // } - // if (ref.read(pRecipient(widget.index))?.amount != null && - // ref.read(pRecipient(widget.index))?.amount == - // cryptoAmount) { - // return; - // } - // - // final amountString = - // ref.read(pAmountFormatter(widget.coin)).format( - // cryptoAmount, - // withUnitName: false, - // ); - // - // _cryptoAmountChangeLock = true; - // amountController.text = amountString; - // _cryptoAmountChangeLock = false; - // } else { - // cryptoAmount = 0.toAmountAsRaw( - // fractionDigits: decimals, - // ); - // _cryptoAmountChangeLock = true; - // amountController.text = ""; - // _cryptoAmountChangeLock = false; - // } - // - // _updateRecipientData(); - // }, - // 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()! - // .accentColorDark), - // ), - // ), - // ), - // ), - // ), - if (widget.remove != null) - const SizedBox( - height: 6, - ), - if (widget.remove != null) - Row( - children: [ - const Spacer(), - CustomTextButton( - text: "Remove", - onTap: () { - ref.read(pRecipient(widget.index).notifier).state = null; - widget.remove?.call(); - }, - ), - ], - ), ], ), ); diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1a.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1a.dart new file mode 100644 index 000000000..d49474f6d --- /dev/null +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1a.dart @@ -0,0 +1,236 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; +import 'package:stackwallet/widgets/custom_buttons/checkbox_text_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/detail_item.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class FrostSendStep1a extends ConsumerStatefulWidget { + const FrostSendStep1a({super.key}); + + static const String routeName = "/FrostSendStep1a"; + static const String title = "FROST transaction"; + + @override + ConsumerState createState() => _FrostSendStep1aState(); +} + +class _FrostSendStep1aState extends ConsumerState { + static const steps2to4 = [ + "Wait for them to import the transaction config.", + "Verify that everyone has filled out their forms before continuing. If you " + "try to continue before everyone is ready, the process will be " + "canceled.", + "Check the box and press “Attempt sign”.", + ]; + + late final int _threshold; + + bool _userVerifyContinue = false; + bool _attemptSignLock = false; + + Future _attemptSign() async { + if (_attemptSignLock) { + return; + } + + _attemptSignLock = true; + + try { + final wallet = ref.read(pWallets).getWallet( + ref.read(pFrostScaffoldArgs)!.walletId!, + ) as BitcoinFrostWallet; + + final attemptSignRes = await wallet.frostAttemptSignConfig( + config: ref.read(pFrostTxData.state).state!.frostMSConfig!, + ); + + ref.read(pFrostAttemptSignData.notifier).state = attemptSignRes; + + ref.read(pFrostCreateCurrentStep.state).state = 2; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Error, + ); + } finally { + _attemptSignLock = false; + } + } + + @override + void initState() { + final wallet = ref.read(pWallets).getWallet( + ref.read(pFrostScaffoldArgs)!.walletId!, + ) as BitcoinFrostWallet; + + _threshold = wallet.frostInfo.threshold; + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(pFrostMyName.state).state = wallet.frostInfo.myName; + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final double qrImageSize = + Util.isDesktop ? 360 : MediaQuery.of(context).size.width / 1.67; + + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "1.", + style: STextStyles.w500_12(context), + ), + const SizedBox( + width: 4, + ), + Expanded( + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: + "Share this config with the group members. ", + style: STextStyles.w500_12(context), + ), + TextSpan( + text: + "You must have the threshold number of signatures (including yours) to send the transaction.", + style: STextStyles.w600_12(context).copyWith( + color: Theme.of(context) + .extension()! + .customTextButtonEnabledText, + ), + ), + ], + ), + ), + ), + ], + ), + for (int i = 0; i < steps2to4.length; i++) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${i + 2}.", + style: STextStyles.w500_12(context), + ), + const SizedBox( + width: 4, + ), + Expanded( + child: Text( + steps2to4[i], + style: STextStyles.w500_12(context), + ), + ), + ], + ), + ), + ], + ), + ), + SizedBox( + height: Util.isDesktop ? 20 : 12, + ), + SizedBox( + height: qrImageSize, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QrImageView( + data: ref.watch(pFrostTxData.state).state!.frostMSConfig!, + size: qrImageSize, + backgroundColor: + Theme.of(context).extension()!.background, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark, + ), + ], + ), + ), + SizedBox( + height: Util.isDesktop ? 20 : 12, + ), + DetailItem( + title: "Encoded transaction config", + detail: ref.watch(pFrostTxData.state).state!.frostMSConfig!, + button: Util.isDesktop + ? IconCopyButton( + data: ref.watch(pFrostTxData.state).state!.frostMSConfig!, + ) + : SimpleCopyButton( + data: ref.watch(pFrostTxData.state).state!.frostMSConfig!, + ), + ), + SizedBox( + height: Util.isDesktop ? 20 : 12, + ), + DetailItem( + title: "Threshold", + detail: "$_threshold signatures", + horizontal: true, + ), + SizedBox( + height: Util.isDesktop ? 20 : 12, + ), + if (!Util.isDesktop) + const Spacer( + flex: 2, + ), + CheckboxTextButton( + label: "I have verified that everyone has imported the config and " + "is ready to sign", + onChanged: (value) { + setState(() { + _userVerifyContinue = value; + }); + }, + ), + SizedBox( + height: Util.isDesktop ? 20 : 12, + ), + PrimaryButton( + label: "Attempt sign", + enabled: _userVerifyContinue, + onPressed: () { + _attemptSign(); + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1b.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1b.dart new file mode 100644 index 000000000..197d72e63 --- /dev/null +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1b.dart @@ -0,0 +1,196 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/models/isar/models/isar_models.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/utilities/format.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/models/tx_data.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; +import 'package:stackwallet/widgets/custom_buttons/checkbox_text_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/frost_step_user_steps.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; +import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; + +class FrostSendStep1b extends ConsumerStatefulWidget { + const FrostSendStep1b({super.key}); + + static const String routeName = "/FrostSendStep1b"; + static const String title = "Sign FROST transaction"; + + @override + ConsumerState createState() => _FrostSendStep1bState(); +} + +class _FrostSendStep1bState extends ConsumerState { + static const info = [ + "Scan the config QR code or paste the code provided by the member " + "initiating this transaction.", + "Wait for other members to finish entering their information.", + "Verify that everyone has filled out their forms before continuing. If you " + "try to continue before everyone is ready, the process will be " + "canceled.", + "Check the box and press “Start signing”.", + ]; + + late final TextEditingController configFieldController; + late final FocusNode configFocusNode; + + bool _configEmpty = true, _userVerifyContinue = false; + + bool _attemptSignLock = false; + + Future _attemptSign() async { + if (_attemptSignLock) { + return; + } + + _attemptSignLock = true; + + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + } + + final config = configFieldController.text; + final wallet = ref.read(pWallets).getWallet( + ref.read(pFrostScaffoldArgs)!.walletId!, + ) as BitcoinFrostWallet; + + final data = Frost.extractDataFromSignConfig( + signConfig: config, + coin: wallet.cryptoCurrency, + ); + + final utxos = await ref + .read(mainDBProvider) + .getUTXOs(wallet.walletId) + .filter() + .anyOf( + data.inputs, + (q, e) => q + .txidEqualTo(Format.uint8listToString(e.hash)) + .and() + .valueEqualTo(e.value) + .and() + .voutEqualTo(e.vout)) + .findAll(); + + // TODO add more data from 'data' and display to user ? + ref.read(pFrostTxData.notifier).state = TxData( + frostMSConfig: config, + recipients: data.recipients + .map((e) => (address: e.address, amount: e.amount, isChange: false)) + .toList(), + utxos: utxos.toSet(), + ); + + final attemptSignRes = await wallet.frostAttemptSignConfig( + config: ref.read(pFrostTxData.state).state!.frostMSConfig!, + ); + + ref.read(pFrostAttemptSignData.notifier).state = attemptSignRes; + + ref.read(pFrostCreateCurrentStep.state).state = 2; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Error, + ); + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Import and attempt sign config failed", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } finally { + _attemptSignLock = false; + } + } + + @override + void initState() { + configFieldController = TextEditingController(); + configFocusNode = FocusNode(); + final wallet = ref.read(pWallets).getWallet( + ref.read(pFrostScaffoldArgs)!.walletId!, + ) as BitcoinFrostWallet; + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(pFrostMyName.state).state = wallet.frostInfo.myName; + }); + super.initState(); + } + + @override + void dispose() { + configFieldController.dispose(); + configFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const FrostStepUserSteps( + userSteps: info, + ), + const SizedBox(height: 20), + FrostStepField( + controller: configFieldController, + focusNode: configFocusNode, + showQrScanOption: true, + label: "Import sign config", + hint: "Enter config", + onChanged: (_) { + setState(() { + _configEmpty = configFieldController.text.isEmpty; + }); + }, + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 12, + ), + CheckboxTextButton( + label: "I have verified that everyone has imported he config and" + " is ready to sign", + onChanged: (value) { + setState(() { + _userVerifyContinue = value; + }); + }, + ), + const SizedBox( + height: 12, + ), + PrimaryButton( + label: "Start signing", + enabled: !_configEmpty && _userVerifyContinue, + onPressed: () { + _attemptSign(); + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_2.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_2.dart new file mode 100644 index 000000000..66f088b39 --- /dev/null +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_2.dart @@ -0,0 +1,310 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; +import 'package:stackwallet/widgets/custom_buttons/frost_qr_dialog_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/detail_item.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; +import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; + +class FrostSendStep2 extends ConsumerStatefulWidget { + const FrostSendStep2({super.key}); + + static const String routeName = "/FrostSendStep2"; + static const String title = "Preprocesses"; + + @override + ConsumerState createState() => _FrostSendStep2State(); +} + +class _FrostSendStep2State extends ConsumerState { + final List controllers = []; + final List focusNodes = []; + + late final String myName; + late final List participantsWithoutMe; + late final String myPreprocess; + late final int myIndex; + late final int threshold; + + final List fieldIsEmptyFlags = []; + + int countPreprocesses() { + // own preprocess is not included in controllers and must be set here + int count = 1; + + for (final controller in controllers) { + if (controller.text.isNotEmpty) { + count++; + } + } + + return count; + } + + @override + void initState() { + final wallet = ref.read(pWallets).getWallet( + ref.read(pFrostScaffoldArgs)!.walletId!, + ) as BitcoinFrostWallet; + final frostInfo = wallet.frostInfo; + + myName = frostInfo.myName; + threshold = frostInfo.threshold; + participantsWithoutMe = + List.from(frostInfo.participants); // Copy so it isn't fixed-length. + myIndex = participantsWithoutMe.indexOf(frostInfo.myName); + myPreprocess = ref.read(pFrostAttemptSignData.state).state!.preprocess; + + participantsWithoutMe.removeAt(myIndex); + + for (int i = 0; i < participantsWithoutMe.length; i++) { + controllers.add(TextEditingController()); + focusNodes.add(FocusNode()); + fieldIsEmptyFlags.add(true); + } + super.initState(); + } + + @override + void dispose() { + for (int i = 0; i < controllers.length; i++) { + controllers[i].dispose(); + } + for (int i = 0; i < focusNodes.length; i++) { + focusNodes[i].dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "1.", + style: STextStyles.w500_12(context), + ), + const SizedBox( + width: 4, + ), + Expanded( + child: Text( + "Share your preprocess with other signing group members.", + style: STextStyles.w500_12(context), + ), + ), + ], + ), + const SizedBox( + height: 4, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "2.", + style: STextStyles.w500_12(context), + ), + const SizedBox( + width: 4, + ), + Expanded( + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: + "Enter their preprocesses into the corresponding fields. ", + style: STextStyles.w500_12(context), + ), + TextSpan( + text: "You must have the threshold number of " + "preprocesses (including yours) to send this transaction.", + style: STextStyles.w600_12(context).copyWith( + color: Theme.of(context) + .extension()! + .customTextButtonEnabledText, + ), + ), + ], + ), + ), + ), + ], + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + DetailItem( + title: "Threshold", + detail: "$threshold signatures", + horizontal: true, + ), + const SizedBox( + height: 12, + ), + DetailItem( + title: "My name", + detail: myName, + button: Util.isDesktop + ? IconCopyButton( + data: myName, + ) + : SimpleCopyButton( + data: myName, + ), + ), + const SizedBox( + height: 12, + ), + DetailItem( + title: "My preprocess", + detail: myPreprocess, + button: Util.isDesktop + ? IconCopyButton( + data: myPreprocess, + ) + : SimpleCopyButton( + data: myPreprocess, + ), + ), + const SizedBox(height: 12), + FrostQrDialogPopupButton( + data: myPreprocess, + ), + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Text( + "You need to obtain ${threshold - 1} preprocess from signing members to send this transaction.", + style: STextStyles.label(context), + ), + ), + const SizedBox( + height: 12, + ), + Builder(builder: (context) { + final count = countPreprocesses(); + final colors = Theme.of(context).extension()!; + return DetailItem( + title: "Required preprocesses", + detail: "$count of $threshold", + horizontal: true, + overrideDetailTextColor: count >= threshold + ? colors.accentColorGreen + : colors.accentColorRed, + ); + }), + const SizedBox( + height: 12, + ), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < participantsWithoutMe.length; i++) + FrostStepField( + label: participantsWithoutMe[i], + hint: "Enter ${participantsWithoutMe[i]}'s preprocess", + controller: controllers[i], + focusNode: focusNodes[i], + onChanged: (_) { + setState(() { + fieldIsEmptyFlags[i] = controllers[i].text.isEmpty; + }); + }, + showQrScanOption: true, + ), + ], + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 12, + ), + PrimaryButton( + label: "Generate shares", + enabled: countPreprocesses() >= threshold, + onPressed: () async { + // collect Preprocess strings (not including my own) + final preprocesses = controllers.map((e) => e.text).toList(); + + // collect participants who are involved in this transaction + final List requiredParticipantsUnordered = []; + for (int i = 0; i < participantsWithoutMe.length; i++) { + if (preprocesses[i].isNotEmpty) { + requiredParticipantsUnordered.add(participantsWithoutMe[i]); + } + } + ref.read(pFrostSelectParticipantsUnordered.notifier).state = + requiredParticipantsUnordered; + + // insert an empty string at my index + preprocesses.insert(myIndex, ""); + + try { + ref.read(pFrostContinueSignData.notifier).state = + Frost.continueSigning( + machinePtr: + ref.read(pFrostAttemptSignData.state).state!.machinePtr, + preprocesses: preprocesses, + ); + + ref.read(pFrostCreateCurrentStep.state).state = 3; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + + // await Navigator.of(context).pushNamed( + // FrostContinueSignView.routeName, + // arguments: widget.walletId, + // ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Failed to continue signing", + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart new file mode 100644 index 000000000..bb7e462b9 --- /dev/null +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart @@ -0,0 +1,254 @@ +import 'package:coinlib_flutter/coinlib_flutter.dart' as cl; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; +import 'package:stackwallet/widgets/custom_buttons/checkbox_text_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/frost_qr_dialog_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/detail_item.dart'; +import 'package:stackwallet/widgets/frost_step_user_steps.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; +import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; + +class FrostSendStep3 extends ConsumerStatefulWidget { + const FrostSendStep3({super.key}); + + static const String routeName = "/FrostSendStep3"; + static const String title = "Shares"; + + @override + ConsumerState createState() => _FrostSendStep3State(); +} + +class _FrostSendStep3State extends ConsumerState { + static const info = [ + "Send your share to other signing group members.", + "Enter their shares into the corresponding fields.", + ]; + + final List controllers = []; + final List focusNodes = []; + + late final String myName; + late final List participantsWithoutMe; + late final List participantsAll; + late final String myShare; + late final int myIndex; + + final List fieldIsEmptyFlags = []; + + bool _userVerifyContinue = false; + + @override + void initState() { + final wallet = ref.read(pWallets).getWallet( + ref.read(pFrostScaffoldArgs)!.walletId!, + ) as BitcoinFrostWallet; + + final frostInfo = wallet.frostInfo; + + myName = frostInfo.myName; + participantsAll = frostInfo.participants; + myIndex = frostInfo.participants.indexOf(frostInfo.myName); + myShare = ref.read(pFrostContinueSignData.state).state!.share; + + participantsWithoutMe = frostInfo.participants + .toSet() + .intersection( + ref.read(pFrostSelectParticipantsUnordered.state).state!.toSet()) + .toList(); + + participantsWithoutMe.remove(myName); + + for (int i = 0; i < participantsWithoutMe.length; i++) { + controllers.add(TextEditingController()); + focusNodes.add(FocusNode()); + fieldIsEmptyFlags.add(true); + } + super.initState(); + } + + @override + void dispose() { + for (int i = 0; i < controllers.length; i++) { + controllers[i].dispose(); + } + for (int i = 0; i < focusNodes.length; i++) { + focusNodes[i].dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const FrostStepUserSteps( + userSteps: info, + ), + const SizedBox( + height: 12, + ), + DetailItem( + title: "My name", + detail: myName, + button: Util.isDesktop + ? IconCopyButton( + data: myName, + ) + : SimpleCopyButton( + data: myName, + ), + ), + const SizedBox( + height: 12, + ), + DetailItem( + title: "My share", + detail: myShare, + button: Util.isDesktop + ? IconCopyButton( + data: myShare, + ) + : SimpleCopyButton( + data: myShare, + ), + ), + const SizedBox( + height: 12, + ), + FrostQrDialogPopupButton( + data: myShare, + ), + const SizedBox( + height: 12, + ), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < participantsWithoutMe.length; i++) + FrostStepField( + label: participantsWithoutMe[i], + hint: "Enter ${participantsWithoutMe[i]}'s share", + controller: controllers[i], + focusNode: focusNodes[i], + onChanged: (_) { + setState(() { + fieldIsEmptyFlags[i] = controllers[i].text.isEmpty; + }); + }, + showQrScanOption: true, + ), + ], + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 12, + ), + CheckboxTextButton( + label: "I have verified that everyone has my share", + onChanged: (value) { + setState(() { + _userVerifyContinue = value; + }); + }, + ), + const SizedBox( + height: 12, + ), + PrimaryButton( + label: "Generate transaction", + enabled: _userVerifyContinue && + !fieldIsEmptyFlags.reduce((v, e) => v |= e), + onPressed: () async { + // collect Share strings + final sharesCollected = controllers.map((e) => e.text).toList(); + + final List shares = []; + for (final participant in participantsAll) { + if (participantsWithoutMe.contains(participant)) { + shares.add(sharesCollected[ + participantsWithoutMe.indexOf(participant)]); + } else { + shares.add(""); + } + } + + try { + final rawTx = Frost.completeSigning( + machinePtr: + ref.read(pFrostContinueSignData.state).state!.machinePtr, + shares: shares, + ); + + final tx = cl.Transaction.fromHex(rawTx); + final txData = ref.read(pFrostTxData)!; + + final fractionDigits = + txData.recipients!.first.amount.fractionDigits; + + final inputTotal = Amount( + rawValue: txData.utxos! + .map((e) => BigInt.from(e.value)) + .reduce((v, e) => v += e), + fractionDigits: fractionDigits, + ); + final outputTotal = Amount( + rawValue: + tx.outputs.map((e) => e.value).reduce((v, e) => v += e), + fractionDigits: fractionDigits, + ); + + ref.read(pFrostTxData.state).state = txData.copyWith( + raw: rawTx, + fee: inputTotal - outputTotal, + frostSigners: [ + myName, + ...participantsWithoutMe, + ], + ); + + ref.read(pFrostCreateCurrentStep.state).state = 4; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + if (context.mounted) { + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Failed to complete signing process", + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_4.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_4.dart new file mode 100644 index 000000000..2267e7226 --- /dev/null +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_4.dart @@ -0,0 +1,296 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart'; +import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/my_stack_view.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/amount/amount_formatter.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/show_loading.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/detail_item.dart'; +import 'package:stackwallet/widgets/expandable.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; + +class FrostSendStep4 extends ConsumerStatefulWidget { + const FrostSendStep4({super.key}); + + static const String routeName = "/FrostSendStep4"; + static const String title = "Preview transaction"; + + @override + ConsumerState createState() => _FrostSendStep4State(); +} + +class _FrostSendStep4State extends ConsumerState { + final List _expandedStates = []; + + bool _broadcastLock = false; + + late final CryptoCurrency cryptoCurrency; + + @override + void initState() { + final wallet = ref.read(pWallets).getWallet( + ref.read(pFrostScaffoldArgs)!.walletId!, + ) as BitcoinFrostWallet; + + cryptoCurrency = wallet.cryptoCurrency; + + for (final _ in ref.read(pFrostTxData)!.recipients!) { + _expandedStates.add(false); + } + + super.initState(); + } + + @override + Widget build(BuildContext context) { + final signerNames = ref.watch(pFrostTxData)!.frostSigners!; + final recipients = ref.watch(pFrostTxData)!.recipients!; + + final String signers; + if (signerNames.length > 1) { + signers = signerNames + .sublist(1) + .fold(signerNames.first, (pv, e) => pv += ", $e"); + } else { + signers = signerNames.first; + } + + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (kDebugMode) + DetailItem( + title: "Tx hex (debug mode only)", + detail: ref.watch(pFrostTxData)!.raw!, + button: Util.isDesktop + ? IconCopyButton( + data: ref.watch(pFrostTxData)!.raw!, + ) + : SimpleCopyButton( + data: ref.watch(pFrostTxData)!.raw!, + ), + ), + if (kDebugMode) + const SizedBox( + height: 12, + ), + Text( + "Send ${cryptoCurrency.coin.ticker}", + style: STextStyles.w600_20(context), + ), + const SizedBox( + height: 12, + ), + recipients.length == 1 + ? _Recipient( + address: recipients[0].address, + amount: ref + .watch(pAmountFormatter(cryptoCurrency.coin)) + .format(recipients[0].amount), + ) + : Column( + children: [ + for (int i = 0; i < recipients.length; i++) + Padding( + padding: const EdgeInsets.only(top: 10), + child: Expandable( + onExpandChanged: (state) { + setState(() { + _expandedStates[i] = + state == ExpandableState.expanded; + }); + }, + header: Padding( + padding: const EdgeInsets.only(top: 12, bottom: 6), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Recipient ${i + 1}", + style: STextStyles.itemSubtitle(context), + ), + SvgPicture.asset( + _expandedStates[i] + ? Assets.svg.chevronUp + : Assets.svg.chevronDown, + width: 12, + height: 6, + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ], + ), + ), + body: _Recipient( + address: recipients[i].address, + amount: ref + .watch(pAmountFormatter(cryptoCurrency.coin)) + .format(recipients[i].amount), + ), + ), + ), + ], + ), + const SizedBox( + height: 12, + ), + DetailItem( + title: "Transaction fee", + detail: ref + .watch(pAmountFormatter(cryptoCurrency.coin)) + .format(ref.watch(pFrostTxData)!.fee!), + horizontal: true, + ), + const SizedBox( + height: 12, + ), + DetailItem( + title: "Total", + detail: ref.watch(pAmountFormatter(cryptoCurrency.coin)).format( + ref.watch(pFrostTxData)!.fee! + + recipients.map((e) => e.amount).reduce((v, e) => v += e)), + horizontal: true, + ), + const SizedBox( + height: 12, + ), + DetailItem( + title: "Note", + detail: ref.watch(pFrostTxData)!.note ?? "", + ), + const SizedBox( + height: 12, + ), + DetailItem( + title: "Signers", + detail: signers, + ), + const SizedBox( + height: 12, + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 12, + ), + PrimaryButton( + label: "Approve transaction", + onPressed: () async { + if (_broadcastLock) { + return; + } + _broadcastLock = true; + + try { + Exception? ex; + final txData = await showLoading( + whileFuture: ref + .read(pWallets) + .getWallet( + ref.read(pFrostScaffoldArgs)!.walletId!, + ) + .confirmSend( + txData: ref.read(pFrostTxData)!, + ), + context: context, + message: "Broadcasting transaction to network", + rootNavigator: true, // used to pop using root nav + onException: (e) { + ex = e; + }, + ); + + if (ex != null) { + throw ex!; + } + + if (context.mounted) { + if (txData != null) { + ref.read(pFrostScaffoldCanPopDesktop.notifier).state = true; + ref.read(pFrostTxData.state).state = txData; + ref.read(pFrostScaffoldArgs)!.parentNav.popUntil( + ModalRoute.withName( + Util.isDesktop + ? MyStackView.routeName + : WalletView.routeName, + ), + ); + } + } + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + if (context.mounted) { + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Broadcast error", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + onOkPressed: + Navigator.of(context, rootNavigator: true).pop, + ), + ); + } + } finally { + _broadcastLock = false; + } + }, + ), + ], + ), + ); + } +} + +class _Recipient extends StatelessWidget { + const _Recipient({ + super.key, + required this.address, + required this.amount, + }); + + final String address; + final String amount; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + DetailItem( + title: "Address", + detail: address, + ), + const SizedBox( + height: 6, + ), + DetailItem( + title: "Amount", + detail: amount, + horizontal: true, + ), + ], + ); + } +} diff --git a/lib/pages/settings_views/sub_widgets/settings_list_button.dart b/lib/pages/settings_views/sub_widgets/settings_list_button.dart index 2e4c19d01..62b4a2aec 100644 --- a/lib/pages/settings_views/sub_widgets/settings_list_button.dart +++ b/lib/pages/settings_views/sub_widgets/settings_list_button.dart @@ -16,17 +16,19 @@ import 'package:stackwallet/utilities/text_styles.dart'; class SettingsListButton extends StatelessWidget { const SettingsListButton({ - Key? key, + super.key, required this.iconAssetName, required this.title, this.onPressed, this.iconSize = 20.0, - }) : super(key: key); + this.padding = const EdgeInsets.all(8.0), + }); final String iconAssetName; final String title; final VoidCallback? onPressed; final double iconSize; + final EdgeInsetsGeometry padding; @override Widget build(BuildContext context) { @@ -44,7 +46,7 @@ class SettingsListButton extends StatelessWidget { ), onPressed: onPressed, child: Padding( - padding: const EdgeInsets.all(8.0), + padding: padding, child: Row( children: [ Container( diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart index 7fc7236a4..c18d78477 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart @@ -10,34 +10,40 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/pages/settings_views/sub_widgets/settings_list_button.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/begin_reshare_config_view.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1b/import_reshare_config_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/initiate_resharing_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/themes/stack_colors.dart'; -import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.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_app_bar.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/frost_scaffold.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class FrostMSWalletOptionsView extends ConsumerWidget { const FrostMSWalletOptionsView({ - Key? key, + super.key, required this.walletId, - }) : super(key: key); + }); static const String routeName = "/frostMSWalletOptionsView"; final String walletId; + static const _padding = 12.0; + @override Widget build(BuildContext context, WidgetRef ref) { return ConditionalParent( @@ -83,56 +89,86 @@ class FrostMSWalletOptionsView extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _OptionButton( - label: "Show participants", - onPressed: () { - Navigator.of(context).pushNamed( - FrostParticipantsView.routeName, - arguments: walletId, - ); - }, + RoundedWhiteContainer( + padding: EdgeInsets.zero, + child: SettingsListButton( + padding: const EdgeInsets.all(_padding), + title: "Show participants", + iconAssetName: Assets.svg.peers, + onPressed: () { + Navigator.of(context).pushNamed( + FrostParticipantsView.routeName, + arguments: walletId, + ); + }, + ), ), const SizedBox( height: 8, ), - _OptionButton( - label: "Initiate resharing", - onPressed: () { - // TODO: optimize this by creating watcher providers (similar to normal WalletInfo) - final frostInfo = ref - .read(mainDBProvider) - .isar - .frostWalletInfo - .getByWalletIdSync(walletId)!; + RoundedWhiteContainer( + padding: EdgeInsets.zero, + child: SettingsListButton( + padding: const EdgeInsets.all(_padding), + title: "Initiate resharing", + iconAssetName: Assets.svg.swap2, + onPressed: () { + // TODO: optimize this by creating watcher providers (similar to normal WalletInfo) + final frostInfo = ref + .read(mainDBProvider) + .isar + .frostWalletInfo + .getByWalletIdSync(walletId)!; - ref.read(pFrostMyName.state).state = frostInfo.myName; + ref.read(pFrostMyName.state).state = frostInfo.myName; - Navigator.of(context).pushNamed( - BeginReshareConfigView.routeName, - arguments: walletId, - ); - }, + Navigator.of(context).pushNamed( + InitiateResharingView.routeName, + arguments: walletId, + ); + }, + ), ), const SizedBox( height: 8, ), - _OptionButton( - label: "Import reshare config", - onPressed: () { - // TODO: optimize this by creating watcher providers (similar to normal WalletInfo) - final frostInfo = ref - .read(mainDBProvider) - .isar - .frostWalletInfo - .getByWalletIdSync(walletId)!; + RoundedWhiteContainer( + padding: EdgeInsets.zero, + child: SettingsListButton( + padding: const EdgeInsets.all(_padding), + title: "Import reshare config", + iconAssetName: Assets.svg.downloadFolder, + iconSize: 16, + onPressed: () { + // TODO: optimize this by creating watcher providers (similar to normal WalletInfo) + final frostInfo = ref + .read(mainDBProvider) + .isar + .frostWalletInfo + .getByWalletIdSync(walletId)!; - ref.read(pFrostMyName.state).state = frostInfo.myName; + ref.read(pFrostMyName.state).state = frostInfo.myName; - Navigator.of(context).pushNamed( - ImportReshareConfigView.routeName, - arguments: walletId, - ); - }, + final wallet = ref.read(pWallets).getWallet(walletId) + as BitcoinFrostWallet; + + ref.read(pFrostScaffoldArgs.state).state = ( + info: ( + walletName: wallet.info.name, + frostCurrency: wallet.cryptoCurrency, + ), + walletId: wallet.walletId, + stepRoutes: FrostRouteGenerator.importReshareStepRoutes, + parentNav: Navigator.of(context), + frostInterruptionDialogType: + FrostInterruptionDialogType.resharing, + ); + + Navigator.of(context).pushNamed( + FrostStepScaffold.routeName, + ); + }, + ), ), ], ), @@ -142,45 +178,3 @@ class FrostMSWalletOptionsView extends ConsumerWidget { ); } } - -class _OptionButton extends StatelessWidget { - const _OptionButton({ - super.key, - required this.label, - required this.onPressed, - }); - - final String label; - final VoidCallback onPressed; - - @override - Widget build(BuildContext context) { - return RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - onPressed: onPressed, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 20, - ), - child: Row( - children: [ - Text( - label, - style: STextStyles.titleBold12(context), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart index b4710bfe7..364859ef0 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart @@ -1,8 +1,11 @@ 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/transaction_views/tx_v2/transaction_v2_details_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart'; @@ -11,6 +14,7 @@ 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'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; class FrostParticipantsView extends ConsumerWidget { const FrostParticipantsView({ @@ -84,31 +88,56 @@ class FrostParticipantsView extends ConsumerWidget { ), ), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: Util.isDesktop + ? CrossAxisAlignment.start + : CrossAxisAlignment.stretch, children: [ for (int i = 0; i < frostInfo.participants.length; i++) Padding( padding: const EdgeInsets.symmetric( - vertical: 8, + vertical: 5, ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Index $i", - style: STextStyles.label(context), - ), - const SizedBox( - height: 6, - ), - SelectableText( - frostInfo.participants[i] == frostInfo.myName - ? "${frostInfo.participants[i]} (me)" - : frostInfo.participants[i], - style: STextStyles.itemSubtitle12(context), - ), - ], + child: RoundedWhiteContainer( + child: Row( + children: [ + Container( + width: 26, + height: 26, + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .textFieldActiveBG, + borderRadius: BorderRadius.circular( + 200, + ), + ), + child: Center( + child: SvgPicture.asset( + Assets.svg.user, + width: 16, + height: 16, + ), + ), + ), + const SizedBox( + width: 8, + ), + Expanded( + child: Text( + frostInfo.participants[i] == frostInfo.myName + ? "${frostInfo.participants[i]} (me)" + : frostInfo.participants[i], + style: STextStyles.w500_14(context), + ), + ), + const SizedBox( + width: 8, + ), + IconCopyButton( + data: frostInfo.participants[i], + ), + ], + ), ), ), ], diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/complete_reshare_config_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart similarity index 59% rename from lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/complete_reshare_config_view.dart rename to lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart index 74cfaee17..7d8a61bcf 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/complete_reshare_config_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart @@ -1,11 +1,14 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:frostdart/frostdart.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/display_reshare_config_view.dart'; +import 'package:stackwallet/frost_route_generator.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/services/frost.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/format.dart'; @@ -13,12 +16,15 @@ import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.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_app_bar.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/frost_scaffold.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; final class CompleteReshareConfigView extends ConsumerStatefulWidget { @@ -31,7 +37,7 @@ final class CompleteReshareConfigView extends ConsumerStatefulWidget { static const String routeName = "/completeReshareConfigView"; final String walletId; - final List resharers; + final Map resharers; @override ConsumerState createState() => @@ -45,9 +51,12 @@ class _CompleteReshareConfigViewState final List controllers = []; + late final String myName; + int _participantsCount = 0; bool _buttonLock = false; + bool _includeMeInReshare = false; Future _onPressed() async { if (_buttonLock) { @@ -74,10 +83,16 @@ class _CompleteReshareConfigViewState ); } + final List newParticipants = + controllers.map((e) => e.text.trim()).toList(); + if (_includeMeInReshare) { + newParticipants.insert(0, myName); + } + final config = Frost.createResharerConfig( newThreshold: int.parse(_newThresholdController.text), - resharers: widget.resharers, - newParticipants: controllers.map((e) => e.text).toList(), + resharers: widget.resharers.values.toList(), + newParticipants: newParticipants, ); final salt = Format.uint8listToString( @@ -105,13 +120,29 @@ class _CompleteReshareConfigViewState }); } - ref.read(pFrostResharingData).myName = frostInfo.myName; - ref.read(pFrostResharingData).resharerConfig = config; + ref.read(pFrostResharingData).myName = myName; + ref.read(pFrostResharingData).resharerRConfig = Frost.encodeRConfig( + config, + widget.resharers, + ); + + final wallet = + ref.read(pWallets).getWallet(widget.walletId) as BitcoinFrostWallet; + + ref.read(pFrostScaffoldArgs.state).state = ( + info: ( + walletName: wallet.info.name, + frostCurrency: wallet.cryptoCurrency, + ), + walletId: wallet.walletId, + stepRoutes: FrostRouteGenerator.initiateReshareStepRoutes, + parentNav: Navigator.of(context), + frostInterruptionDialogType: FrostInterruptionDialogType.resharing, + ); if (mounted) { await Navigator.of(context).pushNamed( - DisplayReshareConfigView.routeName, - arguments: widget.walletId, + FrostStepScaffold.routeName, ); } } catch (e, s) { @@ -152,18 +183,29 @@ class _CompleteReshareConfigViewState return "At least two participants required"; } - if (controllers.length != partsCount) { + final newParticipants = controllers.map((e) => e.text.trim()).toList(); + + if (newParticipants.contains(myName)) { + return "Using your own name should be done using the checkbox to include" + " yourself"; + } + + if (_includeMeInReshare) { + newParticipants.add(myName); + } + + if (newParticipants.length != partsCount) { return "Participants count error"; } - final hasEmptyParticipants = controllers - .map((e) => e.text.isEmpty) + final hasEmptyParticipants = newParticipants + .map((e) => e.trim().isEmpty) .reduce((value, element) => value |= element); if (hasEmptyParticipants) { return "Participants must not be empty"; } - if (controllers.length != controllers.map((e) => e.text).toSet().length) { + if (newParticipants.length != newParticipants.toSet().length) { return "Duplicate participant name found"; } @@ -171,8 +213,12 @@ class _CompleteReshareConfigViewState } void _participantsCountChanged(String newValue) { - final count = int.tryParse(newValue); + int? count = int.tryParse(newValue); if (count != null) { + if (_includeMeInReshare) { + count = max(0, count - 1); + } + if (count > _participantsCount) { for (int i = _participantsCount; i < count; i++) { controllers.add(TextEditingController()); @@ -192,6 +238,17 @@ class _CompleteReshareConfigViewState } } + @override + void initState() { + final frostInfo = ref + .read(mainDBProvider) + .isar + .frostWalletInfo + .getByWalletIdSync(widget.walletId)!; + myName = frostInfo.myName; + super.initState(); + } + @override void dispose() { _newThresholdController.dispose(); @@ -231,7 +288,7 @@ class _CompleteReshareConfigViewState }, ), title: Text( - "Modify Participants", + "Edit group details", style: STextStyles.navBarTitle(context), ), ), @@ -258,11 +315,62 @@ class _CompleteReshareConfigViewState ), child: Column( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: Util.isDesktop + ? CrossAxisAlignment.start + : CrossAxisAlignment.stretch, children: [ + const SizedBox( + height: 8, + ), + GestureDetector( + onTap: () { + setState(() { + _includeMeInReshare = !_includeMeInReshare; + }); + _participantsCountChanged(_newParticipantsCountController.text); + }, + child: Container( + color: Colors.transparent, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: 20, + height: 26, + child: Checkbox( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: _includeMeInReshare, + onChanged: (value) { + setState( + () => _includeMeInReshare = value == true, + ); + _participantsCountChanged( + _newParticipantsCountController.text); + }, + ), + ), + const SizedBox( + width: 12, + ), + Expanded( + child: Text( + "I will be a signer in the new config", + style: STextStyles.w500_14(context), + ), + ), + ], + ), + ), + ), + const SizedBox( + height: 20, + ), Text( "New threshold", - style: STextStyles.label(context), + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of(context).extension()!.textSubtitle1, + ), ), const SizedBox( height: 10, @@ -271,13 +379,32 @@ class _CompleteReshareConfigViewState keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], controller: _newThresholdController, + decoration: InputDecoration( + hintText: "Enter number of signatures", + hintStyle: STextStyles.fieldLabel(context), + ), + ), + const SizedBox( + height: 6, + ), + RoundedWhiteContainer( + child: Text( + "Enter number of signatures required for fund management.", + style: STextStyles.w500_12(context).copyWith( + color: + Theme.of(context).extension()!.textSubtitle2, + ), + ), ), const SizedBox( height: 16, ), Text( - "Number of participants", - style: STextStyles.label(context), + "New number of participants", + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of(context).extension()!.textSubtitle1, + ), ), const SizedBox( height: 10, @@ -287,6 +414,23 @@ class _CompleteReshareConfigViewState inputFormatters: [FilteringTextInputFormatter.digitsOnly], controller: _newParticipantsCountController, onChanged: _participantsCountChanged, + decoration: InputDecoration( + hintText: "Enter number of participants", + hintStyle: STextStyles.fieldLabel(context), + ), + ), + const SizedBox( + height: 6, + ), + RoundedWhiteContainer( + child: Text( + "The number of participants must be equal to or less than the" + " number of required signatures.", + style: STextStyles.w500_12(context).copyWith( + color: + Theme.of(context).extension()!.textSubtitle2, + ), + ), ), const SizedBox( height: 16, @@ -294,12 +438,26 @@ class _CompleteReshareConfigViewState if (controllers.isNotEmpty) Text( "Participants", - style: STextStyles.label(context), + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of(context).extension()!.textSubtitle1, + ), ), if (controllers.isNotEmpty) const SizedBox( height: 10, ), + if (controllers.isNotEmpty) + RoundedWhiteContainer( + child: Text( + "Type each name in one word without spaces.", + style: STextStyles.w500_12(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle2, + ), + ), + ), if (controllers.isNotEmpty) Column( children: [ @@ -310,6 +468,10 @@ class _CompleteReshareConfigViewState ), child: TextField( controller: controllers[i], + decoration: InputDecoration( + hintText: "Enter name", + hintStyle: STextStyles.fieldLabel(context), + ), ), ), ], diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/begin_reshare_config_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/initiate_resharing_view.dart similarity index 53% rename from lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/begin_reshare_config_view.dart rename to lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/initiate_resharing_view.dart index 94d2de0b2..ca5ab67e7 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/begin_reshare_config_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/initiate_resharing_view.dart @@ -1,11 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/complete_reshare_config_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; import 'package:stackwallet/themes/stack_colors.dart'; -import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart'; @@ -15,9 +14,10 @@ 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'; -final class BeginReshareConfigView extends ConsumerStatefulWidget { - const BeginReshareConfigView({ +final class InitiateResharingView extends ConsumerStatefulWidget { + const InitiateResharingView({ super.key, required this.walletId, }); @@ -27,16 +27,18 @@ final class BeginReshareConfigView extends ConsumerStatefulWidget { final String walletId; @override - ConsumerState createState() => + ConsumerState createState() => _BeginReshareConfigViewState(); } class _BeginReshareConfigViewState - extends ConsumerState { + extends ConsumerState { + late final String myName; late final int currentThreshold; - late final List currentParticipants; + late final List originalParticipants; + late final List currentParticipantsWithoutMe; - final Map pFrostResharersMap = {}; + final Set selectedParticipants = {}; @override void initState() { @@ -50,7 +52,14 @@ class _BeginReshareConfigViewState .getByWalletIdSync(widget.walletId)!; currentThreshold = frostInfo.threshold; - currentParticipants = frostInfo.participants; + originalParticipants = frostInfo.participants.toList(growable: false); + currentParticipantsWithoutMe = originalParticipants.toList(); + + // sanity check (should never actually fail, but very bad if it does) + assert(originalParticipants.length == currentParticipantsWithoutMe.length); + + myName = frostInfo.myName; + currentParticipantsWithoutMe.remove(myName); super.initState(); } @@ -83,10 +92,10 @@ class _BeginReshareConfigViewState Navigator.of(context).pop(); }, ), - // title: Text( - // "Modify Participants", - // style: STextStyles.navBarTitle(context), - // ), + title: Text( + "Initiate resharing", + style: STextStyles.navBarTitle(context), + ), ), body: SafeArea( child: LayoutBuilder( @@ -113,33 +122,48 @@ class _BeginReshareConfigViewState mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Select participants for resharing", - style: STextStyles.label(context), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Select group members who will participate in resharing.", + style: STextStyles.w600_12(context), + ), + const SizedBox( + height: 10, + ), + Text( + "You must have the threshold number of members (including you) to initiate resharing.", + style: STextStyles.w600_12(context).copyWith( + color: Theme.of(context) + .extension()! + .customTextButtonEnabledText, + ), + ), + ], + ), ), const SizedBox( height: 16, ), Column( children: [ - for (int i = 0; i < currentParticipants.length; i++) + for (int i = 0; i < currentParticipantsWithoutMe.length; i++) Padding( padding: const EdgeInsets.only( top: 10, ), - child: RawMaterialButton( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), + child: RoundedWhiteContainer( + padding: EdgeInsets.zero, onPressed: () { - if (pFrostResharersMap[currentParticipants[i]] == - null) { - pFrostResharersMap[currentParticipants[i]] = i; + if (selectedParticipants + .contains(currentParticipantsWithoutMe[i])) { + selectedParticipants + .remove(currentParticipantsWithoutMe[i]); } else { - pFrostResharersMap.remove(currentParticipants[i]); + selectedParticipants + .add(currentParticipantsWithoutMe[i]); } setState(() {}); @@ -150,16 +174,15 @@ class _BeginReshareConfigViewState child: Row( children: [ Checkbox( - value: pFrostResharersMap[ - currentParticipants[i]] == - i, - onChanged: (bool? value) {}, + value: selectedParticipants + .contains(currentParticipantsWithoutMe[i]), + onChanged: (_) {}, ), const SizedBox( width: 10, ), Text( - currentParticipants[i], + currentParticipantsWithoutMe[i], style: STextStyles.itemSubtitle12(context), ), ], @@ -174,16 +197,55 @@ class _BeginReshareConfigViewState const SizedBox( height: 16, ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Required members", + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of(context).extension()!.textDark3, + ), + ), + Text( + // +1 is included as the initiator who will also take part + "${selectedParticipants.length + 1} / $currentThreshold", + style: STextStyles.w500_14(context).copyWith( + color: selectedParticipants.length + 1 >= currentThreshold + ? Theme.of(context) + .extension()! + .accentColorGreen + : Theme.of(context) + .extension()! + .accentColorRed, + ), + ), + ], + ), + ), + const SizedBox( + height: 16, + ), PrimaryButton( label: "Continue", - enabled: pFrostResharersMap.length >= currentThreshold, + // +1 is included as the initiator who will also take part + enabled: selectedParticipants.length + 1 >= currentThreshold, onPressed: () async { + // include self now + selectedParticipants.add(myName); + + final Map resharers = {}; + + for (final name in selectedParticipants) { + resharers[name] = originalParticipants.indexOf(name); + } + await Navigator.of(context).pushNamed( CompleteReshareConfigView.routeName, arguments: ( walletId: widget.walletId, - resharers: - pFrostResharersMap.values.toList(growable: false), + resharers: resharers, ), ); }, diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart deleted file mode 100644 index 5ff5c815d..000000000 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart +++ /dev/null @@ -1,437 +0,0 @@ -import 'dart:ffi'; - -import 'package:barcode_scan2/barcode_scan2.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:qr_flutter/qr_flutter.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/verify_updated_wallet_view.dart'; -import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; -import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; -import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart'; -import 'package:stackwallet/providers/db/main_db_provider.dart'; -import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; -import 'package:stackwallet/services/frost.dart'; -import 'package:stackwallet/themes/stack_colors.dart'; -import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/utilities/util.dart'; -import 'package:stackwallet/wallets/isar/models/frost_wallet_info.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/simple_copy_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/detail_item.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:stackwallet/pages/frost_mascot.dart'; - -class FinishResharingView extends ConsumerStatefulWidget { - const FinishResharingView({ - super.key, - required this.walletId, - }); - - static const String routeName = "/finishResharingView"; - - final String walletId; - - @override - ConsumerState createState() => - _FinishResharingViewState(); -} - -class _FinishResharingViewState extends ConsumerState { - final List controllers = []; - final List focusNodes = []; - - late final List resharerIndexes; - late final String myName; - late final int? myResharerIndexIndex; - late final String? myResharerComplete; - late final bool amOutgoingParticipant; - - final List fieldIsEmptyFlags = []; - - bool _buttonLock = false; - Future _onPressed() async { - if (_buttonLock) { - return; - } - _buttonLock = true; - - try { - if (amOutgoingParticipant) { - ref.read(pFrostResharingData).reset(); - Navigator.of(context).popUntil( - ModalRoute.withName( - Util.isDesktop ? DesktopWalletView.routeName : WalletView.routeName, - ), - ); - } else { - // collect resharer completes strings and insert my own at the correct index - final resharerCompletes = controllers.map((e) => e.text).toList(); - if (myResharerIndexIndex != null && myResharerComplete != null) { - resharerCompletes.insert(myResharerIndexIndex!, myResharerComplete!); - } - - final data = Frost.finishReshared( - prior: ref.read(pFrostResharingData).startResharedData!.prior.ref, - resharerCompletes: resharerCompletes, - ); - - ref.read(pFrostResharingData).newWalletData = data; - - await Navigator.of(context).pushNamed( - VerifyUpdatedWalletView.routeName, - arguments: widget.walletId, - ); - } - } catch (e, s) { - Logging.instance.log( - "$e\n$s", - level: LogLevel.Fatal, - ); - if (mounted) { - await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Error", - message: e.toString(), - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } - } finally { - _buttonLock = false; - } - } - - @override - void initState() { - final amNewParticipant = - ref.read(pFrostResharingData).startResharerData == null && - ref.read(pFrostResharingData).incompleteWallet != null && - ref.read(pFrostResharingData).incompleteWallet?.walletId == - widget.walletId; - - myName = ref.read(pFrostResharingData).myName!; - - resharerIndexes = ref.read(pFrostResharingData).configData!.resharers; - - if (amNewParticipant) { - myResharerComplete = null; - myResharerIndexIndex = null; - amOutgoingParticipant = false; - } else { - myResharerComplete = ref.read(pFrostResharingData).resharerComplete!; - - final frostInfo = ref - .read(mainDBProvider) - .isar - .frostWalletInfo - .getByWalletIdSync(widget.walletId)!; - final myOldIndex = - frostInfo.participants.indexOf(ref.read(pFrostResharingData).myName!); - - myResharerIndexIndex = resharerIndexes.indexOf(myOldIndex); - if (myResharerIndexIndex! >= 0) { - // remove my name for now as we don't need a text field for it - resharerIndexes.removeAt(myResharerIndexIndex!); - } - - amOutgoingParticipant = !ref - .read(pFrostResharingData) - .configData! - .newParticipants - .contains(ref.read(pFrostResharingData).myName!); - } - - for (int i = 0; i < resharerIndexes.length; i++) { - controllers.add(TextEditingController()); - focusNodes.add(FocusNode()); - fieldIsEmptyFlags.add(true); - } - super.initState(); - } - - @override - void dispose() { - for (int i = 0; i < controllers.length; i++) { - controllers[i].dispose(); - } - for (int i = 0; i < focusNodes.length; i++) { - focusNodes[i].dispose(); - } - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton(), - trailing: FrostMascot( - title: 'Lorem ipsum', - body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', - ), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Resharer completes", - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(16), - child: child, - ), - ), - ), - ); - }, - ), - ), - ), - ), - child: Column( - children: [ - if (myResharerComplete != null) - SizedBox( - height: 220, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QrImageView( - data: myResharerComplete!, - size: 220, - backgroundColor: Theme.of(context) - .extension()! - .background, - foregroundColor: Theme.of(context) - .extension()! - .accentColorDark, - ), - ], - ), - ), - if (myResharerComplete != null) const _Div(), - if (myResharerComplete != null) - DetailItem( - title: "My resharer complete", - detail: myResharerComplete!, - button: Util.isDesktop - ? IconCopyButton( - data: myResharerComplete!, - ) - : SimpleCopyButton( - data: myResharerComplete!, - ), - ), - if (!amOutgoingParticipant) const _Div(), - if (!amOutgoingParticipant) - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - for (int i = 0; i < resharerIndexes.length; i++) - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: Key("frostEncryptionKeyTextFieldKey_$i"), - controller: controllers[i], - focusNode: focusNodes[i], - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - onChanged: (_) { - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].text.isEmpty; - }); - }, - decoration: standardInputDecoration( - "Enter index " - "${resharerIndexes[i]}" - "'s resharer complete", - focusNodes[i], - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: fieldIsEmptyFlags[i] - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - !fieldIsEmptyFlags[i] - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Encryption Key Field Input.", - key: Key( - "frostEncryptionKeyClearButtonKey_$i"), - onTap: () { - controllers[i].text = ""; - - setState(() { - fieldIsEmptyFlags[i] = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Encryption Key Field Input.", - key: Key( - "frostEncryptionKeyPasteButtonKey_$i"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - controllers[i].text = - data.text!.trim(); - } - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i] - .text - .isEmpty; - }); - }, - child: fieldIsEmptyFlags[i] - ? const ClipboardIcon() - : const XIcon(), - ), - if (fieldIsEmptyFlags[i]) - TextFieldIconButton( - semanticsLabel: "Scan QR Button. " - "Opens Camera For Scanning QR Code.", - key: Key("frostScanQrButtonKey_$i"), - onTap: () async { - try { - if (FocusScope.of(context) - .hasFocus) { - FocusScope.of(context) - .unfocus(); - await Future.delayed( - const Duration( - milliseconds: 75)); - } - - final qrResult = - await BarcodeScanner.scan(); - - controllers[i].text = - qrResult.rawContent; - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i] - .text - .isEmpty; - }); - } on PlatformException catch (e, s) { - Logging.instance.log( - "Failed to get camera permissions " - "while trying to scan qr code: $e\n$s", - level: LogLevel.Warning, - ); - } - }, - child: const QrCodeIcon(), - ), - ], - ), - ), - ), - ), - ), - ), - ), - ], - ), - ], - ), - if (!Util.isDesktop) const Spacer(), - const _Div(), - PrimaryButton( - label: amOutgoingParticipant ? "Exit" : "Complete", - enabled: amOutgoingParticipant || - !fieldIsEmptyFlags.reduce((v, e) => v |= e), - onPressed: _onPressed, - ), - ], - ), - ), - ); - } -} - -class _Div extends StatelessWidget { - const _Div({super.key}); - - @override - Widget build(BuildContext context) { - return const SizedBox( - height: 12, - ); - } -} diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/display_reshare_config_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/display_reshare_config_view.dart deleted file mode 100644 index 2b7f1f899..000000000 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/display_reshare_config_view.dart +++ /dev/null @@ -1,214 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:qr_flutter/qr_flutter.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_view.dart'; -import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; -import 'package:stackwallet/providers/db/main_db_provider.dart'; -import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; -import 'package:stackwallet/providers/global/wallets_provider.dart'; -import 'package:stackwallet/services/frost.dart'; -import 'package:stackwallet/themes/stack_colors.dart'; -import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/utilities/util.dart'; -import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart'; -import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.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/simple_copy_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/detail_item.dart'; -import 'package:stackwallet/widgets/stack_dialog.dart'; - -class DisplayReshareConfigView extends ConsumerStatefulWidget { - const DisplayReshareConfigView({ - super.key, - required this.walletId, - }); - - static const String routeName = "/displayReshareConfigView"; - - final String walletId; - - @override - ConsumerState createState() => - _DisplayReshareConfigViewState(); -} - -class _DisplayReshareConfigViewState - extends ConsumerState { - late final bool iAmInvolved; - - bool _buttonLock = false; - - Future _onPressed() async { - if (_buttonLock) { - return; - } - _buttonLock = true; - - try { - final wallet = - ref.read(pWallets).getWallet(widget.walletId) as BitcoinFrostWallet; - - final serializedKeys = await wallet.getSerializedKeys(); - if (mounted) { - final result = Frost.beginResharer( - serializedKeys: serializedKeys!, - config: ref.read(pFrostResharingData).resharerConfig!, - ); - - ref.read(pFrostResharingData).startResharerData = result; - - await Navigator.of(context).pushNamed( - BeginResharingView.routeName, - arguments: widget.walletId, - ); - } - } catch (e, s) { - Logging.instance.log( - "$e\n$s", - level: LogLevel.Fatal, - ); - - if (mounted) { - await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: e.toString(), - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } - } finally { - _buttonLock = false; - } - } - - @override - void initState() { - // TODO: optimize this by creating watcher providers (similar to normal WalletInfo) - final frostInfo = ref - .read(mainDBProvider) - .isar - .frostWalletInfo - .getByWalletIdSync(widget.walletId)!; - - final myOldIndex = frostInfo.participants.indexOf(frostInfo.myName); - - iAmInvolved = ref - .read(pFrostResharingData) - .configData! - .resharers - .contains(myOldIndex); - super.initState(); - } - - @override - Widget build(BuildContext context) { - return ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: const DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton(), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Resharer config", - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(16), - child: child, - ), - ), - ), - ); - }, - ), - ), - ), - ), - child: Column( - children: [ - if (!Util.isDesktop) const Spacer(), - SizedBox( - height: 220, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QrImageView( - data: ref.watch(pFrostResharingData).resharerConfig!, - size: 220, - backgroundColor: - Theme.of(context).extension()!.background, - foregroundColor: Theme.of(context) - .extension()! - .accentColorDark, - ), - ], - ), - ), - const SizedBox( - height: 32, - ), - DetailItem( - title: "Config", - detail: ref.watch(pFrostResharingData).resharerConfig!, - button: Util.isDesktop - ? IconCopyButton( - data: ref.watch(pFrostResharingData).resharerConfig!, - ) - : SimpleCopyButton( - data: ref.watch(pFrostResharingData).resharerConfig!, - ), - ), - SizedBox( - height: Util.isDesktop ? 64 : 16, - ), - if (!Util.isDesktop) - const Spacer( - flex: 2, - ), - if (iAmInvolved) - PrimaryButton( - label: "Start resharing", - onPressed: _onPressed, - ), - ], - ), - ), - ); - } -} diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1b/import_reshare_config_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1b/import_reshare_config_view.dart deleted file mode 100644 index 966a24710..000000000 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1b/import_reshare_config_view.dart +++ /dev/null @@ -1,338 +0,0 @@ -import 'package:barcode_scan2/barcode_scan2.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:frostdart/frostdart.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_view.dart'; -import 'package:stackwallet/providers/db/main_db_provider.dart'; -import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; -import 'package:stackwallet/providers/global/secure_store_provider.dart'; -import 'package:stackwallet/services/frost.dart'; -import 'package:stackwallet/themes/stack_colors.dart'; -import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/format.dart'; -import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/utilities/util.dart'; -import 'package:stackwallet/wallets/isar/models/frost_wallet_info.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_app_bar.dart'; -import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; -import 'package:stackwallet/widgets/desktop/primary_button.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'; - -class ImportReshareConfigView extends ConsumerStatefulWidget { - const ImportReshareConfigView({ - super.key, - required this.walletId, - }); - - static const String routeName = "/importReshareConfigView"; - - final String walletId; - - @override - ConsumerState createState() => - _ImportReshareConfigViewState(); -} - -class _ImportReshareConfigViewState - extends ConsumerState { - late final TextEditingController configFieldController; - late final FocusNode configFocusNode; - - bool _configEmpty = true; - - bool _buttonLock = false; - - Future _onPressed() async { - if (_buttonLock) { - return; - } - _buttonLock = true; - - try { - // TODO: optimize this by creating watcher providers (similar to normal WalletInfo) - final frostInfo = ref - .read(mainDBProvider) - .isar - .frostWalletInfo - .getByWalletIdSync(widget.walletId)!; - - ref.read(pFrostResharingData).reset(); - ref.read(pFrostResharingData).myName = frostInfo.myName; - ref.read(pFrostResharingData).resharerConfig = configFieldController.text; - - String? salt; - try { - salt = Format.uint8listToString( - resharerSalt( - resharerConfig: ref.read(pFrostResharingData).resharerConfig!, - ), - ); - } catch (_) { - throw Exception("Bad resharer config"); - } - - if (frostInfo.knownSalts.contains(salt)) { - throw Exception("Duplicate config salt"); - } else { - final salts = frostInfo.knownSalts; - salts.add(salt); - final mainDB = ref.read(mainDBProvider); - await mainDB.isar.writeTxn(() async { - final info = frostInfo; - await mainDB.isar.frostWalletInfo.delete(info.id); - await mainDB.isar.frostWalletInfo.put( - info.copyWith(knownSalts: salts), - ); - }); - } - - final serializedKeys = await ref.read(secureStoreProvider).read( - key: "{${widget.walletId}}_serializedFROSTKeys", - ); - if (mounted) { - final result = Frost.beginResharer( - serializedKeys: serializedKeys!, - config: ref.read(pFrostResharingData).resharerConfig!, - ); - - ref.read(pFrostResharingData).startResharerData = result; - - await Navigator.of(context).pushNamed( - BeginResharingView.routeName, - arguments: widget.walletId, - ); - } - } catch (e, s) { - Logging.instance.log( - "$e\n$s", - level: LogLevel.Fatal, - ); - - if (mounted) { - await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: e.toString(), - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } - } finally { - _buttonLock = false; - } - } - - @override - void initState() { - configFieldController = TextEditingController(); - configFocusNode = FocusNode(); - super.initState(); - } - - @override - void dispose() { - configFieldController.dispose(); - configFocusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: const DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton(), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: const AppBarBackButton(), - title: Text( - "Import FROST reshare config", - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(16), - child: child, - ), - ), - ), - ); - }, - ), - ), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 16, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("frConfigTextFieldKey"), - controller: configFieldController, - onChanged: (_) { - setState(() { - _configEmpty = configFieldController.text.isEmpty; - }); - }, - focusNode: configFocusNode, - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter config", - configFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: _configEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - !_configEmpty - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Config Field.", - key: const Key("frConfigClearButtonKey"), - onTap: () { - configFieldController.text = ""; - - setState(() { - _configEmpty = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Config Field Input.", - key: const Key("frConfigPasteButtonKey"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - configFieldController.text = - data.text!.trim(); - } - - setState(() { - _configEmpty = - configFieldController.text.isEmpty; - }); - }, - child: _configEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - if (_configEmpty) - TextFieldIconButton( - semanticsLabel: - "Scan QR Button. Opens Camera For Scanning QR Code.", - key: const Key("frConfigScanQrButtonKey"), - onTap: () async { - try { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 75)); - } - - final qrResult = await BarcodeScanner.scan(); - - configFieldController.text = - qrResult.rawContent; - - setState(() { - _configEmpty = - configFieldController.text.isEmpty; - }); - } on PlatformException catch (e, s) { - Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code: $e\n$s", - level: LogLevel.Warning, - ); - } - }, - child: const QrCodeIcon(), - ) - ], - ), - ), - ), - ), - ), - ), - const SizedBox( - height: 16, - ), - if (!Util.isDesktop) const Spacer(), - const SizedBox( - height: 16, - ), - PrimaryButton( - label: "Start resharing", - enabled: !_configEmpty, - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - } - - await _onPressed(); - }, - ), - ], - ), - ), - ); - } -} diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_view.dart deleted file mode 100644 index 90218529f..000000000 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_view.dart +++ /dev/null @@ -1,439 +0,0 @@ -import 'package:barcode_scan2/barcode_scan2.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:qr_flutter/qr_flutter.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/continue_resharing_view.dart'; -import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; -import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; -import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart'; -import 'package:stackwallet/providers/db/main_db_provider.dart'; -import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; -import 'package:stackwallet/services/frost.dart'; -import 'package:stackwallet/themes/stack_colors.dart'; -import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/utilities/util.dart'; -import 'package:stackwallet/wallets/isar/models/frost_wallet_info.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/simple_copy_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/detail_item.dart'; -import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.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'; - -class BeginResharingView extends ConsumerStatefulWidget { - const BeginResharingView({ - super.key, - required this.walletId, - }); - - static const String routeName = "/beginResharingView"; - - final String walletId; - - @override - ConsumerState createState() => _BeginResharingViewState(); -} - -class _BeginResharingViewState extends ConsumerState { - final List controllers = []; - final List focusNodes = []; - - late final List resharerIndexes; - late final int myResharerIndexIndex; - late final String myResharerStart; - late final bool amOutgoingParticipant; - - final List fieldIsEmptyFlags = []; - - bool _buttonLock = false; - - Future _onPressed() async { - if (_buttonLock) { - return; - } - _buttonLock = true; - - try { - if (!amOutgoingParticipant) { - // collect resharer strings - final resharerStarts = controllers.map((e) => e.text).toList(); - if (myResharerIndexIndex >= 0) { - // only insert my own at the correct index if I am a resharer - resharerStarts.insert(myResharerIndexIndex, myResharerStart); - } - - final result = Frost.beginReshared( - myName: ref.read(pFrostResharingData).myName!, - resharerConfig: ref.read(pFrostResharingData).resharerConfig!, - resharerStarts: resharerStarts, - ); - - ref.read(pFrostResharingData).startResharedData = result; - } - await Navigator.of(context).pushNamed( - ContinueResharingView.routeName, - arguments: widget.walletId, - ); - } catch (e, s) { - Logging.instance.log( - "$e\n$s", - level: LogLevel.Fatal, - ); - - if (mounted) { - await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Error", - message: e.toString(), - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } - } finally { - _buttonLock = false; - } - } - - @override - void initState() { - // TODO: optimize this by creating watcher providers (similar to normal WalletInfo) - final frostInfo = ref - .read(mainDBProvider) - .isar - .frostWalletInfo - .getByWalletIdSync(widget.walletId)!; - final myOldIndex = - frostInfo.participants.indexOf(ref.read(pFrostResharingData).myName!); - - myResharerStart = - ref.read(pFrostResharingData).startResharerData!.resharerStart; - - resharerIndexes = ref.read(pFrostResharingData).configData!.resharers; - myResharerIndexIndex = resharerIndexes.indexOf(myOldIndex); - if (myResharerIndexIndex >= 0) { - // remove my name for now as we don't need a text field for it - resharerIndexes.removeAt(myResharerIndexIndex); - } - - amOutgoingParticipant = !ref - .read(pFrostResharingData) - .configData! - .newParticipants - .contains(ref.read(pFrostResharingData).myName!); - - for (int i = 0; i < resharerIndexes.length; i++) { - controllers.add(TextEditingController()); - focusNodes.add(FocusNode()); - fieldIsEmptyFlags.add(true); - } - super.initState(); - } - - @override - void dispose() { - for (int i = 0; i < controllers.length; i++) { - controllers[i].dispose(); - } - for (int i = 0; i < focusNodes.length; i++) { - focusNodes[i].dispose(); - } - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { - await showDialog( - context: context, - builder: (_) => FrostInterruptionDialog( - type: FrostInterruptionDialogType.resharing, - popUntilOnYesRouteName: Util.isDesktop - ? DesktopWalletView.routeName - : WalletView.routeName, - ), - ); - return false; - }, - child: ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.resharing, - popUntilOnYesRouteName: DesktopWalletView.routeName, - ), - ); - }, - ), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.resharing, - popUntilOnYesRouteName: WalletView.routeName, - ), - ); - }, - ), - title: Text( - "Resharers", - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(16), - child: child, - ), - ), - ), - ); - }, - ), - ), - ), - ), - child: Column( - children: [ - SizedBox( - height: 220, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QrImageView( - data: myResharerStart, - size: 220, - backgroundColor: Theme.of(context) - .extension()! - .background, - foregroundColor: Theme.of(context) - .extension()! - .accentColorDark, - ), - ], - ), - ), - const _Div(), - DetailItem( - title: "My resharer", - detail: myResharerStart, - button: Util.isDesktop - ? IconCopyButton( - data: myResharerStart, - ) - : SimpleCopyButton( - data: myResharerStart, - ), - ), - const _Div(), - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - for (int i = 0; i < resharerIndexes.length; i++) - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: Key("frostResharerTextFieldKey_$i"), - controller: controllers[i], - focusNode: focusNodes[i], - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - onChanged: (_) { - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].text.isEmpty; - }); - }, - decoration: standardInputDecoration( - "Enter index " - "${resharerIndexes[i]}" - "'s resharer", - focusNodes[i], - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: fieldIsEmptyFlags[i] - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - !fieldIsEmptyFlags[i] - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Resharer Field Input.", - key: Key( - "frostResharerClearButtonKey_$i"), - onTap: () { - controllers[i].text = ""; - - setState(() { - fieldIsEmptyFlags[i] = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Resharer Field Input.", - key: Key( - "frostResharerPasteButtonKey_$i"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - controllers[i].text = - data.text!.trim(); - } - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i] - .text - .isEmpty; - }); - }, - child: fieldIsEmptyFlags[i] - ? const ClipboardIcon() - : const XIcon(), - ), - if (fieldIsEmptyFlags[i]) - TextFieldIconButton( - semanticsLabel: "Scan QR Button. " - "Opens Camera For Scanning QR Code.", - key: Key( - "frostCommitmentsScanQrButtonKey_$i"), - onTap: () async { - try { - if (FocusScope.of(context) - .hasFocus) { - FocusScope.of(context) - .unfocus(); - await Future.delayed( - const Duration( - milliseconds: 75)); - } - - final qrResult = - await BarcodeScanner.scan(); - - controllers[i].text = - qrResult.rawContent; - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i] - .text - .isEmpty; - }); - } on PlatformException catch (e, s) { - Logging.instance.log( - "Failed to get camera permissions " - "while trying to scan qr code: $e\n$s", - level: LogLevel.Warning, - ); - } - }, - child: const QrCodeIcon(), - ), - ], - ), - ), - ), - ), - ), - ), - ), - ], - ), - ], - ), - if (!Util.isDesktop) const Spacer(), - const _Div(), - PrimaryButton( - label: "Continue", - enabled: amOutgoingParticipant || - !fieldIsEmptyFlags.reduce((v, e) => v |= e), - onPressed: _onPressed, - ), - ], - ), - ), - ), - ); - } -} - -class _Div extends StatelessWidget { - const _Div({super.key}); - - @override - Widget build(BuildContext context) { - return const SizedBox( - height: 12, - ); - } -} diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/continue_resharing_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/continue_resharing_view.dart deleted file mode 100644 index 75359d266..000000000 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/continue_resharing_view.dart +++ /dev/null @@ -1,429 +0,0 @@ -import 'dart:ffi'; - -import 'package:barcode_scan2/barcode_scan2.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:qr_flutter/qr_flutter.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart'; -import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; -import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; -import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart'; -import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; -import 'package:stackwallet/services/frost.dart'; -import 'package:stackwallet/themes/stack_colors.dart'; -import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/text_styles.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/simple_copy_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/detail_item.dart'; -import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.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'; - -class ContinueResharingView extends ConsumerStatefulWidget { - const ContinueResharingView({ - super.key, - required this.walletId, - }); - - static const String routeName = "/continueResharingView"; - - final String walletId; - - @override - ConsumerState createState() => - _ContinueResharingViewState(); -} - -class _ContinueResharingViewState extends ConsumerState { - final List controllers = []; - final List focusNodes = []; - - late final List newParticipants; - late final int myIndex; - late final String? myEncryptionKey; - late final bool amOutgoingParticipant; - - final List fieldIsEmptyFlags = []; - - bool _buttonLock = false; - Future _onPressed() async { - if (_buttonLock) { - return; - } - _buttonLock = true; - - try { - // collect encryptionKeys strings and insert my own at the correct index - final encryptionKeys = controllers.map((e) => e.text).toList(); - if (!amOutgoingParticipant) { - encryptionKeys.insert(myIndex, myEncryptionKey!); - } - - final result = Frost.finishResharer( - machine: ref.read(pFrostResharingData).startResharerData!.machine.ref, - encryptionKeysOfResharedTo: encryptionKeys, - ); - - ref.read(pFrostResharingData).resharerComplete = result; - - await Navigator.of(context).pushNamed( - FinishResharingView.routeName, - arguments: widget.walletId, - ); - } catch (e, s) { - Logging.instance.log( - "$e\n$s", - level: LogLevel.Fatal, - ); - - await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Error", - message: e.toString(), - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } finally { - _buttonLock = false; - } - } - - @override - void initState() { - myEncryptionKey = - ref.read(pFrostResharingData).startResharedData?.resharedStart; - - newParticipants = ref.read(pFrostResharingData).configData!.newParticipants; - myIndex = newParticipants.indexOf(ref.read(pFrostResharingData).myName!); - - if (myIndex >= 0) { - // remove my name for now as we don't need a text field for it - newParticipants.removeAt(myIndex); - } - - if (myEncryptionKey == null && myIndex == -1) { - amOutgoingParticipant = true; - } else if (myEncryptionKey != null && myIndex >= 0) { - amOutgoingParticipant = false; - } else { - throw Exception("Invalid resharing state"); - } - - for (int i = 0; i < newParticipants.length; i++) { - controllers.add(TextEditingController()); - focusNodes.add(FocusNode()); - fieldIsEmptyFlags.add(true); - } - super.initState(); - } - - @override - void dispose() { - for (int i = 0; i < controllers.length; i++) { - controllers[i].dispose(); - } - for (int i = 0; i < focusNodes.length; i++) { - focusNodes[i].dispose(); - } - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { - await showDialog( - context: context, - builder: (_) => FrostInterruptionDialog( - type: FrostInterruptionDialogType.resharing, - popUntilOnYesRouteName: Util.isDesktop - ? DesktopWalletView.routeName - : WalletView.routeName, - ), - ); - return false; - }, - child: ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.resharing, - popUntilOnYesRouteName: DesktopWalletView.routeName, - ), - ); - }, - ), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.resharing, - popUntilOnYesRouteName: WalletView.routeName, - ), - ); - }, - ), - title: Text( - "Encryption keys", - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(16), - child: child, - ), - ), - ), - ); - }, - ), - ), - ), - ), - child: Column( - children: [ - if (!amOutgoingParticipant) - SizedBox( - height: 220, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QrImageView( - data: myEncryptionKey!, - size: 220, - backgroundColor: Theme.of(context) - .extension()! - .background, - foregroundColor: Theme.of(context) - .extension()! - .accentColorDark, - ), - ], - ), - ), - if (!amOutgoingParticipant) const _Div(), - if (!amOutgoingParticipant) - DetailItem( - title: "My encryption key", - detail: myEncryptionKey!, - button: Util.isDesktop - ? IconCopyButton( - data: myEncryptionKey!, - ) - : SimpleCopyButton( - data: myEncryptionKey!, - ), - ), - if (!amOutgoingParticipant) const _Div(), - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - for (int i = 0; i < newParticipants.length; i++) - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: Key("frostEncryptionKeyTextFieldKey_$i"), - controller: controllers[i], - focusNode: focusNodes[i], - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - onChanged: (_) { - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].text.isEmpty; - }); - }, - decoration: standardInputDecoration( - "Enter " - "${newParticipants[i]}" - "'s encryption key", - focusNodes[i], - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: fieldIsEmptyFlags[i] - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - !fieldIsEmptyFlags[i] - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Encryption Key Field Input.", - key: Key( - "frostEncryptionKeyClearButtonKey_$i"), - onTap: () { - controllers[i].text = ""; - - setState(() { - fieldIsEmptyFlags[i] = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Encryption Key Field Input.", - key: Key( - "frostEncryptionKeyPasteButtonKey_$i"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - controllers[i].text = - data.text!.trim(); - } - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i] - .text - .isEmpty; - }); - }, - child: fieldIsEmptyFlags[i] - ? const ClipboardIcon() - : const XIcon(), - ), - if (fieldIsEmptyFlags[i]) - TextFieldIconButton( - semanticsLabel: "Scan QR Button. " - "Opens Camera For Scanning QR Code.", - key: Key( - "frostCommitmentsScanQrButtonKey_$i"), - onTap: () async { - try { - if (FocusScope.of(context) - .hasFocus) { - FocusScope.of(context) - .unfocus(); - await Future.delayed( - const Duration( - milliseconds: 75)); - } - - final qrResult = - await BarcodeScanner.scan(); - - controllers[i].text = - qrResult.rawContent; - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i] - .text - .isEmpty; - }); - } on PlatformException catch (e, s) { - Logging.instance.log( - "Failed to get camera permissions " - "while trying to scan qr code: $e\n$s", - level: LogLevel.Warning, - ); - } - }, - child: const QrCodeIcon(), - ), - ], - ), - ), - ), - ), - ), - ), - ), - ], - ), - ], - ), - if (!Util.isDesktop) const Spacer(), - const _Div(), - PrimaryButton( - label: "Continue", - enabled: !fieldIsEmptyFlags.reduce((v, e) => v |= e), - onPressed: _onPressed, - ), - ], - ), - ), - ), - ); - } -} - -class _Div extends StatelessWidget { - const _Div({super.key}); - - @override - Widget build(BuildContext context) { - return const SizedBox( - height: 12, - ); - } -} diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart deleted file mode 100644 index 5e4ed4762..000000000 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart +++ /dev/null @@ -1,198 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:qr_flutter/qr_flutter.dart'; -import 'package:stackwallet/pages/home_view/home_view.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart'; -import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; -import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; -import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; -import 'package:stackwallet/themes/stack_colors.dart'; -import 'package:stackwallet/utilities/text_styles.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/simple_copy_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/detail_item.dart'; -import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart'; - -import 'package:stackwallet/pages/frost_mascot.dart'; - -class NewContinueSharingView extends ConsumerStatefulWidget { - const NewContinueSharingView({ - super.key, - required this.walletId, - }); - - static const String routeName = "/NewContinueSharingView"; - - final String walletId; - - @override - ConsumerState createState() => - _NewContinueSharingViewState(); -} - -class _NewContinueSharingViewState - extends ConsumerState { - @override - Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { - await showDialog( - context: context, - builder: (_) => FrostInterruptionDialog( - type: FrostInterruptionDialogType.resharing, - popUntilOnYesRouteName: - Util.isDesktop ? DesktopHomeView.routeName : HomeView.routeName, - ), - ); - return false; - }, - child: ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.resharing, - popUntilOnYesRouteName: DesktopHomeView.routeName, - ), - ); - }, - ), - trailing: FrostMascot( - title: 'Lorem ipsum', - body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', - ), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.resharing, - popUntilOnYesRouteName: HomeView.routeName, - ), - ); - }, - ), - title: Text( - "Encryption keys", - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(16), - child: child, - ), - ), - ), - ); - }, - ), - ), - ), - ), - child: Column( - children: [ - SizedBox( - height: 220, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QrImageView( - data: ref - .watch(pFrostResharingData) - .startResharedData! - .resharedStart, - size: 220, - backgroundColor: Theme.of(context) - .extension()! - .background, - foregroundColor: Theme.of(context) - .extension()! - .accentColorDark, - ), - ], - ), - ), - const _Div(), - DetailItem( - title: "My encryption key", - detail: ref - .watch(pFrostResharingData) - .startResharedData! - .resharedStart, - button: Util.isDesktop - ? IconCopyButton( - data: ref - .watch(pFrostResharingData) - .startResharedData! - .resharedStart, - ) - : SimpleCopyButton( - data: ref - .watch(pFrostResharingData) - .startResharedData! - .resharedStart, - ), - ), - if (!Util.isDesktop) const Spacer(), - const _Div(), - PrimaryButton( - label: "Continue", - onPressed: () { - Navigator.of(context).pushNamed( - FinishResharingView.routeName, - arguments: widget.walletId, - ); - }, - ), - ], - ), - ), - ), - ); - } -} - -class _Div extends StatelessWidget { - const _Div({super.key}); - - @override - Widget build(BuildContext context) { - return const SizedBox( - height: 12, - ); - } -} diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart deleted file mode 100644 index 698363923..000000000 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart +++ /dev/null @@ -1,431 +0,0 @@ -import 'package:barcode_scan2/barcode_scan2.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart'; -import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; -import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; -import 'package:stackwallet/themes/stack_colors.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/show_loading.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/utilities/util.dart'; -import 'package:stackwallet/wallets/isar/models/wallet_info.dart'; -import 'package:stackwallet/wallets/models/incomplete_frost_wallet.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_app_bar.dart'; -import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; -import 'package:stackwallet/widgets/desktop/primary_button.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:stackwallet/pages/frost_mascot.dart'; - -class NewImportResharerConfigView extends ConsumerStatefulWidget { - const NewImportResharerConfigView({ - super.key, - required this.walletName, - required this.coin, - }); - - static const String routeName = "/newImportResharerConfigView"; - - final String walletName; - final Coin coin; - - @override - ConsumerState createState() => - _NewImportResharerConfigViewState(); -} - -class _NewImportResharerConfigViewState - extends ConsumerState { - late final TextEditingController myNameFieldController, configFieldController; - late final FocusNode myNameFocusNode, configFocusNode; - - bool _nameEmpty = true, _configEmpty = true; - - bool _buttonLock = false; - - Future _createWallet() async { - final info = WalletInfo.createNew( - name: widget.walletName, - coin: widget.coin, - ); - - final wallet = IncompleteFrostWallet(); - wallet.info = info; - - return wallet; - } - - @override - void initState() { - myNameFieldController = TextEditingController(); - configFieldController = TextEditingController(); - myNameFocusNode = FocusNode(); - configFocusNode = FocusNode(); - super.initState(); - } - - @override - void dispose() { - myNameFieldController.dispose(); - configFieldController.dispose(); - myNameFocusNode.dispose(); - configFocusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton(), - trailing: FrostMascot( - title: 'Lorem ipsum', - body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', - ), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Import FROST reshare config", - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(16), - child: child, - ), - ), - ), - ); - }, - ), - ), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 16, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("frMyNameTextFieldKey"), - controller: myNameFieldController, - onChanged: (_) { - setState(() { - _nameEmpty = myNameFieldController.text.isEmpty; - }); - }, - focusNode: myNameFocusNode, - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "My name", - myNameFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: _nameEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - !_nameEmpty - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Config Field.", - key: const Key("frMyNameClearButtonKey"), - onTap: () { - myNameFieldController.text = ""; - - setState(() { - _nameEmpty = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Name Field.", - key: const Key("frMyNamePasteButtonKey"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - myNameFieldController.text = - data.text!.trim(); - } - - setState(() { - _nameEmpty = - myNameFieldController.text.isEmpty; - }); - }, - child: _nameEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - ], - ), - ), - ), - ), - ), - ), - const SizedBox( - height: 16, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("frConfigTextFieldKey"), - controller: configFieldController, - onChanged: (_) { - setState(() { - _configEmpty = configFieldController.text.isEmpty; - }); - }, - focusNode: configFocusNode, - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter config", - configFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: _configEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - !_configEmpty - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Config Field.", - key: const Key("frConfigClearButtonKey"), - onTap: () { - configFieldController.text = ""; - - setState(() { - _configEmpty = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Config Field Input.", - key: const Key("frConfigPasteButtonKey"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - configFieldController.text = - data.text!.trim(); - } - - setState(() { - _configEmpty = - configFieldController.text.isEmpty; - }); - }, - child: _configEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - if (_configEmpty) - TextFieldIconButton( - semanticsLabel: - "Scan QR Button. Opens Camera For Scanning QR Code.", - key: const Key("frConfigScanQrButtonKey"), - onTap: () async { - try { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 75)); - } - - final qrResult = await BarcodeScanner.scan(); - - configFieldController.text = - qrResult.rawContent; - - setState(() { - _configEmpty = - configFieldController.text.isEmpty; - }); - } on PlatformException catch (e, s) { - Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code: $e\n$s", - level: LogLevel.Warning, - ); - } - }, - child: const QrCodeIcon(), - ) - ], - ), - ), - ), - ), - ), - ), - const SizedBox( - height: 16, - ), - if (!Util.isDesktop) const Spacer(), - const SizedBox( - height: 16, - ), - PrimaryButton( - label: "Start", - enabled: !_nameEmpty && !_configEmpty, - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - } - if (_buttonLock) { - return; - } - _buttonLock = true; - - try { - ref.read(pFrostResharingData).reset(); - ref.read(pFrostResharingData).myName = - myNameFieldController.text; - ref.read(pFrostResharingData).resharerConfig = - configFieldController.text; - - if (!ref - .read(pFrostResharingData) - .configData! - .newParticipants - .contains(ref.read(pFrostResharingData).myName!)) { - ref.read(pFrostResharingData).reset(); - return await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "My name not found in config participants", - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } - - Exception? ex; - final wallet = await showLoading( - whileFuture: _createWallet(), - context: context, - message: "Setting up wallet", - isDesktop: Util.isDesktop, - onException: (e) => ex = e, - ); - - if (ex != null) { - throw ex!; - } - - if (mounted) { - ref.read(pFrostResharingData).incompleteWallet = wallet!; - await Navigator.of(context).pushNamed( - NewStartResharingView.routeName, - arguments: wallet.walletId, - ); - } - } catch (e, s) { - Logging.instance.log( - "$e\n$s", - level: LogLevel.Fatal, - ); - - if (mounted) { - await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: e.toString(), - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } - } finally { - _buttonLock = false; - } - }, - ), - ], - ), - ), - ); - } -} diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart deleted file mode 100644 index 7173eff3d..000000000 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart +++ /dev/null @@ -1,373 +0,0 @@ -import 'package:barcode_scan2/barcode_scan2.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/pages/home_view/home_view.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart'; -import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; -import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; -import 'package:stackwallet/services/frost.dart'; -import 'package:stackwallet/themes/stack_colors.dart'; -import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/text_styles.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_app_bar.dart'; -import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; -import 'package:stackwallet/widgets/desktop/primary_button.dart'; -import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.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:stackwallet/pages/frost_mascot.dart'; - -class NewStartResharingView extends ConsumerStatefulWidget { - const NewStartResharingView({ - super.key, - required this.walletId, - }); - - static const String routeName = "/newStartResharingView"; - - final String walletId; - - @override - ConsumerState createState() => - _NewStartResharingViewState(); -} - -class _NewStartResharingViewState extends ConsumerState { - final List controllers = []; - final List focusNodes = []; - - late final List resharerIndexes; - - final List fieldIsEmptyFlags = []; - - bool _buttonLock = false; - Future _onPressed() async { - if (_buttonLock) { - return; - } - _buttonLock = true; - - try { - // collect resharer strings - final resharerStarts = controllers.map((e) => e.text).toList(); - - final result = Frost.beginReshared( - myName: ref.read(pFrostResharingData).myName!, - resharerConfig: ref.read(pFrostResharingData).resharerConfig!, - resharerStarts: resharerStarts, - ); - - ref.read(pFrostResharingData).startResharedData = result; - - await Navigator.of(context).pushNamed( - NewContinueSharingView.routeName, - arguments: widget.walletId, - ); - } catch (e, s) { - Logging.instance.log( - "$e\n$s", - level: LogLevel.Fatal, - ); - - await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Error", - message: e.toString(), - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } finally { - _buttonLock = false; - } - } - - @override - void initState() { - resharerIndexes = ref.read(pFrostResharingData).configData!.resharers; - - for (int i = 0; i < resharerIndexes.length; i++) { - controllers.add(TextEditingController()); - focusNodes.add(FocusNode()); - fieldIsEmptyFlags.add(true); - } - super.initState(); - } - - @override - void dispose() { - for (int i = 0; i < controllers.length; i++) { - controllers[i].dispose(); - } - for (int i = 0; i < focusNodes.length; i++) { - focusNodes[i].dispose(); - } - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { - await showDialog( - context: context, - builder: (_) => FrostInterruptionDialog( - type: FrostInterruptionDialogType.resharing, - popUntilOnYesRouteName: - Util.isDesktop ? DesktopHomeView.routeName : HomeView.routeName, - ), - ); - return false; - }, - child: ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.resharing, - popUntilOnYesRouteName: DesktopHomeView.routeName, - ), - ); - }, - ), - trailing: FrostMascot( - title: 'Lorem ipsum', - body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', - ), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.resharing, - popUntilOnYesRouteName: HomeView.routeName, - ), - ); - }, - ), - title: Text( - "Resharers", - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(16), - child: child, - ), - ), - ), - ); - }, - ), - ), - ), - ), - child: Column( - children: [ - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - for (int i = 0; i < resharerIndexes.length; i++) - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: Key("frostResharerTextFieldKey_$i"), - controller: controllers[i], - focusNode: focusNodes[i], - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - onChanged: (_) { - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].text.isEmpty; - }); - }, - decoration: standardInputDecoration( - "Enter index " - "${resharerIndexes[i]}" - "'s resharer", - focusNodes[i], - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: fieldIsEmptyFlags[i] - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - !fieldIsEmptyFlags[i] - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Resharer Field Input.", - key: Key( - "frostResharerClearButtonKey_$i"), - onTap: () { - controllers[i].text = ""; - - setState(() { - fieldIsEmptyFlags[i] = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Resharer Field Input.", - key: Key( - "frostResharerPasteButtonKey_$i"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - controllers[i].text = - data.text!.trim(); - } - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i] - .text - .isEmpty; - }); - }, - child: fieldIsEmptyFlags[i] - ? const ClipboardIcon() - : const XIcon(), - ), - if (fieldIsEmptyFlags[i]) - TextFieldIconButton( - semanticsLabel: "Scan QR Button. " - "Opens Camera For Scanning QR Code.", - key: Key( - "frostCommitmentsScanQrButtonKey_$i"), - onTap: () async { - try { - if (FocusScope.of(context) - .hasFocus) { - FocusScope.of(context) - .unfocus(); - await Future.delayed( - const Duration( - milliseconds: 75)); - } - - final qrResult = - await BarcodeScanner.scan(); - - controllers[i].text = - qrResult.rawContent; - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i] - .text - .isEmpty; - }); - } on PlatformException catch (e, s) { - Logging.instance.log( - "Failed to get camera permissions " - "while trying to scan qr code: $e\n$s", - level: LogLevel.Warning, - ); - } - }, - child: const QrCodeIcon(), - ), - ], - ), - ), - ), - ), - ), - ), - ), - ], - ), - ], - ), - if (!Util.isDesktop) const Spacer(), - const _Div(), - PrimaryButton( - label: "Continue", - enabled: !fieldIsEmptyFlags.reduce((v, e) => v |= e), - onPressed: _onPressed, - ), - ], - ), - ), - ), - ); - } -} - -class _Div extends StatelessWidget { - const _Div({super.key}); - - @override - Widget build(BuildContext context) { - return const SizedBox( - height: 12, - ); - } -} diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/verify_updated_wallet_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/verify_updated_wallet_view.dart deleted file mode 100644 index 85d02c0ff..000000000 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/verify_updated_wallet_view.dart +++ /dev/null @@ -1,315 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/pages/home_view/home_view.dart'; -import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; -import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; -import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; -import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; -import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart'; -import 'package:stackwallet/providers/db/main_db_provider.dart'; -import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; -import 'package:stackwallet/providers/global/node_service_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/themes/stack_colors.dart'; -import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/show_loading.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/utilities/util.dart'; -import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.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/simple_copy_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/detail_item.dart'; -import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart'; -import 'package:stackwallet/widgets/stack_dialog.dart'; - -class VerifyUpdatedWalletView extends ConsumerStatefulWidget { - const VerifyUpdatedWalletView({ - super.key, - required this.walletId, - }); - - static const String routeName = "/verifyUpdatedWalletView"; - - final String walletId; - - @override - ConsumerState createState() => - _VerifyUpdatedWalletViewState(); -} - -class _VerifyUpdatedWalletViewState - extends ConsumerState { - late final String config; - late final String serializedKeys; - late final String reshareId; - - late final bool isNew; - - bool _buttonLock = false; - Future _onPressed() async { - if (_buttonLock) { - return; - } - _buttonLock = true; - - try { - Exception? ex; - - final BitcoinFrostWallet wallet; - - if (isNew) { - wallet = await ref - .read(pFrostResharingData) - .incompleteWallet! - .toBitcoinFrostWallet( - mainDB: ref.read(mainDBProvider), - secureStorageInterface: ref.read(secureStoreProvider), - nodeService: ref.read(nodeServiceChangeNotifierProvider), - prefs: ref.read(prefsChangeNotifierProvider), - ); - - await wallet.info.setMnemonicVerified( - isar: ref.read(mainDBProvider).isar, - ); - - ref.read(pWallets).addWallet(wallet); - } else { - wallet = - ref.read(pWallets).getWallet(widget.walletId) as BitcoinFrostWallet; - } - - if (mounted) { - await showLoading( - whileFuture: wallet.updateWithResharedData( - serializedKeys: serializedKeys, - multisigConfig: config, - isNewWallet: isNew, - ), - context: context, - message: isNew ? "Creating wallet" : "Updating wallet data", - isDesktop: Util.isDesktop, - onException: (e) => ex = e, - ); - - if (ex != null) { - throw ex!; - } - - if (mounted) { - ref.read(pFrostResharingData).reset(); - - Navigator.of(context).popUntil( - ModalRoute.withName( - _popUntilPath, - ), - ); - } - } - } catch (e, s) { - Logging.instance.log( - "$e\n$s", - level: LogLevel.Fatal, - ); - if (mounted) { - await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Error", - message: e.toString(), - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } - } finally { - _buttonLock = false; - } - } - - String get _popUntilPath => isNew - ? Util.isDesktop - ? DesktopHomeView.routeName - : HomeView.routeName - : Util.isDesktop - ? DesktopWalletView.routeName - : WalletView.routeName; - - @override - void initState() { - config = ref.read(pFrostResharingData).newWalletData!.multisigConfig; - serializedKeys = - ref.read(pFrostResharingData).newWalletData!.serializedKeys; - reshareId = ref.read(pFrostResharingData).newWalletData!.resharedId; - - isNew = ref.read(pFrostResharingData).incompleteWallet != null && - ref.read(pFrostResharingData).incompleteWallet!.walletId == - widget.walletId; - - super.initState(); - } - - @override - Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { - await showDialog( - context: context, - builder: (_) => FrostInterruptionDialog( - type: FrostInterruptionDialogType.resharing, - popUntilOnYesRouteName: _popUntilPath, - ), - ); - return false; - }, - child: ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => FrostInterruptionDialog( - type: FrostInterruptionDialogType.resharing, - popUntilOnYesRouteName: _popUntilPath, - ), - ); - }, - ), - trailing: ExitToMyStackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => FrostInterruptionDialog( - type: FrostInterruptionDialogType.resharing, - popUntilOnYesRouteName: _popUntilPath, - ), - ); - }, - ), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => FrostInterruptionDialog( - type: FrostInterruptionDialogType.resharing, - popUntilOnYesRouteName: _popUntilPath, - ), - ); - }, - ), - ), - body: SafeArea( - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(16), - child: child, - ), - ), - ), - ); - }, - ), - ), - ), - ), - child: Column( - children: [ - Text( - "Ensure your reshare ID matches that of each other participant", - style: STextStyles.pageTitleH2(context), - ), - const _Div(), - DetailItem( - title: "ID", - detail: reshareId, - button: Util.isDesktop - ? IconCopyButton( - data: reshareId, - ) - : SimpleCopyButton( - data: reshareId, - ), - ), - const _Div(), - const _Div(), - Text( - "Back up your keys and config", - style: STextStyles.pageTitleH2(context), - ), - const _Div(), - DetailItem( - title: "Config", - detail: config, - button: Util.isDesktop - ? IconCopyButton( - data: config, - ) - : SimpleCopyButton( - data: config, - ), - ), - const _Div(), - DetailItem( - title: "Keys", - detail: serializedKeys, - button: Util.isDesktop - ? IconCopyButton( - data: serializedKeys, - ) - : SimpleCopyButton( - data: serializedKeys, - ), - ), - if (!Util.isDesktop) const Spacer(), - const _Div(), - PrimaryButton( - label: "Confirm", - onPressed: _onPressed, - ), - ], - ), - ), - ), - ); - } -} - -class _Div extends StatelessWidget { - const _Div({super.key}); - - @override - Widget build(BuildContext context) { - return const SizedBox( - height: 12, - ); - } -} 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 fee8781ff..ab6baf455 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 @@ -35,12 +35,12 @@ import 'package:stackwallet/widgets/stack_dialog.dart'; class WalletBackupView extends ConsumerWidget { const WalletBackupView({ - Key? key, + super.key, required this.walletId, required this.mnemonic, this.frostWalletData, this.clipboardInterface = const ClipboardWrapper(), - }) : super(key: key); + }); static const String routeName = "/walletBackup"; 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 04de48cb4..66dfb8c23 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 @@ -53,13 +53,13 @@ import 'package:tuple/tuple.dart'; /// [eventBus] should only be set during testing class WalletSettingsView extends ConsumerStatefulWidget { const WalletSettingsView({ - Key? key, + super.key, required this.walletId, required this.coin, required this.initialSyncStatus, required this.initialNodeStatus, this.eventBus, - }) : super(key: key); + }); static const String routeName = "/walletSettings"; @@ -195,21 +195,6 @@ class _WalletSettingsViewState extends ConsumerState { padding: const EdgeInsets.all(4), child: Column( children: [ - if (coin == Coin.bitcoinFrost || - coin == Coin.bitcoinFrostTestNet) - if (coin == Coin.bitcoinFrost || - coin == Coin.bitcoinFrostTestNet) - SettingsListButton( - iconAssetName: Assets.svg.addressBook2, - iconSize: 16, - title: "FROST Multisig settings", - onPressed: () { - Navigator.of(context).pushNamed( - FrostMSWalletOptionsView.routeName, - arguments: walletId, - ); - }, - ), SettingsListButton( iconAssetName: Assets.svg.addressBook, iconSize: 16, @@ -221,6 +206,22 @@ class _WalletSettingsViewState extends ConsumerState { ); }, ), + if (coin.isFrost) + const SizedBox( + height: 8, + ), + if (coin.isFrost) + SettingsListButton( + iconAssetName: Assets.svg.addressBook2, + iconSize: 16, + title: "FROST Multisig settings", + onPressed: () { + Navigator.of(context).pushNamed( + FrostMSWalletOptionsView.routeName, + arguments: walletId, + ); + }, + ), const SizedBox( height: 8, ), @@ -266,31 +267,27 @@ class _WalletSettingsViewState extends ConsumerState { })? prevGen, })? frostWalletData; if (wallet is BitcoinFrostWallet) { - List> futures = []; - - futures.addAll( - [ - wallet.getSerializedKeys(), - wallet.getMultisigConfig(), - wallet.getSerializedKeysPrevGen(), - wallet.getMultisigConfigPrevGen(), - ], - ); + final futures = [ + wallet.getSerializedKeys(), + wallet.getMultisigConfig(), + wallet.getSerializedKeysPrevGen(), + wallet.getMultisigConfigPrevGen(), + ]; final results = await Future.wait(futures); - if (results.length == 5) { + if (results.length == 4) { frostWalletData = ( myName: wallet.frostInfo.myName, - config: results[1], - keys: results[0], + config: results[1]!, + keys: results[0]!, prevGen: results[2] == null || results[3] == null ? null : ( - config: results[3], - keys: results[2], + config: results[3]!, + keys: results[2]!, ), ); } @@ -300,7 +297,7 @@ class _WalletSettingsViewState extends ConsumerState { await wallet.getMnemonicAsWords(); } - if (mounted) { + if (context.mounted) { await Navigator.push( context, RouteGenerator.getRoute( diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart index 0876c92e0..03b131993 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart @@ -83,7 +83,7 @@ class _ChangeRepresentativeViewState whileFuture: changeFuture(_textController.text), context: context, message: "Updating representative...", - isDesktop: Util.isDesktop, + rootNavigator: Util.isDesktop, onException: (ex) { String msg = ex.toString(); while (msg.isNotEmpty && msg.startsWith("Exception:")) { diff --git a/lib/pages/token_view/sub_widgets/my_token_select_item.dart b/lib/pages/token_view/sub_widgets/my_token_select_item.dart index 003fd515a..2fd42300d 100644 --- a/lib/pages/token_view/sub_widgets/my_token_select_item.dart +++ b/lib/pages/token_view/sub_widgets/my_token_select_item.dart @@ -98,7 +98,7 @@ class _MyTokenSelectItemState extends ConsumerState { final success = await showLoading( whileFuture: _loadTokenWallet(context, ref), context: context, - isDesktop: isDesktop, + rootNavigator: isDesktop, message: "Loading ${widget.token.name}", ); diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index cfa690f11..a211cddd8 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -16,6 +16,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:isar/isar.dart'; +import 'package:stackwallet/frost_route_generator.dart'; import 'package:stackwallet/models/isar/exchange_cache/currency.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/buy_view/buy_in_wallet_view.dart'; @@ -78,12 +79,14 @@ 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'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/frost_scaffold.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/small_tor_icon.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/buy_nav_icon.dart'; import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/coin_control_nav_icon.dart'; import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/exchange_nav_icon.dart'; +import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/frost_sign_nav_icon.dart'; import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/fusion_nav_icon.dart'; import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/ordinals_nav_icon.dart'; import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/paynym_nav_icon.dart'; @@ -356,7 +359,26 @@ class _WalletViewState extends ConsumerState { } } - void _onExchangePressed(BuildContext context) async { + Future _onFrostSignPressed(BuildContext context) async { + final wallet = ref.read(pWallets).getWallet(walletId) as BitcoinFrostWallet; + ref.read(pFrostScaffoldArgs.state).state = ( + info: ( + walletName: wallet.info.name, + frostCurrency: wallet.cryptoCurrency, + ), + walletId: walletId, + stepRoutes: FrostRouteGenerator.signFrostTxStepRoutes, + parentNav: Navigator.of(context), + frostInterruptionDialogType: + FrostInterruptionDialogType.transactionCreation, + ); + + await Navigator.of(context).pushNamed( + FrostStepScaffold.routeName, + ); + } + + Future _onExchangePressed(BuildContext context) async { final Coin coin = ref.read(pWalletCoin(walletId)); if (coin.isTestNet) { @@ -401,7 +423,7 @@ class _WalletViewState extends ConsumerState { } } - void _onBuyPressed(BuildContext context) async { + Future _onBuyPressed(BuildContext context) async { final Coin coin = ref.read(pWalletCoin(walletId)); if (coin.isTestNet) { @@ -976,6 +998,12 @@ class _WalletViewState extends ConsumerState { } }, ), + if (ref.watch(pWalletCoin(walletId)).isFrost) + WalletNavigationBarItemData( + label: "Sign", + icon: const FrostSignNavIcon(), + onTap: () => _onFrostSignPressed(context), + ), WalletNavigationBarItemData( label: "Send", icon: const SendNavIcon(), @@ -1007,13 +1035,15 @@ class _WalletViewState extends ConsumerState { ); }, ), - if (Constants.enableExchange) + if (Constants.enableExchange && + !ref.watch(pWalletCoin(walletId)).isFrost) WalletNavigationBarItemData( label: "Swap", icon: const ExchangeNavIcon(), onTap: () => _onExchangePressed(context), ), - if (Constants.enableExchange) + if (Constants.enableExchange && + !ref.watch(pWalletCoin(walletId)).isFrost) WalletNavigationBarItemData( label: "Buy", icon: const BuyNavIcon(), diff --git a/lib/pages/wallets_view/sub_widgets/favorite_card.dart b/lib/pages/wallets_view/sub_widgets/favorite_card.dart index 8aafe8e30..4ef59bf0b 100644 --- a/lib/pages/wallets_view/sub_widgets/favorite_card.dart +++ b/lib/pages/wallets_view/sub_widgets/favorite_card.dart @@ -127,7 +127,7 @@ class _FavoriteCardState extends ConsumerState { whileFuture: loadFuture, context: context, message: 'Opening ${wallet.info.name}', - isDesktop: Util.isDesktop, + rootNavigator: Util.isDesktop, ); if (mounted) { diff --git a/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart b/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart index 2099b05e9..fada0b998 100644 --- a/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart +++ b/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart @@ -95,7 +95,7 @@ class WalletListItem extends ConsumerWidget { whileFuture: loadFuture, context: context, message: 'Opening ${wallet.info.name}', - isDesktop: Util.isDesktop, + rootNavigator: Util.isDesktop, ); if (context.mounted) { unawaited( diff --git a/lib/pages_desktop_specific/cashfusion/sub_widgets/fusion_dialog.dart b/lib/pages_desktop_specific/cashfusion/sub_widgets/fusion_dialog.dart index 2e02aeef9..6d4225fab 100644 --- a/lib/pages_desktop_specific/cashfusion/sub_widgets/fusion_dialog.dart +++ b/lib/pages_desktop_specific/cashfusion/sub_widgets/fusion_dialog.dart @@ -132,7 +132,7 @@ class _FusionDialogViewState extends ConsumerState { Future.delayed(const Duration(seconds: 2)), ]), context: context, - isDesktop: true, + rootNavigator: true, message: "Stopping fusion", ); diff --git a/lib/pages_desktop_specific/my_stack_view/coin_wallets_table.dart b/lib/pages_desktop_specific/my_stack_view/coin_wallets_table.dart index 7f138509f..94cdf75f8 100644 --- a/lib/pages_desktop_specific/my_stack_view/coin_wallets_table.dart +++ b/lib/pages_desktop_specific/my_stack_view/coin_wallets_table.dart @@ -92,7 +92,7 @@ class CoinWalletsTable extends ConsumerWidget { whileFuture: loadFuture, context: context, message: 'Opening ${wallet.info.name}', - isDesktop: Util.isDesktop, + rootNavigator: Util.isDesktop, ); if (context.mounted) { diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart index 5b0cac5f7..da7cd15e9 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart @@ -10,7 +10,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/pages/send_view/frost_ms/frost_import_sign_config_view.dart'; +import 'package:stackwallet/frost_route_generator.dart'; import 'package:stackwallet/pages/send_view/frost_ms/frost_send_view.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/tx_v2/transaction_v2_list.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart'; @@ -21,14 +21,15 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; import 'package:stackwallet/widgets/custom_tab_view.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/frost_scaffold.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class MyWallet extends ConsumerStatefulWidget { const MyWallet({ - Key? key, + super.key, required this.walletId, this.contractAddress, - }) : super(key: key); + }); final String walletId; final String? contractAddress; @@ -85,10 +86,28 @@ class _MyWalletState extends ConsumerState { width: 200, buttonHeight: ButtonHeight.l, label: "Import sign config", - onPressed: () { - Navigator.of(context).pushNamed( - FrostImportSignConfigView.routeName, - arguments: widget.walletId, + onPressed: () async { + final wallet = ref + .read(pWallets) + .getWallet(widget.walletId) + as BitcoinFrostWallet; + ref.read(pFrostScaffoldArgs.state).state = + ( + info: ( + walletName: wallet.info.name, + frostCurrency: wallet.cryptoCurrency, + ), + walletId: widget.walletId, + stepRoutes: FrostRouteGenerator + .signFrostTxStepRoutes, + parentNav: Navigator.of(context), + frostInterruptionDialogType: + FrostInterruptionDialogType + .transactionCreation, + ); + + await Navigator.of(context).pushNamed( + FrostStepScaffold.routeName, ); }, ), diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart index 163052ec0..52fe50a4f 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart @@ -92,8 +92,6 @@ class _UnlockWalletKeysDesktopState keys: (await wallet.getSerializedKeys())!, config: (await wallet.getMultisigConfig())!, ); - print(1111111); - print(frostData); } else { throw Exception("FIXME ~= see todo in code"); } diff --git a/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart b/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart index c28524635..b258d0a5e 100644 --- a/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart +++ b/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart @@ -234,7 +234,7 @@ class _DesktopOrdinalDetailsViewState final path = await showLoading( whileFuture: _savePngToFile(), context: context, - isDesktop: true, + rootNavigator: true, message: "Saving ordinal image", onException: (e) { didError = true; diff --git a/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart b/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart index 1fb4f29af..c710a4d10 100644 --- a/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart +++ b/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart @@ -210,7 +210,7 @@ class _DesktopOrdinals extends ConsumerState { onPressed: () async { // show loading for a minimum of 2 seconds on refreshing await showLoading( - isDesktop: true, + rootNavigator: true, whileFuture: Future.wait([ Future.delayed(const Duration(seconds: 2)), (ref.read(pWallets).getWallet(widget.walletId) diff --git a/lib/providers/frost_wallet/frost_wallet_providers.dart b/lib/providers/frost_wallet/frost_wallet_providers.dart index 3b181b7b8..7b3ee3eda 100644 --- a/lib/providers/frost_wallet/frost_wallet_providers.dart +++ b/lib/providers/frost_wallet/frost_wallet_providers.dart @@ -60,13 +60,14 @@ class _ResharingData { IncompleteFrostWallet? incompleteWallet; // resharer encoded config string - String? resharerConfig; + String? resharerRConfig; + ({ int newThreshold, - List resharers, + Map resharers, List newParticipants, - })? get configData => resharerConfig != null - ? Frost.extractResharerConfigData(resharerConfig: resharerConfig!) + })? get configData => resharerRConfig != null + ? Frost.extractResharerConfigData(rConfig: resharerRConfig!) : null; // resharer start string (for sharing) and machine @@ -93,7 +94,7 @@ class _ResharingData { // reset/clear all data void reset() { - resharerConfig = null; + resharerRConfig = null; startResharerData = null; startResharedData = null; resharerComplete = null; diff --git a/lib/route_generator.dart b/lib/route_generator.dart index e4d188cdf..f6dcaa6da 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -26,12 +26,8 @@ import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_custom_tok import 'package:stackwallet/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart'; import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/new_wallet_options/new_wallet_options_view.dart'; @@ -83,9 +79,6 @@ import 'package:stackwallet/pages/receive_view/addresses/wallet_addresses_view.d 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/frost_ms/frost_attempt_sign_config_view.dart'; -import 'package:stackwallet/pages/send_view/frost_ms/frost_create_sign_config_view.dart'; -import 'package:stackwallet/pages/send_view/frost_ms/frost_import_sign_config_view.dart'; import 'package:stackwallet/pages/send_view/frost_ms/frost_send_view.dart'; import 'package:stackwallet/pages/send_view/send_view.dart'; import 'package:stackwallet/pages/send_view/token_send_view.dart'; @@ -126,17 +119,8 @@ import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_pr import 'package:stackwallet/pages/settings_views/global_settings_view/tor_settings/tor_settings_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/begin_reshare_config_view.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/complete_reshare_config_view.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/display_reshare_config_view.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1b/import_reshare_config_view.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_view.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/continue_resharing_view.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/verify_updated_wallet_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/initiate_resharing_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_view.dart'; @@ -205,9 +189,11 @@ import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_ import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/add_wallet_type_enum.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/frost_currency.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/wallet.dart'; import 'package:stackwallet/widgets/choose_coin_view.dart'; +import 'package:stackwallet/widgets/frost_scaffold.dart'; import 'package:tuple/tuple.dart'; /* @@ -450,13 +436,13 @@ class RouteGenerator { case CreateNewFrostMsWalletView.routeName: if (args is ({ String walletName, - Coin coin, + FrostCurrency frostCurrency, })) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => CreateNewFrostMsWalletView( walletName: args.walletName, - coin: args.coin, + frostCurrency: args.frostCurrency, ), settings: RouteSettings( name: settings.name, @@ -468,13 +454,13 @@ class RouteGenerator { case RestoreFrostMsWalletView.routeName: if (args is ({ String walletName, - Coin coin, + FrostCurrency frostCurrency, })) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => RestoreFrostMsWalletView( walletName: args.walletName, - coin: args.coin, + frostCurrency: args.frostCurrency, ), settings: RouteSettings( name: settings.name, @@ -483,16 +469,16 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); - case ShareNewMultisigConfigView.routeName: + case SelectNewFrostImportTypeView.routeName: if (args is ({ String walletName, - Coin coin, + FrostCurrency frostCurrency, })) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ShareNewMultisigConfigView( + builder: (_) => SelectNewFrostImportTypeView( walletName: args.walletName, - coin: args.coin, + frostCurrency: args.frostCurrency, ), settings: RouteSettings( name: settings.name, @@ -501,123 +487,14 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); - case ImportNewFrostMsWalletView.routeName: - if (args is ({ - String walletName, - Coin coin, - })) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ImportNewFrostMsWalletView( - walletName: args.walletName, - coin: args.coin, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); - - case NewImportResharerConfigView.routeName: - if (args is ({ - String walletName, - Coin coin, - })) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => NewImportResharerConfigView( - walletName: args.walletName, - coin: args.coin, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); - - case NewStartResharingView.routeName: - if (args is String) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => NewStartResharingView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); - - case NewContinueSharingView.routeName: - if (args is String) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => NewContinueSharingView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); - - case FrostShareCommitmentsView.routeName: - if (args is ({ - String walletName, - Coin coin, - })) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => FrostShareCommitmentsView( - walletName: args.walletName, - coin: args.coin, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); - - case FrostShareSharesView.routeName: - if (args is ({ - String walletName, - Coin coin, - })) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => FrostShareSharesView( - walletName: args.walletName, - coin: args.coin, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); - - case ConfirmNewFrostMSWalletCreationView.routeName: - if (args is ({ - String walletName, - Coin coin, - })) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ConfirmNewFrostMSWalletCreationView( - walletName: args.walletName, - coin: args.coin, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); + case FrostStepScaffold.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostStepScaffold(), + settings: RouteSettings( + name: settings.name, + ), + ); case FrostMSWalletOptionsView.routeName: if (args is String) { @@ -647,25 +524,11 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); - case ImportReshareConfigView.routeName: + case InitiateResharingView.routeName: if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ImportReshareConfigView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); - - case BeginReshareConfigView.routeName: - if (args is String) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => BeginReshareConfigView( + builder: (_) => InitiateResharingView( walletId: args, ), settings: RouteSettings( @@ -676,7 +539,7 @@ class RouteGenerator { return _routeError("${settings.name} invalid args: ${args.toString()}"); case CompleteReshareConfigView.routeName: - if (args is ({String walletId, List resharers})) { + if (args is ({String walletId, Map resharers})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => CompleteReshareConfigView( @@ -690,76 +553,6 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); - case DisplayReshareConfigView.routeName: - if (args is String) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DisplayReshareConfigView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); - - case BeginResharingView.routeName: - if (args is String) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => BeginResharingView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); - - case ContinueResharingView.routeName: - if (args is String) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ContinueResharingView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); - - case FinishResharingView.routeName: - if (args is String) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => FinishResharingView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); - - case VerifyUpdatedWalletView.routeName: - if (args is String) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => VerifyUpdatedWalletView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); - case FrostSendView.routeName: if (args is ({ String walletId, @@ -778,48 +571,6 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); - case FrostImportSignConfigView.routeName: - if (args is String) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => FrostImportSignConfigView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); - - case FrostCreateSignConfigView.routeName: - if (args is String) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => FrostCreateSignConfigView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); - - case FrostAttemptSignConfigView.routeName: - if (args is String) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => FrostAttemptSignConfigView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); - // case MonkeyLoadedView.routeName: // if (args is Tuple2>) { // return getRoute( diff --git a/lib/services/frost.dart b/lib/services/frost.dart index a420a5b16..adf88695a 100644 --- a/lib/services/frost.dart +++ b/lib/services/frost.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:ffi'; import 'dart:typed_data'; @@ -551,11 +552,14 @@ abstract class Frost { static ({ int newThreshold, - List resharers, + Map resharers, List newParticipants, }) extractResharerConfigData({ - required String resharerConfig, + required String rConfig, }) { + final decoded = _decodeRConfigWithResharers(rConfig); + final resharerConfig = decoded.config; + try { final newThreshold = resharerNewThreshold( resharerConfigPointer: decodedResharerConfig( @@ -597,9 +601,17 @@ abstract class Frost { ); } + final Map resharersMap = {}; + + for (final resharer in resharers) { + resharersMap[decoded.resharers.entries + .firstWhere((e) => e.value == resharer) + .key] = resharer; + } + return ( newThreshold: newThreshold, - resharers: resharers, + resharers: resharersMap, newParticipants: newParticipants, ); } catch (e, s) { @@ -610,4 +622,29 @@ abstract class Frost { rethrow; } } + + static String encodeRConfig( + String config, + Map resharers, + ) { + return base64Encode("$config@${jsonEncode(resharers)}".toUint8ListFromUtf8); + } + + static String decodeRConfig( + String rConfig, + ) { + return base64Decode(rConfig).toUtf8String.split("@").first; + } + + static ({Map resharers, String config}) + _decodeRConfigWithResharers( + String rConfig, + ) { + final parts = base64Decode(rConfig).toUtf8String.split("@"); + + final config = parts[0]; + final resharers = Map.from(jsonDecode(parts[1]) as Map); + + return (resharers: resharers, config: config); + } } diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index ff385a44d..9213fd1b2 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -143,6 +143,7 @@ class _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 swap2 => "assets/svg/swap2.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"; diff --git a/lib/utilities/prefs.dart b/lib/utilities/prefs.dart index d380901c3..fc3d29e71 100644 --- a/lib/utilities/prefs.dart +++ b/lib/utilities/prefs.dart @@ -69,7 +69,6 @@ class Prefs extends ChangeNotifier { _useTor = await _getUseTor(); _fusionServerInfo = await _getFusionServerInfo(); _solanaEnabled = await _getSolanaEnabled(); - _frostEnabled = await _getFrostEnabled(); _initialized = true; } @@ -1031,25 +1030,4 @@ class Prefs extends ChangeNotifier { boxName: DB.boxNamePrefs, key: "solanaEnabled") as bool? ?? false; } - - // FROST multisig - - bool _frostEnabled = false; - - bool get frostEnabled => _frostEnabled; - - set frostEnabled(bool frostEnabled) { - if (_frostEnabled != frostEnabled) { - DB.instance.put( - boxName: DB.boxNamePrefs, key: "frostEnabled", value: frostEnabled); - _frostEnabled = frostEnabled; - notifyListeners(); - } - } - - Future _getFrostEnabled() async { - return await DB.instance.get( - boxName: DB.boxNamePrefs, key: "frostEnabled") as bool? ?? - false; - } } diff --git a/lib/utilities/show_loading.dart b/lib/utilities/show_loading.dart index 759eaa05c..e01a86441 100644 --- a/lib/utilities/show_loading.dart +++ b/lib/utilities/show_loading.dart @@ -20,7 +20,7 @@ Future showLoading({ required BuildContext context, required String message, String? subMessage, - bool isDesktop = false, + bool rootNavigator = false, bool opaqueBG = false, void Function(Exception)? onException, }) async { @@ -59,7 +59,7 @@ Future showLoading({ } if (context.mounted) { - Navigator.of(context, rootNavigator: isDesktop).pop(); + Navigator.of(context, rootNavigator: rootNavigator).pop(); if (ex != null) { onException?.call(ex); } diff --git a/lib/utilities/text_styles.dart b/lib/utilities/text_styles.dart index 21089d30d..0a7383e9f 100644 --- a/lib/utilities/text_styles.dart +++ b/lib/utilities/text_styles.dart @@ -383,6 +383,28 @@ class STextStyles { } } + static TextStyle w400_16(BuildContext context) { + switch (_theme(context).themeId) { + default: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 16, + ); + } + } + + static TextStyle w400_14(BuildContext context) { + switch (_theme(context).themeId) { + default: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 14, + ); + } + } + static TextStyle w600_20(BuildContext context) { switch (_theme(context).themeId) { default: diff --git a/lib/wallets/crypto_currency/coins/bitcoin_frost.dart b/lib/wallets/crypto_currency/coins/bitcoin_frost.dart index 38530f056..bd5216350 100644 --- a/lib/wallets/crypto_currency/coins/bitcoin_frost.dart +++ b/lib/wallets/crypto_currency/coins/bitcoin_frost.dart @@ -6,15 +6,15 @@ import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/crypto_currency/intermediate/bip39_hd_currency.dart'; -import 'package:stackwallet/wallets/crypto_currency/intermediate/private_key_currency.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/frost_currency.dart'; class BitcoinFrost extends FrostCurrency { BitcoinFrost(super.network) { switch (network) { case CryptoCurrencyNetwork.main: - coin = Coin.bitcoin; + coin = Coin.bitcoinFrost; case CryptoCurrencyNetwork.test: - coin = Coin.bitcoinTestNet; + coin = Coin.bitcoinFrostTestNet; default: throw Exception("Unsupported network: $network"); } diff --git a/lib/wallets/crypto_currency/intermediate/private_key_currency.dart b/lib/wallets/crypto_currency/intermediate/frost_currency.dart similarity index 100% rename from lib/wallets/crypto_currency/intermediate/private_key_currency.dart rename to lib/wallets/crypto_currency/intermediate/frost_currency.dart diff --git a/lib/wallets/models/incomplete_frost_wallet.dart b/lib/wallets/models/incomplete_frost_wallet.dart index e5075da63..015ca9fb9 100644 --- a/lib/wallets/models/incomplete_frost_wallet.dart +++ b/lib/wallets/models/incomplete_frost_wallet.dart @@ -35,7 +35,9 @@ class IncompleteFrostWallet { threshold: -1, ); - await mainDB.isar.frostWalletInfo.put(frostInfo); + await mainDB.isar.writeTxn(() async { + await mainDB.isar.frostWalletInfo.put(frostInfo); + }); return wallet as BitcoinFrostWallet; } diff --git a/lib/wallets/models/tx_data.dart b/lib/wallets/models/tx_data.dart index 22101003c..d98dd088e 100644 --- a/lib/wallets/models/tx_data.dart +++ b/lib/wallets/models/tx_data.dart @@ -33,7 +33,9 @@ class TxData { final String? changeAddress; + // frost specific final String? frostMSConfig; + final List? frostSigners; // paynym specific final PaynymAccountLite? paynymAccountLite; @@ -91,6 +93,7 @@ class TxData { this.usedUTXOs, this.changeAddress, this.frostMSConfig, + this.frostSigners, this.paynymAccountLite, this.web3dartTransaction, this.nonce, @@ -166,6 +169,7 @@ class TxData { })>? recipients, String? frostMSConfig, + List? frostSigners, String? changeAddress, PaynymAccountLite? paynymAccountLite, web3dart.Transaction? web3dartTransaction, @@ -209,6 +213,7 @@ class TxData { usedUTXOs: usedUTXOs ?? this.usedUTXOs, recipients: recipients ?? this.recipients, frostMSConfig: frostMSConfig ?? this.frostMSConfig, + frostSigners: frostSigners ?? this.frostSigners, changeAddress: changeAddress ?? this.changeAddress, paynymAccountLite: paynymAccountLite ?? this.paynymAccountLite, web3dartTransaction: web3dartTransaction ?? this.web3dartTransaction, @@ -249,7 +254,7 @@ class TxData { 'recipients: $recipients, ' 'utxos: $utxos, ' 'usedUTXOs: $usedUTXOs, ' - 'frostMSConfig: $frostMSConfig, ' + 'frostSigners: $frostSigners, ' 'changeAddress: $changeAddress, ' 'paynymAccountLite: $paynymAccountLite, ' 'web3dartTransaction: $web3dartTransaction, ' diff --git a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart index d4cd2b1e2..1798fd79f 100644 --- a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart @@ -23,7 +23,7 @@ import 'package:stackwallet/utilities/extensions/extensions.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin_frost.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; -import 'package:stackwallet/wallets/crypto_currency/intermediate/private_key_currency.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/frost_currency.dart'; import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/wallet.dart'; @@ -41,7 +41,6 @@ class BitcoinFrostWallet extends Wallet { late CachedElectrumXClient electrumXCachedClient; Future initializeNewFrost({ - required String mnemonic, required String multisigConfig, required String recoveryString, required String serializedKeys, @@ -70,14 +69,6 @@ class BitcoinFrostWallet extends Wallet { threshold: threshold, ); - await secureStorageInterface.write( - key: Wallet.mnemonicKey(walletId: info.walletId), - value: mnemonic, - ); - await secureStorageInterface.write( - key: Wallet.mnemonicPassphraseKey(walletId: info.walletId), - value: "", - ); await _saveSerializedKeys(serializedKeys); await _saveRecoveryString(recoveryString); await _saveMultisigId(multisigId); diff --git a/lib/wallets/wallet/impl/epiccash_wallet.dart b/lib/wallets/wallet/impl/epiccash_wallet.dart index 356cf962d..b2e5ad3f1 100644 --- a/lib/wallets/wallet/impl/epiccash_wallet.dart +++ b/lib/wallets/wallet/impl/epiccash_wallet.dart @@ -50,9 +50,9 @@ class EpiccashWallet extends Bip39Wallet { double highestPercent = 0; Future get getSyncPercent async { - int lastScannedBlock = info.epicData?.lastScannedBlock ?? 0; + final int lastScannedBlock = info.epicData?.lastScannedBlock ?? 0; final _chainHeight = await chainHeight; - double restorePercent = lastScannedBlock / _chainHeight; + final double restorePercent = lastScannedBlock / _chainHeight; GlobalEventBus.instance .fire(RefreshPercentChangedEvent(highestPercent, walletId)); if (restorePercent > highestPercent) { @@ -67,7 +67,7 @@ class EpiccashWallet extends Bip39Wallet { } Future updateEpicboxConfig(String host, int port) async { - String stringConfig = jsonEncode({ + final String stringConfig = jsonEncode({ "epicbox_domain": host, "epicbox_port": port, "epicbox_protocol_unsecure": false, @@ -103,7 +103,7 @@ class EpiccashWallet extends Bip39Wallet { } Future getEpicBoxConfig() async { - EpicBoxConfigModel? _epicBoxConfig = EpicBoxConfigModel.fromServer( + final EpicBoxConfigModel _epicBoxConfig = EpicBoxConfigModel.fromServer( DefaultEpicBoxes.defaultEpicBoxServer, ); @@ -156,12 +156,12 @@ class EpiccashWallet extends Bip39Wallet { config["api_listen_port"] = port; config["api_listen_interface"] = nodeApiAddress.replaceFirst(uri.scheme, ""); - String stringConfig = jsonEncode(config); + final String stringConfig = jsonEncode(config); return stringConfig; } Future _currentWalletDirPath() async { - Directory appDir = await StackFileSystem.applicationRootDirectory(); + final Directory appDir = await StackFileSystem.applicationRootDirectory(); final path = "${appDir.path}/epiccash"; final String name = walletId.trim(); @@ -176,7 +176,7 @@ class EpiccashWallet extends Bip39Wallet { try { final available = info.cachedBalance.spendable.raw.toInt(); - var transactionFees = await epiccash.LibEpiccash.getTransactionFees( + final transactionFees = await epiccash.LibEpiccash.getTransactionFees( wallet: wallet!, amount: satoshiAmount, minimumConfirmations: cryptoCurrency.minConfirms, @@ -303,21 +303,20 @@ class EpiccashWallet extends Bip39Wallet { Future
_generateAndStoreReceivingAddressForIndex( int index, ) async { - Address? address = await getCurrentReceivingAddress(); - EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig(); if (address != null) { final splitted = address.value.split('@'); //Check if the address is the same as the current epicbox domain //Since we're only using one epicbpox now this doesn't apply but will be // useful in the future - final encodedConfig = jsonEncode(epicboxConfig); + final epicboxConfig = await getEpicBoxConfig(); if (splitted[1] != epicboxConfig.host) { //Update the address address = await thisWalletAddress(index, epicboxConfig); } } else { + final epicboxConfig = await getEpicBoxConfig(); address = await thisWalletAddress(index, epicboxConfig); } @@ -331,9 +330,10 @@ class EpiccashWallet extends Bip39Wallet { } Future
thisWalletAddress( - int index, EpicBoxConfigModel epicboxConfig) async { + int index, + EpicBoxConfigModel epicboxConfig, + ) async { final wallet = await secureStorageInterface.read(key: '${walletId}_wallet'); - // EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig(); final walletAddress = await epiccash.LibEpiccash.getAddressInfo( wallet: wallet!, @@ -384,7 +384,7 @@ class EpiccashWallet extends Bip39Wallet { level: LogLevel.Info, ); - int nextScannedBlock = await epiccash.LibEpiccash.scanOutputs( + final int nextScannedBlock = await epiccash.LibEpiccash.scanOutputs( wallet: wallet!, startHeight: lastScannedBlock, numberOfBlocks: scanChunkSize, @@ -424,7 +424,7 @@ class EpiccashWallet extends Bip39Wallet { Future _listenToEpicbox() async { Logging.instance.log("STARTING WALLET LISTENER ....", level: LogLevel.Info); final wallet = await secureStorageInterface.read(key: '${walletId}_wallet'); - EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig(); + final EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig(); epiccash.LibEpiccash.startEpicboxListener( wallet: wallet!, epicboxConfig: epicboxConfig.toString(), @@ -438,7 +438,7 @@ class EpiccashWallet extends Bip39Wallet { ); if (Platform.isIOS) { final walletDir = await _currentWalletDirPath(); - var editConfig = jsonDecode(config as String); + final editConfig = jsonDecode(config as String); editConfig["wallet_dir"] = walletDir; config = jsonEncode(editConfig); @@ -448,14 +448,11 @@ class EpiccashWallet extends Bip39Wallet { // TODO: make more robust estimate of date maybe using https://explorer.epic.tech/api-index int _calculateRestoreHeightFrom({required DateTime date}) { - int secondsSinceEpoch = date.millisecondsSinceEpoch ~/ 1000; + final int secondsSinceEpoch = date.millisecondsSinceEpoch ~/ 1000; const int epicCashFirstBlock = 1565370278; const double overestimateSecondsPerBlock = 61; - int chosenSeconds = secondsSinceEpoch - epicCashFirstBlock; - int approximateHeight = chosenSeconds ~/ overestimateSecondsPerBlock; - //todo: check if print needed - // debugPrint( - // "approximate height: $approximateHeight chosen_seconds: $chosenSeconds"); + final int chosenSeconds = secondsSinceEpoch - epicCashFirstBlock; + final int approximateHeight = chosenSeconds ~/ overestimateSecondsPerBlock; int height = approximateHeight; if (height < 0) { height = 0; @@ -503,7 +500,7 @@ class EpiccashWallet extends Bip39Wallet { await secureStorageInterface.write( key: '${walletId}_epicboxConfig', value: epicboxConfig.toString()); - String name = walletId; + final String name = walletId; await epiccash.LibEpiccash.initializeNewWallet( config: stringConfig, @@ -587,7 +584,7 @@ class EpiccashWallet extends Bip39Wallet { if (!receiverAddress.startsWith("http://") || !receiverAddress.startsWith("https://")) { - bool isEpicboxConnected = await _testEpicboxServer( + final bool isEpicboxConnected = await _testEpicboxServer( epicboxConfig, ); if (!isEpicboxConnected) { @@ -969,7 +966,7 @@ class EpiccashWallet extends Bip39Wallet { ], walletOwns: true, ); - InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( + final InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( scriptSigHex: null, scriptSigAsm: null, sequence: null, @@ -1106,7 +1103,7 @@ class EpiccashWallet extends Bip39Wallet { @override Future estimateFeeFor(Amount amount, int feeRate) async { // setting ifErrorEstimateFee doesn't do anything as its not used in the nativeFee function????? - int currentFee = await _nativeFee( + final int currentFee = await _nativeFee( amount.raw.toInt(), ifErrorEstimateFee: true, ); @@ -1151,13 +1148,13 @@ Future deleteEpicWallet({ final wallet = await secureStore.read(key: '${walletId}_wallet'); String? config = await secureStore.read(key: '${walletId}_config'); if (Platform.isIOS) { - Directory appDir = await StackFileSystem.applicationRootDirectory(); + final Directory appDir = await StackFileSystem.applicationRootDirectory(); final path = "${appDir.path}/epiccash"; final String name = walletId.trim(); final walletDir = '$path/$name'; - var editConfig = jsonDecode(config as String); + final editConfig = jsonDecode(config as String); editConfig["wallet_dir"] = walletDir; config = jsonEncode(editConfig); diff --git a/lib/wallets/wallet/impl/stellar_wallet.dart b/lib/wallets/wallet/impl/stellar_wallet.dart index edea60fe5..657fd7676 100644 --- a/lib/wallets/wallet/impl/stellar_wallet.dart +++ b/lib/wallets/wallet/impl/stellar_wallet.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:isar/isar.dart'; +import 'package:mutex/mutex.dart'; import 'package:socks5_proxy/socks.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; @@ -11,6 +12,9 @@ import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart' import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart'; +import 'package:stackwallet/services/event_bus/events/global/tor_status_changed_event.dart'; +import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/services/tor_service.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; @@ -23,11 +27,47 @@ import 'package:stackwallet/wallets/wallet/intermediate/bip39_wallet.dart'; import 'package:stellar_flutter_sdk/stellar_flutter_sdk.dart' as stellar; class StellarWallet extends Bip39Wallet { - StellarWallet(CryptoCurrencyNetwork network) : super(Stellar(network)); + StellarWallet(CryptoCurrencyNetwork network) : super(Stellar(network)) { + final bus = GlobalEventBus.instance; - stellar.StellarSDK get stellarSdk { - if (_stellarSdk == null) { - _updateSdk(); + // Listen for tor status changes. + _torStatusListener = bus.on().listen( + (event) async { + switch (event.newStatus) { + case TorConnectionStatus.connecting: + if (!_torConnectingLock.isLocked) { + await _torConnectingLock.acquire(); + } + _requireMutex = true; + break; + + case TorConnectionStatus.connected: + case TorConnectionStatus.disconnected: + if (_torConnectingLock.isLocked) { + _torConnectingLock.release(); + } + _requireMutex = false; + break; + } + }, + ); + + // Listen for tor preference changes. + _torPreferenceListener = bus.on().listen( + (event) async { + _stellarSdk?.httpClient.close(); + _stellarSdk = null; + }, + ); + } + + Future get stellarSdk async { + if (_requireMutex) { + await _torConnectingLock.protect(() async { + _stellarSdk ??= _getFreshSdk(); + }); + } else { + _stellarSdk ??= _getFreshSdk(); } return _stellarSdk!; } @@ -44,39 +84,60 @@ class StellarWallet extends Bip39Wallet { } // ============== Private ==================================================== + // add finalizer to cancel stream subscription when all references to an + // instance of this becomes inaccessible + final _ = Finalizer( + (p0) { + p0._torPreferenceListener?.cancel(); + p0._torStatusListener?.cancel(); + }, + ); + + StreamSubscription? _torStatusListener; + StreamSubscription? _torPreferenceListener; + + final Mutex _torConnectingLock = Mutex(); + bool _requireMutex = false; stellar.StellarSDK? _stellarSdk; - HttpClient? _httpClient; Future _getBaseFee() async { - final fees = await stellarSdk.feeStats.execute(); + final fees = await (await stellarSdk).feeStats.execute(); return int.parse(fees.lastLedgerBaseFee); } - void _updateSdk() { + stellar.StellarSDK _getFreshSdk() { final currentNode = getCurrentNode(); + HttpClient? _httpClient; - // TODO [prio=med]: refactor out and call before requests in case Tor is enabled/disabled, listen to prefs change, or similar. if (prefs.useTor) { final ({InternetAddress host, int port}) proxyInfo = TorService.sharedInstance.getProxyInfo(); _httpClient = HttpClient(); SocksTCPClient.assignToHttpClient( - _httpClient!, [ProxySettings(proxyInfo.host, proxyInfo.port)]); - } else { - _httpClient = null; + _httpClient, + [ + ProxySettings( + proxyInfo.host, + proxyInfo.port, + ), + ], + ); } - _stellarSdk = stellar.StellarSDK("${currentNode.host}:${currentNode.port}", - httpClient: _httpClient); + return stellar.StellarSDK( + "${currentNode.host}:${currentNode.port}", + httpClient: _httpClient, + ); } Future _accountExists(String accountId) async { bool exists = false; try { - final receiverAccount = await stellarSdk.accounts.account(accountId); + final receiverAccount = + await (await stellarSdk).accounts.account(accountId); if (receiverAccount.accountId != "") { exists = true; } @@ -183,7 +244,8 @@ class StellarWallet extends Bip39Wallet { @override Future confirmSend({required TxData txData}) async { final senderKeyPair = await _getSenderKeyPair(index: 0); - final sender = await stellarSdk.accounts.account(senderKeyPair.accountId); + final sender = + await (await stellarSdk).accounts.account(senderKeyPair.accountId); final address = txData.recipients!.first.address; final amountToSend = txData.recipients!.first.amount; @@ -221,7 +283,7 @@ class StellarWallet extends Bip39Wallet { transaction.sign(senderKeyPair, stellarNetwork); try { - final response = await stellarSdk.submitTransaction(transaction); + final response = await (await stellarSdk).submitTransaction(transaction); if (!response.success) { throw Exception("${response.extras?.resultCodes?.transactionResultCode}" " ::: ${response.extras?.resultCodes?.operationsResultCodes}"); @@ -248,7 +310,7 @@ class StellarWallet extends Bip39Wallet { @override Future get fees async { - int fee = await _getBaseFee(); + final int fee = await _getBaseFee(); return FeeObject( numberOfBlocksFast: 1, numberOfBlocksAverage: 1, @@ -286,7 +348,8 @@ class StellarWallet extends Bip39Wallet { stellar.AccountResponse accountResponse; try { - accountResponse = await stellarSdk.accounts + accountResponse = await (await stellarSdk) + .accounts .account((await getCurrentReceivingAddress())!.value) .onError((error, stackTrace) => throw error!); } catch (e) { @@ -307,7 +370,7 @@ class StellarWallet extends Bip39Wallet { } } - for (stellar.Balance balance in accountResponse.balances) { + for (final stellar.Balance balance in accountResponse.balances) { switch (balance.assetType) { case stellar.Asset.TYPE_NATIVE: final swBalance = Balance( @@ -344,7 +407,8 @@ class StellarWallet extends Bip39Wallet { @override Future updateChainHeight() async { try { - final height = await stellarSdk.ledgers + final height = await (await stellarSdk) + .ledgers .order(stellar.RequestBuilderOrder.DESC) .limit(1) .execute() @@ -362,7 +426,8 @@ class StellarWallet extends Bip39Wallet { @override Future updateNode() async { - _updateSdk(); + _stellarSdk?.httpClient.close(); + _stellarSdk = _getFreshSdk(); } @override @@ -370,10 +435,11 @@ class StellarWallet extends Bip39Wallet { try { final myAddress = (await getCurrentReceivingAddress())!; - List transactionList = []; + final List transactionList = []; stellar.Page payments; try { - payments = await stellarSdk.payments + payments = await (await stellarSdk) + .payments .forAccount(myAddress.value) .order(stellar.RequestBuilderOrder.DESC) .execute(); @@ -393,7 +459,7 @@ class StellarWallet extends Bip39Wallet { rethrow; } } - for (stellar.OperationResponse response in payments.records!) { + for (final stellar.OperationResponse response in payments.records!) { // PaymentOperationResponse por; if (response is stellar.PaymentOperationResponse) { final por = response; @@ -423,7 +489,8 @@ class StellarWallet extends Bip39Wallet { final List outputs = []; final List inputs = []; - OutputV2 output = OutputV2.isarCantDoRequiredInDefaultConstructor( + final OutputV2 output = + OutputV2.isarCantDoRequiredInDefaultConstructor( scriptPubKeyHex: "00", valueStringSats: amount.raw.toString(), addresses: [ @@ -431,7 +498,7 @@ class StellarWallet extends Bip39Wallet { ], walletOwns: addressTo == myAddress.value, ); - InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( + final InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( scriptSigHex: null, scriptSigAsm: null, sequence: null, @@ -451,8 +518,9 @@ class StellarWallet extends Bip39Wallet { int height = 0; //Query the transaction linked to the payment, // por.transaction returns a null sometimes - stellar.TransactionResponse tx = - await stellarSdk.transactions.transaction(por.transactionHash!); + final stellar.TransactionResponse tx = await (await stellarSdk) + .transactions + .transaction(por.transactionHash!); if (tx.hash.isNotEmpty) { fee = tx.feeCharged!; @@ -503,7 +571,8 @@ class StellarWallet extends Bip39Wallet { final List outputs = []; final List inputs = []; - OutputV2 output = OutputV2.isarCantDoRequiredInDefaultConstructor( + final OutputV2 output = + OutputV2.isarCantDoRequiredInDefaultConstructor( scriptPubKeyHex: "00", valueStringSats: amount.raw.toString(), addresses: [ @@ -512,7 +581,7 @@ class StellarWallet extends Bip39Wallet { ], walletOwns: caor.sourceAccount! == myAddress.value, ); - InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( + final InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( scriptSigHex: null, scriptSigAsm: null, sequence: null, @@ -533,8 +602,9 @@ class StellarWallet extends Bip39Wallet { int fee = 0; int height = 0; - final tx = - await stellarSdk.transactions.transaction(caor.transactionHash!); + final tx = await (await stellarSdk) + .transactions + .transaction(caor.transactionHash!); if (tx.hash.isNotEmpty) { fee = tx.feeCharged!; height = tx.ledger; diff --git a/lib/widgets/custom_buttons/checkbox_text_button.dart b/lib/widgets/custom_buttons/checkbox_text_button.dart new file mode 100644 index 000000000..8ad254ae9 --- /dev/null +++ b/lib/widgets/custom_buttons/checkbox_text_button.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; + +class CheckboxTextButton extends StatefulWidget { + const CheckboxTextButton({super.key, required this.label, this.onChanged}); + + final String label; + final void Function(bool)? onChanged; + + @override + State createState() => _CheckboxTextButtonState(); +} + +class _CheckboxTextButtonState extends State { + bool _value = false; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + setState(() { + _value = !_value; + }); + widget.onChanged?.call(_value); + }, + child: Container( + color: Colors.transparent, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 26, + child: IgnorePointer( + child: Checkbox( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: _value, + onChanged: (_) {}, + ), + ), + ), + const SizedBox( + width: 12, + ), + Expanded( + child: Text( + widget.label, + style: STextStyles.w500_14(context), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/custom_buttons/frost_qr_dialog_button.dart b/lib/widgets/custom_buttons/frost_qr_dialog_button.dart new file mode 100644 index 000000000..0b63f097e --- /dev/null +++ b/lib/widgets/custom_buttons/frost_qr_dialog_button.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/dialogs/frost/frost_step_qr_dialog.dart'; + +class FrostQrDialogPopupButton extends ConsumerWidget { + const FrostQrDialogPopupButton({super.key, required this.data}); + + final String data; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SecondaryButton( + label: "View QR code", + icon: SvgPicture.asset( + Assets.svg.qrcode, + colorFilter: ColorFilter.mode( + Theme.of(context).extension()!.buttonTextSecondary, + BlendMode.srcIn, + ), + ), + onPressed: () async { + await showDialog( + context: context, + builder: (_) => FrostStepQrDialog( + myName: ref.read(pFrostMyName)!, + title: "Step " + "${ref.read(pFrostCreateCurrentStep)}" + " of " + "${ref.read(pFrostScaffoldArgs)!.stepRoutes.length}" + " - ${ref.read(pFrostScaffoldArgs)!.stepRoutes[ref.watch(pFrostCreateCurrentStep) - 1].title}", + data: data, + ), + ); + }, + ); + } +} diff --git a/lib/widgets/detail_item.dart b/lib/widgets/detail_item.dart index 75e0e6a1e..ee3694d0a 100644 --- a/lib/widgets/detail_item.dart +++ b/lib/widgets/detail_item.dart @@ -7,22 +7,35 @@ import 'package:stackwallet/widgets/rounded_white_container.dart'; class DetailItem extends StatelessWidget { const DetailItem({ - Key? key, + super.key, required this.title, required this.detail, this.button, + this.overrideDetailTextColor, this.showEmptyDetail = true, + this.horizontal = false, this.disableSelectableText = false, - }) : super(key: key); + }); final String title; final String detail; final Widget? button; final bool showEmptyDetail; + final bool horizontal; final bool disableSelectableText; + final Color? overrideDetailTextColor; @override Widget build(BuildContext context) { + final TextStyle detailStyle; + if (overrideDetailTextColor != null) { + detailStyle = STextStyles.w500_14(context).copyWith( + color: overrideDetailTextColor, + ); + } else { + detailStyle = STextStyles.w500_14(context); + } + return ConditionalParent( condition: !Util.isDesktop, builder: (child) => RoundedWhiteContainer( @@ -34,56 +47,80 @@ class DetailItem extends StatelessWidget { padding: const EdgeInsets.all(16), child: child, ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - disableSelectableText - ? Text( - title, - style: STextStyles.itemSubtitle(context), - ) - : SelectableText( - title, - style: STextStyles.itemSubtitle(context), - ), - button ?? Container(), - ], - ), - const SizedBox( - height: 5, - ), - detail.isEmpty && showEmptyDetail - ? disableSelectableText - ? Text( - "$title will appear here", - style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle3, + child: horizontal + ? Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + disableSelectableText + ? Text( + title, + style: STextStyles.itemSubtitle(context), + ) + : SelectableText( + title, + style: STextStyles.itemSubtitle(context), ), - ) - : SelectableText( - "$title will appear here", - style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle3, + disableSelectableText + ? Text( + detail, + style: detailStyle, + ) + : SelectableText( + detail, + style: detailStyle, ), - ) - : disableSelectableText - ? Text( - detail, - style: STextStyles.w500_14(context), - ) - : SelectableText( - detail, - style: STextStyles.w500_14(context), - ), - ], - ), + ], + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + disableSelectableText + ? Text( + title, + style: STextStyles.itemSubtitle(context), + ) + : SelectableText( + title, + style: STextStyles.itemSubtitle(context), + ), + button ?? Container(), + ], + ), + const SizedBox( + height: 5, + ), + detail.isEmpty && showEmptyDetail + ? disableSelectableText + ? Text( + "$title will appear here", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle3, + ), + ) + : SelectableText( + "$title will appear here", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle3, + ), + ) + : disableSelectableText + ? Text( + detail, + style: detailStyle, + ) + : SelectableText( + detail, + style: detailStyle, + ), + ], + ), ), ); } diff --git a/lib/pages/add_wallet_views/frost_ms/frost_step_explanation_dialog.dart b/lib/widgets/dialogs/frost/frost_step_explanation_dialog.dart similarity index 100% rename from lib/pages/add_wallet_views/frost_ms/frost_step_explanation_dialog.dart rename to lib/widgets/dialogs/frost/frost_step_explanation_dialog.dart diff --git a/lib/widgets/dialogs/frost/frost_step_qr_dialog.dart b/lib/widgets/dialogs/frost/frost_step_qr_dialog.dart new file mode 100644 index 000000000..2dd1ace26 --- /dev/null +++ b/lib/widgets/dialogs/frost/frost_step_qr_dialog.dart @@ -0,0 +1,209 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:ui'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +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/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/assets.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/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/dialogs/simple_mobile_dialog.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class FrostStepQrDialog extends StatefulWidget { + const FrostStepQrDialog({ + super.key, + required this.myName, + required this.title, + required this.data, + }); + + final String myName; + final String title; + final String data; + + @override + State createState() => _FrostStepQrDialogState(); +} + +class _FrostStepQrDialogState extends State { + final _qrKey = GlobalKey(); + + Future _capturePng(bool shouldSaveInsteadOfShare) async { + try { + final boundary = + _qrKey.currentContext?.findRenderObject() as RenderRepaintBoundary; + final image = await boundary.toImage(); + final byteData = await image.toByteData(format: ImageByteFormat.png); + final pngBytes = byteData!.buffer.asUint8List(); + + 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, + ); + + if (path != null && context.mounted) { + 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) { + //todo: comeback to this + debugPrint(e.toString()); + } + } + + @override + Widget build(BuildContext context) { + return SimpleMobileDialog( + showCloseButton: false, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + RepaintBoundary( + key: _qrKey, + child: RoundedWhiteContainer( + boxShadow: [ + Theme.of(context).extension()!.standardBoxShadow + ], + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + widget.myName, + style: STextStyles.w600_16(context).copyWith( + color: Theme.of(context) + .extension()! + .customTextButtonEnabledText, + ), + ), + const SizedBox(height: 8), + Text( + widget.title, + style: STextStyles.w600_12(context), + ), + const SizedBox(height: 8), + RoundedContainer( + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + radiusMultiplier: 1, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ConditionalParent( + condition: Util.isDesktop, + builder: (child) => ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 360, + ), + child: child, + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: AspectRatio( + aspectRatio: 1, + child: QrImageView( + data: widget.data, + padding: EdgeInsets.zero, + dataModuleStyle: QrDataModuleStyle( + dataModuleShape: QrDataModuleShape.square, + color: Theme.of(context) + .extension()! + .accentColorDark, + ), + ), + ), + ), + ), + const SizedBox(height: 12), + SelectableText( + widget.data, + style: STextStyles.w500_10(context), + ), + ], + ), + ), + ], + ), + ), + ), + if (!Util.isDesktop) + const SizedBox( + height: 16, + ), + if (!Util.isDesktop) + Row( + children: [ + const Spacer(), + const SizedBox(width: 16), + Expanded( + child: SecondaryButton( + label: "Share", + icon: SvgPicture.asset( + Assets.svg.share, + width: 14, + height: 14, + color: Theme.of(context) + .extension()! + .buttonTextSecondary, + ), + onPressed: () async { + await _capturePng(false); + }, + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/widgets/dialogs/frost_interruption_dialog.dart b/lib/widgets/dialogs/frost_interruption_dialog.dart deleted file mode 100644 index 4d36456bb..000000000 --- a/lib/widgets/dialogs/frost_interruption_dialog.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stackwallet/utilities/util.dart'; -import 'package:stackwallet/widgets/desktop/primary_button.dart'; -import 'package:stackwallet/widgets/desktop/secondary_button.dart'; -import 'package:stackwallet/widgets/stack_dialog.dart'; - -enum FrostInterruptionDialogType { - walletCreation, - resharing, - transactionCreation; -} - -class FrostInterruptionDialog extends StatelessWidget { - const FrostInterruptionDialog({ - super.key, - required this.type, - required this.popUntilOnYesRouteName, - this.onNoPressedOverride, - this.onYesPressedOverride, - }); - - final FrostInterruptionDialogType type; - final String popUntilOnYesRouteName; - final VoidCallback? onNoPressedOverride; - final VoidCallback? onYesPressedOverride; - - String get message { - switch (type) { - case FrostInterruptionDialogType.walletCreation: - return "wallet creation"; - case FrostInterruptionDialogType.resharing: - return "resharing"; - case FrostInterruptionDialogType.transactionCreation: - return "transaction signing"; - } - } - - @override - Widget build(BuildContext context) { - return StackDialog( - title: "Cancel $message process", - message: "Are you sure you want to cancel the $message process?", - leftButton: SecondaryButton( - label: "No", - onPressed: onNoPressedOverride ?? - Navigator.of( - context, - rootNavigator: Util.isDesktop, - ).pop, - ), - rightButton: PrimaryButton( - label: "Yes", - onPressed: onYesPressedOverride ?? - () { - // pop dialog - Navigator.of( - context, - rootNavigator: Util.isDesktop, - ).pop(); - - Navigator.of(context).popUntil( - ModalRoute.withName( - popUntilOnYesRouteName, - ), - ); - }, - ), - ); - } -} diff --git a/lib/widgets/dialogs/simple_mobile_dialog.dart b/lib/widgets/dialogs/simple_mobile_dialog.dart new file mode 100644 index 000000000..1e07e22ae --- /dev/null +++ b/lib/widgets/dialogs/simple_mobile_dialog.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; + +class SimpleMobileDialog extends StatelessWidget { + const SimpleMobileDialog({ + super.key, + required this.child, + this.showCloseButton = true, + this.padding, + }); + + final Widget child; + final bool showCloseButton; + final EdgeInsets? padding; + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Padding( + padding: const EdgeInsets.all(16), + child: Material( + borderRadius: BorderRadius.circular( + 20, + ), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).extension()!.popupBG, + borderRadius: BorderRadius.circular( + 20, + ), + ), + child: Padding( + padding: padding ?? const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: SingleChildScrollView( + child: child, + ), + ), + if (showCloseButton) + const SizedBox( + height: 16, + ), + if (showCloseButton) + Row( + children: [ + const Spacer(), + const SizedBox( + width: 16, + ), + Expanded( + child: SecondaryButton( + label: "Close", + onPressed: Navigator.of(context).pop, + ), + ), + ], + ), + ], + ), + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/fee_slider.dart b/lib/widgets/fee_slider.dart index d125988f3..64e3af12b 100644 --- a/lib/widgets/fee_slider.dart +++ b/lib/widgets/fee_slider.dart @@ -9,9 +9,11 @@ class FeeSlider extends StatefulWidget { super.key, required this.onSatVByteChanged, required this.coin, + this.showWU = false, }); final Coin coin; + final bool showWU; final void Function(int) onSatVByteChanged; @override @@ -34,7 +36,7 @@ class _FeeSliderState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - "sat/vByte", + widget.showWU ? "sat/WU" : "sat/vByte", style: STextStyles.smallMed12(context), ), Text( diff --git a/lib/pages/frost_mascot.dart b/lib/widgets/frost_mascot.dart similarity index 73% rename from lib/pages/frost_mascot.dart rename to lib/widgets/frost_mascot.dart index 3f6c0562d..17743efcc 100644 --- a/lib/pages/frost_mascot.dart +++ b/lib/widgets/frost_mascot.dart @@ -9,15 +9,17 @@ */ import 'package:flutter/material.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/frost_step_explanation_dialog.dart'; import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/widgets/dialogs/frost/frost_step_explanation_dialog.dart'; class FrostMascot extends StatelessWidget { final String title; final String body; - FrostMascot({ + const FrostMascot({ super.key, - this.onPressed, required this.title, required this.body, + this.onPressed, + required this.title, + required this.body, }); final VoidCallback? onPressed; @@ -32,8 +34,10 @@ class FrostMascot extends StatelessWidget { onTap: () async { await showDialog( context: context, - builder: (context) => - FrostStepExplanationDialog(title: title, body: body), + builder: (context) => FrostStepExplanationDialog( + title: title, + body: body, + ), ); }, child: Image( diff --git a/lib/widgets/frost_scaffold.dart b/lib/widgets/frost_scaffold.dart new file mode 100644 index 000000000..e5d83e088 --- /dev/null +++ b/lib/widgets/frost_scaffold.dart @@ -0,0 +1,223 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/text_styles.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/blue_text_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'; + +class FrostStepScaffold extends ConsumerStatefulWidget { + const FrostStepScaffold({super.key}); + + static const String routeName = "/frostStepScaffold"; + + @override + ConsumerState createState() => _FrostScaffoldState(); +} + +class _FrostScaffoldState extends ConsumerState { + static const _titleTextSize = 18.0; + final _navigatorKey = GlobalKey(); + + late final List _routes; + + bool _requestPopLock = false; + + String get _message { + switch (ref.read(pFrostScaffoldArgs)!.frostInterruptionDialogType) { + case FrostInterruptionDialogType.walletCreation: + return "wallet creation"; + case FrostInterruptionDialogType.resharing: + return "resharing"; + case FrostInterruptionDialogType.transactionCreation: + return "transaction signing"; + } + } + + Future _requestPop(BuildContext context) async { + if (_requestPopLock || + (Util.isDesktop && ref.read(pFrostScaffoldCanPopDesktop))) { + return; + } + _requestPopLock = true; + + final resultFuture = showDialog( + context: context, + barrierDismissible: false, + builder: (context) => StackDialog( + title: "Cancel $_message process", + message: "Are you sure you want to cancel the $_message process?", + leftButton: SecondaryButton( + label: "No", + onPressed: () { + // pop dialog + Navigator.of( + context, + rootNavigator: Util.isDesktop, + ).pop("no"); + }, + ), + rightButton: PrimaryButton( + label: "Yes", + onPressed: () { + // pop dialog + Navigator.of( + context, + rootNavigator: Util.isDesktop, + ).pop("yes"); + }, + ), + ), + ); + + // make sure to at least delay some time otherwise flutter pops back more than a single route lol... + final minTimeFuture = + Future.delayed(const Duration(milliseconds: 200)); + + final result = await Future.wait([resultFuture, minTimeFuture]); + + if (context.mounted && result[0] == "yes") { + Navigator.of(context).pop(); + ref.read(pFrostScaffoldArgs.state).state = null; + } + + _requestPopLock = false; + } + + @override + void initState() { + _routes = ref.read(pFrostScaffoldArgs)!.stepRoutes; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: Util.isDesktop && ref.watch(pFrostScaffoldCanPopDesktop), + onPopInvoked: (_) => _requestPop(context), + child: Material( + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) => child, + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + body: SafeArea( + child: child, + ), + ), + ), + child: Column( + children: [ + // header + SizedBox( + height: 56, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: Row( + children: [ + Text( + "${ref.watch(pFrostCreateCurrentStep)} / ${_routes.length}", + style: STextStyles.navBarTitle(context).copyWith( + fontSize: _titleTextSize, + color: Theme.of(context) + .extension()! + .customTextButtonEnabledText, + ), + ), + const SizedBox( + width: 10, + ), + Expanded( + child: Text( + _routes[ref.watch(pFrostCreateCurrentStep) - 1] + .title, + style: STextStyles.navBarTitle(context).copyWith( + fontSize: _titleTextSize, + ), + ), + ), + const SizedBox( + width: 10, + ), + CustomTextButton( + text: "Exit", + textSize: _titleTextSize, + onTap: () => _requestPop(context), + ), + ], + ), + ), + ), + LayoutBuilder( + builder: (subContext, constraints) => ProgressBar( + width: constraints.maxWidth, + height: 3, + fillColor: Theme.of(context) + .extension()! + .customTextButtonEnabledText, + backgroundColor: Theme.of(context) + .extension()! + .customTextButtonEnabledText + .withOpacity(0.1), + percent: + ref.watch(pFrostCreateCurrentStep) / _routes.length, + ), + ), + Expanded( + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) => Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Flexible( + child: SizedBox( + width: 500, + child: child, + ), + ) + ], + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: child, + ), + ), + ); + }, + ), + child: Navigator( + key: _navigatorKey, + initialRoute: _routes[0].routeName, + onGenerateRoute: FrostRouteGenerator.generateRoute, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/frost_step_user_steps.dart b/lib/widgets/frost_step_user_steps.dart new file mode 100644 index 000000000..729264344 --- /dev/null +++ b/lib/widgets/frost_step_user_steps.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class FrostStepUserSteps extends StatelessWidget { + const FrostStepUserSteps({super.key, required this.userSteps}); + + final List userSteps; + + @override + Widget build(BuildContext context) { + return RoundedWhiteContainer( + child: Column( + children: [ + for (int i = 0; i < userSteps.length; i++) + ConditionalParent( + condition: i > 0, + builder: (child) => Padding( + padding: const EdgeInsets.only(top: 4), + child: child, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${i + 1}.", + style: STextStyles.w500_12(context), + ), + const SizedBox( + width: 4, + ), + Expanded( + child: Text( + userSteps[i], + style: STextStyles.w500_12(context), + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/textfields/frost_step_field.dart b/lib/widgets/textfields/frost_step_field.dart new file mode 100644 index 000000000..7fd19ba3b --- /dev/null +++ b/lib/widgets/textfields/frost_step_field.dart @@ -0,0 +1,182 @@ +import 'package:barcode_scan2/barcode_scan2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/logger.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/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/textfield_icon_button.dart'; + +class FrostStepField extends StatefulWidget { + const FrostStepField({ + super.key, + required this.controller, + required this.focusNode, + this.label, + this.hint, + required this.onChanged, + required this.showQrScanOption, + }); + + final TextEditingController controller; + final FocusNode focusNode; + final String? label; + final String? hint; + final void Function(String) onChanged; + final bool showQrScanOption; + + @override + State createState() => _FrostStepFieldState(); +} + +class _FrostStepFieldState extends State { + final _xKey = UniqueKey(); + final _pasteKey = UniqueKey(); + late final Key? _qrKey; + + bool _isEmpty = true; + + final _inputBorder = OutlineInputBorder( + borderSide: const BorderSide( + width: 0, + color: Colors.transparent, + ), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ); + + late final void Function(String) _changed; + + @override + void initState() { + _qrKey = widget.showQrScanOption ? UniqueKey() : null; + _isEmpty = widget.controller.text.isEmpty; + + _changed = (value) { + if (context.mounted) { + widget.onChanged.call(value); + setState(() { + _isEmpty = widget.controller.text.isEmpty; + }); + } + }; + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: widget.label != null, + builder: (child) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + widget.label!, + style: STextStyles.w500_14(context), + ), + const SizedBox( + height: 4, + ), + child, + ], + ), + child: TextField( + controller: widget.controller, + focusNode: widget.focusNode, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + onChanged: _changed, + decoration: InputDecoration( + hintText: widget.hint, + fillColor: widget.focusNode.hasFocus + ? Theme.of(context).extension()!.textFieldActiveBG + : Theme.of(context).extension()!.textFieldDefaultBG, + hintStyle: Util.isDesktop + ? STextStyles.desktopTextFieldLabel(context) + : STextStyles.fieldLabel(context), + enabledBorder: _inputBorder, + focusedBorder: _inputBorder, + errorBorder: _inputBorder, + disabledBorder: _inputBorder, + focusedErrorBorder: _inputBorder, + suffixIcon: Padding( + padding: _isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + !_isEmpty + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Frost Step Field Input.", + key: _xKey, + onTap: () { + widget.controller.text = ""; + + _changed(widget.controller.text); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Frost Step Field Input.", + key: _pasteKey, + onTap: () async { + final ClipboardData? data = + await Clipboard.getData(Clipboard.kTextPlain); + if (data?.text != null && data!.text!.isNotEmpty) { + widget.controller.text = data.text!.trim(); + } + + _changed(widget.controller.text); + }, + child: + _isEmpty ? const ClipboardIcon() : const XIcon(), + ), + if (_isEmpty && widget.showQrScanOption) + TextFieldIconButton( + semanticsLabel: + "Scan QR Button. Opens Camera For Scanning QR Code.", + key: _qrKey, + onTap: () async { + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 75)); + } + + final qrResult = await BarcodeScanner.scan(); + + widget.controller.text = qrResult.rawContent; + + _changed(widget.controller.text); + } on PlatformException catch (e, s) { + Logging.instance.log( + "Failed to get camera permissions while trying to scan qr code: $e\n$s", + level: LogLevel.Warning, + ); + } + }, + child: const QrCodeIcon(), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/wallet_card.dart b/lib/widgets/wallet_card.dart index bc1c80aa4..9345cf271 100644 --- a/lib/widgets/wallet_card.dart +++ b/lib/widgets/wallet_card.dart @@ -105,7 +105,7 @@ class SimpleWalletCard extends ConsumerWidget { whileFuture: loadFuture, context: context, message: 'Opening ${wallet.info.name}', - isDesktop: Util.isDesktop, + rootNavigator: Util.isDesktop, ); if (popPrevious) nav.pop(); @@ -135,7 +135,7 @@ class SimpleWalletCard extends ConsumerWidget { context: desktopNavigatorState?.context ?? context, opaqueBG: true, message: "Loading ${contract.name}", - isDesktop: Util.isDesktop, + rootNavigator: Util.isDesktop, ); if (!success!) { diff --git a/lib/widgets/wallet_navigation_bar/components/icons/frost_sign_nav_icon.dart b/lib/widgets/wallet_navigation_bar/components/icons/frost_sign_nav_icon.dart new file mode 100644 index 000000000..a7a81c20f --- /dev/null +++ b/lib/widgets/wallet_navigation_bar/components/icons/frost_sign_nav_icon.dart @@ -0,0 +1,36 @@ +/* +* This file is part of Stack Wallet. +* +* Copyright (c) 2023 Cypher Stack +* All Rights Reserved. +* The code is distributed under GPLv3 license, see LICENSE file for details. +* Generated by Cypher Stack on 2023-05-26 +* +*/ + +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/themes/theme_providers.dart'; + +class FrostSignNavIcon extends ConsumerWidget { + const FrostSignNavIcon({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SvgPicture.file( + File( + ref.watch( + themeProvider.select( + // TODO: [prio=high] update themes with icon asset + (value) => value.assets.stackIcon, + ), + ), + ), + width: 24, + height: 24, + ); + } +}