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/resharing/involved/step_1a/complete_reshare_config_view.dart index 74cfaee17..caa77af5f 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/resharing/involved/step_1a/complete_reshare_config_view.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -19,6 +21,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/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; final class CompleteReshareConfigView extends ConsumerStatefulWidget { @@ -45,9 +48,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 +80,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(), + newParticipants: newParticipants, ); final salt = Format.uint8listToString( @@ -105,7 +117,7 @@ class _CompleteReshareConfigViewState }); } - ref.read(pFrostResharingData).myName = frostInfo.myName; + ref.read(pFrostResharingData).myName = myName; ref.read(pFrostResharingData).resharerConfig = config; if (mounted) { @@ -152,18 +164,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 +194,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 +219,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 +269,7 @@ class _CompleteReshareConfigViewState }, ), title: Text( - "Modify Participants", + "Edit group details", style: STextStyles.navBarTitle(context), ), ), @@ -258,11 +296,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 +360,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 +395,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 +419,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 +449,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/display_reshare_config_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/display_reshare_config_view.dart index 2b7f1f899..fa538f29e 100644 --- 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 @@ -1,5 +1,6 @@ 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/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'; @@ -8,6 +9,7 @@ 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'; @@ -20,7 +22,10 @@ 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/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 DisplayReshareConfigView extends ConsumerStatefulWidget { @@ -40,9 +45,19 @@ class DisplayReshareConfigView extends ConsumerStatefulWidget { class _DisplayReshareConfigViewState 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) { @@ -88,6 +103,111 @@ class _DisplayReshareConfigViewState } } + 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) @@ -162,7 +282,13 @@ class _DisplayReshareConfigViewState ), child: Column( children: [ - if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 16, + ), + const FrostStepUserSteps( + userSteps: info, + ), + const SizedBox(height: 20), SizedBox( height: 220, child: Row( @@ -197,13 +323,66 @@ class _DisplayReshareConfigViewState SizedBox( height: Util.isDesktop ? 64 : 16, ), - if (!Util.isDesktop) - const Spacer( - flex: 2, + 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) + GestureDetector( + onTap: () { + setState(() { + _userVerifyContinue = !_userVerifyContinue; + }); + }, + child: Container( + color: Colors.transparent, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 26, + child: Checkbox( + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + value: _userVerifyContinue, + onChanged: (value) => setState( + () => _userVerifyContinue = value == true, + ), + ), + ), + const SizedBox( + width: 12, + ), + Expanded( + child: Text( + "I have verified that everyone has imported the config", + style: STextStyles.w500_14(context), + ), + ), + ], + ), + ), + ), + if (iAmInvolved) + const SizedBox( + height: 16, ), if (iAmInvolved) PrimaryButton( label: "Start resharing", + enabled: _userVerifyContinue, 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 index 966a24710..a19d0ec75 100644 --- 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 @@ -21,6 +21,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_step_user_steps.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'; @@ -45,12 +46,22 @@ class ImportReshareConfigView extends ConsumerStatefulWidget { class _ImportReshareConfigViewState 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) { @@ -84,14 +95,14 @@ class _ImportReshareConfigViewState if (frostInfo.knownSalts.contains(salt)) { throw Exception("Duplicate config salt"); } else { - final salts = frostInfo.knownSalts; + final salts = frostInfo.knownSalts.toList(); salts.add(salt); final mainDB = ref.read(mainDBProvider); await mainDB.isar.writeTxn(() async { - final info = frostInfo; - await mainDB.isar.frostWalletInfo.delete(info.id); + final id = frostInfo.id; + await mainDB.isar.frostWalletInfo.delete(id); await mainDB.isar.frostWalletInfo.put( - info.copyWith(knownSalts: salts), + frostInfo.copyWith(knownSalts: salts), ); }); } @@ -201,6 +212,20 @@ class _ImportReshareConfigViewState const SizedBox( height: 16, ), + const FrostStepUserSteps( + userSteps: info, + ), + const SizedBox(height: 20), + Text( + "Enter config", + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of(context).extension()!.textSubtitle1, + ), + ), + const SizedBox( + height: 10, + ), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -319,9 +344,47 @@ class _ImportReshareConfigViewState const SizedBox( height: 16, ), + GestureDetector( + onTap: () { + setState(() { + _userVerifyContinue = !_userVerifyContinue; + }); + }, + child: Container( + color: Colors.transparent, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 26, + child: Checkbox( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: _userVerifyContinue, + onChanged: (value) => setState( + () => _userVerifyContinue = value == true, + ), + ), + ), + const SizedBox( + width: 12, + ), + Expanded( + child: Text( + "I have verified that everyone has imported the config", + style: STextStyles.w500_14(context), + ), + ), + ], + ), + ), + ), + const SizedBox( + height: 16, + ), PrimaryButton( label: "Start resharing", - enabled: !_configEmpty, + enabled: !_configEmpty && _userVerifyContinue, onPressed: () async { if (FocusScope.of(context).hasFocus) { FocusScope.of(context).unfocus();