rough gui refactor for frost wallet creation on mobile

This commit is contained in:
julian 2024-04-25 15:54:40 -06:00
parent 3e74683d6c
commit 3c76cc115c
29 changed files with 2030 additions and 1406 deletions

View file

@ -51,12 +51,12 @@ class ClientManager {
if (_map[key] == null) {
throw Exception(
"No managed ElectrumClient for $cryptoCurrency found.",
"No managed ElectrumClient for $key found.",
);
}
if (_heightCompleters[key] == null) {
throw Exception(
"No managed _heightCompleters for $cryptoCurrency found.",
"No managed _heightCompleters for $key found.",
);
}

View file

@ -0,0 +1,177 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
import 'package:stackwallet/widgets/progress_bar.dart';
class FrostStepScaffold extends ConsumerStatefulWidget {
const FrostStepScaffold({super.key});
static const String routeName = "/frostStepScaffold";
@override
ConsumerState<FrostStepScaffold> createState() => _FrostScaffoldState();
}
class _FrostScaffoldState extends ConsumerState<FrostStepScaffold> {
static const _titleTextSize = 18.0;
final _navigatorKey = GlobalKey<NavigatorState>();
late final List<FrostStepRoute> _routes;
bool _requestPopLock = false;
Future<void> _requestPop(BuildContext context) async {
if (_requestPopLock) {
return;
}
_requestPopLock = true;
// TODO: dialog to confirm exit
// make sure to at least delay some time otherwise flutter pops back more than a single route lol...
await Future<void>.delayed(const Duration(milliseconds: 200));
if (context.mounted) {
Navigator.of(context).pop();
ref.read(pFrostCreateNewArgs.state).state = null;
}
_requestPopLock = false;
}
@override
void initState() {
_routes = ref.read(pFrostCreateNewArgs)!.$2;
super.initState();
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
onPopInvoked: (_) => _requestPop(context),
child: Material(
child: ConditionalParent(
condition: Util.isDesktop,
builder: (child) => child,
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
body: SafeArea(
child: child,
),
),
),
child: Column(
children: [
// header
SizedBox(
height: 56,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
),
child: Row(
children: [
Text(
"${ref.watch(pFrostCreateCurrentStep)} / ${_routes.length}",
style: STextStyles.navBarTitle(context).copyWith(
fontSize: _titleTextSize,
color: Theme.of(context)
.extension<StackColors>()!
.customTextButtonEnabledText,
),
),
const SizedBox(
width: 10,
),
Expanded(
child: Text(
_routes[ref.watch(pFrostCreateCurrentStep) - 1]
.title,
style: STextStyles.navBarTitle(context).copyWith(
fontSize: _titleTextSize,
),
),
),
const SizedBox(
width: 10,
),
CustomTextButton(
text: "Exit",
textSize: _titleTextSize,
onTap: () => _requestPop(context),
),
],
),
),
),
LayoutBuilder(
builder: (subContext, constraints) => ProgressBar(
width: constraints.maxWidth,
height: 3,
fillColor: Theme.of(context)
.extension<StackColors>()!
.customTextButtonEnabledText,
backgroundColor: Theme.of(context)
.extension<StackColors>()!
.customTextButtonEnabledText
.withOpacity(0.1),
percent:
ref.watch(pFrostCreateCurrentStep) / _routes.length,
),
),
Expanded(
child: ConditionalParent(
condition: Util.isDesktop,
builder: (child) => Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Flexible(
child: SizedBox(
width: 500,
child: child,
),
)
],
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: child,
),
),
);
},
),
child: Navigator(
key: _navigatorKey,
initialRoute: _routes[0].routeName,
onGenerateRoute: FrostRouteGenerator.generateRoute,
),
),
),
),
],
),
),
),
),
);
}
}

View file

@ -1,337 +0,0 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/home_view/home_view.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart';
import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/providers/global/node_service_provider.dart';
import 'package:stackwallet/providers/global/prefs_provider.dart';
import 'package:stackwallet/providers/global/secure_store_provider.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/crypto_currency/intermediate/frost_currency.dart';
import 'package:stackwallet/wallets/isar/models/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/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/frost_mascot.dart';
import 'package:stackwallet/widgets/loading_indicator.dart';
class ConfirmNewFrostMSWalletCreationView extends ConsumerStatefulWidget {
const ConfirmNewFrostMSWalletCreationView({
super.key,
required this.walletName,
required this.frostCurrency,
});
static const String routeName = "/confirmNewFrostMSWalletCreationView";
final String walletName;
final FrostCurrency frostCurrency;
@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,
),
);
},
),
// TODO: [prio=high] get rid of placeholder text??
trailing: const FrostMascot(
title: 'Lorem ipsum',
body:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ',
),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<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.frostCurrency.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

@ -11,10 +11,13 @@ import 'package:stackwallet/wallets/crypto_currency/intermediate/frost_currency.
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/dialogs/simple_mobile_dialog.dart';
import 'package:stackwallet/widgets/frost_mascot.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
class CreateNewFrostMsWalletView extends ConsumerStatefulWidget {
@ -67,13 +70,14 @@ class _NewFrostMsWalletViewState
}
final hasEmptyParticipants = controllers
.map((e) => e.text.isEmpty)
.map((e) => e.text.trim().isEmpty)
.reduce((value, element) => value |= element);
if (hasEmptyParticipants) {
return "Participants must not be empty";
}
if (controllers.length != controllers.map((e) => e.text).toSet().length) {
if (controllers.length !=
controllers.map((e) => e.text.trim()).toSet().length) {
return "Duplicate participant name found";
}
@ -102,6 +106,31 @@ class _NewFrostMsWalletViewState
}
}
void _showWhatIsThresholdDialog() {
showDialog<void>(
context: context,
builder: (_) => SimpleMobileDialog(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// TODO: [prio=high] need text from designers!
Text(
"What is a threshold?",
style: STextStyles.w600_20(context),
),
const SizedBox(
height: 12,
),
Text(
"Text here",
style: STextStyles.w400_16(context),
),
],
),
),
);
}
@override
void dispose() {
_thresholdController.dispose();
@ -146,7 +175,7 @@ class _NewFrostMsWalletViewState
},
),
title: Text(
"New FROST multisig config",
"Create new group",
style: STextStyles.navBarTitle(context),
),
),
@ -174,9 +203,21 @@ class _NewFrostMsWalletViewState
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Threshold",
style: STextStyles.label(context),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Threshold",
style: STextStyles.w500_14(context).copyWith(
color:
Theme.of(context).extension<StackColors>()!.textDark3,
),
),
CustomTextButton(
text: "What is a threshold?",
onTap: _showWhatIsThresholdDialog,
),
],
),
const SizedBox(
height: 10,
@ -185,22 +226,53 @@ class _NewFrostMsWalletViewState
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
controller: _thresholdController,
decoration: InputDecoration(
hintText: "Enter number of signatures",
hintStyle: STextStyles.fieldLabel(context),
),
),
const SizedBox(
height: 16,
),
Text(
"Number of participants",
style: STextStyles.label(context),
style: STextStyles.w500_14(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark3,
),
),
const SizedBox(
height: 10,
),
TextField(
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
controller: _participantsController,
onChanged: _participantsCountChanged,
Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: [
TextField(
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
controller: _participantsController,
onChanged: _participantsCountChanged,
decoration: InputDecoration(
hintText: "Enter number of participants",
hintStyle: STextStyles.fieldLabel(context),
),
),
const SizedBox(
height: 6,
),
Row(
children: [
Expanded(
child: RoundedWhiteContainer(
child: Text(
"Enter number of signatures required for fund management",
style: STextStyles.label(context),
),
),
),
],
),
],
),
const SizedBox(
height: 16,
@ -208,24 +280,75 @@ class _NewFrostMsWalletViewState
if (controllers.isNotEmpty)
Text(
"My name",
style: STextStyles.label(context),
style: STextStyles.w500_14(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark3,
),
),
if (controllers.isNotEmpty)
const SizedBox(
height: 10,
),
if (controllers.isNotEmpty)
TextField(
controller: controllers.first,
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: controllers.first,
decoration: InputDecoration(
hintText: "Enter your name",
hintStyle: STextStyles.fieldLabel(context),
),
),
const SizedBox(
height: 6,
),
Row(
children: [
Expanded(
child: RoundedWhiteContainer(
child: Text(
"Type your name in one word without spaces",
style: STextStyles.label(context),
),
),
),
],
),
],
),
if (controllers.length > 1)
const SizedBox(
height: 16,
),
if (controllers.length > 1)
Text(
"Remaining participants",
style: STextStyles.label(context),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Remaining participants",
style: STextStyles.w500_14(context).copyWith(
color:
Theme.of(context).extension<StackColors>()!.textDark3,
),
),
const SizedBox(
height: 6,
),
Row(
children: [
Expanded(
child: RoundedWhiteContainer(
child: Text(
"Type each name in one word without spaces",
style: STextStyles.label(context),
),
),
),
],
),
],
),
if (controllers.length > 1)
Column(
@ -237,6 +360,10 @@ class _NewFrostMsWalletViewState
),
child: TextField(
controller: controllers[i],
decoration: InputDecoration(
hintText: "Enter name",
hintStyle: STextStyles.fieldLabel(context),
),
),
),
],
@ -246,7 +373,7 @@ class _NewFrostMsWalletViewState
height: 16,
),
PrimaryButton(
label: "Generate",
label: "Create new group",
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
@ -265,12 +392,13 @@ class _NewFrostMsWalletViewState
}
final config = Frost.createMultisigConfig(
name: controllers.first.text,
name: controllers.first.text.trim(),
threshold: int.parse(_thresholdController.text),
participants: controllers.map((e) => e.text).toList(),
participants: controllers.map((e) => e.text.trim()).toList(),
);
ref.read(pFrostMyName.notifier).state = controllers.first.text;
ref.read(pFrostMyName.notifier).state =
controllers.first.text.trim();
ref.read(pFrostMultisigConfig.notifier).state = config;
await Navigator.of(context).pushNamed(

View file

@ -1,438 +0,0 @@
import 'package:barcode_scan2/barcode_scan2.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart';
import 'package:stackwallet/pages/home_view/home_view.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/crypto_currency/intermediate/frost_currency.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/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/frost_mascot.dart';
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
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.frostCurrency,
});
static const String routeName = "/frostShareCommitmentsView";
final String walletName;
final FrostCurrency frostCurrency;
@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,
),
);
},
),
// TODO: [prio=high] get rid of placeholder text??
trailing: const FrostMascot(
title: 'Lorem ipsum',
body:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ',
),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<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,
frostCurrency: widget.frostCurrency,
),
);
} 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

@ -1,404 +0,0 @@
import 'package:barcode_scan2/barcode_scan2.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart';
import 'package:stackwallet/pages/home_view/home_view.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/crypto_currency/intermediate/frost_currency.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/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/frost_mascot.dart';
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
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.frostCurrency,
});
static const String routeName = "/frostShareSharesView";
final String walletName;
final FrostCurrency frostCurrency;
@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,
),
);
},
),
// TODO: [prio=high] get rid of placeholder text??
trailing: const FrostMascot(
title: 'Lorem ipsum',
body:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ',
),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<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,
frostCurrency: widget.frostCurrency,
),
);
} 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

