untested: Bitcoin frost

This commit is contained in:
julian 2024-01-19 17:44:01 -06:00
parent 8ae2faa91f
commit 6a7ec2d5d2
33 changed files with 7349 additions and 77 deletions

@ -1 +1 @@
Subproject commit 2fa7e46669a023d270cad4552b5151b138738790
Subproject commit 0fbc038a262e3c2d82c7c6e34e194e9a47011d91

View file

@ -0,0 +1,343 @@
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/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/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/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 '../../../../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<ConfirmNewFrostMSWalletCreationView> createState() =>
_ConfirmNewFrostMSWalletCreationViewState();
}
class _ConfirmNewFrostMSWalletCreationViewState
extends ConsumerState<ConfirmNewFrostMSWalletCreationView> {
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<void>(
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<StackColors>()!.background,
appBar: DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => const FrostInterruptionDialog(
type: FrostInterruptionDialogType.walletCreation,
popUntilOnYesRouteName: DesktopHomeView.routeName,
),
);
},
),
trailing: ExitToMyStackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => const FrostInterruptionDialog(
type: FrostInterruptionDialogType.walletCreation,
popUntilOnYesRouteName: DesktopHomeView.routeName,
),
);
},
),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
await showDialog<void>(
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<dynamic>(
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,
);
}
}

View file

@ -0,0 +1,285 @@
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/share_new_multisig_config_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/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/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/stack_dialog.dart';
class CreateNewFrostMsWalletView extends ConsumerStatefulWidget {
const CreateNewFrostMsWalletView({
super.key,
required this.walletName,
required this.coin,
});
static const String routeName = "/createNewFrostMsWalletView";
final String walletName;
final Coin coin;
@override
ConsumerState<CreateNewFrostMsWalletView> createState() =>
_NewFrostMsWalletViewState();
}
class _NewFrostMsWalletViewState
extends ConsumerState<CreateNewFrostMsWalletView> {
final _thresholdController = TextEditingController();
final _participantsController = TextEditingController();
final List<TextEditingController> controllers = [];
int _participantsCount = 0;
String _validateInputData() {
final threshold = int.tryParse(_thresholdController.text);
if (threshold == null) {
return "Choose a threshold";
}
final partsCount = int.tryParse(_participantsController.text);
if (partsCount == null) {
return "Choose total number of participants";
}
if (threshold > partsCount) {
return "Threshold cannot be greater than the number of participants";
}
if (partsCount < 2) {
return "At least two participants required";
}
if (controllers.length != partsCount) {
return "Participants count error";
}
final hasEmptyParticipants = controllers
.map((e) => e.text.isEmpty)
.reduce((value, element) => value |= element);
if (hasEmptyParticipants) {
return "Participants must not be empty";
}
if (controllers.length != controllers.map((e) => e.text).toSet().length) {
return "Duplicate participant name found";
}
return "valid";
}
void _participantsCountChanged(String newValue) {
final count = int.tryParse(newValue);
if (count != null) {
if (count > _participantsCount) {
for (int i = _participantsCount; i < count; i++) {
controllers.add(TextEditingController());
}
_participantsCount = count;
setState(() {});
} else if (count < _participantsCount) {
for (int i = _participantsCount; i > count; i--) {
final last = controllers.removeLast();
last.dispose();
}
_participantsCount = count;
setState(() {});
}
}
}
@override
void dispose() {
_thresholdController.dispose();
_participantsController.dispose();
for (final e in controllers) {
e.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: const DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(),
trailing: ExitToMyStackButton(),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
title: Text(
"New 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: [
Text(
"Threshold",
style: STextStyles.label(context),
),
const SizedBox(
height: 10,
),
TextField(
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
controller: _thresholdController,
),
const SizedBox(
height: 16,
),
Text(
"Number of participants",
style: STextStyles.label(context),
),
const SizedBox(
height: 10,
),
TextField(
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
controller: _participantsController,
onChanged: _participantsCountChanged,
),
const SizedBox(
height: 16,
),
if (controllers.isNotEmpty)
Text(
"My name",
style: STextStyles.label(context),
),
if (controllers.isNotEmpty)
const SizedBox(
height: 10,
),
if (controllers.isNotEmpty)
TextField(
controller: controllers.first,
),
if (controllers.length > 1)
const SizedBox(
height: 16,
),
if (controllers.length > 1)
Text(
"Remaining participants",
style: STextStyles.label(context),
),
if (controllers.length > 1)
Column(
children: [
for (int i = 1; i < controllers.length; i++)
Padding(
padding: const EdgeInsets.only(
top: 10,
),
child: TextField(
controller: controllers[i],
),
),
],
),
if (!Util.isDesktop) const Spacer(),
const SizedBox(
height: 16,
),
PrimaryButton(
label: "Generate",
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
}
final validationMessage = _validateInputData();
if (validationMessage != "valid") {
return await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: validationMessage,
desktopPopRootNavigator: Util.isDesktop,
),
);
}
final config = Frost.createMultisigConfig(
name: controllers.first.text,
threshold: int.parse(_thresholdController.text),
participants: controllers.map((e) => e.text).toList(),
);
ref.read(pFrostMyName.notifier).state = controllers.first.text;
ref.read(pFrostMultisigConfig.notifier).state = config;
await Navigator.of(context).pushNamed(
ShareNewMultisigConfigView.routeName,
arguments: (
walletName: widget.walletName,
coin: widget.coin,
),
);
},
),
],
),
),
);
}
}

View file

@ -0,0 +1,443 @@
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/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/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/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<FrostShareCommitmentsView> createState() =>
_FrostShareCommitmentsViewState();
}
class _FrostShareCommitmentsViewState
extends ConsumerState<FrostShareCommitmentsView> {
final List<TextEditingController> controllers = [];
final List<FocusNode> focusNodes = [];
late final List<String> participants;
late final String myCommitment;
late final int myIndex;
final List<bool> 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<void>(
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<StackColors>()!.background,
appBar: DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => const FrostInterruptionDialog(
type: FrostInterruptionDialogType.walletCreation,
popUntilOnYesRouteName: DesktopHomeView.routeName,
),
);
},
),
trailing: ExitToMyStackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => const FrostInterruptionDialog(
type: FrostInterruptionDialogType.walletCreation,
popUntilOnYesRouteName: DesktopHomeView.routeName,
),
);
},
),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
await showDialog<void>(
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<StackColors>()!
.background,
foregroundColor: Theme.of(context)
.extension<StackColors>()!
.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<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(),
)
],
),
),
),
),
),
),
),
],
),
],
),
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<void>(
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<void>(
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,
);
}
}

View file

@ -0,0 +1,409 @@
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/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/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/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<FrostShareSharesView> createState() =>
_FrostShareSharesViewState();
}
class _FrostShareSharesViewState extends ConsumerState<FrostShareSharesView> {
final List<TextEditingController> controllers = [];
final List<FocusNode> focusNodes = [];
late final List<String> participants;
late final String myShare;
late final int myIndex;
final List<bool> 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<void>(
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<StackColors>()!.background,
appBar: DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => const FrostInterruptionDialog(
type: FrostInterruptionDialogType.walletCreation,
popUntilOnYesRouteName: DesktopHomeView.routeName,
),
);
},
),
trailing: ExitToMyStackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => const FrostInterruptionDialog(
type: FrostInterruptionDialogType.walletCreation,
popUntilOnYesRouteName: DesktopHomeView.routeName,
),
);
},
),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
await showDialog<void>(
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<StackColors>()!
.background,
foregroundColor: Theme.of(context)
.extension<StackColors>()!
.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<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(),
)
],
),
),
),
),
),
),
),
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<void>(
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<void>(
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,
);
}
}

View file

@ -0,0 +1,386 @@
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/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/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';
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<ImportNewFrostMsWalletView> createState() =>
_ImportNewFrostMsWalletViewState();
}
class _ImportNewFrostMsWalletViewState
extends ConsumerState<ImportNewFrostMsWalletView> {
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<StackColors>()!.background,
appBar: const DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(),
trailing: ExitToMyStackButton(),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.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<void>.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<void>(
context: context,
builder: (_) => StackOkDialog(
title: "Invalid config",
desktopPopRootNavigator: Util.isDesktop,
),
);
}
if (!Frost.getParticipants(multisigConfig: config)
.contains(myNameFieldController.text)) {
return await showDialog<void>(
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,
),
);
},
),
],
),
),
);
}
}

View file

@ -0,0 +1,162 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qr_flutter/qr_flutter.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/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/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<ShareNewMultisigConfigView> createState() =>
_ShareNewMultisigConfigViewState();
}
class _ShareNewMultisigConfigViewState
extends ConsumerState<ShareNewMultisigConfigView> {
@override
Widget build(BuildContext context) {
return ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: const DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(),
trailing: ExitToMyStackButton(),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.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<StackColors>()!.background,
foregroundColor: Theme.of(context)
.extension<StackColors>()!
.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,
),
);
},
),
],
),
),
);
}
}

View file

@ -0,0 +1,478 @@
import 'dart:async';
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/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';
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/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/frost_wallet_info.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/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 RestoreFrostMsWalletView extends ConsumerStatefulWidget {
const RestoreFrostMsWalletView({
super.key,
required this.walletName,
required this.coin,
});
static const String routeName = "/restoreFrostMsWalletView";
final String walletName;
final Coin coin;
@override
ConsumerState<RestoreFrostMsWalletView> createState() =>
_RestoreFrostMsWalletViewState();
}
class _RestoreFrostMsWalletViewState
extends ConsumerState<RestoreFrostMsWalletView> {
late final TextEditingController keysFieldController, configFieldController;
late final FocusNode keysFocusNode, configFocusNode;
bool _keysEmpty = true, _configEmpty = true;
bool _restoreButtonLock = false;
Future<Wallet> _createWalletAndRecover() async {
final keys = keysFieldController.text;
final config = configFieldController.text;
final myNameIndex = getParticipantIndexFromKeys(serializedKeys: keys);
final participants = Frost.getParticipants(multisigConfig: config);
final myName = participants[myNameIndex];
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),
);
final frostInfo = FrostWalletInfo(
walletId: info.walletId,
knownSalts: [],
participants: participants,
myName: myName,
threshold: multisigThreshold(
multisigConfig: config,
),
);
await ref.read(mainDBProvider).isar.frostWalletInfo.put(frostInfo);
await (wallet as BitcoinFrostWallet).recover(
serializedKeys: keys,
multisigConfig: config,
isRescan: false,
);
await info.setMnemonicVerified(
isar: ref.read(mainDBProvider).isar,
);
return wallet;
}
Future<void> _restore() async {
if (_restoreButtonLock) {
return;
}
_restoreButtonLock = true;
try {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
}
Exception? ex;
final wallet = await showLoading(
whileFuture: _createWalletAndRecover(),
context: context,
message: "Restoring wallet...",
isDesktop: Util.isDesktop,
onException: (e) {
ex = e;
},
);
if (ex != null) {
throw ex!;
}
ref.read(pWallets).addWallet(wallet!);
if (mounted) {
if (Util.isDesktop) {
Navigator.of(context).popUntil(
ModalRoute.withName(
DesktopHomeView.routeName,
),
);
} else {
unawaited(
Navigator.of(context).pushNamedAndRemoveUntil(
HomeView.routeName,
(route) => false,
),
);
}
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,
);
if (mounted) {
await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: "Failed to restore",
message: e.toString(),
desktopPopRootNavigator: Util.isDesktop,
),
);
}
} finally {
_restoreButtonLock = false;
}
}
@override
void initState() {
keysFieldController = TextEditingController();
configFieldController = TextEditingController();
keysFocusNode = FocusNode();
configFocusNode = FocusNode();
super.initState();
}
@override
void dispose() {
keysFieldController.dispose();
configFieldController.dispose();
keysFocusNode.dispose();
configFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: const DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(),
trailing: ExitToMyStackButton(),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
title: Text(
"Restore 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: [
const SizedBox(
height: 16,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("frMyNameTextFieldKey"),
controller: keysFieldController,
onChanged: (_) {
setState(() {
_keysEmpty = keysFieldController.text.isEmpty;
});
},
focusNode: keysFocusNode,
readOnly: false,
autocorrect: false,
enableSuggestions: false,
style: STextStyles.field(context),
decoration: standardInputDecoration(
"Keys",
keysFocusNode,
context,
).copyWith(
contentPadding: const EdgeInsets.only(
left: 16,
top: 6,
bottom: 8,
right: 5,
),
suffixIcon: Padding(
padding: _keysEmpty
? const EdgeInsets.only(right: 8)
: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
!_keysEmpty
? TextFieldIconButton(
semanticsLabel:
"Clear Button. Clears The Keys Field.",
key: const Key("frMyNameClearButtonKey"),
onTap: () {
keysFieldController.text = "";
setState(() {
_keysEmpty = true;
});
},
child: const XIcon(),
)
: TextFieldIconButton(
semanticsLabel:
"Paste Button. Pastes From Clipboard To Keys Field.",
key: const Key("frKeysPasteButtonKey"),
onTap: () async {
final ClipboardData? data =
await Clipboard.getData(
Clipboard.kTextPlain);
if (data?.text != null &&
data!.text!.isNotEmpty) {
keysFieldController.text =
data.text!.trim();
}
setState(() {
_keysEmpty =
keysFieldController.text.isEmpty;
});
},
child: _keysEmpty
? 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<void>.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: "Restore",
enabled: !_keysEmpty && !_configEmpty,
onPressed: _restore,
),
],
),
),
);
}
}

