various frost settings view ui tweaks and fixes

This commit is contained in:
julian 2024-04-29 15:35:15 -06:00
parent 3bddec00f1
commit a9449ace0d
4 changed files with 456 additions and 42 deletions

View file

@ -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<StackColors>()!
.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],
),
],
),
),
),
],

View file

@ -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<TextEditingController> controllers = [];
late final String myName;
int _participantsCount = 0;
bool _buttonLock = false;
bool _includeMeInReshare = false;
Future<void> _onPressed() async {
if (_buttonLock) {
@ -74,10 +80,16 @@ class _CompleteReshareConfigViewState
);
}
final List<String> 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<StackColors>()!.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<StackColors>()!.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<StackColors>()!.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<StackColors>()!.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<StackColors>()!.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<StackColors>()!
.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),
),
),
),
],

View file

@ -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<DisplayReshareConfigView> {
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<void> _onPressed() async {
if (_buttonLock) {
@ -88,6 +103,111 @@ class _DisplayReshareConfigViewState
}
}
void _showParticipantsDialog() {
final participants =
ref.read(pFrostResharingData).configData!.newParticipants;
showDialog<void>(
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<StackColors>()!.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<StackColors>()!.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<StackColors>()!
.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,
),
],

View file

@ -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<ImportReshareConfigView> {
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<void> _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<StackColors>()!.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();