@ -1,11 +1,18 @@
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:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/frost_scaffold.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart';
import 'package:stackwallet/pages/home_view/home_view.dart';
import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
@ -374,12 +381,47 @@ class _ImportNewFrostMsWalletViewState
myName: ref.read(pFrostMyName.state).state!,
);
await Navigator.of(context).pushNamed(
FrostShareCommitmentsView.routeName,
arguments: (
ref.read(pFrostCreateNewArgs.state).state = (
(
walletName: widget.walletName,
frostCurrency: widget.frostCurrency,
),
FrostRouteGenerator.createNewConfigStepRoutes,
() {
// successful completion of steps
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;
ref.read(pFrostCreateNewArgs.state).state = null;
unawaited(
showFloatingFlushBar(
type: FlushBarType.success,
message: "Your wallet is set up.",
iconAsset: Assets.svg.check,
context: context,
),
);
}
);
await Navigator.of(context).pushNamed(
FrostStepScaffold.routeName,
);
},
),

View file

@ -14,7 +14,7 @@ import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/dialogs/simple_mobile_dialog.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class SelectNewFrostImportTypeView extends StatefulWidget {
@ -268,124 +268,70 @@ class _FrostJoinInfoDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SafeArea(
return SimpleMobileDialog(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
child: Padding(
padding: const EdgeInsets.all(16),
child: Material(
borderRadius: BorderRadius.circular(
20,
),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).extension<StackColors>()!.popupBG,
borderRadius: BorderRadius.circular(
20,
),
),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// TODO: [prio=high] need text from designers!
Text(
"Join a group",
style: STextStyles.w600_20(context),
),
const SizedBox(
height: 12,
),
Text(
"Text here",
style: STextStyles.w400_16(context),
),
const SizedBox(
height: 8,
),
Text(
"What is resharing?",
style: STextStyles.w600_16(context),
),
const SizedBox(
height: 8,
),
Text(
"In cryptocurrency, you are your own bank."
" Imagine keeping cash at home. If that cash"
" burns down or gets stolen, you lose it and"
" nobody will help you get your money back.",
style: STextStyles.w400_16(context),
),
const SizedBox(
height: 8,
),
Text(
"Since cryptocurrency is digital money, your "
"wallet key is like that “cash” you keep at "
"home. If you lose your phone or if you "
"forget your wallet PIN, but you have your "
"wallet key, your crypto money will be safe. "
"That is why you should keep your wallet key "
"safe.",
style: STextStyles.w400_16(context),
),
const SizedBox(
height: 12,
),
Text(
"Why write it down?",
style: STextStyles.w600_16(context),
),
const SizedBox(
height: 8,
),
Text(
"You do not put your cash on display, do you?"
" Keeping your wallet key on a digital device"
" is like having it on display for thieves - "
"malicious software and hackers. Write your "
"wallet key down on paper in multiple copies "
"and keep them in a real, physical safe.",
style: STextStyles.w400_16(context),
),
],
),
),
),
const SizedBox(
height: 16,
),
Row(
children: [
const Spacer(),
const SizedBox(
width: 16,
),
Expanded(
child: SecondaryButton(
label: "Close",
onPressed: Navigator.of(context).pop,
),
),
],
),
],
),
),
),
),
),
// TODO: [prio=high] need text from designers!
Text(
"Join a group",
style: STextStyles.w600_20(context),
),
const SizedBox(
height: 12,
),
Text(
"Text here",
style: STextStyles.w400_16(context),
),
const SizedBox(
height: 8,
),
Text(
"What is resharing?",
style: STextStyles.w600_16(context),
),
const SizedBox(
height: 8,
),
Text(
"In cryptocurrency, you are your own bank."
" Imagine keeping cash at home. If that cash"
" burns down or gets stolen, you lose it and"
" nobody will help you get your money back.",
style: STextStyles.w400_16(context),
),
const SizedBox(
height: 8,
),
Text(
"Since cryptocurrency is digital money, your "
"wallet key is like that “cash” you keep at "
"home. If you lose your phone or if you "
"forget your wallet PIN, but you have your "
"wallet key, your crypto money will be safe. "
"That is why you should keep your wallet key "
"safe.",
style: STextStyles.w400_16(context),
),
const SizedBox(
height: 12,
),
Text(
"Why write it down?",
style: STextStyles.w600_16(context),
),
const SizedBox(
height: 8,
),
Text(
"You do not put your cash on display, do you?"
" Keeping your wallet key on a digital device"
" is like having it on display for thieves - "
"malicious software and hackers. Write your "
"wallet key down on paper in multiple copies "
"and keep them in a real, physical safe.",
style: STextStyles.w400_16(context),
),
],
),

View file

