mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2025-01-22 02:24:30 +00:00
rough gui refactor for frost wallet creation on mobile
This commit is contained in:
parent
3e74683d6c
commit
3c76cc115c
29 changed files with 2030 additions and 1406 deletions
|
@ -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.",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
177
lib/pages/add_wallet_views/frost_ms/frost_scaffold.dart
Normal file
177
lib/pages/add_wallet_views/frost_ms/frost_scaffold.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
199
lib/widgets/dialogs/frost/frost_step_qr_dialog.dart
Normal file
199
lib/widgets/dialogs/frost/frost_step_qr_dialog.dart
Normal 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);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
38
lib/widgets/frost_step_user_steps.dart
Normal file
38
lib/widgets/frost_step_user_steps.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue