frost transaction sending ui update

This commit is contained in:
julian 2024-05-02 16:43:02 -06:00
parent 1be51a666b
commit 832be89cab
6 changed files with 377 additions and 362 deletions

View file

@ -82,6 +82,9 @@ class _FrostSendStep1aState extends ConsumerState<FrostSendStep1a> {
) as BitcoinFrostWallet; ) as BitcoinFrostWallet;
_threshold = wallet.frostInfo.threshold; _threshold = wallet.frostInfo.threshold;
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(pFrostMyName.state).state = wallet.frostInfo.myName;
});
super.initState(); super.initState();
} }
@ -116,7 +119,7 @@ class _FrostSendStep1aState extends ConsumerState<FrostSendStep1a> {
TextSpan( TextSpan(
text: text:
"Share this config with the group members. ", "Share this config with the group members. ",
style: STextStyles.w600_12(context), style: STextStyles.w500_12(context),
), ),
TextSpan( TextSpan(
text: text:
@ -159,7 +162,7 @@ class _FrostSendStep1aState extends ConsumerState<FrostSendStep1a> {
), ),
), ),
SizedBox( SizedBox(
height: Util.isDesktop ? 20 : 16, height: Util.isDesktop ? 20 : 12,
), ),
SizedBox( SizedBox(
height: qrImageSize, height: qrImageSize,
@ -179,7 +182,7 @@ class _FrostSendStep1aState extends ConsumerState<FrostSendStep1a> {
), ),
), ),
SizedBox( SizedBox(
height: Util.isDesktop ? 20 : 16, height: Util.isDesktop ? 20 : 12,
), ),
DetailItem( DetailItem(
title: "Encoded transaction config", title: "Encoded transaction config",
@ -193,7 +196,7 @@ class _FrostSendStep1aState extends ConsumerState<FrostSendStep1a> {
), ),
), ),
SizedBox( SizedBox(
height: Util.isDesktop ? 20 : 16, height: Util.isDesktop ? 20 : 12,
), ),
DetailItem( DetailItem(
title: "Threshold", title: "Threshold",
@ -201,7 +204,7 @@ class _FrostSendStep1aState extends ConsumerState<FrostSendStep1a> {
horizontal: true, horizontal: true,
), ),
SizedBox( SizedBox(
height: Util.isDesktop ? 20 : 16, height: Util.isDesktop ? 20 : 12,
), ),
if (!Util.isDesktop) if (!Util.isDesktop)
const Spacer( const Spacer(
@ -217,7 +220,7 @@ class _FrostSendStep1aState extends ConsumerState<FrostSendStep1a> {
}, },
), ),
SizedBox( SizedBox(
height: Util.isDesktop ? 20 : 16, height: Util.isDesktop ? 20 : 12,
), ),
PrimaryButton( PrimaryButton(
label: "Attempt sign", label: "Attempt sign",

View file

@ -12,6 +12,7 @@ import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/models/tx_data.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.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/desktop/primary_button.dart';
import 'package:stackwallet/widgets/frost_step_user_steps.dart'; import 'package:stackwallet/widgets/frost_step_user_steps.dart';
import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/stack_dialog.dart';
@ -41,7 +42,7 @@ class _FrostSendStep1bState extends ConsumerState<FrostSendStep1b> {
late final TextEditingController configFieldController; late final TextEditingController configFieldController;
late final FocusNode configFocusNode; late final FocusNode configFocusNode;
bool _configEmpty = true; bool _configEmpty = true, _userVerifyContinue = false;
bool _attemptSignLock = false; bool _attemptSignLock = false;
@ -125,6 +126,12 @@ class _FrostSendStep1bState extends ConsumerState<FrostSendStep1b> {
void initState() { void initState() {
configFieldController = TextEditingController(); configFieldController = TextEditingController();
configFocusNode = FocusNode(); 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(); super.initState();
} }
@ -146,7 +153,7 @@ class _FrostSendStep1bState extends ConsumerState<FrostSendStep1b> {
const FrostStepUserSteps( const FrostStepUserSteps(
userSteps: info, userSteps: info,
), ),
const SizedBox(height: 12), const SizedBox(height: 20),
FrostStepField( FrostStepField(
controller: configFieldController, controller: configFieldController,
focusNode: configFocusNode, focusNode: configFocusNode,
@ -159,16 +166,25 @@ class _FrostSendStep1bState extends ConsumerState<FrostSendStep1b> {
}); });
}, },
), ),
const SizedBox(
height: 16,
),
if (!Util.isDesktop) const Spacer(), if (!Util.isDesktop) const Spacer(),
const SizedBox( const SizedBox(
height: 16, 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( PrimaryButton(
label: "Start signing", label: "Start signing",
enabled: !_configEmpty, enabled: !_configEmpty && _userVerifyContinue,
onPressed: () { onPressed: () {
_attemptSign(); _attemptSign();
}, },

View file

@ -1,6 +1,4 @@
import 'package:barcode_scan2/barcode_scan2.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/frost_route_generator.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/transaction_views/transaction_details_view.dart';
@ -8,7 +6,6 @@ import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/services/frost.dart'; import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/utilities/util.dart';
@ -17,13 +14,9 @@ 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/custom_buttons/simple_copy_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/detail_item.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/rounded_white_container.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfields/frost_step_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
class FrostSendStep2 extends ConsumerStatefulWidget { class FrostSendStep2 extends ConsumerStatefulWidget {
const FrostSendStep2({super.key}); const FrostSendStep2({super.key});
@ -47,7 +40,7 @@ class _FrostSendStep2State extends ConsumerState<FrostSendStep2> {
final List<bool> fieldIsEmptyFlags = []; final List<bool> fieldIsEmptyFlags = [];
bool hasEnoughPreprocesses() { int countPreprocesses() {
// own preprocess is not included in controllers and must be set here // own preprocess is not included in controllers and must be set here
int count = 1; int count = 1;
@ -57,7 +50,7 @@ class _FrostSendStep2State extends ConsumerState<FrostSendStep2> {
} }
} }
return count >= threshold; return count;
} }
@override @override
@ -124,11 +117,14 @@ class _FrostSendStep2State extends ConsumerState<FrostSendStep2> {
), ),
], ],
), ),
const SizedBox(
height: 4,
),
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
"1.", "2.",
style: STextStyles.w500_12(context), style: STextStyles.w500_12(context),
), ),
const SizedBox( const SizedBox(
@ -141,7 +137,7 @@ class _FrostSendStep2State extends ConsumerState<FrostSendStep2> {
TextSpan( TextSpan(
text: text:
"Enter their preprocesses into the corresponding fields. ", "Enter their preprocesses into the corresponding fields. ",
style: STextStyles.w600_12(context), style: STextStyles.w500_12(context),
), ),
TextSpan( TextSpan(
text: "You must have the threshold number of " text: "You must have the threshold number of "
@ -164,9 +160,24 @@ class _FrostSendStep2State extends ConsumerState<FrostSendStep2> {
const SizedBox( const SizedBox(
height: 12, height: 12,
), ),
DetailItem(
title: "Threshold",
detail: "$threshold signatures",
horizontal: true,
),
const SizedBox(
height: 12,
),
DetailItem( DetailItem(
title: "My name", title: "My name",
detail: myName, detail: myName,
button: Util.isDesktop
? IconCopyButton(
data: myName,
)
: SimpleCopyButton(
data: myName,
),
), ),
const SizedBox( const SizedBox(
height: 12, height: 12,
@ -189,141 +200,46 @@ class _FrostSendStep2State extends ConsumerState<FrostSendStep2> {
const SizedBox( const SizedBox(
height: 12, 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<StackColors>()!;
return DetailItem(
title: "Required preprocesses",
detail: "$count of $threshold",
horizontal: true,
overrideDetailTextColor: count >= threshold
? colors.accentColorGreen
: colors.accentColorRed,
);
}),
const SizedBox(
height: 12,
),
Column( Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
for (int i = 0; i < participantsWithoutMe.length; i++) for (int i = 0; i < participantsWithoutMe.length; i++)
Column( FrostStepField(
mainAxisSize: MainAxisSize.min, label: participantsWithoutMe[i],
crossAxisAlignment: CrossAxisAlignment.start, hint: "Enter ${participantsWithoutMe[i]}'s preprocess",
children: [ controller: controllers[i],
Padding( focusNode: focusNodes[i],
padding: const EdgeInsets.symmetric(vertical: 8), onChanged: (_) {
child: ClipRRect( setState(() {
borderRadius: BorderRadius.circular( fieldIsEmptyFlags[i] = controllers[i].text.isEmpty;
Constants.size.circularBorderRadius, });
), },
child: TextField( showQrScanOption: true,
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<void>.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(),
)
],
),
),
),
),
),
),
),
],
), ),
], ],
), ),
@ -332,8 +248,8 @@ class _FrostSendStep2State extends ConsumerState<FrostSendStep2> {
height: 12, height: 12,
), ),
PrimaryButton( PrimaryButton(
label: "Continue signing", label: "Generate shares",
enabled: hasEnoughPreprocesses(), enabled: countPreprocesses() >= threshold,
onPressed: () async { onPressed: () async {
// collect Preprocess strings (not including my own) // collect Preprocess strings (not including my own)
final preprocesses = controllers.map((e) => e.text).toList(); final preprocesses = controllers.map((e) => e.text).toList();

View file

@ -1,27 +1,23 @@
import 'package:barcode_scan2/barcode_scan2.dart'; import 'package:coinlib_flutter/coinlib_flutter.dart' as cl;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/frost_route_generator.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/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/services/frost.dart'; import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.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/frost_qr_dialog_button.dart';
import 'package:stackwallet/widgets/custom_buttons/simple_copy_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/primary_button.dart';
import 'package:stackwallet/widgets/detail_item.dart'; import 'package:stackwallet/widgets/detail_item.dart';
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart'; import 'package:stackwallet/widgets/frost_step_user_steps.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_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfields/frost_step_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
class FrostSendStep3 extends ConsumerStatefulWidget { class FrostSendStep3 extends ConsumerStatefulWidget {
const FrostSendStep3({super.key}); const FrostSendStep3({super.key});
@ -34,6 +30,11 @@ class FrostSendStep3 extends ConsumerStatefulWidget {
} }
class _FrostSendStep3State extends ConsumerState<FrostSendStep3> { class _FrostSendStep3State extends ConsumerState<FrostSendStep3> {
static const info = [
"Send your share to other signing group members.",
"Enter their shares into the corresponding fields.",
];
final List<TextEditingController> controllers = []; final List<TextEditingController> controllers = [];
final List<FocusNode> focusNodes = []; final List<FocusNode> focusNodes = [];
@ -45,6 +46,8 @@ class _FrostSendStep3State extends ConsumerState<FrostSendStep3> {
final List<bool> fieldIsEmptyFlags = []; final List<bool> fieldIsEmptyFlags = [];
bool _userVerifyContinue = false;
@override @override
void initState() { void initState() {
final wallet = ref.read(pWallets).getWallet( final wallet = ref.read(pWallets).getWallet(
@ -93,15 +96,28 @@ class _FrostSendStep3State extends ConsumerState<FrostSendStep3> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
DetailItem( const FrostStepUserSteps(
title: "My name", userSteps: info,
detail: myName,
), ),
const SizedBox( const SizedBox(
height: 12, height: 12,
), ),
DetailItem( DetailItem(
title: "My shares", title: "My name",
detail: myName,
button: Util.isDesktop
? IconCopyButton(
data: myName,
)
: SimpleCopyButton(
data: myName,
),
),
const SizedBox(
height: 12,
),
DetailItem(
title: "My share",
detail: myShare, detail: myShare,
button: Util.isDesktop button: Util.isDesktop
? IconCopyButton( ? IconCopyButton(
@ -125,133 +141,17 @@ class _FrostSendStep3State extends ConsumerState<FrostSendStep3> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
for (int i = 0; i < participantsWithoutMe.length; i++) for (int i = 0; i < participantsWithoutMe.length; i++)
Column( FrostStepField(
mainAxisSize: MainAxisSize.min, label: participantsWithoutMe[i],
crossAxisAlignment: CrossAxisAlignment.start, hint: "Enter ${participantsWithoutMe[i]}'s share",
children: [ controller: controllers[i],
Padding( focusNode: focusNodes[i],
padding: const EdgeInsets.symmetric(vertical: 8), onChanged: (_) {
child: ClipRRect( setState(() {
borderRadius: BorderRadius.circular( fieldIsEmptyFlags[i] = controllers[i].text.isEmpty;
Constants.size.circularBorderRadius, });
), },
child: TextField( showQrScanOption: true,
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<void>.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(),
)
],
),
),
),
),
),
),
),
],
), ),
], ],
), ),
@ -259,22 +159,22 @@ class _FrostSendStep3State extends ConsumerState<FrostSendStep3> {
const SizedBox( const SizedBox(
height: 12, height: 12,
), ),
CheckboxTextButton(
label: "I have verified that everyone has my share",
onChanged: (value) {
setState(() {
_userVerifyContinue = value;
});
},
),
const SizedBox(
height: 12,
),
PrimaryButton( PrimaryButton(
label: "Complete signing", label: "Generate transaction",
enabled: _userVerifyContinue &&
!fieldIsEmptyFlags.reduce((v, e) => v |= e),
onPressed: () async { onPressed: () async {
// check for empty shares
if (controllers
.map((e) => e.text.isEmpty)
.reduce((value, element) => value |= element)) {
return await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: "Missing Shares",
desktopPopRootNavigator: Util.isDesktop,
),
);
}
// collect Share strings // collect Share strings
final sharesCollected = controllers.map((e) => e.text).toList(); final sharesCollected = controllers.map((e) => e.text).toList();
@ -295,10 +195,32 @@ class _FrostSendStep3State extends ConsumerState<FrostSendStep3> {
shares: shares, shares: shares,
); );
ref.read(pFrostTxData.state).state = final tx = cl.Transaction.fromHex(rawTx);
ref.read(pFrostTxData.state).state!.copyWith( final txData = ref.read(pFrostTxData)!;
raw: rawTx,
); 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; ref.read(pFrostCreateCurrentStep.state).state = 4;
await Navigator.of(context).pushNamed( await Navigator.of(context).pushNamed(
@ -313,13 +235,15 @@ class _FrostSendStep3State extends ConsumerState<FrostSendStep3> {
level: LogLevel.Fatal, level: LogLevel.Fatal,
); );
return await showDialog<void>( if (context.mounted) {
context: context, return await showDialog<void>(
builder: (_) => StackOkDialog( context: context,
title: "Failed to complete signing process", builder: (_) => StackOkDialog(
desktopPopRootNavigator: Util.isDesktop, title: "Failed to complete signing process",
), desktopPopRootNavigator: Util.isDesktop,
); ),
);
}
} }
}, },
), ),

View file

@ -1,19 +1,27 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qr_flutter/qr_flutter.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:stackwallet/frost_route_generator.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/transaction_views/tx_v2/transaction_v2_details_view.dart';
import 'package:stackwallet/pages/wallet_view/wallet_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/pages_desktop_specific/my_stack_view/my_stack_view.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/themes/stack_colors.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/logger.dart';
import 'package:stackwallet/utilities/show_loading.dart'; import 'package:stackwallet/utilities/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.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/custom_buttons/simple_copy_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/detail_item.dart'; import 'package:stackwallet/widgets/detail_item.dart';
import 'package:stackwallet/widgets/expandable.dart';
import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/stack_dialog.dart';
class FrostSendStep4 extends ConsumerStatefulWidget { class FrostSendStep4 extends ConsumerStatefulWidget {
@ -27,46 +35,154 @@ class FrostSendStep4 extends ConsumerStatefulWidget {
} }
class _FrostSendStep4State extends ConsumerState<FrostSendStep4> { class _FrostSendStep4State extends ConsumerState<FrostSendStep4> {
final List<bool> _expandedStates = [];
bool _broadcastLock = false; 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 @override
Widget build(BuildContext context) { 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( return Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
SizedBox( if (kDebugMode)
height: 220, DetailItem(
child: Row( title: "Tx hex (debug mode only)",
mainAxisAlignment: MainAxisAlignment.center, detail: ref.watch(pFrostTxData)!.raw!,
children: [ button: Util.isDesktop
QrImageView( ? IconCopyButton(
data: ref.watch(pFrostTxData.state).state!.raw!, data: ref.watch(pFrostTxData)!.raw!,
size: 220, )
backgroundColor: : SimpleCopyButton(
Theme.of(context).extension<StackColors>()!.background, data: ref.watch(pFrostTxData)!.raw!,
foregroundColor: Theme.of(context) ),
.extension<StackColors>()!
.accentColorDark,
),
],
), ),
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<StackColors>()!
.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( const SizedBox(
height: 12, height: 12,
), ),
DetailItem( DetailItem(
title: "Raw transaction hex", title: "Total",
detail: ref.watch(pFrostTxData.state).state!.raw!, detail: ref.watch(pAmountFormatter(cryptoCurrency.coin)).format(
button: Util.isDesktop ref.watch(pFrostTxData)!.fee! +
? IconCopyButton( recipients.map((e) => e.amount).reduce((v, e) => v += e)),
data: ref.watch(pFrostTxData.state).state!.raw!, horizontal: true,
) ),
: SimpleCopyButton( const SizedBox(
data: ref.watch(pFrostTxData.state).state!.raw!, height: 12,
), ),
DetailItem(
title: "Note",
detail: ref.watch(pFrostTxData)!.note ?? "",
),
const SizedBox(
height: 12,
),
DetailItem(
title: "Signers",
detail: signers,
), ),
const SizedBox( const SizedBox(
height: 12, height: 12,
@ -76,7 +192,7 @@ class _FrostSendStep4State extends ConsumerState<FrostSendStep4> {
height: 12, height: 12,
), ),
PrimaryButton( PrimaryButton(
label: "Broadcast Transaction", label: "Approve transaction",
onPressed: () async { onPressed: () async {
if (_broadcastLock) { if (_broadcastLock) {
return; return;
@ -92,11 +208,11 @@ class _FrostSendStep4State extends ConsumerState<FrostSendStep4> {
ref.read(pFrostScaffoldArgs)!.walletId!, ref.read(pFrostScaffoldArgs)!.walletId!,
) )
.confirmSend( .confirmSend(
txData: ref.read(pFrostTxData.state).state!, txData: ref.read(pFrostTxData)!,
), ),
context: context, context: context,
message: "Broadcasting transaction to network", message: "Broadcasting transaction to network",
isDesktop: Util.isDesktop, isDesktop: true, // used to pop using root nav
onException: (e) { onException: (e) {
ex = e; ex = e;
}, },
@ -106,7 +222,7 @@ class _FrostSendStep4State extends ConsumerState<FrostSendStep4> {
throw ex!; throw ex!;
} }
if (mounted) { if (context.mounted) {
if (txData != null) { if (txData != null) {
ref.read(pFrostTxData.state).state = txData; ref.read(pFrostTxData.state).state = txData;
Navigator.of(context).popUntil( Navigator.of(context).popUntil(
@ -123,15 +239,18 @@ class _FrostSendStep4State extends ConsumerState<FrostSendStep4> {
"$e\n$s", "$e\n$s",
level: LogLevel.Fatal, level: LogLevel.Fatal,
); );
if (context.mounted) {
return await showDialog<void>( return await showDialog<void>(
context: context, context: context,
builder: (_) => StackOkDialog( builder: (_) => StackOkDialog(
title: "Broadcast error", title: "Broadcast error",
message: e.toString(), message: e.toString(),
desktopPopRootNavigator: Util.isDesktop, desktopPopRootNavigator: Util.isDesktop,
), onOkPressed:
); Navigator.of(context, rootNavigator: true).pop,
),
);
}
} finally { } finally {
_broadcastLock = false; _broadcastLock = false;
} }
@ -142,3 +261,35 @@ class _FrostSendStep4State extends ConsumerState<FrostSendStep4> {
); );
} }
} }
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,
),
],
);
}
}

View file

@ -33,7 +33,9 @@ class TxData {
final String? changeAddress; final String? changeAddress;
// frost specific
final String? frostMSConfig; final String? frostMSConfig;
final List<String>? frostSigners;
// paynym specific // paynym specific
final PaynymAccountLite? paynymAccountLite; final PaynymAccountLite? paynymAccountLite;
@ -91,6 +93,7 @@ class TxData {
this.usedUTXOs, this.usedUTXOs,
this.changeAddress, this.changeAddress,
this.frostMSConfig, this.frostMSConfig,
this.frostSigners,
this.paynymAccountLite, this.paynymAccountLite,
this.web3dartTransaction, this.web3dartTransaction,
this.nonce, this.nonce,
@ -166,6 +169,7 @@ class TxData {
})>? })>?
recipients, recipients,
String? frostMSConfig, String? frostMSConfig,
List<String>? frostSigners,
String? changeAddress, String? changeAddress,
PaynymAccountLite? paynymAccountLite, PaynymAccountLite? paynymAccountLite,
web3dart.Transaction? web3dartTransaction, web3dart.Transaction? web3dartTransaction,
@ -209,6 +213,7 @@ class TxData {
usedUTXOs: usedUTXOs ?? this.usedUTXOs, usedUTXOs: usedUTXOs ?? this.usedUTXOs,
recipients: recipients ?? this.recipients, recipients: recipients ?? this.recipients,
frostMSConfig: frostMSConfig ?? this.frostMSConfig, frostMSConfig: frostMSConfig ?? this.frostMSConfig,
frostSigners: frostSigners ?? this.frostSigners,
changeAddress: changeAddress ?? this.changeAddress, changeAddress: changeAddress ?? this.changeAddress,
paynymAccountLite: paynymAccountLite ?? this.paynymAccountLite, paynymAccountLite: paynymAccountLite ?? this.paynymAccountLite,
web3dartTransaction: web3dartTransaction ?? this.web3dartTransaction, web3dartTransaction: web3dartTransaction ?? this.web3dartTransaction,
@ -249,7 +254,7 @@ class TxData {
'recipients: $recipients, ' 'recipients: $recipients, '
'utxos: $utxos, ' 'utxos: $utxos, '
'usedUTXOs: $usedUTXOs, ' 'usedUTXOs: $usedUTXOs, '
'frostMSConfig: $frostMSConfig, ' 'frostSigners: $frostSigners, '
'changeAddress: $changeAddress, ' 'changeAddress: $changeAddress, '
'paynymAccountLite: $paynymAccountLite, ' 'paynymAccountLite: $paynymAccountLite, '
'web3dartTransaction: $web3dartTransaction, ' 'web3dartTransaction: $web3dartTransaction, '