@ -1,11 +1,19 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/frost_scaffold.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart';
import 'package:stackwallet/pages/home_view/home_view.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/crypto_currency/intermediate/frost_currency.dart';
@ -16,8 +24,11 @@ import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/detail_item.dart';
import 'package:stackwallet/widgets/dialogs/simple_mobile_dialog.dart';
import 'package:stackwallet/widgets/frost_mascot.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class ShareNewMultisigConfigView extends ConsumerStatefulWidget {
const ShareNewMultisigConfigView({
@ -38,6 +49,114 @@ class ShareNewMultisigConfigView extends ConsumerStatefulWidget {
class _ShareNewMultisigConfigViewState
extends ConsumerState<ShareNewMultisigConfigView> {
bool _userVerifyContinue = false;
void _showParticipantsDialog() {
final participants = Frost.getParticipants(
multisigConfig: ref.read(pFrostMultisigConfig.state).state!,
);
showDialog<void>(
context: context,
builder: (_) => SimpleMobileDialog(
showCloseButton: false,
padding: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 24,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Text(
"Group participants",
style: STextStyles.w600_20(context),
),
),
const SizedBox(
height: 12,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Text(
"The names are case-sensitive and must be entered exactly.",
style: STextStyles.w400_16(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark3,
),
),
),
const SizedBox(
height: 12,
),
for (final participant in participants)
Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: double.infinity,
height: 1.5,
color:
Theme.of(context).extension<StackColors>()!.background,
),
const SizedBox(
height: 12,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Row(
children: [
Container(
width: 26,
height: 26,
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveBG,
borderRadius: BorderRadius.circular(
200,
),
),
child: Center(
child: SvgPicture.asset(
Assets.svg.user,
width: 16,
height: 16,
),
),
),
const SizedBox(
width: 8,
),
Expanded(
child: Text(
participant,
style: STextStyles.w500_14(context),
),
),
const SizedBox(
width: 8,
),
IconCopyButton(
data: participant,
),
],
),
),
const SizedBox(
height: 12,
),
],
),
const SizedBox(
height: 24,
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return ConditionalParent(
@ -72,7 +191,7 @@ class _ShareNewMultisigConfigViewState
},
),
title: Text(
"Multisig config",
"Share multisig group info",
style: STextStyles.navBarTitle(context),
),
),
@ -99,7 +218,10 @@ class _ShareNewMultisigConfigViewState
),
child: Column(
children: [
if (!Util.isDesktop) const Spacer(),
const _SharingStepsInfo(),
const SizedBox(
height: 20,
),
SizedBox(
height: 220,
child: Row(
@ -119,7 +241,7 @@ class _ShareNewMultisigConfigViewState
),
),
const SizedBox(
height: 32,
height: 20,
),
DetailItem(
title: "Encoded config",
@ -137,12 +259,64 @@ class _ShareNewMultisigConfigViewState
SizedBox(
height: Util.isDesktop ? 64 : 16,
),
Row(
children: [
Expanded(
child: SecondaryButton(
label: "Show group participants",
onPressed: _showParticipantsDialog,
),
),
],
),
if (!Util.isDesktop)
const Spacer(
flex: 2,
),
const SizedBox(
height: 16,
),
GestureDetector(
onTap: () {
setState(() {
_userVerifyContinue = !_userVerifyContinue;
});
},
child: Container(
color: Colors.transparent,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 20,
height: 26,
child: Checkbox(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
value: _userVerifyContinue,
onChanged: (value) => setState(
() => _userVerifyContinue = value == true,
),
),
),
const SizedBox(
width: 12,
),
Expanded(
child: Text(
"I have verified that everyone has joined the group",
style: STextStyles.w500_14(context),
),
),
],
),
),
),
const SizedBox(
height: 16,
),
PrimaryButton(
label: "Start key generation",
enabled: _userVerifyContinue,
onPressed: () async {
ref.read(pFrostStartKeyGenData.notifier).state =
Frost.startKeyGeneration(
@ -150,12 +324,48 @@ class _ShareNewMultisigConfigViewState
myName: ref.read(pFrostMyName.state).state!,
);
await Navigator.of(context).pushNamed(
FrostShareCommitmentsView.routeName,
arguments: (
ref.read(pFrostCreateNewArgs.state).state = (
(
walletName: widget.walletName,
frostCurrency: widget.frostCurrency,
),
FrostRouteGenerator.createNewConfigStepRoutes,
() {
// successful completion of steps
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;
ref.read(pFrostCreateNewArgs.state).state = null;
unawaited(
showFloatingFlushBar(
type: FlushBarType.success,
message: "Your wallet is set up.",
iconAsset: Assets.svg.check,
context: context,
),
);
}
);
await Navigator.of(context).pushNamed(
FrostStepScaffold.routeName,
// FrostShareCommitmentsView.routeName,
);
},
),
@ -165,3 +375,45 @@ class _ShareNewMultisigConfigViewState
);
}
}
class _SharingStepsInfo extends StatelessWidget {
const _SharingStepsInfo({super.key});
static const steps = [
"Share this config with the group participants.",
"Wait for them to join the group.",
"Verify that everyone has filled out their forms before continuing. If you "
"try to continue before everyone is ready, the process will be canceled.",
"Check the box and press “Generate keys”.",
];
@override
Widget build(BuildContext context) {
final style = STextStyles.w500_12(context);
return RoundedWhiteContainer(
child: Column(
children: [
for (int i = 0; i < steps.length; i++)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${i + 1}.",
style: style,
),
const SizedBox(
width: 4,
),
Expanded(
child: Text(
steps[i],
style: style,
),
),
],
),
],
),
);
}
}

View file