View file

@ -14,9 +14,13 @@ import 'package:flutter/material.dart';
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/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';
@ -32,6 +36,8 @@ import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/icon_widgets/dice_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
@ -77,6 +83,52 @@ class _NameYourWalletViewState extends ConsumerState<NameYourWalletView> {
return name;
}
Future<void> _nextPressed() async {
final name = textEditingController.text;
if (mounted) {
// hide keyboard if has focus
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 50));
}
if (mounted) {
ref.read(mnemonicWordCountStateProvider.state).state =
Constants.possibleLengthsForCoin(coin).last;
ref.read(pNewWalletOptions.notifier).state = null;
switch (widget.addWalletType) {
case AddWalletType.New:
unawaited(
Navigator.of(context).pushNamed(
coin.hasMnemonicPassphraseSupport
? NewWalletOptionsView.routeName
: NewWalletRecoveryPhraseWarningView.routeName,
arguments: Tuple2(
name,
coin,
),
),
);
break;
case AddWalletType.Restore:
unawaited(
Navigator.of(context).pushNamed(
RestoreOptionsView.routeName,
arguments: Tuple2(
name,
coin,
),
),
);
break;
}
}
}
}
@override
void initState() {
isDesktop = Util.isDesktop;
@ -330,78 +382,104 @@ class _NameYourWalletViewState extends ConsumerState<NameYourWalletView> {
const SizedBox(
height: 32,
),
ConstrainedBox(
constraints: BoxConstraints(
minWidth: isDesktop ? 480 : 0,
minHeight: isDesktop ? 70 : 0,
if (widget.coin.isFrost)
if (widget.addWalletType == AddWalletType.Restore)
PrimaryButton(
label: "Next",
enabled: _nextEnabled,
onPressed: () async {
final name = textEditingController.text;
await Navigator.of(context).pushNamed(
RestoreFrostMsWalletView.routeName,
arguments: (
walletName: name,
coin: coin,
),
);
},
),
if (widget.addWalletType == AddWalletType.New)
Column(
children: [
PrimaryButton(
label: "Create config",
enabled: _nextEnabled,
onPressed: () async {
final name = textEditingController.text;
await Navigator.of(context).pushNamed(
CreateNewFrostMsWalletView.routeName,
arguments: (
walletName: name,
coin: coin,
),
);
},
),
const SizedBox(
height: 12,
),
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,
),
);
},
),
],
),
child: TextButton(
onPressed: _nextEnabled
? () async {
final name = textEditingController.text;
if (mounted) {
// hide keyboard if has focus
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(milliseconds: 50));
}
if (mounted) {
ref.read(mnemonicWordCountStateProvider.state).state =
Constants.possibleLengthsForCoin(coin).last;
ref.read(pNewWalletOptions.notifier).state = null;
switch (widget.addWalletType) {
case AddWalletType.New:
unawaited(
Navigator.of(context).pushNamed(
coin.hasMnemonicPassphraseSupport
? NewWalletOptionsView.routeName
: NewWalletRecoveryPhraseWarningView
.routeName,
arguments: Tuple2(
name,
coin,
),
),
);
break;
case AddWalletType.Restore:
unawaited(
Navigator.of(context).pushNamed(
RestoreOptionsView.routeName,
arguments: Tuple2(
name,
coin,
),
),
);
break;
}
}
}
}
: null,
style: _nextEnabled
? Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonStyle(context)
: Theme.of(context)
.extension<StackColors>()!
.getPrimaryDisabledButtonStyle(context),
child: Text(
"Next",
style: isDesktop
? _nextEnabled
? STextStyles.desktopButtonEnabled(context)
: STextStyles.desktopButtonDisabled(context)
: STextStyles.button(context),
if (!widget.coin.isFrost)
ConstrainedBox(
constraints: BoxConstraints(
minWidth: isDesktop ? 480 : 0,
minHeight: isDesktop ? 70 : 0,
),
child: TextButton(
onPressed: _nextEnabled ? _nextPressed : null,
style: _nextEnabled
? Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonStyle(context)
: Theme.of(context)
.extension<StackColors>()!
.getPrimaryDisabledButtonStyle(context),
child: Text(
"Next",
style: isDesktop
? _nextEnabled
? STextStyles.desktopButtonEnabled(context)
: STextStyles.desktopButtonDisabled(context)
: STextStyles.button(context),
),
),
),
),
if (isDesktop)
const Spacer(
flex: 15,

View file

@ -0,0 +1,162 @@
/*
* 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 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.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/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/wallets/isar/models/frost_wallet_info.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class FrostMSWalletOptionsView extends ConsumerWidget {
const FrostMSWalletOptionsView({
Key? key,
required this.walletId,
}) : super(key: key);
static const String routeName = "/frostMSWalletOptionsView";
final String walletId;
@override
Widget build(BuildContext context, WidgetRef ref) {
return Background(
child: Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
title: Text(
"Multisig settings",
style: STextStyles.navBarTitle(context),
),
),
body: Padding(
padding: const EdgeInsets.only(
top: 12,
left: 16,
right: 16,
),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_OptionButton(
label: "Show participants",
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)!;
ref.read(pFrostMyName.state).state = frostInfo.myName;
Navigator.of(context).pushNamed(
BeginReshareConfigView.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)!;
ref.read(pFrostMyName.state).state = frostInfo.myName;
Navigator.of(context).pushNamed(
ImportReshareConfigView.routeName,
arguments: walletId,
);
},
),
],
),
),
),
),
);
}
}
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<StackColors>()!.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),
),
],
),
),
),
);
}
}

View file

@ -0,0 +1,119 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.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/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';
class FrostParticipantsView extends ConsumerWidget {
const FrostParticipantsView({
super.key,
required this.walletId,
});
static const String routeName = "/frostParticipantsView";
final String walletId;
@override
Widget build(BuildContext context, WidgetRef ref) {
// TODO: optimize this by creating watcher providers (similar to normal WalletInfo)
final frostInfo = ref
.read(mainDBProvider)
.isar
.frostWalletInfo
.getByWalletIdSync(walletId)!;
return ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: const DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(),
trailing: ExitToMyStackButton(),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
title: Text(
"Participants",
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: [
for (int i = 0; i < frostInfo.participants.length; i++)
Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
),
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),
),
],
),
),
],
),
),
);
}
}

View file

@ -0,0 +1,433 @@
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/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/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';
class FinishResharingView extends ConsumerStatefulWidget {
const FinishResharingView({
super.key,
required this.walletId,
});
static const String routeName = "/finishResharingView";
final String walletId;
@override
ConsumerState<FinishResharingView> createState() =>
_FinishResharingViewState();
}
class _FinishResharingViewState extends ConsumerState<FinishResharingView> {
final List<TextEditingController> controllers = [];
final List<FocusNode> focusNodes = [];
late final List<int> resharerIndexes;
late final String myName;
late final int? myResharerIndexIndex;
late final String? myResharerComplete;
late final bool amOutgoingParticipant;
final List<bool> fieldIsEmptyFlags = [];
bool _buttonLock = false;
Future<void> _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<void>(
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<StackColors>()!.background,
appBar: const DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(),
trailing: ExitToMyStackButton(),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.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<StackColors>()!
.background,
foregroundColor: Theme.of(context)
.extension<StackColors>()!
.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<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(),
),
],
),
),
),
),
),
),
),
],
),
],
),
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,
);
}
}

View file

@ -0,0 +1,196 @@
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_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';
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';
final class BeginReshareConfigView extends ConsumerStatefulWidget {
const BeginReshareConfigView({
super.key,
required this.walletId,
});
static const String routeName = "/beginReshareConfigView";
final String walletId;
@override
ConsumerState<BeginReshareConfigView> createState() =>
_BeginReshareConfigViewState();
}
class _BeginReshareConfigViewState
extends ConsumerState<BeginReshareConfigView> {
late final int currentThreshold;
late final List<String> currentParticipants;
final Map<String, int> pFrostResharersMap = {};
@override
void initState() {
ref.read(pFrostResharingData).reset();
// TODO: optimize this by creating watcher providers (similar to normal WalletInfo)
final frostInfo = ref
.read(mainDBProvider)
.isar
.frostWalletInfo
.getByWalletIdSync(widget.walletId)!;
currentThreshold = frostInfo.threshold;
currentParticipants = frostInfo.participants;
super.initState();
}
@override
Widget build(BuildContext context) {
return ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: const DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(),
trailing: ExitToMyStackButton(),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
// title: Text(
// "Modify Participants",
// 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(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Select participants for resharing",
style: STextStyles.label(context),
),
const SizedBox(
height: 16,
),
Column(
children: [
for (int i = 0; i < currentParticipants.length; i++)
Padding(
padding: const EdgeInsets.only(
top: 10,
),
child: RawMaterialButton(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
onPressed: () {
if (pFrostResharersMap[currentParticipants[i]] ==
null) {
pFrostResharersMap[currentParticipants[i]] = i;
} else {
pFrostResharersMap.remove(currentParticipants[i]);
}
setState(() {});
},
child: Container(
color: Colors.transparent,
child: IgnorePointer(
child: Row(
children: [
Checkbox(
value: pFrostResharersMap[
currentParticipants[i]] ==
i,
onChanged: (bool? value) {},
),
const SizedBox(
width: 10,
),
Text(
currentParticipants[i],
style: STextStyles.itemSubtitle12(context),
),
],
),
),
),
),
),
],
),
if (!Util.isDesktop) const Spacer(),
const SizedBox(
height: 16,
),
PrimaryButton(
label: "Continue",
enabled: pFrostResharersMap.length >= currentThreshold,
onPressed: () async {
await Navigator.of(context).pushNamed(
CompleteReshareConfigView.routeName,
arguments: (
walletId: widget.walletId,
resharers:
pFrostResharersMap.values.toList(growable: false),
),
);
},
),
],
),
),
);
}
}

View file

@ -0,0 +1,335 @@
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/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/services/frost.dart';
import 'package:stackwallet/themes/stack_colors.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/stack_dialog.dart';
final class CompleteReshareConfigView extends ConsumerStatefulWidget {
const CompleteReshareConfigView({
super.key,
required this.walletId,
required this.resharers,
});
static const String routeName = "/completeReshareConfigView";
final String walletId;
final List<int> resharers;
@override
ConsumerState<CompleteReshareConfigView> createState() =>
_CompleteReshareConfigViewState();
}
class _CompleteReshareConfigViewState
extends ConsumerState<CompleteReshareConfigView> {
final _newThresholdController = TextEditingController();
final _newParticipantsCountController = TextEditingController();
final List<TextEditingController> controllers = [];
int _participantsCount = 0;
bool _buttonLock = false;
Future<void> _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)!;
final validationMessage = _validateInputData();
if (validationMessage != "valid") {
return await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: validationMessage,
desktopPopRootNavigator: Util.isDesktop,
),
);
}
final config = Frost.createResharerConfig(
newThreshold: int.parse(_newThresholdController.text),
resharers: widget.resharers,
newParticipants: controllers.map((e) => e.text).toList(),
);
final salt = Format.uint8listToString(
resharerSalt(resharerConfig: config),
);
if (frostInfo.knownSalts.contains(salt)) {
return await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: "Duplicate config salt",
desktopPopRootNavigator: Util.isDesktop,
),
);
} 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),
);
});
}
ref.read(pFrostResharingData).myName = frostInfo.myName;
ref.read(pFrostResharingData).resharerConfig = config;
if (mounted) {
await Navigator.of(context).pushNamed(
DisplayReshareConfigView.routeName,
arguments: widget.walletId,
);
}
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
if (mounted) {
await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: e.toString(),
desktopPopRootNavigator: Util.isDesktop,
),
);
}
} finally {
_buttonLock = false;
}
}
String _validateInputData() {
final threshold = int.tryParse(_newThresholdController.text);
if (threshold == null) {
return "Choose a threshold";
}
final partsCount = int.tryParse(_newParticipantsCountController.text);
if (partsCount == null) {
return "Choose total number of participants";
}
if (threshold > partsCount) {
return "Threshold cannot be greater than the number of participants";
}
if (partsCount < 2) {
return "At least two participants required";
}
if (controllers.length != partsCount) {
return "Participants count error";
}
final hasEmptyParticipants = controllers
.map((e) => e.text.isEmpty)
.reduce((value, element) => value |= element);
if (hasEmptyParticipants) {
return "Participants must not be empty";
}
if (controllers.length != controllers.map((e) => e.text).toSet().length) {
return "Duplicate participant name found";
}
return "valid";
}
void _participantsCountChanged(String newValue) {
final count = int.tryParse(newValue);
if (count != null) {
if (count > _participantsCount) {
for (int i = _participantsCount; i < count; i++) {
controllers.add(TextEditingController());
}
_participantsCount = count;
setState(() {});
} else if (count < _participantsCount) {
for (int i = _participantsCount; i > count; i--) {
final last = controllers.removeLast();
last.dispose();
}
_participantsCount = count;
setState(() {});
}
}
}
@override
void dispose() {
_newThresholdController.dispose();
_newParticipantsCountController.dispose();
for (final e in controllers) {
e.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: const DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(),
trailing: ExitToMyStackButton(),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
title: Text(
"Modify Participants",
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(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"New threshold",
style: STextStyles.label(context),
),
const SizedBox(
height: 10,
),
TextField(
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
controller: _newThresholdController,
),
const SizedBox(
height: 16,
),
Text(
"Number of participants",
style: STextStyles.label(context),
),
const SizedBox(
height: 10,
),
TextField(
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
controller: _newParticipantsCountController,
onChanged: _participantsCountChanged,
),
const SizedBox(
height: 16,
),
if (controllers.isNotEmpty)
Text(
"Participants",
style: STextStyles.label(context),
),
if (controllers.isNotEmpty)
const SizedBox(
height: 10,
),
if (controllers.isNotEmpty)
Column(
children: [
for (int i = 0; i < controllers.length; i++)
Padding(
padding: const EdgeInsets.only(
top: 10,
),
child: TextField(
controller: controllers[i],
),
),
],
),
if (!Util.isDesktop) const Spacer(),
const SizedBox(
height: 16,
),
PrimaryButton(
label: "Generate config",
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
}
await _onPressed();
},
),
],
),
),
);
}
}

View file

@ -0,0 +1,214 @@
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<DisplayReshareConfigView> createState() =>
_DisplayReshareConfigViewState();
}
class _DisplayReshareConfigViewState
extends ConsumerState<DisplayReshareConfigView> {
late final bool iAmInvolved;
bool _buttonLock = false;
Future<void> _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<void>(
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<StackColors>()!.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<StackColors>()!.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<StackColors>()!.background,
foregroundColor: Theme.of(context)
.extension<StackColors>()!
.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,
),
],
),
),
);
}
}

View file

@ -0,0 +1,338 @@
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<ImportReshareConfigView> createState() =>
_ImportReshareConfigViewState();
}
class _ImportReshareConfigViewState
extends ConsumerState<ImportReshareConfigView> {
late final TextEditingController configFieldController;
late final FocusNode configFocusNode;
bool _configEmpty = true;
bool _buttonLock = false;
Future<void> _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<void>(
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<StackColors>()!.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<StackColors>()!.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<void>.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();
},
),
],
),
),
);
}
}

View file

@ -0,0 +1,439 @@
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<BeginResharingView> createState() => _BeginResharingViewState();
}
class _BeginResharingViewState extends ConsumerState<BeginResharingView> {
final List<TextEditingController> controllers = [];
final List<FocusNode> focusNodes = [];
late final List<int> resharerIndexes;
late final int myResharerIndexIndex;
late final String myResharerStart;
late final bool amOutgoingParticipant;
final List<bool> fieldIsEmptyFlags = [];
bool _buttonLock = false;
Future<void> _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<void>(
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<void>(
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<StackColors>()!.background,
appBar: DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(
onPressed: () async {
await showDialog<void>(
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<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
await showDialog<void>(
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<StackColors>()!
.background,
foregroundColor: Theme.of(context)
.extension<StackColors>()!
.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<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(),
),
],
),
),
),
),
),
),
),
],
),
],
),
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,
);
}
}

View file

@ -0,0 +1,429 @@
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<ContinueResharingView> createState() =>
_ContinueResharingViewState();
}
class _ContinueResharingViewState extends ConsumerState<ContinueResharingView> {
final List<TextEditingController> controllers = [];
final List<FocusNode> focusNodes = [];
late final List<String> newParticipants;
late final int myIndex;
late final String? myEncryptionKey;
late final bool amOutgoingParticipant;
final List<bool> fieldIsEmptyFlags = [];
bool _buttonLock = false;
Future<void> _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<void>(
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<void>(
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<StackColors>()!.background,
appBar: DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(
onPressed: () async {
await showDialog<void>(
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<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
await showDialog<void>(
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<StackColors>()!
.background,
foregroundColor: Theme.of(context)
.extension<StackColors>()!
.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<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(),
),
],
),
),
),
),
),
),
),
],
),
],
),
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,
);
}
}

View file

@ -0,0 +1,204 @@
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/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/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';
class NewContinueSharingView extends ConsumerStatefulWidget {
const NewContinueSharingView({
super.key,
required this.walletId,
});
static const String routeName = "/NewContinueSharingView";
final String walletId;
@override
ConsumerState<NewContinueSharingView> createState() =>
_NewContinueSharingViewState();
}
class _NewContinueSharingViewState
extends ConsumerState<NewContinueSharingView> {
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
await showDialog<void>(
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<StackColors>()!.background,
appBar: DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => const FrostInterruptionDialog(
type: FrostInterruptionDialogType.resharing,
popUntilOnYesRouteName: DesktopHomeView.routeName,
),
);
},
),
trailing: ExitToMyStackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => const FrostInterruptionDialog(
type: FrostInterruptionDialogType.resharing,
popUntilOnYesRouteName: DesktopHomeView.routeName,
),
);
},
),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
await showDialog<void>(
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<StackColors>()!
.background,
foregroundColor: Theme.of(context)
.extension<StackColors>()!
.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,
);
}
}

View file

@ -0,0 +1,426 @@
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';
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<NewImportResharerConfigView> createState() =>
_NewImportResharerConfigViewState();
}
class _NewImportResharerConfigViewState
extends ConsumerState<NewImportResharerConfigView> {
late final TextEditingController myNameFieldController, configFieldController;
late final FocusNode myNameFocusNode, configFocusNode;
bool _nameEmpty = true, _configEmpty = true;
bool _buttonLock = false;
Future<IncompleteFrostWallet> _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<StackColors>()!.background,
appBar: const DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(),
trailing: ExitToMyStackButton(),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.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<void>.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<void>(
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<void>(
context: context,
builder: (_) => StackOkDialog(
title: e.toString(),
desktopPopRootNavigator: Util.isDesktop,
),
);
}
} finally {
_buttonLock = false;
}
},
),
],
),
),
);
}
}

View file

@ -0,0 +1,379 @@
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/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/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';
class NewStartResharingView extends ConsumerStatefulWidget {
const NewStartResharingView({
super.key,
required this.walletId,
});
static const String routeName = "/newStartResharingView";
final String walletId;
@override
ConsumerState<NewStartResharingView> createState() =>
_NewStartResharingViewState();
}
class _NewStartResharingViewState extends ConsumerState<NewStartResharingView> {
final List<TextEditingController> controllers = [];
final List<FocusNode> focusNodes = [];
late final List<int> resharerIndexes;
final List<bool> fieldIsEmptyFlags = [];
bool _buttonLock = false;
Future<void> _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<void>(
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<void>(
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<StackColors>()!.background,
appBar: DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => const FrostInterruptionDialog(
type: FrostInterruptionDialogType.resharing,
popUntilOnYesRouteName: DesktopHomeView.routeName,
),
);
},
),
trailing: ExitToMyStackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => const FrostInterruptionDialog(
type: FrostInterruptionDialogType.resharing,
popUntilOnYesRouteName: DesktopHomeView.routeName,
),
);
},
),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
await showDialog<void>(
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<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(),
),
],
),
),
),
),
),
),
),
],
),
],
),
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,
);
}
}

View file

@ -0,0 +1,315 @@
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<VerifyUpdatedWalletView> createState() =>
_VerifyUpdatedWalletViewState();
}
class _VerifyUpdatedWalletViewState
extends ConsumerState<VerifyUpdatedWalletView> {
late final String config;
late final String serializedKeys;
late final String reshareId;
late final bool isNew;
bool _buttonLock = false;
Future<void> _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<void>(
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<void>(
context: context,
builder: (_) => FrostInterruptionDialog(
type: FrostInterruptionDialogType.resharing,
popUntilOnYesRouteName: _popUntilPath,
),
);
return false;
},
child: ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => FrostInterruptionDialog(
type: FrostInterruptionDialogType.resharing,
popUntilOnYesRouteName: _popUntilPath,
),
);
},
),
trailing: ExitToMyStackButton(
onPressed: () async {
await showDialog<void>(
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<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
await showDialog<void>(
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,
);
}
}

View file

@ -0,0 +1,103 @@
import 'dart:ffi';
import 'dart:typed_data';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:frostdart/frostdart_bindings_generated.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/wallets/models/incomplete_frost_wallet.dart';
import 'package:stackwallet/wallets/models/tx_data.dart';
// =================== wallet creation =========================================
final pFrostMultisigConfig = StateProvider<String?>((ref) => null);
final pFrostMyName = StateProvider<String?>((ref) => null);
final pFrostStartKeyGenData = StateProvider<
({
String seed,
String commitments,
Pointer<MultisigConfigWithName> multisigConfigWithNamePtr,
Pointer<SecretShareMachineWrapper> secretShareMachineWrapperPtr,
})?>((_) => null);
final pFrostSecretSharesData = StateProvider<
({
String share,
Pointer<SecretSharesRes> secretSharesResPtr,
})?>((ref) => null);
final pFrostCompletedKeyGenData = StateProvider<
({
Uint8List multisigId,
String recoveryString,
String serializedKeys,
})?>((ref) => null);
// ================= transaction creation ======================================
final pFrostTxData = StateProvider<TxData?>((ref) => null);
final pFrostAttemptSignData = StateProvider<
({
Pointer<TransactionSignMachineWrapper> machinePtr,
String preprocess,
})?>((ref) => null);
final pFrostContinueSignData = StateProvider<
({
Pointer<TransactionSignatureMachineWrapper> machinePtr,
String share,
})?>((ref) => null);
// ===================== shared/util ===========================================
final pFrostSelectParticipantsUnordered =
StateProvider<List<String>?>((ref) => null);
// ========================= resharing =========================================
final pFrostResharingData = Provider((ref) => _ResharingData());
class _ResharingData {
String? myName;
IncompleteFrostWallet? incompleteWallet;
// resharer encoded config string
String? resharerConfig;
({
int newThreshold,
List<int> resharers,
List<String> newParticipants,
})? get configData => resharerConfig != null
? Frost.extractResharerConfigData(resharerConfig: resharerConfig!)
: null;
// resharer start string (for sharing) and machine
({
String resharerStart,
Pointer<StartResharerRes> machine,
})? startResharerData;
// reshared start string (for sharing) and machine
({
String resharedStart,
Pointer<StartResharedRes> prior,
})? startResharedData;
// resharer complete string (for sharing)
String? resharerComplete;
// new keys and config with an ID
({
String multisigConfig,
String serializedKeys,
String resharedId,
})? newWalletData;
// reset/clear all data
void reset() {
resharerConfig = null;
startResharerData = null;
startResharedData = null;
resharerComplete = null;
newWalletData = null;
incompleteWallet = null;
}
}

View file

@ -26,6 +26,13 @@ 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/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';
import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart';
@ -113,6 +120,19 @@ import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_pr
import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart';
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/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';
@ -423,6 +443,319 @@ class RouteGenerator {
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case CreateNewFrostMsWalletView.routeName:
if (args is ({
String walletName,
Coin coin,
})) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => CreateNewFrostMsWalletView(
walletName: args.walletName,
coin: args.coin,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case RestoreFrostMsWalletView.routeName:
if (args is ({
String walletName,
Coin coin,
})) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => RestoreFrostMsWalletView(
walletName: args.walletName,
coin: args.coin,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case ShareNewMultisigConfigView.routeName:
if (args is ({
String walletName,
Coin coin,
})) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => ShareNewMultisigConfigView(
walletName: args.walletName,
coin: args.coin,
),
settings: RouteSettings(
name: settings.name,
),
);
}
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 FrostMSWalletOptionsView.routeName:
if (args is String) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => FrostMSWalletOptionsView(
walletId: args,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case FrostParticipantsView.routeName:
if (args is String) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => FrostParticipantsView(
walletId: args,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case ImportReshareConfigView.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(
walletId: args,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case CompleteReshareConfigView.routeName:
if (args is ({String walletId, List<int> resharers})) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => CompleteReshareConfigView(
walletId: args.walletId,
resharers: args.resharers,
),
settings: RouteSettings(
name: settings.name,
),
);
}
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 MonkeyLoadedView.routeName:
// if (args is Tuple2<String, ChangeNotifierProvider<Manager>>) {
// return getRoute(

View file

@ -16,6 +16,13 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart';
final coinCardProvider = Provider.family<String?, Coin>((ref, coin) {
final assets = ref.watch(themeAssetsProvider);
// TODO: handle this differently by adding proper frost assets to themes
if (coin == Coin.bitcoinFrost) {
coin = Coin.bitcoin;
} else if (coin == Coin.bitcoinFrostTestNet) {
coin = Coin.bitcoinTestNet;
}
if (assets is ThemeAssetsV3) {
return assets.coinCardImages?[coin.mainNetVersion];
} else {
@ -26,6 +33,13 @@ final coinCardProvider = Provider.family<String?, Coin>((ref, coin) {
final coinCardFavoritesProvider = Provider.family<String?, Coin>((ref, coin) {
final assets = ref.watch(themeAssetsProvider);
// TODO: handle this differently by adding proper frost assets to themes
if (coin == Coin.bitcoinFrost) {
coin = Coin.bitcoin;
} else if (coin == Coin.bitcoinFrostTestNet) {
coin = Coin.bitcoinTestNet;
}
if (assets is ThemeAssetsV3) {
return assets.coinCardFavoritesImages?[coin.mainNetVersion] ??
assets.coinCardImages?[coin.mainNetVersion];

View file

@ -16,6 +16,13 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart';
final coinIconProvider = Provider.family<String, Coin>((ref, coin) {
final assets = ref.watch(themeAssetsProvider);
// TODO: handle this differently by adding proper frost assets to themes
if (coin == Coin.bitcoinFrost) {
coin = Coin.bitcoin;
} else if (coin == Coin.bitcoinFrostTestNet) {
coin = Coin.bitcoinTestNet;
}
if (assets is ThemeAssets) {
switch (coin) {
case Coin.bitcoin:

View file

@ -16,6 +16,13 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart';
final coinImageProvider = Provider.family<String, Coin>((ref, coin) {
final assets = ref.watch(themeAssetsProvider);
// TODO: handle this differently by adding proper frost assets to themes
if (coin == Coin.bitcoinFrost) {
coin = Coin.bitcoin;
} else if (coin == Coin.bitcoinFrostTestNet) {
coin = Coin.bitcoinTestNet;
}
if (assets is ThemeAssets) {
switch (coin) {
case Coin.bitcoin:
@ -64,6 +71,13 @@ final coinImageProvider = Provider.family<String, Coin>((ref, coin) {
final coinImageSecondaryProvider = Provider.family<String, Coin>((ref, coin) {
final assets = ref.watch(themeAssetsProvider);
// TODO: handle this differently by adding proper frost assets to themes
if (coin == Coin.bitcoinFrost) {
coin = Coin.bitcoin;
} else if (coin == Coin.bitcoinFrostTestNet) {
coin = Coin.bitcoinTestNet;
}
if (assets is ThemeAssets) {
switch (coin) {
case Coin.bitcoin:

View file

@ -297,6 +297,17 @@ extension CoinExt on Coin {
}
}
bool get isFrost {
switch (this) {
case Coin.bitcoinFrost:
case Coin.bitcoinFrostTestNet:
return true;
default:
return false;
}
}
Coin get mainNetVersion {
switch (this) {
case Coin.bitcoin:
@ -495,6 +506,15 @@ Coin coinFromPrettyName(String name) {
case "tStellar":
return Coin.stellarTestnet;
case "Bitcoin Frost":
case "bitcoinFrost":
return Coin.bitcoinFrost;
case "Bitcoin Frost Testnet":
case "tBitcoin Frost":
case "bitcoinFrostTestNet":
return Coin.bitcoinFrostTestNet;
default:
throw ArgumentError.value(
name,

View file

@ -0,0 +1,42 @@
import 'package:stackwallet/db/isar/main_db.dart';
import 'package:stackwallet/services/node_service.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/utilities/prefs.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';
import 'package:stackwallet/wallets/wallet/wallet.dart';
class IncompleteFrostWallet {
WalletInfo? info;
String? get walletId => info?.walletId;
Future<BitcoinFrostWallet> toBitcoinFrostWallet({
required MainDB mainDB,
required SecureStorageInterface secureStorageInterface,
required NodeService nodeService,
required Prefs prefs,
}) async {
final wallet = await Wallet.create(
walletInfo: info!,
mainDB: mainDB,
secureStorageInterface: secureStorageInterface,
nodeService: nodeService,
prefs: prefs,
);
// dummy entry so updaters work when `wallet.updateWithResharedData` is called
final frostInfo = FrostWalletInfo(
walletId: info!.walletId,
knownSalts: [],
participants: [],
myName: "",
threshold: -1,
);
await mainDB.isar.frostWalletInfo.put(frostInfo);
return wallet as BitcoinFrostWallet;
}
}

View file

@ -171,7 +171,7 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
}
}
final serializedKeys = await _getSerializedKeys();
final serializedKeys = await getSerializedKeys();
final keys = frost.deserializeKeys(keys: serializedKeys!);
final int network = cryptoCurrency.network == CryptoCurrencyNetwork.main
@ -213,7 +213,7 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
final int network = cryptoCurrency.network == CryptoCurrencyNetwork.main
? Network.Mainnet
: Network.Testnet;
final serializedKeys = await _getSerializedKeys();
final serializedKeys = await getSerializedKeys();
return Frost.attemptSignConfig(
network: network,
@ -859,7 +859,7 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
// =================== Secure storage ========================================
Future<String?> _getSerializedKeys() async =>
Future<String?> getSerializedKeys() async =>
await secureStorageInterface.read(
key: "{$walletId}_serializedFROSTKeys",
);
@ -867,7 +867,7 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
Future<void> _saveSerializedKeys(
String keys,
) async {
final current = await _getSerializedKeys();
final current = await getSerializedKeys();
if (current == null) {
// do nothing

View file

@ -0,0 +1,90 @@
import 'package:flutter/material.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/conditional_parent.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class DetailItem extends StatelessWidget {
const DetailItem({
Key? key,
required this.title,
required this.detail,
this.button,
this.showEmptyDetail = true,
this.disableSelectableText = false,
}) : super(key: key);
final String title;
final String detail;
final Widget? button;
final bool showEmptyDetail;
final bool disableSelectableText;
@override
Widget build(BuildContext context) {
return ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => RoundedWhiteContainer(
child: child,
),
child: ConditionalParent(
condition: Util.isDesktop,
builder: (child) => Padding(
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<StackColors>()!
.textSubtitle3,
),
)
: SelectableText(
"$title will appear here",
style: STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle3,
),
)
: disableSelectableText
? Text(
detail,
style: STextStyles.w500_14(context),
)
: SelectableText(
detail,
style: STextStyles.w500_14(context),
),
],
),
),
);
}
}

View file

@ -0,0 +1,70 @@
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,
),
);
},
),
);
}
}

View file

@ -147,8 +147,10 @@ class StackOkDialog extends StatelessWidget {
this.icon,
required this.title,
this.message,
this.desktopPopRootNavigator = false,
}) : super(key: key);
final bool desktopPopRootNavigator;
final Widget? leftButton;
final void Function(String)? onOkPressed;
@ -208,9 +210,13 @@ class StackOkDialog extends StatelessWidget {
onOkPressed?.call("OK");
}
: () {
int count = 0;
Navigator.of(context).popUntil((_) => count++ >= 2);
// onOkPressed?.call("OK");
if (desktopPopRootNavigator) {
Navigator.of(context, rootNavigator: true).pop();
} else {
int count = 0;
Navigator.of(context).popUntil((_) => count++ >= 2);
// onOkPressed?.call("OK");
}
},
style: Theme.of(context)
.extension<StackColors>()!