@ -0,0 +1,370 @@
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:flutter_svg/flutter_svg.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.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/custom_buttons/simple_copy_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/detail_item.dart';
import 'package:stackwallet/widgets/dialogs/frost/frost_step_qr_dialog.dart';
import 'package:stackwallet/widgets/frost_step_user_steps.dart';
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
class FrostCreateStep1 extends ConsumerStatefulWidget {
const FrostCreateStep1({
super.key,
});
static const String routeName = "/frostCreateStep1";
static const String title = "Commitments";
@override
ConsumerState<FrostCreateStep1> createState() => _FrostCreateStep1State();
}
class _FrostCreateStep1State extends ConsumerState<FrostCreateStep1> {
static const info = [
"Share your commitment with other group members.",
"Enter their commitments into the corresponding fields.",
];
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 = [];
bool _userVerifyContinue = false;
Future<void> _showQrCodeDialog() async {
await showDialog<void>(
context: context,
builder: (_) => FrostStepQrDialog(
myName: ref.read(pFrostMyName)!,
title: "Step 1 of 4 - ${FrostCreateStep1.title}",
data: myCommitment,
),
);
}
@override
void initState() {
participants = Frost.getParticipants(
multisigConfig: ref.read(pFrostMultisigConfig.state).state!,
);
myIndex = participants.indexOf(ref.read(pFrostMyName.state).state!);
myCommitment = ref.read(pFrostStartKeyGenData.state).state!.commitments;
// temporarily remove my name
participants.removeAt(myIndex);
for (int i = 0; i < participants.length; i++) {
controllers.add(TextEditingController());
focusNodes.add(FocusNode());
fieldIsEmptyFlags.add(true);
}
super.initState();
}
@override
void dispose() {
for (int i = 0; i < controllers.length; i++) {
controllers[i].dispose();
}
for (int i = 0; i < focusNodes.length; i++) {
focusNodes[i].dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
const FrostStepUserSteps(
userSteps: info,
),
const SizedBox(height: 12),
DetailItem(
title: "My name",
detail: ref.watch(pFrostMyName.state).state!,
),
const SizedBox(height: 12),
DetailItem(
title: "My commitment",
detail: myCommitment,
button: Util.isDesktop
? IconCopyButton(
data: myCommitment,
)
: SimpleCopyButton(
data: myCommitment,
),
),
const SizedBox(height: 12),
SecondaryButton(
label: "View QR code",
icon: SvgPicture.asset(
Assets.svg.qrcode,
colorFilter: ColorFilter.mode(
Theme.of(context).extension<StackColors>()!.buttonTextSecondary,
BlendMode.srcIn,
),
),
onPressed: _showQrCodeDialog,
),
const SizedBox(height: 12),
for (int i = 0; i < participants.length; i++)
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 12,
),
Text(
participants[i],
style: STextStyles.w500_14(context),
),
const SizedBox(
height: 4,
),
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 SizedBox(height: 12),
GestureDetector(
onTap: () {
setState(() {
_userVerifyContinue = !_userVerifyContinue;
});
},
child: Container(
color: Colors.transparent,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 20,
height: 26,
child: Checkbox(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
value: _userVerifyContinue,
onChanged: (value) => setState(
() => _userVerifyContinue = value == true,
),
),
),
const SizedBox(
width: 12,
),
Expanded(
child: Text(
"I have verified that everyone has all commitments",
style: STextStyles.w500_14(context),
),
),
],
),
),
),
const SizedBox(height: 12),
PrimaryButton(
label: "Generate shares",
enabled: _userVerifyContinue &&
!fieldIsEmptyFlags.reduce((v, e) => v |= e),
onPressed: () async {
// check for empty commitments
if (controllers
.map((e) => e.text.isEmpty)
.reduce((value, element) => value |= element)) {
return await showDialog<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,
);
ref.read(pFrostCreateCurrentStep.state).state = 2;
await Navigator.of(context).pushNamed(
FrostCreateStep2.routeName,
);
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
if (context.mounted) {
return await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: "Failed to generate shares",
message: e.toString(),
desktopPopRootNavigator: Util.isDesktop,
),
);
}
}
},
),
],
),
);
}
}

View file

@ -0,0 +1,320 @@
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:flutter_svg/flutter_svg.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.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/custom_buttons/simple_copy_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/detail_item.dart';
import 'package:stackwallet/widgets/dialogs/frost/frost_step_qr_dialog.dart';
import 'package:stackwallet/widgets/frost_step_user_steps.dart';
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
class FrostCreateStep2 extends ConsumerStatefulWidget {
const FrostCreateStep2({super.key});
static const String routeName = "/frostCreateStep2";
static const String title = "Shares";
@override
ConsumerState<FrostCreateStep2> createState() => _FrostCreateStep2State();
}
class _FrostCreateStep2State extends ConsumerState<FrostCreateStep2> {
static const info = [
"Send your share to other group members.",
"Enter their shares into the corresponding fields.",
];
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 = [];
Future<void> _showQrCodeDialog() async {
await showDialog<void>(
context: context,
builder: (_) => FrostStepQrDialog(
myName: ref.read(pFrostMyName)!,
title: "Step 2 of 4 - ${FrostCreateStep2.title}",
data: myShare,
),
);
}
@override
void initState() {
participants = Frost.getParticipants(
multisigConfig: ref.read(pFrostMultisigConfig.state).state!,
);
myIndex = participants.indexOf(ref.read(pFrostMyName.state).state!);
myShare = ref.read(pFrostSecretSharesData.state).state!.share;
// temporarily remove my name. Added back later
participants.removeAt(myIndex);
for (int i = 0; i < participants.length; i++) {
controllers.add(TextEditingController());
focusNodes.add(FocusNode());
fieldIsEmptyFlags.add(true);
}
super.initState();
}
@override
void dispose() {
for (int i = 0; i < controllers.length; i++) {
controllers[i].dispose();
}
for (int i = 0; i < focusNodes.length; i++) {
focusNodes[i].dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
const FrostStepUserSteps(
userSteps: info,
),
const SizedBox(height: 12),
DetailItem(
title: "My name",
detail: ref.watch(pFrostMyName.state).state!,
),
const SizedBox(height: 12),
DetailItem(
title: "My share",
detail: myShare,
button: Util.isDesktop
? IconCopyButton(
data: myShare,
)
: SimpleCopyButton(
data: myShare,
),
),
const SizedBox(height: 12),
SecondaryButton(
label: "View QR code",
icon: SvgPicture.asset(
Assets.svg.qrcode,
colorFilter: ColorFilter.mode(
Theme.of(context).extension<StackColors>()!.buttonTextSecondary,
BlendMode.srcIn,
),
),
onPressed: _showQrCodeDialog,
),
const SizedBox(height: 12),
for (int i = 0; i < participants.length; i++)
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 12,
),
Text(
participants[i],
style: STextStyles.w500_14(context),
),
const SizedBox(
height: 4,
),
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 SizedBox(height: 12),
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,
);
ref.read(pFrostCreateCurrentStep.state).state = 3;
await Navigator.of(context).pushNamed(
FrostCreateStep3.routeName,
);
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
if (context.mounted) {
return await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: "Failed to complete key generation",
desktopPopRootNavigator: Util.isDesktop,
),
);
}
}
},
),
],
),
);
}
}

View file

@ -0,0 +1,75 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_4.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/detail_item.dart';
import 'package:stackwallet/widgets/frost_step_user_steps.dart';
class FrostCreateStep3 extends ConsumerStatefulWidget {
const FrostCreateStep3({super.key});
static const String routeName = "/frostCreateStep3";
static const String title = "Verify multisig ID";
@override
ConsumerState<FrostCreateStep3> createState() => _FrostCreateStep3State();
}
class _FrostCreateStep3State extends ConsumerState<FrostCreateStep3> {
static const info = [
"Ensure your multisig ID matches that of each other participant.",
];
late final Uint8List multisigId;
@override
void initState() {
multisigId = ref.read(pFrostCompletedKeyGenData.state).state!.multisigId;
super.initState();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
const FrostStepUserSteps(
userSteps: info,
),
const SizedBox(height: 12),
DetailItem(
title: "Multisig ID",
detail: multisigId.toString(),
button: Util.isDesktop
? IconCopyButton(
data: multisigId.toString(),
)
: SimpleCopyButton(
data: multisigId.toString(),
),
),
if (!Util.isDesktop) const Spacer(),
const SizedBox(height: 12),
PrimaryButton(
label: "Confirm",
onPressed: () {
ref.read(pFrostCreateCurrentStep.state).state = 4;
Navigator.of(context).pushNamed(
FrostCreateStep4.routeName,
);
},
)
],
),
);
}
}

View file

@ -0,0 +1,231 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/tx_v2/transaction_v2_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/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/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/isar/models/wallet_info.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
import 'package:stackwallet/wallets/wallet/wallet.dart';
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/detail_item.dart';
import 'package:stackwallet/widgets/loading_indicator.dart';
import 'package:stackwallet/widgets/rounded_container.dart';
class FrostCreateStep4 extends ConsumerStatefulWidget {
const FrostCreateStep4({super.key});
static const String routeName = "/frostCreateStep4";
static const String title = "Back up your keys";
@override
ConsumerState<FrostCreateStep4> createState() => _FrostCreateStep4State();
}
class _FrostCreateStep4State extends ConsumerState<FrostCreateStep4> {
static const _warning = "These are your private keys. Please back them up, "
"keep them safe and never share it with anyone. Your private keys are the"
" only way you can access your funds if you forget PIN, lose your phone, "
"etc. Stack Wallet does not keep nor is able to restore your private keys"
".";
late final String seed, recoveryString, serializedKeys, multisigConfig;
late final Uint8List multisigId;
bool _userVerifyContinue = false;
@override
void initState() {
seed = ref.read(pFrostStartKeyGenData.state).state!.seed;
serializedKeys =
ref.read(pFrostCompletedKeyGenData.state).state!.serializedKeys;
recoveryString =
ref.read(pFrostCompletedKeyGenData.state).state!.recoveryString;
multisigConfig = ref.read(pFrostMultisigConfig.state).state!;
multisigId = ref.read(pFrostCompletedKeyGenData.state).state!.multisigId;
super.initState();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
RoundedContainer(
color:
Theme.of(context).extension<StackColors>()!.warningBackground,
child: Text(
_warning,
style: STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.warningForeground,
),
),
),
const SizedBox(height: 12),
DetailItem(
title: "Multisig Config",
detail: multisigConfig,
button: Util.isDesktop
? IconCopyButton(
data: multisigConfig,
)
: SimpleCopyButton(
data: multisigConfig,
),
),
const SizedBox(height: 12),
DetailItem(
title: "Keys",
detail: serializedKeys,
button: Util.isDesktop
? IconCopyButton(
data: serializedKeys,
)
: SimpleCopyButton(
data: serializedKeys,
),
),
if (!Util.isDesktop) const Spacer(),
const SizedBox(height: 12),
GestureDetector(
onTap: () {
setState(() {
_userVerifyContinue = !_userVerifyContinue;
});
},
child: Container(
color: Colors.transparent,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 20,
height: 26,
child: Checkbox(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
value: _userVerifyContinue,
onChanged: (value) => setState(
() => _userVerifyContinue = value == true,
),
),
),
const SizedBox(
width: 12,
),
Expanded(
child: Text(
"I have backed up my keys and the config",
style: STextStyles.w500_14(context),
),
),
],
),
),
),
const SizedBox(height: 12),
PrimaryButton(
label: "Continue",
enabled: _userVerifyContinue,
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 data = ref.read(pFrostCreateNewArgs)!;
final info = WalletInfo.createNew(
coin: data.$1.frostCurrency.coin,
name: data.$1.walletName,
);
final wallet = await Wallet.create(
walletInfo: info,
mainDB: ref.read(mainDBProvider),
secureStorageInterface: ref.read(secureStoreProvider),
nodeService: ref.read(nodeServiceChangeNotifierProvider),
prefs: ref.read(prefsChangeNotifierProvider),
mnemonic: seed,
mnemonicPassphrase: "",
);
await (wallet as BitcoinFrostWallet).initializeNewFrost(
multisigConfig: multisigConfig,
recoveryString: recoveryString,
serializedKeys: serializedKeys,
multisigId: multisigId,
myName: ref.read(pFrostMyName.state).state!,
participants: Frost.getParticipants(
multisigConfig: ref.read(pFrostMultisigConfig.state).state!,
),
threshold: Frost.getThreshold(
multisigConfig: ref.read(pFrostMultisigConfig.state).state!,
),
);
await info.setMnemonicVerified(
isar: ref.read(mainDBProvider).isar,
);
ref.read(pWallets).addWallet(wallet);
// pop progress dialog
if (context.mounted) {
Navigator.pop(context);
progressPopped = true;
}
if (mounted) {
ref.read(pFrostCreateNewArgs)!.$3();
}
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
// pop progress dialog
if (context.mounted && !progressPopped) {
Navigator.pop(context);
progressPopped = true;
}
// TODO: handle gracefully
rethrow;
}
},
),
],
),
);
}
}

View file

@ -0,0 +1,78 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_4.dart';
import 'package:stackwallet/route_generator.dart';
import 'package:stackwallet/wallets/crypto_currency/intermediate/frost_currency.dart';
typedef FrostStepRoute = ({String routeName, String title});
final pFrostCreateCurrentStep = StateProvider.autoDispose((ref) => 1);
final pFrostCreateNewArgs = StateProvider<
(
({String walletName, FrostCurrency frostCurrency}),
List<FrostStepRoute>,
VoidCallback,
)?>((ref) => null);
abstract class FrostRouteGenerator {
static const bool useMaterialPageRoute = true;
static const List<FrostStepRoute> createNewConfigStepRoutes = [
(routeName: FrostCreateStep1.routeName, title: FrostCreateStep1.title),
(routeName: FrostCreateStep2.routeName, title: FrostCreateStep2.title),
(routeName: FrostCreateStep3.routeName, title: FrostCreateStep3.title),
(routeName: FrostCreateStep4.routeName, title: FrostCreateStep4.title),
];
static Route<dynamic> generateRoute(RouteSettings settings) {
final args = settings.arguments;
switch (settings.name) {
case FrostCreateStep1.routeName:
return RouteGenerator.getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => const FrostCreateStep1(),
settings: settings,
);
case FrostCreateStep2.routeName:
return RouteGenerator.getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => const FrostCreateStep2(),
settings: settings,
);
case FrostCreateStep3.routeName:
return RouteGenerator.getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => const FrostCreateStep3(),
settings: settings,
);
case FrostCreateStep4.routeName:
return RouteGenerator.getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => const FrostCreateStep4(),
settings: settings,
);
default:
return _routeError("");
}
}
static Route<dynamic> _routeError(String message) {
return RouteGenerator.getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => Placeholder(
child: Center(
child: Text(message),
),
),
);
}
}

View file

@ -413,7 +413,10 @@ class _NameYourWalletViewState extends ConsumerState<NameYourWalletView> {
CreateNewFrostMsWalletView.routeName,
arguments: (
walletName: name,
coin: coin,
// TODO: [prio=med] this will cause issues if frost is ever applied to other coins
frostCurrency: coin.isTestNet
? BitcoinFrost(CryptoCurrencyNetwork.test)
: BitcoinFrost(CryptoCurrencyNetwork.main),
),
);
},

View file

@ -24,7 +24,7 @@ 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/dialogs/frost/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';

View file

@ -24,7 +24,7 @@ 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/dialogs/frost/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';

View file

@ -24,7 +24,7 @@ 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/dialogs/frost/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';

View file

@ -17,7 +17,7 @@ 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/dialogs/frost/frost_interruption_dialog.dart';
import 'package:stackwallet/widgets/frost_mascot.dart';
class NewContinueSharingView extends ConsumerStatefulWidget {

View file

@ -18,7 +18,7 @@ import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart';
import 'package:stackwallet/widgets/dialogs/frost/frost_interruption_dialog.dart';
import 'package:stackwallet/widgets/frost_mascot.dart';
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';

View file

@ -26,7 +26,7 @@ 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/dialogs/frost/frost_interruption_dialog.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
class VerifyUpdatedWalletView extends ConsumerStatefulWidget {

View file

@ -26,10 +26,8 @@ import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_custom_tok
import 'package:stackwallet/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart';
import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart';
import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/frost_scaffold.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/select_new_frost_import_type_view.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart';
@ -585,59 +583,14 @@ class RouteGenerator {
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case FrostShareCommitmentsView.routeName:
if (args is ({
String walletName,
FrostCurrency frostCurrency,
})) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => FrostShareCommitmentsView(
walletName: args.walletName,
frostCurrency: args.frostCurrency,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case FrostShareSharesView.routeName:
if (args is ({
String walletName,
FrostCurrency frostCurrency,
})) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => FrostShareSharesView(
walletName: args.walletName,
frostCurrency: args.frostCurrency,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case ConfirmNewFrostMSWalletCreationView.routeName:
if (args is ({
String walletName,
FrostCurrency frostCurrency,
})) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => ConfirmNewFrostMSWalletCreationView(
walletName: args.walletName,
frostCurrency: args.frostCurrency,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case FrostStepScaffold.routeName:
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => const FrostStepScaffold(),
settings: RouteSettings(
name: settings.name,
),
);
case FrostMSWalletOptionsView.routeName:
if (args is String) {

View file

@ -12,9 +12,9 @@ class BitcoinFrost extends FrostCurrency {
BitcoinFrost(super.network) {
switch (network) {
case CryptoCurrencyNetwork.main:
coin = Coin.bitcoin;
coin = Coin.bitcoinFrost;
case CryptoCurrencyNetwork.test:
coin = Coin.bitcoinTestNet;
coin = Coin.bitcoinFrostTestNet;
default:
throw Exception("Unsupported network: $network");
}

View file

@ -41,7 +41,6 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
late CachedElectrumXClient electrumXCachedClient;
Future<void> initializeNewFrost({
required String mnemonic,
required String multisigConfig,
required String recoveryString,
required String serializedKeys,
@ -70,14 +69,6 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
threshold: threshold,
);
await secureStorageInterface.write(
key: Wallet.mnemonicKey(walletId: info.walletId),
value: mnemonic,
);
await secureStorageInterface.write(
key: Wallet.mnemonicPassphraseKey(walletId: info.walletId),
value: "",
);
await _saveSerializedKeys(serializedKeys);
await _saveRecoveryString(recoveryString);
await _saveMultisigId(multisigId);

View file

@ -0,0 +1,199 @@
import 'dart:async';
import 'dart:io';
import 'dart:ui';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:path_provider/path_provider.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:share_plus/share_plus.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/dialogs/simple_mobile_dialog.dart';
import 'package:stackwallet/widgets/rounded_container.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class FrostStepQrDialog extends StatefulWidget {
const FrostStepQrDialog({
super.key,
required this.myName,
required this.title,
required this.data,
});
final String myName;
final String title;
final String data;
@override
State<FrostStepQrDialog> createState() => _FrostStepQrDialogState();
}
class _FrostStepQrDialogState extends State<FrostStepQrDialog> {
final _qrKey = GlobalKey();
Future<void> _capturePng(bool shouldSaveInsteadOfShare) async {
try {
final boundary =
_qrKey.currentContext?.findRenderObject() as RenderRepaintBoundary;
final image = await boundary.toImage();
final byteData = await image.toByteData(format: ImageByteFormat.png);
final pngBytes = byteData!.buffer.asUint8List();
if (shouldSaveInsteadOfShare) {
if (Util.isDesktop) {
final dir = Directory("${Platform.environment['HOME']}");
if (!dir.existsSync()) {
throw Exception(
"Home dir not found while trying to open filepicker on QR image save");
}
final path = await FilePicker.platform.saveFile(
fileName: "qrcode.png",
initialDirectory: dir.path,
);
if (path != null && context.mounted) {
final file = File(path);
if (file.existsSync()) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.warning,
message: "$path already exists!",
context: context,
),
);
} else {
await file.writeAsBytes(pngBytes);
unawaited(
showFloatingFlushBar(
type: FlushBarType.success,
message: "$path saved!",
context: context,
),
);
}
}
} else {
// await DocumentFileSavePlus.saveFile(
// pngBytes,
// "receive_qr_code_${DateTime.now().toLocal().toIso8601String()}.png",
// "image/png");
}
} else {
final tempDir = await getTemporaryDirectory();
final file = await File("${tempDir.path}/qrcode.png").create();
await file.writeAsBytes(pngBytes);
await Share.shareFiles(["${tempDir.path}/qrcode.png"],
text: "Receive URI QR Code");
}
} catch (e) {
//todo: comeback to this
debugPrint(e.toString());
}
}
@override
Widget build(BuildContext context) {
return SimpleMobileDialog(
showCloseButton: false,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
RepaintBoundary(
key: _qrKey,
child: RoundedWhiteContainer(
boxShadow: [
Theme.of(context).extension<StackColors>()!.standardBoxShadow
],
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
widget.myName,
style: STextStyles.w600_16(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.customTextButtonEnabledText,
),
),
const SizedBox(height: 8),
Text(
widget.title,
style: STextStyles.w600_12(context),
),
const SizedBox(height: 8),
RoundedContainer(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
radiusMultiplier: 1,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: AspectRatio(
aspectRatio: 1,
child: QrImageView(
data: widget.data,
padding: EdgeInsets.zero,
dataModuleStyle: QrDataModuleStyle(
dataModuleShape: QrDataModuleShape.square,
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
),
),
),
const SizedBox(height: 12),
SelectableText(
widget.data,
style: STextStyles.w500_10(context),
),
],
),
),
],
),
),
),
if (!Util.isDesktop)
const SizedBox(
height: 16,
),
if (!Util.isDesktop)
Row(
children: [
const Spacer(),
const SizedBox(width: 16),
Expanded(
child: SecondaryButton(
label: "Share",
icon: SvgPicture.asset(
Assets.svg.share,
width: 14,
height: 14,
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextSecondary,
),
onPressed: () async {
await _capturePng(false);
},
),
),
],
),
],
),
);
}
}

View file

@ -9,8 +9,8 @@
*/
import 'package:flutter/material.dart';
import 'package:stackwallet/pages/add_wallet_views/frost_ms/frost_step_explanation_dialog.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/widgets/dialogs/frost/frost_step_explanation_dialog.dart';
class FrostMascot extends StatelessWidget {
final String title;

View file

@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class FrostStepUserSteps extends StatelessWidget {
const FrostStepUserSteps({super.key, required this.userSteps});
final List<String> userSteps;
@override
Widget build(BuildContext context) {
return RoundedWhiteContainer(
child: Column(
children: [
for (int i = 0; i < userSteps.length; i++)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${i + 1}.",
style: STextStyles.w500_12(context),
),
const SizedBox(
width: 4,
),
Expanded(
child: Text(
userSteps[i],
style: STextStyles.w500_12(context),
),
),
],
),
],
),
);
}
}