mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2025-01-20 17:44:31 +00:00
untested: Bitcoin frost
This commit is contained in:
parent
8ae2faa91f
commit
6a7ec2d5d2
33 changed files with 7349 additions and 77 deletions
|
@ -1 +1 @@
|
||||||
Subproject commit 2fa7e46669a023d270cad4552b5151b138738790
|
Subproject commit 0fbc038a262e3c2d82c7c6e34e194e9a47011d91
|
|
@ -0,0 +1,343 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:stackwallet/notifications/show_flush_bar.dart';
|
||||||
|
import 'package:stackwallet/pages/home_view/home_view.dart';
|
||||||
|
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
|
||||||
|
import 'package:stackwallet/providers/db/main_db_provider.dart';
|
||||||
|
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||||
|
import 'package:stackwallet/providers/global/node_service_provider.dart';
|
||||||
|
import 'package:stackwallet/providers/global/prefs_provider.dart';
|
||||||
|
import 'package:stackwallet/providers/global/secure_store_provider.dart';
|
||||||
|
import 'package:stackwallet/providers/global/wallets_provider.dart';
|
||||||
|
import 'package:stackwallet/services/frost.dart';
|
||||||
|
import 'package:stackwallet/themes/stack_colors.dart';
|
||||||
|
import 'package:stackwallet/utilities/assets.dart';
|
||||||
|
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||||
|
import 'package:stackwallet/utilities/logger.dart';
|
||||||
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
|
import 'package:stackwallet/utilities/util.dart';
|
||||||
|
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
|
||||||
|
import 'package:stackwallet/wallets/wallet/wallet.dart';
|
||||||
|
import 'package:stackwallet/widgets/background.dart';
|
||||||
|
import 'package:stackwallet/widgets/conditional_parent.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/detail_item.dart';
|
||||||
|
import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart';
|
||||||
|
import 'package:stackwallet/widgets/loading_indicator.dart';
|
||||||
|
|
||||||
|
import '../../../../wallets/isar/models/wallet_info.dart';
|
||||||
|
|
||||||
|
class ConfirmNewFrostMSWalletCreationView extends ConsumerStatefulWidget {
|
||||||
|
const ConfirmNewFrostMSWalletCreationView({
|
||||||
|
super.key,
|
||||||
|
required this.walletName,
|
||||||
|
required this.coin,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const String routeName = "/confirmNewFrostMSWalletCreationView";
|
||||||
|
|
||||||
|
final String walletName;
|
||||||
|
final Coin coin;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<ConfirmNewFrostMSWalletCreationView> createState() =>
|
||||||
|
_ConfirmNewFrostMSWalletCreationViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConfirmNewFrostMSWalletCreationViewState
|
||||||
|
extends ConsumerState<ConfirmNewFrostMSWalletCreationView> {
|
||||||
|
late final String seed, recoveryString, serializedKeys, multisigConfig;
|
||||||
|
late final Uint8List multisigId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
seed = ref.read(pFrostStartKeyGenData.state).state!.seed;
|
||||||
|
serializedKeys =
|
||||||
|
ref.read(pFrostCompletedKeyGenData.state).state!.serializedKeys;
|
||||||
|
recoveryString =
|
||||||
|
ref.read(pFrostCompletedKeyGenData.state).state!.recoveryString;
|
||||||
|
multisigId = ref.read(pFrostCompletedKeyGenData.state).state!.multisigId;
|
||||||
|
multisigConfig = ref.read(pFrostMultisigConfig.state).state!;
|
||||||
|
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return WillPopScope(
|
||||||
|
onWillPop: () async {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => FrostInterruptionDialog(
|
||||||
|
type: FrostInterruptionDialogType.walletCreation,
|
||||||
|
popUntilOnYesRouteName:
|
||||||
|
Util.isDesktop ? DesktopHomeView.routeName : HomeView.routeName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
child: ConditionalParent(
|
||||||
|
condition: Util.isDesktop,
|
||||||
|
builder: (child) => DesktopScaffold(
|
||||||
|
background: Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: DesktopAppBar(
|
||||||
|
isCompactHeight: false,
|
||||||
|
leading: AppBarBackButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => const FrostInterruptionDialog(
|
||||||
|
type: FrostInterruptionDialogType.walletCreation,
|
||||||
|
popUntilOnYesRouteName: DesktopHomeView.routeName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
trailing: ExitToMyStackButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => const FrostInterruptionDialog(
|
||||||
|
type: FrostInterruptionDialogType.walletCreation,
|
||||||
|
popUntilOnYesRouteName: DesktopHomeView.routeName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SizedBox(
|
||||||
|
width: 480,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ConditionalParent(
|
||||||
|
condition: !Util.isDesktop,
|
||||||
|
builder: (child) => Background(
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: AppBarBackButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => const FrostInterruptionDialog(
|
||||||
|
type: FrostInterruptionDialogType.walletCreation,
|
||||||
|
popUntilOnYesRouteName: HomeView.routeName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
"Finalize FROST multisig wallet",
|
||||||
|
style: STextStyles.navBarTitle(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: constraints.maxHeight,
|
||||||
|
),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Ensure your multisig ID matches that of each other participant",
|
||||||
|
style: STextStyles.pageTitleH2(context),
|
||||||
|
),
|
||||||
|
const _Div(),
|
||||||
|
DetailItem(
|
||||||
|
title: "ID",
|
||||||
|
detail: multisigId.toString(),
|
||||||
|
button: Util.isDesktop
|
||||||
|
? IconCopyButton(
|
||||||
|
data: multisigId.toString(),
|
||||||
|
)
|
||||||
|
: SimpleCopyButton(
|
||||||
|
data: multisigId.toString(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const _Div(),
|
||||||
|
const _Div(),
|
||||||
|
Text(
|
||||||
|
"Back up your keys and config",
|
||||||
|
style: STextStyles.pageTitleH2(context),
|
||||||
|
),
|
||||||
|
const _Div(),
|
||||||
|
DetailItem(
|
||||||
|
title: "Multisig Config",
|
||||||
|
detail: multisigConfig,
|
||||||
|
button: Util.isDesktop
|
||||||
|
? IconCopyButton(
|
||||||
|
data: multisigConfig,
|
||||||
|
)
|
||||||
|
: SimpleCopyButton(
|
||||||
|
data: multisigConfig,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const _Div(),
|
||||||
|
DetailItem(
|
||||||
|
title: "Keys",
|
||||||
|
detail: serializedKeys,
|
||||||
|
button: Util.isDesktop
|
||||||
|
? IconCopyButton(
|
||||||
|
data: serializedKeys,
|
||||||
|
)
|
||||||
|
: SimpleCopyButton(
|
||||||
|
data: serializedKeys,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!Util.isDesktop) const Spacer(),
|
||||||
|
const _Div(),
|
||||||
|
PrimaryButton(
|
||||||
|
label: "Confirm",
|
||||||
|
onPressed: () async {
|
||||||
|
bool progressPopped = false;
|
||||||
|
try {
|
||||||
|
unawaited(
|
||||||
|
showDialog<dynamic>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
useSafeArea: true,
|
||||||
|
builder: (ctx) {
|
||||||
|
return const Center(
|
||||||
|
child: LoadingIndicator(
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final info = WalletInfo.createNew(
|
||||||
|
coin: widget.coin,
|
||||||
|
name: widget.walletName,
|
||||||
|
);
|
||||||
|
|
||||||
|
final wallet = await Wallet.create(
|
||||||
|
walletInfo: info,
|
||||||
|
mainDB: ref.read(mainDBProvider),
|
||||||
|
secureStorageInterface: ref.read(secureStoreProvider),
|
||||||
|
nodeService: ref.read(nodeServiceChangeNotifierProvider),
|
||||||
|
prefs: ref.read(prefsChangeNotifierProvider),
|
||||||
|
);
|
||||||
|
|
||||||
|
await (wallet as BitcoinFrostWallet).initializeNewFrost(
|
||||||
|
mnemonic: seed,
|
||||||
|
multisigConfig: multisigConfig,
|
||||||
|
recoveryString: recoveryString,
|
||||||
|
serializedKeys: serializedKeys,
|
||||||
|
multisigId: multisigId,
|
||||||
|
myName: ref.read(pFrostMyName.state).state!,
|
||||||
|
participants: Frost.getParticipants(
|
||||||
|
multisigConfig:
|
||||||
|
ref.read(pFrostMultisigConfig.state).state!,
|
||||||
|
),
|
||||||
|
threshold: Frost.getThreshold(
|
||||||
|
multisigConfig:
|
||||||
|
ref.read(pFrostMultisigConfig.state).state!,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await info.setMnemonicVerified(
|
||||||
|
isar: ref.read(mainDBProvider).isar,
|
||||||
|
);
|
||||||
|
|
||||||
|
ref.read(pWallets).addWallet(wallet);
|
||||||
|
|
||||||
|
// pop progress dialog
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
progressPopped = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
if (Util.isDesktop) {
|
||||||
|
Navigator.of(context).popUntil(
|
||||||
|
ModalRoute.withName(
|
||||||
|
DesktopHomeView.routeName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
unawaited(
|
||||||
|
Navigator.of(context).pushNamedAndRemoveUntil(
|
||||||
|
HomeView.routeName,
|
||||||
|
(route) => false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.read(pFrostMultisigConfig.state).state = null;
|
||||||
|
ref.read(pFrostStartKeyGenData.state).state = null;
|
||||||
|
ref.read(pFrostSecretSharesData.state).state = null;
|
||||||
|
|
||||||
|
unawaited(
|
||||||
|
showFloatingFlushBar(
|
||||||
|
type: FlushBarType.success,
|
||||||
|
message: "Your wallet is set up.",
|
||||||
|
iconAsset: Assets.svg.check,
|
||||||
|
context: context,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"$e\n$s",
|
||||||
|
level: LogLevel.Fatal,
|
||||||
|
);
|
||||||
|
|
||||||
|
// pop progress dialog
|
||||||
|
if (mounted && !progressPopped) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
progressPopped = true;
|
||||||
|
}
|
||||||
|
// TODO: handle gracefully
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Div extends StatelessWidget {
|
||||||
|
const _Div({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const SizedBox(
|
||||||
|
height: 12,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,285 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
|
||||||
|
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||||
|
import 'package:stackwallet/services/frost.dart';
|
||||||
|
import 'package:stackwallet/themes/stack_colors.dart';
|
||||||
|
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||||
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
|
import 'package:stackwallet/utilities/util.dart';
|
||||||
|
import 'package:stackwallet/widgets/background.dart';
|
||||||
|
import 'package:stackwallet/widgets/conditional_parent.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||||
|
|
||||||
|
class CreateNewFrostMsWalletView extends ConsumerStatefulWidget {
|
||||||
|
const CreateNewFrostMsWalletView({
|
||||||
|
super.key,
|
||||||
|
required this.walletName,
|
||||||
|
required this.coin,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const String routeName = "/createNewFrostMsWalletView";
|
||||||
|
|
||||||
|
final String walletName;
|
||||||
|
final Coin coin;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<CreateNewFrostMsWalletView> createState() =>
|
||||||
|
_NewFrostMsWalletViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NewFrostMsWalletViewState
|
||||||
|
extends ConsumerState<CreateNewFrostMsWalletView> {
|
||||||
|
final _thresholdController = TextEditingController();
|
||||||
|
final _participantsController = TextEditingController();
|
||||||
|
|
||||||
|
final List<TextEditingController> controllers = [];
|
||||||
|
|
||||||
|
int _participantsCount = 0;
|
||||||
|
|
||||||
|
String _validateInputData() {
|
||||||
|
final threshold = int.tryParse(_thresholdController.text);
|
||||||
|
if (threshold == null) {
|
||||||
|
return "Choose a threshold";
|
||||||
|
}
|
||||||
|
|
||||||
|
final partsCount = int.tryParse(_participantsController.text);
|
||||||
|
if (partsCount == null) {
|
||||||
|
return "Choose total number of participants";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (threshold > partsCount) {
|
||||||
|
return "Threshold cannot be greater than the number of participants";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (partsCount < 2) {
|
||||||
|
return "At least two participants required";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controllers.length != partsCount) {
|
||||||
|
return "Participants count error";
|
||||||
|
}
|
||||||
|
|
||||||
|
final hasEmptyParticipants = controllers
|
||||||
|
.map((e) => e.text.isEmpty)
|
||||||
|
.reduce((value, element) => value |= element);
|
||||||
|
if (hasEmptyParticipants) {
|
||||||
|
return "Participants must not be empty";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controllers.length != controllers.map((e) => e.text).toSet().length) {
|
||||||
|
return "Duplicate participant name found";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "valid";
|
||||||
|
}
|
||||||
|
|
||||||
|
void _participantsCountChanged(String newValue) {
|
||||||
|
final count = int.tryParse(newValue);
|
||||||
|
if (count != null) {
|
||||||
|
if (count > _participantsCount) {
|
||||||
|
for (int i = _participantsCount; i < count; i++) {
|
||||||
|
controllers.add(TextEditingController());
|
||||||
|
}
|
||||||
|
|
||||||
|
_participantsCount = count;
|
||||||
|
setState(() {});
|
||||||
|
} else if (count < _participantsCount) {
|
||||||
|
for (int i = _participantsCount; i > count; i--) {
|
||||||
|
final last = controllers.removeLast();
|
||||||
|
last.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_participantsCount = count;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_thresholdController.dispose();
|
||||||
|
_participantsController.dispose();
|
||||||
|
for (final e in controllers) {
|
||||||
|
e.dispose();
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ConditionalParent(
|
||||||
|
condition: Util.isDesktop,
|
||||||
|
builder: (child) => DesktopScaffold(
|
||||||
|
background: Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: const DesktopAppBar(
|
||||||
|
isCompactHeight: false,
|
||||||
|
leading: AppBarBackButton(),
|
||||||
|
trailing: ExitToMyStackButton(),
|
||||||
|
),
|
||||||
|
body: SizedBox(
|
||||||
|
width: 480,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ConditionalParent(
|
||||||
|
condition: !Util.isDesktop,
|
||||||
|
builder: (child) => Background(
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: AppBarBackButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
"New FROST multisig config",
|
||||||
|
style: STextStyles.navBarTitle(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: constraints.maxHeight,
|
||||||
|
),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Threshold",
|
||||||
|
style: STextStyles.label(context),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 10,
|
||||||
|
),
|
||||||
|
TextField(
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||||
|
controller: _thresholdController,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"Number of participants",
|
||||||
|
style: STextStyles.label(context),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 10,
|
||||||
|
),
|
||||||
|
TextField(
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||||
|
controller: _participantsController,
|
||||||
|
onChanged: _participantsCountChanged,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
if (controllers.isNotEmpty)
|
||||||
|
Text(
|
||||||
|
"My name",
|
||||||
|
style: STextStyles.label(context),
|
||||||
|
),
|
||||||
|
if (controllers.isNotEmpty)
|
||||||
|
const SizedBox(
|
||||||
|
height: 10,
|
||||||
|
),
|
||||||
|
if (controllers.isNotEmpty)
|
||||||
|
TextField(
|
||||||
|
controller: controllers.first,
|
||||||
|
),
|
||||||
|
if (controllers.length > 1)
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
if (controllers.length > 1)
|
||||||
|
Text(
|
||||||
|
"Remaining participants",
|
||||||
|
style: STextStyles.label(context),
|
||||||
|
),
|
||||||
|
if (controllers.length > 1)
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
for (int i = 1; i < controllers.length; i++)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
top: 10,
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
controller: controllers[i],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (!Util.isDesktop) const Spacer(),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
PrimaryButton(
|
||||||
|
label: "Generate",
|
||||||
|
onPressed: () async {
|
||||||
|
if (FocusScope.of(context).hasFocus) {
|
||||||
|
FocusScope.of(context).unfocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
final validationMessage = _validateInputData();
|
||||||
|
|
||||||
|
if (validationMessage != "valid") {
|
||||||
|
return await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => StackOkDialog(
|
||||||
|
title: validationMessage,
|
||||||
|
desktopPopRootNavigator: Util.isDesktop,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final config = Frost.createMultisigConfig(
|
||||||
|
name: controllers.first.text,
|
||||||
|
threshold: int.parse(_thresholdController.text),
|
||||||
|
participants: controllers.map((e) => e.text).toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
ref.read(pFrostMyName.notifier).state = controllers.first.text;
|
||||||
|
ref.read(pFrostMultisigConfig.notifier).state = config;
|
||||||
|
|
||||||
|
await Navigator.of(context).pushNamed(
|
||||||
|
ShareNewMultisigConfigView.routeName,
|
||||||
|
arguments: (
|
||||||
|
walletName: widget.walletName,
|
||||||
|
coin: widget.coin,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,443 @@
|
||||||
|
import 'package:barcode_scan2/barcode_scan2.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:qr_flutter/qr_flutter.dart';
|
||||||
|
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart';
|
||||||
|
import 'package:stackwallet/pages/home_view/home_view.dart';
|
||||||
|
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
|
||||||
|
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||||
|
import 'package:stackwallet/services/frost.dart';
|
||||||
|
import 'package:stackwallet/themes/stack_colors.dart';
|
||||||
|
import 'package:stackwallet/utilities/constants.dart';
|
||||||
|
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||||
|
import 'package:stackwallet/utilities/logger.dart';
|
||||||
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
|
import 'package:stackwallet/utilities/util.dart';
|
||||||
|
import 'package:stackwallet/widgets/background.dart';
|
||||||
|
import 'package:stackwallet/widgets/conditional_parent.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/detail_item.dart';
|
||||||
|
import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart';
|
||||||
|
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
|
||||||
|
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
|
||||||
|
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
|
||||||
|
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||||
|
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||||
|
import 'package:stackwallet/widgets/textfield_icon_button.dart';
|
||||||
|
|
||||||
|
class FrostShareCommitmentsView extends ConsumerStatefulWidget {
|
||||||
|
const FrostShareCommitmentsView({
|
||||||
|
super.key,
|
||||||
|
required this.walletName,
|
||||||
|
required this.coin,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const String routeName = "/frostShareCommitmentsView";
|
||||||
|
|
||||||
|
final String walletName;
|
||||||
|
final Coin coin;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<FrostShareCommitmentsView> createState() =>
|
||||||
|
_FrostShareCommitmentsViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FrostShareCommitmentsViewState
|
||||||
|
extends ConsumerState<FrostShareCommitmentsView> {
|
||||||
|
final List<TextEditingController> controllers = [];
|
||||||
|
final List<FocusNode> focusNodes = [];
|
||||||
|
|
||||||
|
late final List<String> participants;
|
||||||
|
late final String myCommitment;
|
||||||
|
late final int myIndex;
|
||||||
|
|
||||||
|
final List<bool> fieldIsEmptyFlags = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
participants = Frost.getParticipants(
|
||||||
|
multisigConfig: ref.read(pFrostMultisigConfig.state).state!,
|
||||||
|
);
|
||||||
|
myIndex = participants.indexOf(ref.read(pFrostMyName.state).state!);
|
||||||
|
myCommitment = ref.read(pFrostStartKeyGenData.state).state!.commitments;
|
||||||
|
|
||||||
|
// temporarily remove my name
|
||||||
|
participants.removeAt(myIndex);
|
||||||
|
|
||||||
|
for (int i = 0; i < participants.length; i++) {
|
||||||
|
controllers.add(TextEditingController());
|
||||||
|
focusNodes.add(FocusNode());
|
||||||
|
fieldIsEmptyFlags.add(true);
|
||||||
|
}
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
for (int i = 0; i < controllers.length; i++) {
|
||||||
|
controllers[i].dispose();
|
||||||
|
}
|
||||||
|
for (int i = 0; i < focusNodes.length; i++) {
|
||||||
|
focusNodes[i].dispose();
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return WillPopScope(
|
||||||
|
onWillPop: () async {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => FrostInterruptionDialog(
|
||||||
|
type: FrostInterruptionDialogType.walletCreation,
|
||||||
|
popUntilOnYesRouteName:
|
||||||
|
Util.isDesktop ? DesktopHomeView.routeName : HomeView.routeName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
child: ConditionalParent(
|
||||||
|
condition: Util.isDesktop,
|
||||||
|
builder: (child) => DesktopScaffold(
|
||||||
|
background: Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: DesktopAppBar(
|
||||||
|
isCompactHeight: false,
|
||||||
|
leading: AppBarBackButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => const FrostInterruptionDialog(
|
||||||
|
type: FrostInterruptionDialogType.walletCreation,
|
||||||
|
popUntilOnYesRouteName: DesktopHomeView.routeName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
trailing: ExitToMyStackButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => const FrostInterruptionDialog(
|
||||||
|
type: FrostInterruptionDialogType.walletCreation,
|
||||||
|
popUntilOnYesRouteName: DesktopHomeView.routeName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SizedBox(
|
||||||
|
width: 480,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ConditionalParent(
|
||||||
|
condition: !Util.isDesktop,
|
||||||
|
builder: (child) => Background(
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: AppBarBackButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => const FrostInterruptionDialog(
|
||||||
|
type: FrostInterruptionDialogType.walletCreation,
|
||||||
|
popUntilOnYesRouteName: HomeView.routeName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
"Commitments",
|
||||||
|
style: STextStyles.navBarTitle(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: constraints.maxHeight,
|
||||||
|
),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 220,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
QrImageView(
|
||||||
|
data: myCommitment,
|
||||||
|
size: 220,
|
||||||
|
backgroundColor: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.background,
|
||||||
|
foregroundColor: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.accentColorDark,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const _Div(),
|
||||||
|
DetailItem(
|
||||||
|
title: "My name",
|
||||||
|
detail: ref.watch(pFrostMyName.state).state!,
|
||||||
|
),
|
||||||
|
const _Div(),
|
||||||
|
DetailItem(
|
||||||
|
title: "My commitment",
|
||||||
|
detail: myCommitment,
|
||||||
|
button: Util.isDesktop
|
||||||
|
? IconCopyButton(
|
||||||
|
data: myCommitment,
|
||||||
|
)
|
||||||
|
: SimpleCopyButton(
|
||||||
|
data: myCommitment,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const _Div(),
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
for (int i = 0; i < participants.length; i++)
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
Constants.size.circularBorderRadius,
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
key: Key("frostCommitmentsTextFieldKey_$i"),
|
||||||
|
controller: controllers[i],
|
||||||
|
focusNode: focusNodes[i],
|
||||||
|
readOnly: false,
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
style: STextStyles.field(context),
|
||||||
|
onChanged: (_) {
|
||||||
|
setState(() {
|
||||||
|
fieldIsEmptyFlags[i] =
|
||||||
|
controllers[i].text.isEmpty;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
decoration: standardInputDecoration(
|
||||||
|
"Enter ${participants[i]}'s commitment",
|
||||||
|
focusNodes[i],
|
||||||
|
context,
|
||||||
|
).copyWith(
|
||||||
|
contentPadding: const EdgeInsets.only(
|
||||||
|
left: 16,
|
||||||
|
top: 6,
|
||||||
|
bottom: 8,
|
||||||
|
right: 5,
|
||||||
|
),
|
||||||
|
suffixIcon: Padding(
|
||||||
|
padding: fieldIsEmptyFlags[i]
|
||||||
|
? const EdgeInsets.only(right: 8)
|
||||||
|
: const EdgeInsets.only(right: 0),
|
||||||
|
child: UnconstrainedBox(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
!fieldIsEmptyFlags[i]
|
||||||
|
? TextFieldIconButton(
|
||||||
|
semanticsLabel:
|
||||||
|
"Clear Button. Clears The Commitment Field Input.",
|
||||||
|
key: Key(
|
||||||
|
"frostCommitmentsClearButtonKey_$i"),
|
||||||
|
onTap: () {
|
||||||
|
controllers[i].text = "";
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
fieldIsEmptyFlags[i] = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const XIcon(),
|
||||||
|
)
|
||||||
|
: TextFieldIconButton(
|
||||||
|
semanticsLabel:
|
||||||
|
"Paste Button. Pastes From Clipboard To Commitment Field Input.",
|
||||||
|
key: Key(
|
||||||
|
"frostCommitmentsPasteButtonKey_$i"),
|
||||||
|
onTap: () async {
|
||||||
|
final ClipboardData? data =
|
||||||
|
await Clipboard.getData(
|
||||||
|
Clipboard.kTextPlain);
|
||||||
|
if (data?.text != null &&
|
||||||
|
data!.text!.isNotEmpty) {
|
||||||
|
controllers[i].text =
|
||||||
|
data.text!.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
fieldIsEmptyFlags[i] =
|
||||||
|
controllers[i]
|
||||||
|
.text
|
||||||
|
.isEmpty;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: fieldIsEmptyFlags[i]
|
||||||
|
? const ClipboardIcon()
|
||||||
|
: const XIcon(),
|
||||||
|
),
|
||||||
|
if (fieldIsEmptyFlags[i])
|
||||||
|
TextFieldIconButton(
|
||||||
|
semanticsLabel:
|
||||||
|
"Scan QR Button. Opens Camera For Scanning QR Code.",
|
||||||
|
key: Key(
|
||||||
|
"frostCommitmentsScanQrButtonKey_$i"),
|
||||||
|
onTap: () async {
|
||||||
|
try {
|
||||||
|
if (FocusScope.of(context)
|
||||||
|
.hasFocus) {
|
||||||
|
FocusScope.of(context)
|
||||||
|
.unfocus();
|
||||||
|
await Future<void>.delayed(
|
||||||
|
const Duration(
|
||||||
|
milliseconds: 75));
|
||||||
|
}
|
||||||
|
|
||||||
|
final qrResult =
|
||||||
|
await BarcodeScanner.scan();
|
||||||
|
|
||||||
|
controllers[i].text =
|
||||||
|
qrResult.rawContent;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
fieldIsEmptyFlags[i] =
|
||||||
|
controllers[i]
|
||||||
|
.text
|
||||||
|
.isEmpty;
|
||||||
|
});
|
||||||
|
} on PlatformException catch (e, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"Failed to get camera permissions while trying to scan qr code: $e\n$s",
|
||||||
|
level: LogLevel.Warning,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const QrCodeIcon(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (!Util.isDesktop) const Spacer(),
|
||||||
|
const _Div(),
|
||||||
|
PrimaryButton(
|
||||||
|
label: "Generate shares",
|
||||||
|
enabled: !fieldIsEmptyFlags.reduce((v, e) => v |= e),
|
||||||
|
onPressed: () async {
|
||||||
|
// check for empty commitments
|
||||||
|
if (controllers
|
||||||
|
.map((e) => e.text.isEmpty)
|
||||||
|
.reduce((value, element) => value |= element)) {
|
||||||
|
return await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => StackOkDialog(
|
||||||
|
title: "Missing commitments",
|
||||||
|
desktopPopRootNavigator: Util.isDesktop,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// collect commitment strings and insert my own at the correct index
|
||||||
|
final commitments = controllers.map((e) => e.text).toList();
|
||||||
|
commitments.insert(myIndex, myCommitment);
|
||||||
|
|
||||||
|
try {
|
||||||
|
ref.read(pFrostSecretSharesData.notifier).state =
|
||||||
|
Frost.generateSecretShares(
|
||||||
|
multisigConfigWithNamePtr: ref
|
||||||
|
.read(pFrostStartKeyGenData.state)
|
||||||
|
.state!
|
||||||
|
.multisigConfigWithNamePtr,
|
||||||
|
mySeed: ref.read(pFrostStartKeyGenData.state).state!.seed,
|
||||||
|
secretShareMachineWrapperPtr: ref
|
||||||
|
.read(pFrostStartKeyGenData.state)
|
||||||
|
.state!
|
||||||
|
.secretShareMachineWrapperPtr,
|
||||||
|
commitments: commitments,
|
||||||
|
);
|
||||||
|
|
||||||
|
await Navigator.of(context).pushNamed(
|
||||||
|
FrostShareSharesView.routeName,
|
||||||
|
arguments: (
|
||||||
|
walletName: widget.walletName,
|
||||||
|
coin: widget.coin,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"$e\n$s",
|
||||||
|
level: LogLevel.Fatal,
|
||||||
|
);
|
||||||
|
|
||||||
|
return await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => StackOkDialog(
|
||||||
|
title: "Failed to generate shares",
|
||||||
|
message: e.toString(),
|
||||||
|
desktopPopRootNavigator: Util.isDesktop,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Div extends StatelessWidget {
|
||||||
|
const _Div({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const SizedBox(
|
||||||
|
height: 12,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,409 @@
|
||||||
|
import 'package:barcode_scan2/barcode_scan2.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:qr_flutter/qr_flutter.dart';
|
||||||
|
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart';
|
||||||
|
import 'package:stackwallet/pages/home_view/home_view.dart';
|
||||||
|
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
|
||||||
|
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||||
|
import 'package:stackwallet/services/frost.dart';
|
||||||
|
import 'package:stackwallet/themes/stack_colors.dart';
|
||||||
|
import 'package:stackwallet/utilities/constants.dart';
|
||||||
|
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||||
|
import 'package:stackwallet/utilities/logger.dart';
|
||||||
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
|
import 'package:stackwallet/utilities/util.dart';
|
||||||
|
import 'package:stackwallet/widgets/background.dart';
|
||||||
|
import 'package:stackwallet/widgets/conditional_parent.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/detail_item.dart';
|
||||||
|
import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart';
|
||||||
|
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
|
||||||
|
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
|
||||||
|
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
|
||||||
|
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||||
|
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||||
|
import 'package:stackwallet/widgets/textfield_icon_button.dart';
|
||||||
|
|
||||||
|
class FrostShareSharesView extends ConsumerStatefulWidget {
|
||||||
|
const FrostShareSharesView({
|
||||||
|
super.key,
|
||||||
|
required this.walletName,
|
||||||
|
required this.coin,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const String routeName = "/frostShareSharesView";
|
||||||
|
|
||||||
|
final String walletName;
|
||||||
|
final Coin coin;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<FrostShareSharesView> createState() =>
|
||||||
|
_FrostShareSharesViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FrostShareSharesViewState extends ConsumerState<FrostShareSharesView> {
|
||||||
|
final List<TextEditingController> controllers = [];
|
||||||
|
final List<FocusNode> focusNodes = [];
|
||||||
|
|
||||||
|
late final List<String> participants;
|
||||||
|
late final String myShare;
|
||||||
|
late final int myIndex;
|
||||||
|
|
||||||
|
final List<bool> fieldIsEmptyFlags = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
participants = Frost.getParticipants(
|
||||||
|
multisigConfig: ref.read(pFrostMultisigConfig.state).state!,
|
||||||
|
);
|
||||||
|
myIndex = participants.indexOf(ref.read(pFrostMyName.state).state!);
|
||||||
|
myShare = ref.read(pFrostSecretSharesData.state).state!.share;
|
||||||
|
|
||||||
|
// temporarily remove my name. Added back later
|
||||||
|
participants.removeAt(myIndex);
|
||||||
|
|
||||||
|
for (int i = 0; i < participants.length; i++) {
|
||||||
|
controllers.add(TextEditingController());
|
||||||
|
focusNodes.add(FocusNode());
|
||||||
|
fieldIsEmptyFlags.add(true);
|
||||||
|
}
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
for (int i = 0; i < controllers.length; i++) {
|
||||||
|
controllers[i].dispose();
|
||||||
|
}
|
||||||
|
for (int i = 0; i < focusNodes.length; i++) {
|
||||||
|
focusNodes[i].dispose();
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return WillPopScope(
|
||||||
|
onWillPop: () async {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => FrostInterruptionDialog(
|
||||||
|
type: FrostInterruptionDialogType.walletCreation,
|
||||||
|
popUntilOnYesRouteName:
|
||||||
|
Util.isDesktop ? DesktopHomeView.routeName : HomeView.routeName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
child: ConditionalParent(
|
||||||
|
condition: Util.isDesktop,
|
||||||
|
builder: (child) => DesktopScaffold(
|
||||||
|
background: Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: DesktopAppBar(
|
||||||
|
isCompactHeight: false,
|
||||||
|
leading: AppBarBackButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => const FrostInterruptionDialog(
|
||||||
|
type: FrostInterruptionDialogType.walletCreation,
|
||||||
|
popUntilOnYesRouteName: DesktopHomeView.routeName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
trailing: ExitToMyStackButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => const FrostInterruptionDialog(
|
||||||
|
type: FrostInterruptionDialogType.walletCreation,
|
||||||
|
popUntilOnYesRouteName: DesktopHomeView.routeName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SizedBox(
|
||||||
|
width: 480,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ConditionalParent(
|
||||||
|
condition: !Util.isDesktop,
|
||||||
|
builder: (child) => Background(
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: AppBarBackButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => const FrostInterruptionDialog(
|
||||||
|
type: FrostInterruptionDialogType.walletCreation,
|
||||||
|
popUntilOnYesRouteName: HomeView.routeName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
"Generate shares",
|
||||||
|
style: STextStyles.navBarTitle(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: constraints.maxHeight,
|
||||||
|
),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 220,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
QrImageView(
|
||||||
|
data: myShare,
|
||||||
|
size: 220,
|
||||||
|
backgroundColor: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.background,
|
||||||
|
foregroundColor: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.accentColorDark,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const _Div(),
|
||||||
|
DetailItem(
|
||||||
|
title: "My name",
|
||||||
|
detail: ref.watch(pFrostMyName.state).state!,
|
||||||
|
),
|
||||||
|
const _Div(),
|
||||||
|
DetailItem(
|
||||||
|
title: "My share",
|
||||||
|
detail: myShare,
|
||||||
|
button: Util.isDesktop
|
||||||
|
? IconCopyButton(
|
||||||
|
data: myShare,
|
||||||
|
)
|
||||||
|
: SimpleCopyButton(
|
||||||
|
data: myShare,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const _Div(),
|
||||||
|
for (int i = 0; i < participants.length; i++)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
Constants.size.circularBorderRadius,
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
key: Key("frSharesTextFieldKey_$i"),
|
||||||
|
controller: controllers[i],
|
||||||
|
focusNode: focusNodes[i],
|
||||||
|
readOnly: false,
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
style: STextStyles.field(context),
|
||||||
|
decoration: standardInputDecoration(
|
||||||
|
"Enter ${participants[i]}'s share",
|
||||||
|
focusNodes[i],
|
||||||
|
context,
|
||||||
|
).copyWith(
|
||||||
|
contentPadding: const EdgeInsets.only(
|
||||||
|
left: 16,
|
||||||
|
top: 6,
|
||||||
|
bottom: 8,
|
||||||
|
right: 5,
|
||||||
|
),
|
||||||
|
suffixIcon: Padding(
|
||||||
|
padding: fieldIsEmptyFlags[i]
|
||||||
|
? const EdgeInsets.only(right: 8)
|
||||||
|
: const EdgeInsets.only(right: 0),
|
||||||
|
child: UnconstrainedBox(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
!fieldIsEmptyFlags[i]
|
||||||
|
? TextFieldIconButton(
|
||||||
|
semanticsLabel:
|
||||||
|
"Clear Button. Clears The Share Field Input.",
|
||||||
|
key: Key("frSharesClearButtonKey_$i"),
|
||||||
|
onTap: () {
|
||||||
|
controllers[i].text = "";
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
fieldIsEmptyFlags[i] = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const XIcon(),
|
||||||
|
)
|
||||||
|
: TextFieldIconButton(
|
||||||
|
semanticsLabel:
|
||||||
|
"Paste Button. Pastes From Clipboard To Share Field Input.",
|
||||||
|
key: Key("frSharesPasteButtonKey_$i"),
|
||||||
|
onTap: () async {
|
||||||
|
final ClipboardData? data =
|
||||||
|
await Clipboard.getData(
|
||||||
|
Clipboard.kTextPlain);
|
||||||
|
if (data?.text != null &&
|
||||||
|
data!.text!.isNotEmpty) {
|
||||||
|
controllers[i].text =
|
||||||
|
data.text!.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
fieldIsEmptyFlags[i] =
|
||||||
|
controllers[i].text.isEmpty;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: fieldIsEmptyFlags[i]
|
||||||
|
? const ClipboardIcon()
|
||||||
|
: const XIcon(),
|
||||||
|
),
|
||||||
|
if (fieldIsEmptyFlags[i])
|
||||||
|
TextFieldIconButton(
|
||||||
|
semanticsLabel:
|
||||||
|
"Scan QR Button. Opens Camera For Scanning QR Code.",
|
||||||
|
key: Key("frSharesScanQrButtonKey_$i"),
|
||||||
|
onTap: () async {
|
||||||
|
try {
|
||||||
|
if (FocusScope.of(context).hasFocus) {
|
||||||
|
FocusScope.of(context).unfocus();
|
||||||
|
await Future<void>.delayed(
|
||||||
|
const Duration(milliseconds: 75));
|
||||||
|
}
|
||||||
|
|
||||||
|
final qrResult =
|
||||||
|
await BarcodeScanner.scan();
|
||||||
|
|
||||||
|
controllers[i].text =
|
||||||
|
qrResult.rawContent;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
fieldIsEmptyFlags[i] =
|
||||||
|
controllers[i].text.isEmpty;
|
||||||
|
});
|
||||||
|
} on PlatformException catch (e, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"Failed to get camera permissions while trying to scan qr code: $e\n$s",
|
||||||
|
level: LogLevel.Warning,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const QrCodeIcon(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!Util.isDesktop) const Spacer(),
|
||||||
|
const _Div(),
|
||||||
|
PrimaryButton(
|
||||||
|
label: "Generate",
|
||||||
|
onPressed: () async {
|
||||||
|
// check for empty commitments
|
||||||
|
if (controllers
|
||||||
|
.map((e) => e.text.isEmpty)
|
||||||
|
.reduce((value, element) => value |= element)) {
|
||||||
|
return await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => StackOkDialog(
|
||||||
|
title: "Missing shares",
|
||||||
|
desktopPopRootNavigator: Util.isDesktop,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// collect commitment strings and insert my own at the correct index
|
||||||
|
final shares = controllers.map((e) => e.text).toList();
|
||||||
|
shares.insert(myIndex, myShare);
|
||||||
|
|
||||||
|
try {
|
||||||
|
ref.read(pFrostCompletedKeyGenData.notifier).state =
|
||||||
|
Frost.completeKeyGeneration(
|
||||||
|
multisigConfigWithNamePtr: ref
|
||||||
|
.read(pFrostStartKeyGenData.state)
|
||||||
|
.state!
|
||||||
|
.multisigConfigWithNamePtr,
|
||||||
|
secretSharesResPtr: ref
|
||||||
|
.read(pFrostSecretSharesData.state)
|
||||||
|
.state!
|
||||||
|
.secretSharesResPtr,
|
||||||
|
shares: shares,
|
||||||
|
);
|
||||||
|
await Navigator.of(context).pushNamed(
|
||||||
|
ConfirmNewFrostMSWalletCreationView.routeName,
|
||||||
|
arguments: (
|
||||||
|
walletName: widget.walletName,
|
||||||
|
coin: widget.coin,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"$e\n$s",
|
||||||
|
level: LogLevel.Fatal,
|
||||||
|
);
|
||||||
|
|
||||||
|
return await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => StackOkDialog(
|
||||||
|
title: "Failed to complete key generation",
|
||||||
|
desktopPopRootNavigator: Util.isDesktop,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Div extends StatelessWidget {
|
||||||
|
const _Div({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const SizedBox(
|
||||||
|
height: 12,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,386 @@
|
||||||
|
import 'package:barcode_scan2/barcode_scan2.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
|
||||||
|
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||||
|
import 'package:stackwallet/services/frost.dart';
|
||||||
|
import 'package:stackwallet/themes/stack_colors.dart';
|
||||||
|
import 'package:stackwallet/utilities/constants.dart';
|
||||||
|
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||||
|
import 'package:stackwallet/utilities/logger.dart';
|
||||||
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
|
import 'package:stackwallet/utilities/util.dart';
|
||||||
|
import 'package:stackwallet/widgets/background.dart';
|
||||||
|
import 'package:stackwallet/widgets/conditional_parent.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
|
||||||
|
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
|
||||||
|
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
|
||||||
|
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||||
|
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||||
|
import 'package:stackwallet/widgets/textfield_icon_button.dart';
|
||||||
|
|
||||||
|
class ImportNewFrostMsWalletView extends ConsumerStatefulWidget {
|
||||||
|
const ImportNewFrostMsWalletView({
|
||||||
|
super.key,
|
||||||
|
required this.walletName,
|
||||||
|
required this.coin,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const String routeName = "/importNewFrostMsWalletView";
|
||||||
|
|
||||||
|
final String walletName;
|
||||||
|
final Coin coin;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<ImportNewFrostMsWalletView> createState() =>
|
||||||
|
_ImportNewFrostMsWalletViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ImportNewFrostMsWalletViewState
|
||||||
|
extends ConsumerState<ImportNewFrostMsWalletView> {
|
||||||
|
late final TextEditingController myNameFieldController, configFieldController;
|
||||||
|
late final FocusNode myNameFocusNode, configFocusNode;
|
||||||
|
|
||||||
|
bool _nameEmpty = true, _configEmpty = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
myNameFieldController = TextEditingController();
|
||||||
|
configFieldController = TextEditingController();
|
||||||
|
myNameFocusNode = FocusNode();
|
||||||
|
configFocusNode = FocusNode();
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
myNameFieldController.dispose();
|
||||||
|
configFieldController.dispose();
|
||||||
|
myNameFocusNode.dispose();
|
||||||
|
configFocusNode.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ConditionalParent(
|
||||||
|
condition: Util.isDesktop,
|
||||||
|
builder: (child) => DesktopScaffold(
|
||||||
|
background: Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: const DesktopAppBar(
|
||||||
|
isCompactHeight: false,
|
||||||
|
leading: AppBarBackButton(),
|
||||||
|
trailing: ExitToMyStackButton(),
|
||||||
|
),
|
||||||
|
body: SizedBox(
|
||||||
|
width: 480,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ConditionalParent(
|
||||||
|
condition: !Util.isDesktop,
|
||||||
|
builder: (child) => Background(
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: AppBarBackButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
"Import FROST multisig config",
|
||||||
|
style: STextStyles.navBarTitle(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: constraints.maxHeight,
|
||||||
|
),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
Constants.size.circularBorderRadius,
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
key: const Key("frMyNameTextFieldKey"),
|
||||||
|
controller: myNameFieldController,
|
||||||
|
onChanged: (_) {
|
||||||
|
setState(() {
|
||||||
|
_nameEmpty = myNameFieldController.text.isEmpty;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
focusNode: myNameFocusNode,
|
||||||
|
readOnly: false,
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
style: STextStyles.field(context),
|
||||||
|
decoration: standardInputDecoration(
|
||||||
|
"My name",
|
||||||
|
myNameFocusNode,
|
||||||
|
context,
|
||||||
|
).copyWith(
|
||||||
|
contentPadding: const EdgeInsets.only(
|
||||||
|
left: 16,
|
||||||
|
top: 6,
|
||||||
|
bottom: 8,
|
||||||
|
right: 5,
|
||||||
|
),
|
||||||
|
suffixIcon: Padding(
|
||||||
|
padding: _nameEmpty
|
||||||
|
? const EdgeInsets.only(right: 8)
|
||||||
|
: const EdgeInsets.only(right: 0),
|
||||||
|
child: UnconstrainedBox(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
!_nameEmpty
|
||||||
|
? TextFieldIconButton(
|
||||||
|
semanticsLabel:
|
||||||
|
"Clear Button. Clears The Config Field.",
|
||||||
|
key: const Key("frMyNameClearButtonKey"),
|
||||||
|
onTap: () {
|
||||||
|
myNameFieldController.text = "";
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_nameEmpty = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const XIcon(),
|
||||||
|
)
|
||||||
|
: TextFieldIconButton(
|
||||||
|
semanticsLabel:
|
||||||
|
"Paste Button. Pastes From Clipboard To Name Field.",
|
||||||
|
key: const Key("frMyNamePasteButtonKey"),
|
||||||
|
onTap: () async {
|
||||||
|
final ClipboardData? data =
|
||||||
|
await Clipboard.getData(
|
||||||
|
Clipboard.kTextPlain);
|
||||||
|
if (data?.text != null &&
|
||||||
|
data!.text!.isNotEmpty) {
|
||||||
|
myNameFieldController.text =
|
||||||
|
data.text!.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_nameEmpty =
|
||||||
|
myNameFieldController.text.isEmpty;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: _nameEmpty
|
||||||
|
? const ClipboardIcon()
|
||||||
|
: const XIcon(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
Constants.size.circularBorderRadius,
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
key: const Key("frConfigTextFieldKey"),
|
||||||
|
controller: configFieldController,
|
||||||
|
onChanged: (_) {
|
||||||
|
setState(() {
|
||||||
|
_configEmpty = configFieldController.text.isEmpty;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
focusNode: configFocusNode,
|
||||||
|
readOnly: false,
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
style: STextStyles.field(context),
|
||||||
|
decoration: standardInputDecoration(
|
||||||
|
"Enter config",
|
||||||
|
configFocusNode,
|
||||||
|
context,
|
||||||
|
).copyWith(
|
||||||
|
contentPadding: const EdgeInsets.only(
|
||||||
|
left: 16,
|
||||||
|
top: 6,
|
||||||
|
bottom: 8,
|
||||||
|
right: 5,
|
||||||
|
),
|
||||||
|
suffixIcon: Padding(
|
||||||
|
padding: _configEmpty
|
||||||
|
? const EdgeInsets.only(right: 8)
|
||||||
|
: const EdgeInsets.only(right: 0),
|
||||||
|
child: UnconstrainedBox(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
!_configEmpty
|
||||||
|
? TextFieldIconButton(
|
||||||
|
semanticsLabel:
|
||||||
|
"Clear Button. Clears The Config Field.",
|
||||||
|
key: const Key("frConfigClearButtonKey"),
|
||||||
|
onTap: () {
|
||||||
|
configFieldController.text = "";
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_configEmpty = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const XIcon(),
|
||||||
|
)
|
||||||
|
: TextFieldIconButton(
|
||||||
|
semanticsLabel:
|
||||||
|
"Paste Button. Pastes From Clipboard To Config Field Input.",
|
||||||
|
key: const Key("frConfigPasteButtonKey"),
|
||||||
|
onTap: () async {
|
||||||
|
final ClipboardData? data =
|
||||||
|
await Clipboard.getData(
|
||||||
|
Clipboard.kTextPlain);
|
||||||
|
if (data?.text != null &&
|
||||||
|
data!.text!.isNotEmpty) {
|
||||||
|
configFieldController.text =
|
||||||
|
data.text!.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_configEmpty =
|
||||||
|
configFieldController.text.isEmpty;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: _configEmpty
|
||||||
|
? const ClipboardIcon()
|
||||||
|
: const XIcon(),
|
||||||
|
),
|
||||||
|
if (_configEmpty)
|
||||||
|
TextFieldIconButton(
|
||||||
|
semanticsLabel:
|
||||||
|
"Scan QR Button. Opens Camera For Scanning QR Code.",
|
||||||
|
key: const Key("frConfigScanQrButtonKey"),
|
||||||
|
onTap: () async {
|
||||||
|
try {
|
||||||
|
if (FocusScope.of(context).hasFocus) {
|
||||||
|
FocusScope.of(context).unfocus();
|
||||||
|
await Future<void>.delayed(
|
||||||
|
const Duration(milliseconds: 75));
|
||||||
|
}
|
||||||
|
|
||||||
|
final qrResult = await BarcodeScanner.scan();
|
||||||
|
|
||||||
|
configFieldController.text =
|
||||||
|
qrResult.rawContent;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_configEmpty =
|
||||||
|
configFieldController.text.isEmpty;
|
||||||
|
});
|
||||||
|
} on PlatformException catch (e, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"Failed to get camera permissions while trying to scan qr code: $e\n$s",
|
||||||
|
level: LogLevel.Warning,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const QrCodeIcon(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
if (!Util.isDesktop) const Spacer(),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
PrimaryButton(
|
||||||
|
label: "Start key generation",
|
||||||
|
enabled: !_nameEmpty && !_configEmpty,
|
||||||
|
onPressed: () async {
|
||||||
|
if (FocusScope.of(context).hasFocus) {
|
||||||
|
FocusScope.of(context).unfocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
final config = configFieldController.text;
|
||||||
|
|
||||||
|
if (!Frost.validateEncodedMultisigConfig(
|
||||||
|
encodedConfig: config)) {
|
||||||
|
return await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => StackOkDialog(
|
||||||
|
title: "Invalid config",
|
||||||
|
desktopPopRootNavigator: Util.isDesktop,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Frost.getParticipants(multisigConfig: config)
|
||||||
|
.contains(myNameFieldController.text)) {
|
||||||
|
return await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => StackOkDialog(
|
||||||
|
title: "My name not found in config participants",
|
||||||
|
desktopPopRootNavigator: Util.isDesktop,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.read(pFrostMyName.state).state = myNameFieldController.text;
|
||||||
|
ref.read(pFrostMultisigConfig.notifier).state = config;
|
||||||
|
|
||||||
|
ref.read(pFrostStartKeyGenData.state).state =
|
||||||
|
Frost.startKeyGeneration(
|
||||||
|
multisigConfig: ref.read(pFrostMultisigConfig.state).state!,
|
||||||
|
myName: ref.read(pFrostMyName.state).state!,
|
||||||
|
);
|
||||||
|
|
||||||
|
await Navigator.of(context).pushNamed(
|
||||||
|
FrostShareCommitmentsView.routeName,
|
||||||
|
arguments: (
|
||||||
|
walletName: widget.walletName,
|
||||||
|
coin: widget.coin,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,162 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:qr_flutter/qr_flutter.dart';
|
||||||
|
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart';
|
||||||
|
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
|
||||||
|
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||||
|
import 'package:stackwallet/services/frost.dart';
|
||||||
|
import 'package:stackwallet/themes/stack_colors.dart';
|
||||||
|
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||||
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
|
import 'package:stackwallet/utilities/util.dart';
|
||||||
|
import 'package:stackwallet/widgets/background.dart';
|
||||||
|
import 'package:stackwallet/widgets/conditional_parent.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/detail_item.dart';
|
||||||
|
|
||||||
|
class ShareNewMultisigConfigView extends ConsumerStatefulWidget {
|
||||||
|
const ShareNewMultisigConfigView({
|
||||||
|
super.key,
|
||||||
|
required this.walletName,
|
||||||
|
required this.coin,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const String routeName = "/shareNewMultisigConfigView";
|
||||||
|
|
||||||
|
final String walletName;
|
||||||
|
final Coin coin;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<ShareNewMultisigConfigView> createState() =>
|
||||||
|
_ShareNewMultisigConfigViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ShareNewMultisigConfigViewState
|
||||||
|
extends ConsumerState<ShareNewMultisigConfigView> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ConditionalParent(
|
||||||
|
condition: Util.isDesktop,
|
||||||
|
builder: (child) => DesktopScaffold(
|
||||||
|
background: Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: const DesktopAppBar(
|
||||||
|
isCompactHeight: false,
|
||||||
|
leading: AppBarBackButton(),
|
||||||
|
trailing: ExitToMyStackButton(),
|
||||||
|
),
|
||||||
|
body: SizedBox(
|
||||||
|
width: 480,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ConditionalParent(
|
||||||
|
condition: !Util.isDesktop,
|
||||||
|
builder: (child) => Background(
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: AppBarBackButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
"Multisig config",
|
||||||
|
style: STextStyles.navBarTitle(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: constraints.maxHeight,
|
||||||
|
),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
if (!Util.isDesktop) const Spacer(),
|
||||||
|
SizedBox(
|
||||||
|
height: 220,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
QrImageView(
|
||||||
|
data:
|
||||||
|
ref.watch(pFrostMultisigConfig.state).state ?? "Error",
|
||||||
|
size: 220,
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
foregroundColor: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.accentColorDark,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 32,
|
||||||
|
),
|
||||||
|
DetailItem(
|
||||||
|
title: "Encoded config",
|
||||||
|
detail: ref.watch(pFrostMultisigConfig.state).state ?? "Error",
|
||||||
|
button: Util.isDesktop
|
||||||
|
? IconCopyButton(
|
||||||
|
data: ref.watch(pFrostMultisigConfig.state).state ??
|
||||||
|
"Error",
|
||||||
|
)
|
||||||
|
: SimpleCopyButton(
|
||||||
|
data: ref.watch(pFrostMultisigConfig.state).state ??
|
||||||
|
"Error",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: Util.isDesktop ? 64 : 16,
|
||||||
|
),
|
||||||
|
if (!Util.isDesktop)
|
||||||
|
const Spacer(
|
||||||
|
flex: 2,
|
||||||
|
),
|
||||||
|
PrimaryButton(
|
||||||
|
label: "Start key generation",
|
||||||
|
onPressed: () async {
|
||||||
|
ref.read(pFrostStartKeyGenData.notifier).state =
|
||||||
|
Frost.startKeyGeneration(
|
||||||
|
multisigConfig: ref.watch(pFrostMultisigConfig.state).state!,
|
||||||
|
myName: ref.read(pFrostMyName.state).state!,
|
||||||
|
);
|
||||||
|
|
||||||
|
await Navigator.of(context).pushNamed(
|
||||||
|
FrostShareCommitmentsView.routeName,
|
||||||
|
arguments: (
|
||||||
|
walletName: widget.walletName,
|
||||||
|
coin: widget.coin,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,478 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:barcode_scan2/barcode_scan2.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:frostdart/frostdart.dart';
|
||||||
|
import 'package:stackwallet/notifications/show_flush_bar.dart';
|
||||||
|
import 'package:stackwallet/pages/home_view/home_view.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
|
||||||
|
import 'package:stackwallet/providers/db/main_db_provider.dart';
|
||||||
|
import 'package:stackwallet/providers/global/node_service_provider.dart';
|
||||||
|
import 'package:stackwallet/providers/global/prefs_provider.dart';
|
||||||
|
import 'package:stackwallet/providers/global/secure_store_provider.dart';
|
||||||
|
import 'package:stackwallet/providers/global/wallets_provider.dart';
|
||||||
|
import 'package:stackwallet/services/frost.dart';
|
||||||
|
import 'package:stackwallet/themes/stack_colors.dart';
|
||||||
|
import 'package:stackwallet/utilities/assets.dart';
|
||||||
|
import 'package:stackwallet/utilities/constants.dart';
|
||||||
|
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||||
|
import 'package:stackwallet/utilities/logger.dart';
|
||||||
|
import 'package:stackwallet/utilities/show_loading.dart';
|
||||||
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
|
import 'package:stackwallet/utilities/util.dart';
|
||||||
|
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart';
|
||||||
|
import 'package:stackwallet/wallets/isar/models/wallet_info.dart';
|
||||||
|
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
|
||||||
|
import 'package:stackwallet/wallets/wallet/wallet.dart';
|
||||||
|
import 'package:stackwallet/widgets/background.dart';
|
||||||
|
import 'package:stackwallet/widgets/conditional_parent.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
|
||||||
|
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
|
||||||
|
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
|
||||||
|
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||||
|
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||||
|
import 'package:stackwallet/widgets/textfield_icon_button.dart';
|
||||||
|
|
||||||
|
class RestoreFrostMsWalletView extends ConsumerStatefulWidget {
|
||||||
|
const RestoreFrostMsWalletView({
|
||||||
|
super.key,
|
||||||
|
required this.walletName,
|
||||||
|
required this.coin,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const String routeName = "/restoreFrostMsWalletView";
|
||||||
|
|
||||||
|
final String walletName;
|
||||||
|
final Coin coin;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<RestoreFrostMsWalletView> createState() =>
|
||||||
|
_RestoreFrostMsWalletViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RestoreFrostMsWalletViewState
|
||||||
|
extends ConsumerState<RestoreFrostMsWalletView> {
|
||||||
|
late final TextEditingController keysFieldController, configFieldController;
|
||||||
|
late final FocusNode keysFocusNode, configFocusNode;
|
||||||
|
|
||||||
|
bool _keysEmpty = true, _configEmpty = true;
|
||||||
|
|
||||||
|
bool _restoreButtonLock = false;
|
||||||
|
|
||||||
|
Future<Wallet> _createWalletAndRecover() async {
|
||||||
|
final keys = keysFieldController.text;
|
||||||
|
final config = configFieldController.text;
|
||||||
|
|
||||||
|
final myNameIndex = getParticipantIndexFromKeys(serializedKeys: keys);
|
||||||
|
final participants = Frost.getParticipants(multisigConfig: config);
|
||||||
|
final myName = participants[myNameIndex];
|
||||||
|
|
||||||
|
final info = WalletInfo.createNew(
|
||||||
|
coin: widget.coin,
|
||||||
|
name: widget.walletName,
|
||||||
|
);
|
||||||
|
|
||||||
|
final wallet = await Wallet.create(
|
||||||
|
walletInfo: info,
|
||||||
|
mainDB: ref.read(mainDBProvider),
|
||||||
|
secureStorageInterface: ref.read(secureStoreProvider),
|
||||||
|
nodeService: ref.read(nodeServiceChangeNotifierProvider),
|
||||||
|
prefs: ref.read(prefsChangeNotifierProvider),
|
||||||
|
);
|
||||||
|
|
||||||
|
final frostInfo = FrostWalletInfo(
|
||||||
|
walletId: info.walletId,
|
||||||
|
knownSalts: [],
|
||||||
|
participants: participants,
|
||||||
|
myName: myName,
|
||||||
|
threshold: multisigThreshold(
|
||||||
|
multisigConfig: config,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await ref.read(mainDBProvider).isar.frostWalletInfo.put(frostInfo);
|
||||||
|
|
||||||
|
await (wallet as BitcoinFrostWallet).recover(
|
||||||
|
serializedKeys: keys,
|
||||||
|
multisigConfig: config,
|
||||||
|
isRescan: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
await info.setMnemonicVerified(
|
||||||
|
isar: ref.read(mainDBProvider).isar,
|
||||||
|
);
|
||||||
|
|
||||||
|
return wallet;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _restore() async {
|
||||||
|
if (_restoreButtonLock) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_restoreButtonLock = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (FocusScope.of(context).hasFocus) {
|
||||||
|
FocusScope.of(context).unfocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
Exception? ex;
|
||||||
|
final wallet = await showLoading(
|
||||||
|
whileFuture: _createWalletAndRecover(),
|
||||||
|
context: context,
|
||||||
|
message: "Restoring wallet...",
|
||||||
|
isDesktop: Util.isDesktop,
|
||||||
|
onException: (e) {
|
||||||
|
ex = e;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ex != null) {
|
||||||
|
throw ex!;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.read(pWallets).addWallet(wallet!);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
if (Util.isDesktop) {
|
||||||
|
Navigator.of(context).popUntil(
|
||||||
|
ModalRoute.withName(
|
||||||
|
DesktopHomeView.routeName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
unawaited(
|
||||||
|
Navigator.of(context).pushNamedAndRemoveUntil(
|
||||||
|
HomeView.routeName,
|
||||||
|
(route) => false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
unawaited(
|
||||||
|
showFloatingFlushBar(
|
||||||
|
type: FlushBarType.success,
|
||||||
|
message: "Your wallet is set up.",
|
||||||
|
iconAsset: Assets.svg.check,
|
||||||
|
context: context,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"$e\n$s",
|
||||||
|
level: LogLevel.Fatal,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => StackOkDialog(
|
||||||
|
title: "Failed to restore",
|
||||||
|
message: e.toString(),
|
||||||
|
desktopPopRootNavigator: Util.isDesktop,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_restoreButtonLock = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
keysFieldController = TextEditingController();
|
||||||
|
configFieldController = TextEditingController();
|
||||||
|
keysFocusNode = FocusNode();
|
||||||
|
configFocusNode = FocusNode();
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
keysFieldController.dispose();
|
||||||
|
configFieldController.dispose();
|
||||||
|
keysFocusNode.dispose();
|
||||||
|
configFocusNode.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ConditionalParent(
|
||||||
|
condition: Util.isDesktop,
|
||||||
|
builder: (child) => DesktopScaffold(
|
||||||
|
background: Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: const DesktopAppBar(
|
||||||
|
isCompactHeight: false,
|
||||||
|
leading: AppBarBackButton(),
|
||||||
|
trailing: ExitToMyStackButton(),
|
||||||
|
),
|
||||||
|
body: SizedBox(
|
||||||
|
width: 480,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ConditionalParent(
|
||||||
|
condition: !Util.isDesktop,
|
||||||
|
builder: (child) => Background(
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: AppBarBackButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
"Restore FROST multisig wallet",
|
||||||
|
style: STextStyles.navBarTitle(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: constraints.maxHeight,
|
||||||
|
),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
Constants.size.circularBorderRadius,
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
key: const Key("frMyNameTextFieldKey"),
|
||||||
|
controller: keysFieldController,
|
||||||
|
onChanged: (_) {
|
||||||
|
setState(() {
|
||||||
|
_keysEmpty = keysFieldController.text.isEmpty;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
focusNode: keysFocusNode,
|
||||||
|
readOnly: false,
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
style: STextStyles.field(context),
|
||||||
|
decoration: standardInputDecoration(
|
||||||
|
"Keys",
|
||||||
|
keysFocusNode,
|
||||||
|
context,
|
||||||
|
).copyWith(
|
||||||
|
contentPadding: const EdgeInsets.only(
|
||||||
|
left: 16,
|
||||||
|
top: 6,
|
||||||
|
bottom: 8,
|
||||||
|
right: 5,
|
||||||
|
),
|
||||||
|
suffixIcon: Padding(
|
||||||
|
padding: _keysEmpty
|
||||||
|
? const EdgeInsets.only(right: 8)
|
||||||
|
: const EdgeInsets.only(right: 0),
|
||||||
|
child: UnconstrainedBox(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
!_keysEmpty
|
||||||
|
? TextFieldIconButton(
|
||||||
|
semanticsLabel:
|
||||||
|
"Clear Button. Clears The Keys Field.",
|
||||||
|
key: const Key("frMyNameClearButtonKey"),
|
||||||
|
onTap: () {
|
||||||
|
keysFieldController.text = "";
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_keysEmpty = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const XIcon(),
|
||||||
|
)
|
||||||
|
: TextFieldIconButton(
|
||||||
|
semanticsLabel:
|
||||||
|
"Paste Button. Pastes From Clipboard To Keys Field.",
|
||||||
|
key: const Key("frKeysPasteButtonKey"),
|
||||||
|
onTap: () async {
|
||||||
|
final ClipboardData? data =
|
||||||
|
await Clipboard.getData(
|
||||||
|
Clipboard.kTextPlain);
|
||||||
|
if (data?.text != null &&
|
||||||
|
data!.text!.isNotEmpty) {
|
||||||
|
keysFieldController.text =
|
||||||
|
data.text!.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_keysEmpty =
|
||||||
|
keysFieldController.text.isEmpty;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: _keysEmpty
|
||||||
|
? const ClipboardIcon()
|
||||||
|
: const XIcon(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
Constants.size.circularBorderRadius,
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
key: const Key("frConfigTextFieldKey"),
|
||||||
|
controller: configFieldController,
|
||||||
|
onChanged: (_) {
|
||||||
|
setState(() {
|
||||||
|
_configEmpty = configFieldController.text.isEmpty;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
focusNode: configFocusNode,
|
||||||
|
readOnly: false,
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
style: STextStyles.field(context),
|
||||||
|
decoration: standardInputDecoration(
|
||||||
|
"Enter config",
|
||||||
|
configFocusNode,
|
||||||
|
context,
|
||||||
|
).copyWith(
|
||||||
|
contentPadding: const EdgeInsets.only(
|
||||||
|
left: 16,
|
||||||
|
top: 6,
|
||||||
|
bottom: 8,
|
||||||
|
right: 5,
|
||||||
|
),
|
||||||
|
suffixIcon: Padding(
|
||||||
|
padding: _configEmpty
|
||||||
|
? const EdgeInsets.only(right: 8)
|
||||||
|
: const EdgeInsets.only(right: 0),
|
||||||
|
child: UnconstrainedBox(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
!_configEmpty
|
||||||
|
? TextFieldIconButton(
|
||||||
|
semanticsLabel:
|
||||||
|
"Clear Button. Clears The Config Field.",
|
||||||
|
key: const Key("frConfigClearButtonKey"),
|
||||||
|
onTap: () {
|
||||||
|
configFieldController.text = "";
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_configEmpty = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const XIcon(),
|
||||||
|
)
|
||||||
|
: TextFieldIconButton(
|
||||||
|
semanticsLabel:
|
||||||
|
"Paste Button. Pastes From Clipboard To Config Field Input.",
|
||||||
|
key: const Key("frConfigPasteButtonKey"),
|
||||||
|
onTap: () async {
|
||||||
|
final ClipboardData? data =
|
||||||
|
await Clipboard.getData(
|
||||||
|
Clipboard.kTextPlain);
|
||||||
|
if (data?.text != null &&
|
||||||
|
data!.text!.isNotEmpty) {
|
||||||
|
configFieldController.text =
|
||||||
|
data.text!.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_configEmpty =
|
||||||
|
configFieldController.text.isEmpty;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: _configEmpty
|
||||||
|
? const ClipboardIcon()
|
||||||
|
: const XIcon(),
|
||||||
|
),
|
||||||
|
if (_configEmpty)
|
||||||
|
TextFieldIconButton(
|
||||||
|
semanticsLabel:
|
||||||
|
"Scan QR Button. Opens Camera For Scanning QR Code.",
|
||||||
|
key: const Key("frConfigScanQrButtonKey"),
|
||||||
|
onTap: () async {
|
||||||
|
try {
|
||||||
|
if (FocusScope.of(context).hasFocus) {
|
||||||
|
FocusScope.of(context).unfocus();
|
||||||
|
await Future<void>.delayed(
|
||||||
|
const Duration(milliseconds: 75));
|
||||||
|
}
|
||||||
|
|
||||||
|
final qrResult = await BarcodeScanner.scan();
|
||||||
|
|
||||||
|
configFieldController.text =
|
||||||
|
qrResult.rawContent;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_configEmpty =
|
||||||
|
configFieldController.text.isEmpty;
|
||||||
|
});
|
||||||
|
} on PlatformException catch (e, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"Failed to get camera permissions while trying to scan qr code: $e\n$s",
|
||||||
|
level: LogLevel.Warning,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const QrCodeIcon(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
if (!Util.isDesktop) const Spacer(),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
PrimaryButton(
|
||||||
|
label: "Restore",
|
||||||
|
enabled: !_keysEmpty && !_configEmpty,
|
||||||
|
onPressed: _restore,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,9 +14,13 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/sub_widgets/coin_image.dart';
|
import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/sub_widgets/coin_image.dart';
|
||||||
|
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart';
|
||||||
|
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart';
|
||||||
|
import 'package:stackwallet/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart';
|
||||||
import 'package:stackwallet/pages/add_wallet_views/new_wallet_options/new_wallet_options_view.dart';
|
import 'package:stackwallet/pages/add_wallet_views/new_wallet_options/new_wallet_options_view.dart';
|
||||||
import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart';
|
import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart';
|
||||||
import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart';
|
import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart';
|
||||||
|
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart';
|
||||||
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
|
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
|
||||||
import 'package:stackwallet/providers/db/main_db_provider.dart';
|
import 'package:stackwallet/providers/db/main_db_provider.dart';
|
||||||
import 'package:stackwallet/providers/ui/verify_recovery_phrase/mnemonic_word_count_state_provider.dart';
|
import 'package:stackwallet/providers/ui/verify_recovery_phrase/mnemonic_word_count_state_provider.dart';
|
||||||
|
@ -32,6 +36,8 @@ import 'package:stackwallet/widgets/background.dart';
|
||||||
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
|
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
|
||||||
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
|
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
|
||||||
import 'package:stackwallet/widgets/icon_widgets/dice_icon.dart';
|
import 'package:stackwallet/widgets/icon_widgets/dice_icon.dart';
|
||||||
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
|
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
|
||||||
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
||||||
|
@ -77,6 +83,52 @@ class _NameYourWalletViewState extends ConsumerState<NameYourWalletView> {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _nextPressed() async {
|
||||||
|
final name = textEditingController.text;
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
// hide keyboard if has focus
|
||||||
|
if (FocusScope.of(context).hasFocus) {
|
||||||
|
FocusScope.of(context).unfocus();
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
ref.read(mnemonicWordCountStateProvider.state).state =
|
||||||
|
Constants.possibleLengthsForCoin(coin).last;
|
||||||
|
ref.read(pNewWalletOptions.notifier).state = null;
|
||||||
|
|
||||||
|
switch (widget.addWalletType) {
|
||||||
|
case AddWalletType.New:
|
||||||
|
unawaited(
|
||||||
|
Navigator.of(context).pushNamed(
|
||||||
|
coin.hasMnemonicPassphraseSupport
|
||||||
|
? NewWalletOptionsView.routeName
|
||||||
|
: NewWalletRecoveryPhraseWarningView.routeName,
|
||||||
|
arguments: Tuple2(
|
||||||
|
name,
|
||||||
|
coin,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AddWalletType.Restore:
|
||||||
|
unawaited(
|
||||||
|
Navigator.of(context).pushNamed(
|
||||||
|
RestoreOptionsView.routeName,
|
||||||
|
arguments: Tuple2(
|
||||||
|
name,
|
||||||
|
coin,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
isDesktop = Util.isDesktop;
|
isDesktop = Util.isDesktop;
|
||||||
|
@ -330,61 +382,87 @@ class _NameYourWalletViewState extends ConsumerState<NameYourWalletView> {
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 32,
|
height: 32,
|
||||||
),
|
),
|
||||||
|
if (widget.coin.isFrost)
|
||||||
|
if (widget.addWalletType == AddWalletType.Restore)
|
||||||
|
PrimaryButton(
|
||||||
|
label: "Next",
|
||||||
|
enabled: _nextEnabled,
|
||||||
|
onPressed: () async {
|
||||||
|
final name = textEditingController.text;
|
||||||
|
|
||||||
|
await Navigator.of(context).pushNamed(
|
||||||
|
RestoreFrostMsWalletView.routeName,
|
||||||
|
arguments: (
|
||||||
|
walletName: name,
|
||||||
|
coin: coin,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (widget.addWalletType == AddWalletType.New)
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
PrimaryButton(
|
||||||
|
label: "Create config",
|
||||||
|
enabled: _nextEnabled,
|
||||||
|
onPressed: () async {
|
||||||
|
final name = textEditingController.text;
|
||||||
|
|
||||||
|
await Navigator.of(context).pushNamed(
|
||||||
|
CreateNewFrostMsWalletView.routeName,
|
||||||
|
arguments: (
|
||||||
|
walletName: name,
|
||||||
|
coin: coin,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 12,
|
||||||
|
),
|
||||||
|
SecondaryButton(
|
||||||
|
label: "Import multisig config",
|
||||||
|
enabled: _nextEnabled,
|
||||||
|
onPressed: () async {
|
||||||
|
final name = textEditingController.text;
|
||||||
|
|
||||||
|
await Navigator.of(context).pushNamed(
|
||||||
|
ImportNewFrostMsWalletView.routeName,
|
||||||
|
arguments: (
|
||||||
|
walletName: name,
|
||||||
|
coin: coin,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 12,
|
||||||
|
),
|
||||||
|
SecondaryButton(
|
||||||
|
label: "Import resharer config",
|
||||||
|
enabled: _nextEnabled,
|
||||||
|
onPressed: () async {
|
||||||
|
final name = textEditingController.text;
|
||||||
|
|
||||||
|
await Navigator.of(context).pushNamed(
|
||||||
|
NewImportResharerConfigView.routeName,
|
||||||
|
arguments: (
|
||||||
|
walletName: name,
|
||||||
|
coin: coin,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (!widget.coin.isFrost)
|
||||||
ConstrainedBox(
|
ConstrainedBox(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
minWidth: isDesktop ? 480 : 0,
|
minWidth: isDesktop ? 480 : 0,
|
||||||
minHeight: isDesktop ? 70 : 0,
|
minHeight: isDesktop ? 70 : 0,
|
||||||
),
|
),
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: _nextEnabled
|
onPressed: _nextEnabled ? _nextPressed : null,
|
||||||
? () async {
|
|
||||||
final name = textEditingController.text;
|
|
||||||
|
|
||||||
if (mounted) {
|
|
||||||
// hide keyboard if has focus
|
|
||||||
if (FocusScope.of(context).hasFocus) {
|
|
||||||
FocusScope.of(context).unfocus();
|
|
||||||
await Future<void>.delayed(
|
|
||||||
const Duration(milliseconds: 50));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mounted) {
|
|
||||||
ref.read(mnemonicWordCountStateProvider.state).state =
|
|
||||||
Constants.possibleLengthsForCoin(coin).last;
|
|
||||||
ref.read(pNewWalletOptions.notifier).state = null;
|
|
||||||
|
|
||||||
switch (widget.addWalletType) {
|
|
||||||
case AddWalletType.New:
|
|
||||||
unawaited(
|
|
||||||
Navigator.of(context).pushNamed(
|
|
||||||
coin.hasMnemonicPassphraseSupport
|
|
||||||
? NewWalletOptionsView.routeName
|
|
||||||
: NewWalletRecoveryPhraseWarningView
|
|
||||||
.routeName,
|
|
||||||
arguments: Tuple2(
|
|
||||||
name,
|
|
||||||
coin,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case AddWalletType.Restore:
|
|
||||||
unawaited(
|
|
||||||
Navigator.of(context).pushNamed(
|
|
||||||
RestoreOptionsView.routeName,
|
|
||||||
arguments: Tuple2(
|
|
||||||
name,
|
|
||||||
coin,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
style: _nextEnabled
|
style: _nextEnabled
|
||||||
? Theme.of(context)
|
? Theme.of(context)
|
||||||
.extension<StackColors>()!
|
.extension<StackColors>()!
|
||||||
|
|
|
@ -0,0 +1,162 @@
|
||||||
|
/*
|
||||||
|
* This file is part of Stack Wallet.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2023 Cypher Stack
|
||||||
|
* All Rights Reserved.
|
||||||
|
* The code is distributed under GPLv3 license, see LICENSE file for details.
|
||||||
|
* Generated by Cypher Stack on 2023-05-26
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart';
|
||||||
|
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/begin_reshare_config_view.dart';
|
||||||
|
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1b/import_reshare_config_view.dart';
|
||||||
|
import 'package:stackwallet/providers/db/main_db_provider.dart';
|
||||||
|
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||||
|
import 'package:stackwallet/themes/stack_colors.dart';
|
||||||
|
import 'package:stackwallet/utilities/constants.dart';
|
||||||
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
|
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart';
|
||||||
|
import 'package:stackwallet/widgets/background.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
||||||
|
|
||||||
|
class FrostMSWalletOptionsView extends ConsumerWidget {
|
||||||
|
const FrostMSWalletOptionsView({
|
||||||
|
Key? key,
|
||||||
|
required this.walletId,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
static const String routeName = "/frostMSWalletOptionsView";
|
||||||
|
|
||||||
|
final String walletId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return Background(
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: AppBarBackButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
"Multisig settings",
|
||||||
|
style: STextStyles.navBarTitle(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
top: 12,
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
_OptionButton(
|
||||||
|
label: "Show participants",
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pushNamed(
|
||||||
|
FrostParticipantsView.routeName,
|
||||||
|
arguments: walletId,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
),
|
||||||
|
_OptionButton(
|
||||||
|
label: "Initiate resharing",
|
||||||
|
onPressed: () {
|
||||||
|
// TODO: optimize this by creating watcher providers (similar to normal WalletInfo)
|
||||||
|
final frostInfo = ref
|
||||||
|
.read(mainDBProvider)
|
||||||
|
.isar
|
||||||
|
.frostWalletInfo
|
||||||
|
.getByWalletIdSync(walletId)!;
|
||||||
|
|
||||||
|
ref.read(pFrostMyName.state).state = frostInfo.myName;
|
||||||
|
|
||||||
|
Navigator.of(context).pushNamed(
|
||||||
|
BeginReshareConfigView.routeName,
|
||||||
|
arguments: walletId,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
),
|
||||||
|
_OptionButton(
|
||||||
|
label: "Import reshare config",
|
||||||
|
onPressed: () {
|
||||||
|
// TODO: optimize this by creating watcher providers (similar to normal WalletInfo)
|
||||||
|
final frostInfo = ref
|
||||||
|
.read(mainDBProvider)
|
||||||
|
.isar
|
||||||
|
.frostWalletInfo
|
||||||
|
.getByWalletIdSync(walletId)!;
|
||||||
|
|
||||||
|
ref.read(pFrostMyName.state).state = frostInfo.myName;
|
||||||
|
|
||||||
|
Navigator.of(context).pushNamed(
|
||||||
|
ImportReshareConfigView.routeName,
|
||||||
|
arguments: walletId,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OptionButton extends StatelessWidget {
|
||||||
|
const _OptionButton({
|
||||||
|
super.key,
|
||||||
|
required this.label,
|
||||||
|
required this.onPressed,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final VoidCallback onPressed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return RoundedWhiteContainer(
|
||||||
|
padding: const EdgeInsets.all(0),
|
||||||
|
child: RawMaterialButton(
|
||||||
|
// splashColor: Theme.of(context).extension<StackColors>()!.highlight,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
Constants.size.circularBorderRadius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
onPressed: onPressed,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12.0,
|
||||||
|
vertical: 20,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: STextStyles.titleBold12(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,119 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
|
||||||
|
import 'package:stackwallet/providers/db/main_db_provider.dart';
|
||||||
|
import 'package:stackwallet/themes/stack_colors.dart';
|
||||||
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
|
import 'package:stackwallet/utilities/util.dart';
|
||||||
|
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart';
|
||||||
|
import 'package:stackwallet/widgets/background.dart';
|
||||||
|
import 'package:stackwallet/widgets/conditional_parent.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
|
||||||
|
|
||||||
|
class FrostParticipantsView extends ConsumerWidget {
|
||||||
|
const FrostParticipantsView({
|
||||||
|
super.key,
|
||||||
|
required this.walletId,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const String routeName = "/frostParticipantsView";
|
||||||
|
|
||||||
|
final String walletId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
// TODO: optimize this by creating watcher providers (similar to normal WalletInfo)
|
||||||
|
final frostInfo = ref
|
||||||
|
.read(mainDBProvider)
|
||||||
|
.isar
|
||||||
|
.frostWalletInfo
|
||||||
|
.getByWalletIdSync(walletId)!;
|
||||||
|
|
||||||
|
return ConditionalParent(
|
||||||
|
condition: Util.isDesktop,
|
||||||
|
builder: (child) => DesktopScaffold(
|
||||||
|
background: Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: const DesktopAppBar(
|
||||||
|
isCompactHeight: false,
|
||||||
|
leading: AppBarBackButton(),
|
||||||
|
trailing: ExitToMyStackButton(),
|
||||||
|
),
|
||||||
|
body: SizedBox(
|
||||||
|
width: 480,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ConditionalParent(
|
||||||
|
condition: !Util.isDesktop,
|
||||||
|
builder: (child) => Background(
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: AppBarBackButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
"Participants",
|
||||||
|
style: STextStyles.navBarTitle(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: constraints.maxHeight,
|
||||||
|
),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
for (int i = 0; i < frostInfo.participants.length; i++)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Index $i",
|
||||||
|
style: STextStyles.label(context),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 6,
|
||||||
|
),
|
||||||
|
SelectableText(
|
||||||
|
frostInfo.participants[i] == frostInfo.myName
|
||||||
|
? "${frostInfo.participants[i]} (me)"
|
||||||
|
: frostInfo.participants[i],
|
||||||
|
style: STextStyles.itemSubtitle12(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,433 @@
|
||||||
|
import 'dart:ffi';
|
||||||
|
|
||||||
|
import 'package:barcode_scan2/barcode_scan2.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:qr_flutter/qr_flutter.dart';
|
||||||
|
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/verify_updated_wallet_view.dart';
|
||||||
|
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
|
||||||
|
import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart';
|
||||||
|
import 'package:stackwallet/providers/db/main_db_provider.dart';
|
||||||
|
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||||
|
import 'package:stackwallet/services/frost.dart';
|
||||||
|
import 'package:stackwallet/themes/stack_colors.dart';
|
||||||
|
import 'package:stackwallet/utilities/constants.dart';
|
||||||
|
import 'package:stackwallet/utilities/logger.dart';
|
||||||
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
|
import 'package:stackwallet/utilities/util.dart';
|
||||||
|
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart';
|
||||||
|
import 'package:stackwallet/widgets/background.dart';
|
||||||
|
import 'package:stackwallet/widgets/conditional_parent.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/detail_item.dart';
|
||||||
|
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
|
||||||
|
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
|
||||||
|
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
|
||||||
|
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||||
|
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||||
|
import 'package:stackwallet/widgets/textfield_icon_button.dart';
|
||||||
|
|
||||||
|
class FinishResharingView extends ConsumerStatefulWidget {
|
||||||
|
const FinishResharingView({
|
||||||
|
super.key,
|
||||||
|
required this.walletId,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const String routeName = "/finishResharingView";
|
||||||
|
|
||||||
|
final String walletId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<FinishResharingView> createState() =>
|
||||||
|
_FinishResharingViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FinishResharingViewState extends ConsumerState<FinishResharingView> {
|
||||||
|
final List<TextEditingController> controllers = [];
|
||||||
|
final List<FocusNode> focusNodes = [];
|
||||||
|
|
||||||
|
late final List<int> resharerIndexes;
|
||||||
|
late final String myName;
|
||||||
|
late final int? myResharerIndexIndex;
|
||||||
|
late final String? myResharerComplete;
|
||||||
|
late final bool amOutgoingParticipant;
|
||||||
|
|
||||||
|
final List<bool> fieldIsEmptyFlags = [];
|
||||||
|
|
||||||
|
bool _buttonLock = false;
|
||||||
|
Future<void> _onPressed() async {
|
||||||
|
if (_buttonLock) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_buttonLock = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (amOutgoingParticipant) {
|
||||||
|
ref.read(pFrostResharingData).reset();
|
||||||
|
Navigator.of(context).popUntil(
|
||||||
|
ModalRoute.withName(
|
||||||
|
Util.isDesktop ? DesktopWalletView.routeName : WalletView.routeName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// collect resharer completes strings and insert my own at the correct index
|
||||||
|
final resharerCompletes = controllers.map((e) => e.text).toList();
|
||||||
|
if (myResharerIndexIndex != null && myResharerComplete != null) {
|
||||||
|
resharerCompletes.insert(myResharerIndexIndex!, myResharerComplete!);
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = Frost.finishReshared(
|
||||||
|
prior: ref.read(pFrostResharingData).startResharedData!.prior.ref,
|
||||||
|
resharerCompletes: resharerCompletes,
|
||||||
|
);
|
||||||
|
|
||||||
|
ref.read(pFrostResharingData).newWalletData = data;
|
||||||
|
|
||||||
|
await Navigator.of(context).pushNamed(
|
||||||
|
VerifyUpdatedWalletView.routeName,
|
||||||
|
arguments: widget.walletId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"$e\n$s",
|
||||||
|
level: LogLevel.Fatal,
|
||||||
|
);
|
||||||
|
if (mounted) {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => StackOkDialog(
|
||||||
|
title: "Error",
|
||||||
|
message: e.toString(),
|
||||||
|
desktopPopRootNavigator: Util.isDesktop,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_buttonLock = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
final amNewParticipant =
|
||||||
|
ref.read(pFrostResharingData).startResharerData == null &&
|
||||||
|
ref.read(pFrostResharingData).incompleteWallet != null &&
|
||||||
|
ref.read(pFrostResharingData).incompleteWallet?.walletId ==
|
||||||
|
widget.walletId;
|
||||||
|
|
||||||
|
myName = ref.read(pFrostResharingData).myName!;
|
||||||
|
|
||||||
|
resharerIndexes = ref.read(pFrostResharingData).configData!.resharers;
|
||||||
|
|
||||||
|
if (amNewParticipant) {
|
||||||
|
myResharerComplete = null;
|
||||||
|
myResharerIndexIndex = null;
|
||||||
|
amOutgoingParticipant = false;
|
||||||
|
} else {
|
||||||
|
myResharerComplete = ref.read(pFrostResharingData).resharerComplete!;
|
||||||
|
|
||||||
|
final frostInfo = ref
|
||||||
|
.read(mainDBProvider)
|
||||||
|
.isar
|
||||||
|
.frostWalletInfo
|
||||||
|
.getByWalletIdSync(widget.walletId)!;
|
||||||
|
final myOldIndex =
|
||||||
|
frostInfo.participants.indexOf(ref.read(pFrostResharingData).myName!);
|
||||||
|
|
||||||
|
myResharerIndexIndex = resharerIndexes.indexOf(myOldIndex);
|
||||||
|
if (myResharerIndexIndex! >= 0) {
|
||||||
|
// remove my name for now as we don't need a text field for it
|
||||||
|
resharerIndexes.removeAt(myResharerIndexIndex!);
|
||||||
|
}
|
||||||
|
|
||||||
|
amOutgoingParticipant = !ref
|
||||||
|
.read(pFrostResharingData)
|
||||||
|
.configData!
|
||||||
|
.newParticipants
|
||||||
|
.contains(ref.read(pFrostResharingData).myName!);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < resharerIndexes.length; i++) {
|
||||||
|
controllers.add(TextEditingController());
|
||||||
|
focusNodes.add(FocusNode());
|
||||||
|
fieldIsEmptyFlags.add(true);
|
||||||
|
}
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
for (int i = 0; i < controllers.length; i++) {
|
||||||
|
controllers[i].dispose();
|
||||||
|
}
|
||||||
|
for (int i = 0; i < focusNodes.length; i++) {
|
||||||
|
focusNodes[i].dispose();
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ConditionalParent(
|
||||||
|
condition: Util.isDesktop,
|
||||||
|
builder: (child) => DesktopScaffold(
|
||||||
|
background: Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: const DesktopAppBar(
|
||||||
|
isCompactHeight: false,
|
||||||
|
leading: AppBarBackButton(),
|
||||||
|
trailing: ExitToMyStackButton(),
|
||||||
|
),
|
||||||
|
body: SizedBox(
|
||||||
|
width: 480,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ConditionalParent(
|
||||||
|
condition: !Util.isDesktop,
|
||||||
|
builder: (child) => Background(
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: AppBarBackButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
"Resharer completes",
|
||||||
|
style: STextStyles.navBarTitle(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: constraints.maxHeight,
|
||||||
|
),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
if (myResharerComplete != null)
|
||||||
|
SizedBox(
|
||||||
|
height: 220,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
QrImageView(
|
||||||
|
data: myResharerComplete!,
|
||||||
|
size: 220,
|
||||||
|
backgroundColor: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.background,
|
||||||
|
foregroundColor: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.accentColorDark,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (myResharerComplete != null) const _Div(),
|
||||||
|
if (myResharerComplete != null)
|
||||||
|
DetailItem(
|
||||||
|
title: "My resharer complete",
|
||||||
|
detail: myResharerComplete!,
|
||||||
|
button: Util.isDesktop
|
||||||
|
? IconCopyButton(
|
||||||
|
data: myResharerComplete!,
|
||||||
|
)
|
||||||
|
: SimpleCopyButton(
|
||||||
|
data: myResharerComplete!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!amOutgoingParticipant) const _Div(),
|
||||||
|
if (!amOutgoingParticipant)
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
for (int i = 0; i < resharerIndexes.length; i++)
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
Constants.size.circularBorderRadius,
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
key: Key("frostEncryptionKeyTextFieldKey_$i"),
|
||||||
|
controller: controllers[i],
|
||||||
|
focusNode: focusNodes[i],
|
||||||
|
readOnly: false,
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
style: STextStyles.field(context),
|
||||||
|
onChanged: (_) {
|
||||||
|
setState(() {
|
||||||
|
fieldIsEmptyFlags[i] =
|
||||||
|
controllers[i].text.isEmpty;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
decoration: standardInputDecoration(
|
||||||
|
"Enter index "
|
||||||
|
"${resharerIndexes[i]}"
|
||||||
|
"'s resharer complete",
|
||||||
|
focusNodes[i],
|
||||||
|
context,
|
||||||
|
).copyWith(
|
||||||
|
contentPadding: const EdgeInsets.only(
|
||||||
|
left: 16,
|
||||||
|
top: 6,
|
||||||
|
bottom: 8,
|
||||||
|
right: 5,
|
||||||
|
),
|
||||||
|
suffixIcon: Padding(
|
||||||
|
padding: fieldIsEmptyFlags[i]
|
||||||
|
? const EdgeInsets.only(right: 8)
|
||||||
|
: const EdgeInsets.only(right: 0),
|
||||||
|
child: UnconstrainedBox(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
!fieldIsEmptyFlags[i]
|
||||||
|
? TextFieldIconButton(
|
||||||
|
semanticsLabel:
|
||||||
|
"Clear Button. Clears The Encryption Key Field Input.",
|
||||||
|
key: Key(
|
||||||
|
"frostEncryptionKeyClearButtonKey_$i"),
|
||||||
|
onTap: () {
|
||||||
|
controllers[i].text = "";
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
fieldIsEmptyFlags[i] = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const XIcon(),
|
||||||
|
)
|
||||||
|
: TextFieldIconButton(
|
||||||
|
semanticsLabel:
|
||||||
|
"Paste Button. Pastes From Clipboard To Encryption Key Field Input.",
|
||||||
|
key: Key(
|
||||||
|
"frostEncryptionKeyPasteButtonKey_$i"),
|
||||||
|
onTap: () async {
|
||||||
|
final ClipboardData? data =
|
||||||
|
await Clipboard.getData(
|
||||||
|
Clipboard.kTextPlain);
|
||||||
|
if (data?.text != null &&
|
||||||
|
data!.text!.isNotEmpty) {
|
||||||
|
controllers[i].text =
|
||||||
|
data.text!.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
fieldIsEmptyFlags[i] =
|
||||||
|
controllers[i]
|
||||||
|
.text
|
||||||
|
.isEmpty;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: fieldIsEmptyFlags[i]
|
||||||
|
? const ClipboardIcon()
|
||||||
|
: const XIcon(),
|
||||||
|
),
|
||||||
|
if (fieldIsEmptyFlags[i])
|
||||||
|
TextFieldIconButton(
|
||||||
|
semanticsLabel: "Scan QR Button. "
|
||||||
|
"Opens Camera For Scanning QR Code.",
|
||||||
|
key: Key("frostScanQrButtonKey_$i"),
|
||||||
|
onTap: () async {
|
||||||
|
try {
|
||||||
|
if (FocusScope.of(context)
|
||||||
|
.hasFocus) {
|
||||||
|
FocusScope.of(context)
|
||||||
|
.unfocus();
|
||||||
|
await Future<void>.delayed(
|
||||||
|
const Duration(
|
||||||
|
milliseconds: 75));
|
||||||
|
}
|
||||||
|
|
||||||
|
final qrResult =
|
||||||
|
await BarcodeScanner.scan();
|
||||||
|
|
||||||
|
controllers[i].text =
|
||||||
|
qrResult.rawContent;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
fieldIsEmptyFlags[i] =
|
||||||
|
controllers[i]
|
||||||
|
.text
|
||||||
|
.isEmpty;
|
||||||
|
});
|
||||||
|
} on PlatformException catch (e, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"Failed to get camera permissions "
|
||||||
|
"while trying to scan qr code: $e\n$s",
|
||||||
|
level: LogLevel.Warning,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const QrCodeIcon(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (!Util.isDesktop) const Spacer(),
|
||||||
|
const _Div(),
|
||||||
|
PrimaryButton(
|
||||||
|
label: amOutgoingParticipant ? "Exit" : "Complete",
|
||||||
|
enabled: amOutgoingParticipant ||
|
||||||
|
!fieldIsEmptyFlags.reduce((v, e) => v |= e),
|
||||||
|
onPressed: _onPressed,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Div extends StatelessWidget {
|
||||||
|
const _Div({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const SizedBox(
|
||||||
|
height: 12,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,196 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/complete_reshare_config_view.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
|
||||||
|
import 'package:stackwallet/providers/db/main_db_provider.dart';
|
||||||
|
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||||
|
import 'package:stackwallet/themes/stack_colors.dart';
|
||||||
|
import 'package:stackwallet/utilities/constants.dart';
|
||||||
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
|
import 'package:stackwallet/utilities/util.dart';
|
||||||
|
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart';
|
||||||
|
import 'package:stackwallet/widgets/background.dart';
|
||||||
|
import 'package:stackwallet/widgets/conditional_parent.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||||
|
|
||||||
|
final class BeginReshareConfigView extends ConsumerStatefulWidget {
|
||||||
|
const BeginReshareConfigView({
|
||||||
|
super.key,
|
||||||
|
required this.walletId,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const String routeName = "/beginReshareConfigView";
|
||||||
|
|
||||||
|
final String walletId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<BeginReshareConfigView> createState() =>
|
||||||
|
_BeginReshareConfigViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BeginReshareConfigViewState
|
||||||
|
extends ConsumerState<BeginReshareConfigView> {
|
||||||
|
late final int currentThreshold;
|
||||||
|
late final List<String> currentParticipants;
|
||||||
|
|
||||||
|
final Map<String, int> pFrostResharersMap = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
ref.read(pFrostResharingData).reset();
|
||||||
|
|
||||||
|
// TODO: optimize this by creating watcher providers (similar to normal WalletInfo)
|
||||||
|
final frostInfo = ref
|
||||||
|
.read(mainDBProvider)
|
||||||
|
.isar
|
||||||
|
.frostWalletInfo
|
||||||
|
.getByWalletIdSync(widget.walletId)!;
|
||||||
|
|
||||||
|
currentThreshold = frostInfo.threshold;
|
||||||
|
currentParticipants = frostInfo.participants;
|
||||||
|
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ConditionalParent(
|
||||||
|
condition: Util.isDesktop,
|
||||||
|
builder: (child) => DesktopScaffold(
|
||||||
|
background: Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: const DesktopAppBar(
|
||||||
|
isCompactHeight: false,
|
||||||
|
leading: AppBarBackButton(),
|
||||||
|
trailing: ExitToMyStackButton(),
|
||||||
|
),
|
||||||
|
body: SizedBox(
|
||||||
|
width: 480,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ConditionalParent(
|
||||||
|
condition: !Util.isDesktop,
|
||||||
|
builder: (child) => Background(
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: AppBarBackButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// title: Text(
|
||||||
|
// "Modify Participants",
|
||||||
|
// style: STextStyles.navBarTitle(context),
|
||||||
|
// ),
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: constraints.maxHeight,
|
||||||
|
),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Select participants for resharing",
|
||||||
|
style: STextStyles.label(context),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
for (int i = 0; i < currentParticipants.length; i++)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
top: 10,
|
||||||
|
),
|
||||||
|
child: RawMaterialButton(
|
||||||
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
Constants.size.circularBorderRadius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
if (pFrostResharersMap[currentParticipants[i]] ==
|
||||||
|
null) {
|
||||||
|
pFrostResharersMap[currentParticipants[i]] = i;
|
||||||
|
} else {
|
||||||
|
pFrostResharersMap.remove(currentParticipants[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Checkbox(
|
||||||
|
value: pFrostResharersMap[
|
||||||
|
currentParticipants[i]] ==
|
||||||
|
i,
|
||||||
|
onChanged: (bool? value) {},
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 10,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
currentParticipants[i],
|
||||||
|
style: STextStyles.itemSubtitle12(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (!Util.isDesktop) const Spacer(),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
PrimaryButton(
|
||||||
|
label: "Continue",
|
||||||
|
enabled: pFrostResharersMap.length >= currentThreshold,
|
||||||
|
onPressed: () async {
|
||||||
|
await Navigator.of(context).pushNamed(
|
||||||
|
CompleteReshareConfigView.routeName,
|
||||||
|
arguments: (
|
||||||
|
walletId: widget.walletId,
|
||||||
|
resharers:
|
||||||
|
pFrostResharersMap.values.toList(growable: false),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,335 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:frostdart/frostdart.dart';
|
||||||
|
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/display_reshare_config_view.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
|
||||||
|
import 'package:stackwallet/providers/db/main_db_provider.dart';
|
||||||
|
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||||
|
import 'package:stackwallet/services/frost.dart';
|
||||||
|
import 'package:stackwallet/themes/stack_colors.dart';
|
||||||
|
import 'package:stackwallet/utilities/format.dart';
|
||||||
|
import 'package:stackwallet/utilities/logger.dart';
|
||||||
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
|
import 'package:stackwallet/utilities/util.dart';
|
||||||
|
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart';
|
||||||
|
import 'package:stackwallet/widgets/background.dart';
|
||||||
|
import 'package:stackwallet/widgets/conditional_parent.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||||
|
|
||||||
|
final class CompleteReshareConfigView extends ConsumerStatefulWidget {
|
||||||
|
const CompleteReshareConfigView({
|
||||||
|
super.key,
|
||||||
|
required this.walletId,
|
||||||
|
required this.resharers,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const String routeName = "/completeReshareConfigView";
|
||||||
|
|
||||||
|
final String walletId;
|
||||||
|
final List<int> resharers;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<CompleteReshareConfigView> createState() =>
|
||||||
|
_CompleteReshareConfigViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CompleteReshareConfigViewState
|
||||||
|
extends ConsumerState<CompleteReshareConfigView> {
|
||||||
|
final _newThresholdController = TextEditingController();
|
||||||
|
final _newParticipantsCountController = TextEditingController();
|
||||||
|
|
||||||
|
final List<TextEditingController> controllers = [];
|
||||||
|
|
||||||
|
int _participantsCount = 0;
|
||||||
|
|
||||||
|
bool _buttonLock = false;
|
||||||
|
|
||||||
|
Future<void> _onPressed() async {
|
||||||
|
if (_buttonLock) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_buttonLock = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// TODO: optimize this by creating watcher providers (similar to normal WalletInfo)
|
||||||
|
final frostInfo = ref
|
||||||
|
.read(mainDBProvider)
|
||||||
|
.isar
|
||||||
|
.frostWalletInfo
|
||||||
|
.getByWalletIdSync(widget.walletId)!;
|
||||||
|
final validationMessage = _validateInputData();
|
||||||
|
|
||||||
|
if (validationMessage != "valid") {
|
||||||
|
return await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => StackOkDialog(
|
||||||
|
title: validationMessage,
|
||||||
|
desktopPopRootNavigator: Util.isDesktop,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final config = Frost.createResharerConfig(
|
||||||
|
newThreshold: int.parse(_newThresholdController.text),
|
||||||
|
resharers: widget.resharers,
|
||||||
|
newParticipants: controllers.map((e) => e.text).toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final salt = Format.uint8listToString(
|
||||||
|
resharerSalt(resharerConfig: config),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (frostInfo.knownSalts.contains(salt)) {
|
||||||
|
return await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => StackOkDialog(
|
||||||
|
title: "Duplicate config salt",
|
||||||
|
desktopPopRootNavigator: Util.isDesktop,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final salts = frostInfo.knownSalts;
|
||||||
|
salts.add(salt);
|
||||||
|
final mainDB = ref.read(mainDBProvider);
|
||||||
|
await mainDB.isar.writeTxn(() async {
|
||||||
|
final info = frostInfo;
|
||||||
|
await mainDB.isar.frostWalletInfo.delete(info.id);
|
||||||
|
await mainDB.isar.frostWalletInfo.put(
|
||||||
|
info.copyWith(knownSalts: salts),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.read(pFrostResharingData).myName = frostInfo.myName;
|
||||||
|
ref.read(pFrostResharingData).resharerConfig = config;
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
await Navigator.of(context).pushNamed(
|
||||||
|
DisplayReshareConfigView.routeName,
|
||||||
|
arguments: widget.walletId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"$e\n$s",
|
||||||
|
level: LogLevel.Fatal,
|
||||||
|
);
|
||||||
|
if (mounted) {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => StackOkDialog(
|
||||||
|
title: e.toString(),
|
||||||
|
desktopPopRootNavigator: Util.isDesktop,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_buttonLock = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _validateInputData() {
|
||||||
|
final threshold = int.tryParse(_newThresholdController.text);
|
||||||
|
if (threshold == null) {
|
||||||
|
return "Choose a threshold";
|
||||||
|
}
|
||||||
|
|
||||||
|
final partsCount = int.tryParse(_newParticipantsCountController.text);
|
||||||
|
if (partsCount == null) {
|
||||||
|
return "Choose total number of participants";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (threshold > partsCount) {
|
||||||
|
return "Threshold cannot be greater than the number of participants";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (partsCount < 2) {
|
||||||
|
return "At least two participants required";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controllers.length != partsCount) {
|
||||||
|
return "Participants count error";
|
||||||
|
}
|
||||||
|
|
||||||
|
final hasEmptyParticipants = controllers
|
||||||
|
.map((e) => e.text.isEmpty)
|
||||||
|
.reduce((value, element) => value |= element);
|
||||||
|
if (hasEmptyParticipants) {
|
||||||
|
return "Participants must not be empty";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controllers.length != controllers.map((e) => e.text).toSet().length) {
|
||||||
|
return "Duplicate participant name found";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "valid";
|
||||||
|
}
|
||||||
|
|
||||||
|
void _participantsCountChanged(String newValue) {
|
||||||
|
final count = int.tryParse(newValue);
|
||||||
|
if (count != null) {
|
||||||
|
if (count > _participantsCount) {
|
||||||
|
for (int i = _participantsCount; i < count; i++) {
|
||||||
|
controllers.add(TextEditingController());
|
||||||
|
}
|
||||||
|
|
||||||
|
_participantsCount = count;
|
||||||
|
setState(() {});
|
||||||
|
} else if (count < _participantsCount) {
|
||||||
|
for (int i = _participantsCount; i > count; i--) {
|
||||||
|
final last = controllers.removeLast();
|
||||||
|
last.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_participantsCount = count;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_newThresholdController.dispose();
|
||||||
|
_newParticipantsCountController.dispose();
|
||||||
|
for (final e in controllers) {
|
||||||
|
e.dispose();
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ConditionalParent(
|
||||||
|
condition: Util.isDesktop,
|
||||||
|
builder: (child) => DesktopScaffold(
|
||||||
|
background: Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: const DesktopAppBar(
|
||||||
|
isCompactHeight: false,
|
||||||
|
leading: AppBarBackButton(),
|
||||||
|
trailing: ExitToMyStackButton(),
|
||||||
|
),
|
||||||
|
body: SizedBox(
|
||||||
|
width: 480,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ConditionalParent(
|
||||||
|
condition: !Util.isDesktop,
|
||||||
|
builder: (child) => Background(
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: AppBarBackButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
"Modify Participants",
|
||||||
|
style: STextStyles.navBarTitle(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: constraints.maxHeight,
|
||||||
|
),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"New threshold",
|
||||||
|
style: STextStyles.label(context),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 10,
|
||||||
|
),
|
||||||
|
TextField(
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||||
|
controller: _newThresholdController,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"Number of participants",
|
||||||
|
style: STextStyles.label(context),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 10,
|
||||||
|
),
|
||||||
|
TextField(
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||||
|
controller: _newParticipantsCountController,
|
||||||
|
onChanged: _participantsCountChanged,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
if (controllers.isNotEmpty)
|
||||||
|
Text(
|
||||||
|
"Participants",
|
||||||
|
style: STextStyles.label(context),
|
||||||
|
),
|
||||||
|
if (controllers.isNotEmpty)
|
||||||
|
const SizedBox(
|
||||||
|
height: 10,
|
||||||
|
),
|
||||||
|
if (controllers.isNotEmpty)
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
for (int i = 0; i < controllers.length; i++)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
top: 10,
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
controller: controllers[i],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (!Util.isDesktop) const Spacer(),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
PrimaryButton(
|
||||||
|
label: "Generate config",
|
||||||
|
onPressed: () async {
|
||||||
|
if (FocusScope.of(context).hasFocus) {
|
||||||
|
FocusScope.of(context).unfocus();
|
||||||
|
}
|
||||||
|
await _onPressed();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,214 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:qr_flutter/qr_flutter.dart';
|
||||||
|
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_view.dart';
|
||||||
|
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
|
||||||
|
import 'package:stackwallet/providers/db/main_db_provider.dart';
|
||||||
|
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||||
|
import 'package:stackwallet/providers/global/wallets_provider.dart';
|
||||||
|
import 'package:stackwallet/services/frost.dart';
|
||||||
|
import 'package:stackwallet/themes/stack_colors.dart';
|
||||||
|
import 'package:stackwallet/utilities/logger.dart';
|
||||||
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
|
import 'package:stackwallet/utilities/util.dart';
|
||||||
|
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart';
|
||||||
|
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
|
||||||
|
import 'package:stackwallet/widgets/background.dart';
|
||||||
|
import 'package:stackwallet/widgets/conditional_parent.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/detail_item.dart';
|
||||||
|
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||||
|
|
||||||
|
class DisplayReshareConfigView extends ConsumerStatefulWidget {
|
||||||
|
const DisplayReshareConfigView({
|
||||||
|
super.key,
|
||||||
|
required this.walletId,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const String routeName = "/displayReshareConfigView";
|
||||||
|
|
||||||
|
final String walletId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<DisplayReshareConfigView> createState() =>
|
||||||
|
_DisplayReshareConfigViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DisplayReshareConfigViewState
|
||||||
|
extends ConsumerState<DisplayReshareConfigView> {
|
||||||
|
late final bool iAmInvolved;
|
||||||
|
|
||||||
|
bool _buttonLock = false;
|
||||||
|
|
||||||
|
Future<void> _onPressed() async {
|
||||||
|
if (_buttonLock) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_buttonLock = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final wallet =
|
||||||
|
ref.read(pWallets).getWallet(widget.walletId) as BitcoinFrostWallet;
|
||||||
|
|
||||||
|
final serializedKeys = await wallet.getSerializedKeys();
|
||||||
|
if (mounted) {
|
||||||
|
final result = Frost.beginResharer(
|
||||||
|
serializedKeys: serializedKeys!,
|
||||||
|
config: ref.read(pFrostResharingData).resharerConfig!,
|
||||||
|
);
|
||||||
|
|
||||||
|
ref.read(pFrostResharingData).startResharerData = result;
|
||||||
|
|
||||||
|
await Navigator.of(context).pushNamed(
|
||||||
|
BeginResharingView.routeName,
|
||||||
|
arguments: widget.walletId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"$e\n$s",
|
||||||
|
level: LogLevel.Fatal,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => StackOkDialog(
|
||||||
|
title: e.toString(),
|
||||||
|
desktopPopRootNavigator: Util.isDesktop,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_buttonLock = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
// TODO: optimize this by creating watcher providers (similar to normal WalletInfo)
|
||||||
|
final frostInfo = ref
|
||||||
|
.read(mainDBProvider)
|
||||||
|
.isar
|
||||||
|
.frostWalletInfo
|
||||||
|
.getByWalletIdSync(widget.walletId)!;
|
||||||
|
|
||||||
|
final myOldIndex = frostInfo.participants.indexOf(frostInfo.myName);
|
||||||
|
|
||||||
|
iAmInvolved = ref
|
||||||
|
.read(pFrostResharingData)
|
||||||
|
.configData!
|
||||||
|
.resharers
|
||||||
|
.contains(myOldIndex);
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ConditionalParent(
|
||||||
|
condition: Util.isDesktop,
|
||||||
|
builder: (child) => DesktopScaffold(
|
||||||
|
background: Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: const DesktopAppBar(
|
||||||
|
isCompactHeight: false,
|
||||||
|
leading: AppBarBackButton(),
|
||||||
|
),
|
||||||
|
body: SizedBox(
|
||||||
|
width: 480,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ConditionalParent(
|
||||||
|
condition: !Util.isDesktop,
|
||||||
|
builder: (child) => Background(
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: AppBarBackButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
"Resharer config",
|
||||||
|
style: STextStyles.navBarTitle(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: constraints.maxHeight,
|
||||||
|
),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
if (!Util.isDesktop) const Spacer(),
|
||||||
|
SizedBox(
|
||||||
|
height: 220,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
QrImageView(
|
||||||
|
data: ref.watch(pFrostResharingData).resharerConfig!,
|
||||||
|
size: 220,
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
foregroundColor: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.accentColorDark,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 32,
|
||||||
|
),
|
||||||
|
DetailItem(
|
||||||
|
title: "Config",
|
||||||
|
detail: ref.watch(pFrostResharingData).resharerConfig!,
|
||||||
|
button: Util.isDesktop
|
||||||
|
? IconCopyButton(
|
||||||
|
data: ref.watch(pFrostResharingData).resharerConfig!,
|
||||||
|
)
|
||||||
|
: SimpleCopyButton(
|
||||||
|
data: ref.watch(pFrostResharingData).resharerConfig!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: Util.isDesktop ? 64 : 16,
|
||||||
|
),
|
||||||
|
if (!Util.isDesktop)
|
||||||
|
const Spacer(
|
||||||
|
flex: 2,
|
||||||
|
),
|
||||||
|
if (iAmInvolved)
|
||||||
|
PrimaryButton(
|
||||||
|
label: "Start resharing",
|
||||||
|
onPressed: _onPressed,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,338 @@
|
||||||
|
import 'package:barcode_scan2/barcode_scan2.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:frostdart/frostdart.dart';
|
||||||
|
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_view.dart';
|
||||||
|
import 'package:stackwallet/providers/db/main_db_provider.dart';
|
||||||
|
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||||
|
import 'package:stackwallet/providers/global/secure_store_provider.dart';
|
||||||
|
import 'package:stackwallet/services/frost.dart';
|
||||||
|
import 'package:stackwallet/themes/stack_colors.dart';
|
||||||
|
import 'package:stackwallet/utilities/constants.dart';
|
||||||
|
import 'package:stackwallet/utilities/format.dart';
|
||||||
|
import 'package:stackwallet/utilities/logger.dart';
|
||||||
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
|
import 'package:stackwallet/utilities/util.dart';
|
||||||
|
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart';
|
||||||
|
import 'package:stackwallet/widgets/background.dart';
|
||||||
|
import 'package:stackwallet/widgets/conditional_parent.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
|
||||||
|
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
|
||||||
|
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
|
||||||
|
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||||
|
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||||
|
import 'package:stackwallet/widgets/textfield_icon_button.dart';
|
||||||
|
|
||||||
|
class ImportReshareConfigView extends ConsumerStatefulWidget {
|
||||||
|
const ImportReshareConfigView({
|
||||||
|
super.key,
|
||||||
|
required this.walletId,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const String routeName = "/importReshareConfigView";
|
||||||
|
|
||||||
|
final String walletId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<ImportReshareConfigView> createState() =>
|
||||||
|
_ImportReshareConfigViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ImportReshareConfigViewState
|
||||||
|
extends ConsumerState<ImportReshareConfigView> {
|
||||||
|
late final TextEditingController configFieldController;
|
||||||
|
late final FocusNode configFocusNode;
|
||||||
|
|
||||||
|
bool _configEmpty = true;
|
||||||
|
|
||||||
|
bool _buttonLock = false;
|
||||||
|
|
||||||
|
Future<void> _onPressed() async {
|
||||||
|
if (_buttonLock) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_buttonLock = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// TODO: optimize this by creating watcher providers (similar to normal WalletInfo)
|
||||||
|
final frostInfo = ref
|
||||||
|
.read(mainDBProvider)
|
||||||
|
.isar
|
||||||
|
.frostWalletInfo
|
||||||
|
.getByWalletIdSync(widget.walletId)!;
|
||||||
|
|
||||||
|
ref.read(pFrostResharingData).reset();
|
||||||
|
ref.read(pFrostResharingData).myName = frostInfo.myName;
|
||||||
|
ref.read(pFrostResharingData).resharerConfig = configFieldController.text;
|
||||||
|
|
||||||
|
String? salt;
|
||||||
|
try {
|
||||||
|
salt = Format.uint8listToString(
|
||||||
|
resharerSalt(
|
||||||
|
resharerConfig: ref.read(pFrostResharingData).resharerConfig!,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
throw Exception("Bad resharer config");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frostInfo.knownSalts.contains(salt)) {
|
||||||
|
throw Exception("Duplicate config salt");
|
||||||
|
} else {
|
||||||
|
final salts = frostInfo.knownSalts;
|
||||||
|
salts.add(salt);
|
||||||
|
final mainDB = ref.read(mainDBProvider);
|
||||||
|
await mainDB.isar.writeTxn(() async {
|
||||||
|
final info = frostInfo;
|
||||||
|
await mainDB.isar.frostWalletInfo.delete(info.id);
|
||||||
|
await mainDB.isar.frostWalletInfo.put(
|
||||||
|
info.copyWith(knownSalts: salts),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final serializedKeys = await ref.read(secureStoreProvider).read(
|
||||||
|
key: "{${widget.walletId}}_serializedFROSTKeys",
|
||||||
|
);
|
||||||
|
if (mounted) {
|
||||||
|
final result = Frost.beginResharer(
|
||||||
|
serializedKeys: serializedKeys!,
|
||||||
|
config: ref.read(pFrostResharingData).resharerConfig!,
|
||||||
|
);
|
||||||
|
|
||||||
|
ref.read(pFrostResharingData).startResharerData = result;
|
||||||
|
|
||||||
|
await Navigator.of(context).pushNamed(
|
||||||
|
BeginResharingView.routeName,
|
||||||
|
arguments: widget.walletId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"$e\n$s",
|
||||||
|
level: LogLevel.Fatal,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => StackOkDialog(
|
||||||
|
title: e.toString(),
|
||||||
|
desktopPopRootNavigator: Util.isDesktop,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_buttonLock = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
configFieldController = TextEditingController();
|
||||||
|
configFocusNode = FocusNode();
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
configFieldController.dispose();
|
||||||
|
configFocusNode.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ConditionalParent(
|
||||||
|
condition: Util.isDesktop,
|
||||||
|
builder: (child) => DesktopScaffold(
|
||||||
|
background: Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: const DesktopAppBar(
|
||||||
|
isCompactHeight: false,
|
||||||
|
leading: AppBarBackButton(),
|
||||||
|
),
|
||||||
|
body: SizedBox(
|
||||||
|
width: 480,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ConditionalParent(
|
||||||
|
condition: !Util.isDesktop,
|
||||||
|
builder: (child) => Background(
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: const AppBarBackButton(),
|
||||||
|
title: Text(
|
||||||
|
"Import FROST reshare config",
|
||||||
|
style: STextStyles.navBarTitle(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: constraints.maxHeight,
|
||||||
|
),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
Constants.size.circularBorderRadius,
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
key: const Key("frConfigTextFieldKey"),
|
||||||
|
controller: configFieldController,
|
||||||
|
onChanged: (_) {
|
||||||
|
setState(() {
|
||||||
|
_configEmpty = configFieldController.text.isEmpty;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
focusNode: configFocusNode,
|
||||||
|
readOnly: false,
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
style: STextStyles.field(context),
|
||||||
|
decoration: standardInputDecoration(
|
||||||
|
"Enter config",
|
||||||
|
configFocusNode,
|
||||||
|
context,
|
||||||
|
).copyWith(
|
||||||
|
contentPadding: const EdgeInsets.only(
|
||||||
|
left: 16,
|
||||||
|
top: 6,
|
||||||
|
bottom: 8,
|
||||||
|
right: 5,
|
||||||
|
),
|
||||||
|
suffixIcon: Padding(
|
||||||
|
padding: _configEmpty
|
||||||
|
? const EdgeInsets.only(right: 8)
|
||||||
|
: const EdgeInsets.only(right: 0),
|
||||||
|
child: UnconstrainedBox(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
!_configEmpty
|
||||||
|
? TextFieldIconButton(
|
||||||
|
semanticsLabel:
|
||||||
|
"Clear Button. Clears The Config Field.",
|
||||||
|
key: const Key("frConfigClearButtonKey"),
|
||||||
|
onTap: () {
|
||||||
|
configFieldController.text = "";
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_configEmpty = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const XIcon(),
|
||||||
|
)
|
||||||
|
: TextFieldIconButton(
|
||||||
|
semanticsLabel:
|
||||||
|
"Paste Button. Pastes From Clipboard To Config Field Input.",
|
||||||
|
key: const Key("frConfigPasteButtonKey"),
|
||||||
|
onTap: () async {
|
||||||
|
final ClipboardData? data =
|
||||||
|
await Clipboard.getData(
|
||||||
|
Clipboard.kTextPlain);
|
||||||
|
if (data?.text != null &&
|
||||||
|
data!.text!.isNotEmpty) {
|
||||||
|
configFieldController.text =
|
||||||
|
data.text!.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_configEmpty =
|
||||||
|
configFieldController.text.isEmpty;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: _configEmpty
|
||||||
|
? const ClipboardIcon()
|
||||||
|
: const XIcon(),
|
||||||
|
),
|
||||||
|
if (_configEmpty)
|
||||||
|
TextFieldIconButton(
|
||||||
|
semanticsLabel:
|
||||||
|
"Scan QR Button. Opens Camera For Scanning QR Code.",
|
||||||
|
key: const Key("frConfigScanQrButtonKey"),
|
||||||
|
onTap: () async {
|
||||||
|
try {
|
||||||
|
if (FocusScope.of(context).hasFocus) {
|
||||||
|
FocusScope.of(context).unfocus();
|
||||||
|
await Future<void>.delayed(
|
||||||
|
const Duration(milliseconds: 75));
|
||||||
|
}
|
||||||
|
|
||||||
|
final qrResult = await BarcodeScanner.scan();
|
||||||
|
|
||||||
|
configFieldController.text =
|
||||||
|
qrResult.rawContent;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_configEmpty =
|
||||||
|
configFieldController.text.isEmpty;
|
||||||
|
});
|
||||||
|
} on PlatformException catch (e, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"Failed to get camera permissions while trying to scan qr code: $e\n$s",
|
||||||
|
level: LogLevel.Warning,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const QrCodeIcon(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
if (!Util.isDesktop) const Spacer(),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
PrimaryButton(
|
||||||
|
label: "Start resharing",
|
||||||
|
enabled: !_configEmpty,
|
||||||
|
onPressed: () async {
|
||||||
|
if (FocusScope.of(context).hasFocus) {
|
||||||
|
FocusScope.of(context).unfocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
await _onPressed();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,439 @@
|
||||||
|
import 'package:barcode_scan2/barcode_scan2.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:qr_flutter/qr_flutter.dart';
|
||||||
|
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/continue_resharing_view.dart';
|
||||||
|
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
|
||||||
|
import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart';
|
||||||
|
import 'package:stackwallet/providers/db/main_db_provider.dart';
|
||||||
|
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||||
|
import 'package:stackwallet/services/frost.dart';
|
||||||
|
import 'package:stackwallet/themes/stack_colors.dart';
|
||||||
|
import 'package:stackwallet/utilities/constants.dart';
|
||||||
|
import 'package:stackwallet/utilities/logger.dart';
|
||||||
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
|
import 'package:stackwallet/utilities/util.dart';
|
||||||
|
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart';
|
||||||
|
import 'package:stackwallet/widgets/background.dart';
|
||||||
|
import 'package:stackwallet/widgets/conditional_parent.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/detail_item.dart';
|
||||||
|
import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart';
|
||||||
|
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
|
||||||
|
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
|
||||||
|
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
|
||||||
|
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||||
|
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||||
|
import 'package:stackwallet/widgets/textfield_icon_button.dart';
|
||||||
|
|
||||||
|
class BeginResharingView extends ConsumerStatefulWidget {
|
||||||
|
const BeginResharingView({
|
||||||
|
super.key,
|
||||||
|
required this.walletId,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const String routeName = "/beginResharingView";
|
||||||
|
|
||||||
|
final String walletId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<BeginResharingView> createState() => _BeginResharingViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BeginResharingViewState extends ConsumerState<BeginResharingView> {
|
||||||
|
final List<TextEditingController> controllers = [];
|
||||||
|
final List<FocusNode> focusNodes = [];
|
||||||
|
|
||||||
|
late final List<int> resharerIndexes;
|
||||||
|
late final int myResharerIndexIndex;
|
||||||
|
late final String myResharerStart;
|
||||||
|
late final bool amOutgoingParticipant;
|
||||||
|
|
||||||
|
final List<bool> fieldIsEmptyFlags = [];
|
||||||
|
|
||||||
|
bool _buttonLock = false;
|
||||||
|
|
||||||
|
Future<void> _onPressed() async {
|
||||||
|
if (_buttonLock) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_buttonLock = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!amOutgoingParticipant) {
|
||||||
|
// collect resharer strings
|
||||||
|
final resharerStarts = controllers.map((e) => e.text).toList();
|
||||||
|
if (myResharerIndexIndex >= 0) {
|
||||||
|
// only insert my own at the correct index if I am a resharer
|
||||||
|
resharerStarts.insert(myResharerIndexIndex, myResharerStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = Frost.beginReshared(
|
||||||
|
myName: ref.read(pFrostResharingData).myName!,
|
||||||
|
resharerConfig: ref.read(pFrostResharingData).resharerConfig!,
|
||||||
|
resharerStarts: resharerStarts,
|
||||||
|
);
|
||||||
|
|
||||||
|
ref.read(pFrostResharingData).startResharedData = result;
|
||||||
|
}
|
||||||
|
await Navigator.of(context).pushNamed(
|
||||||
|
ContinueResharingView.routeName,
|
||||||
|
arguments: widget.walletId,
|
||||||
|
);
|
||||||
|
} catch (e, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"$e\n$s",
|
||||||
|
level: LogLevel.Fatal,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => StackOkDialog(
|
||||||
|
title: "Error",
|
||||||
|
message: e.toString(),
|
||||||
|
desktopPopRootNavigator: Util.isDesktop,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_buttonLock = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
// TODO: optimize this by creating watcher providers (similar to normal WalletInfo)
|
||||||
|
final frostInfo = ref
|
||||||
|
.read(mainDBProvider)
|
||||||
|
.isar
|
||||||
|
.frostWalletInfo
|
||||||
|
.getByWalletIdSync(widget.walletId)!;
|
||||||
|
final myOldIndex =
|
||||||
|
frostInfo.participants.indexOf(ref.read(pFrostResharingData).myName!);
|
||||||
|
|
||||||
|
myResharerStart =
|
||||||
|
ref.read(pFrostResharingData).startResharerData!.resharerStart;
|
||||||
|
|
||||||
|
resharerIndexes = ref.read(pFrostResharingData).configData!.resharers;
|
||||||
|
myResharerIndexIndex = resharerIndexes.indexOf(myOldIndex);
|
||||||
|
if (myResharerIndexIndex >= 0) {
|
||||||
|
// remove my name for now as we don't need a text field for it
|
||||||
|
resharerIndexes.removeAt(myResharerIndexIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
amOutgoingParticipant = !ref
|
||||||
|
.read(pFrostResharingData)
|
||||||
|
.configData!
|
||||||
|
.newParticipants
|
||||||
|
.contains(ref.read(pFrostResharingData).myName!);
|
||||||
|
|
||||||
|
for (int i = 0; i < resharerIndexes.length; i++) {
|
||||||
|
controllers.add(TextEditingController());
|
||||||
|
focusNodes.add(FocusNode());
|
||||||
|
fieldIsEmptyFlags.add(true);
|
||||||
|
}
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
for (int i = 0; i < controllers.length; i++) {
|
||||||
|
controllers[i].dispose();
|
||||||
|
}
|
||||||
|
for (int i = 0; i < focusNodes.length; i++) {
|
||||||
|
focusNodes[i].dispose();
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return WillPopScope(
|
||||||
|
onWillPop: () async {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => FrostInterruptionDialog(
|
||||||
|
type: FrostInterruptionDialogType.resharing,
|
||||||
|
popUntilOnYesRouteName: Util.isDesktop
|
||||||
|
? DesktopWalletView.routeName
|
||||||
|
: WalletView.routeName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
child: ConditionalParent(
|
||||||
|
condition: Util.isDesktop,
|
||||||
|
builder: (child) => DesktopScaffold(
|
||||||
|
background: Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: DesktopAppBar(
|
||||||
|
isCompactHeight: false,
|
||||||
|
leading: AppBarBackButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => const FrostInterruptionDialog(
|
||||||
|
type: FrostInterruptionDialogType.resharing,
|
||||||
|
popUntilOnYesRouteName: DesktopWalletView.routeName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SizedBox(
|
||||||
|
width: 480,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ConditionalParent(
|
||||||
|
condition: !Util.isDesktop,
|
||||||
|
builder: (child) => Background(
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: AppBarBackButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => const FrostInterruptionDialog(
|
||||||
|
type: FrostInterruptionDialogType.resharing,
|
||||||
|
popUntilOnYesRouteName: WalletView.routeName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
"Resharers",
|
||||||
|
style: STextStyles.navBarTitle(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: constraints.maxHeight,
|
||||||
|
),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 220,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
QrImageView(
|
||||||
|
data: myResharerStart,
|
||||||
|
size: 220,
|
||||||
|
backgroundColor: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.background,
|
||||||
|
foregroundColor: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.accentColorDark,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const _Div(),
|
||||||
|
DetailItem(
|
||||||
|
title: "My resharer",
|
||||||
|
detail: myResharerStart,
|
||||||
|
button: Util.isDesktop
|
||||||
|
? IconCopyButton(
|
||||||
|
data: myResharerStart,
|
||||||
|
)
|
||||||
|
: SimpleCopyButton(
|
||||||
|
data: myResharerStart,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const _Div(),
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
for (int i = 0; i < resharerIndexes.length; i++)
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
Constants.size.circularBorderRadius,
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
key: Key("frostResharerTextFieldKey_$i"),
|
||||||
|
controller: controllers[i],
|
||||||
|
focusNode: focusNodes[i],
|
||||||
|
readOnly: false,
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
style: STextStyles.field(context),
|
||||||
|
onChanged: (_) {
|
||||||
|
setState(() {
|
||||||
|
fieldIsEmptyFlags[i] =
|
||||||
|
controllers[i].text.isEmpty;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
decoration: standardInputDecoration(
|
||||||
|
"Enter index "
|
||||||
|
"${resharerIndexes[i]}"
|
||||||
|
"'s resharer",
|
||||||
|
focusNodes[i],
|
||||||
|
context,
|
||||||
|
).copyWith(
|
||||||
|
contentPadding: const EdgeInsets.only(
|
||||||
|
left: 16,
|
||||||
|
top: 6,
|
||||||
|
bottom: 8,
|
||||||
|
right: 5,
|
||||||
|
),
|
||||||
|
suffixIcon: Padding(
|
||||||
|
padding: fieldIsEmptyFlags[i]
|
||||||
|
? const EdgeInsets.only(right: 8)
|
||||||
|
: const EdgeInsets.only(right: 0),
|
||||||
|
child: UnconstrainedBox(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
!fieldIsEmptyFlags[i]
|
||||||
|
? TextFieldIconButton(
|
||||||
|
semanticsLabel:
|
||||||
|
"Clear Button. Clears The Resharer Field Input.",
|
||||||
|
key: Key(
|
||||||
|
"frostResharerClearButtonKey_$i"),
|
||||||
|
onTap: () {
|
||||||
|
controllers[i].text = "";
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
fieldIsEmptyFlags[i] = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const XIcon(),
|
||||||
|
)
|
||||||
|
: TextFieldIconButton(
|
||||||
|
semanticsLabel:
|
||||||
|
"Paste Button. Pastes From Clipboard To Resharer Field Input.",
|
||||||
|
key: Key(
|
||||||
|
"frostResharerPasteButtonKey_$i"),
|
||||||
|
onTap: () async {
|
||||||
|
final ClipboardData? data =
|
||||||
|
await Clipboard.getData(
|
||||||
|
Clipboard.kTextPlain);
|
||||||
|
if (data?.text != null &&
|
||||||
|
data!.text!.isNotEmpty) {
|
||||||
|
controllers[i].text =
|
||||||
|
data.text!.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
fieldIsEmptyFlags[i] =
|
||||||
|
controllers[i]
|
||||||
|
.text
|
||||||
|
.isEmpty;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: fieldIsEmptyFlags[i]
|
||||||
|
? const ClipboardIcon()
|
||||||
|
: const XIcon(),
|
||||||
|
),
|
||||||
|
if (fieldIsEmptyFlags[i])
|
||||||
|
TextFieldIconButton(
|
||||||
|
semanticsLabel: "Scan QR Button. "
|
||||||
|
"Opens Camera For Scanning QR Code.",
|
||||||
|
key: Key(
|
||||||
|
"frostCommitmentsScanQrButtonKey_$i"),
|
||||||
|
onTap: () async {
|
||||||
|
try {
|
||||||
|
if (FocusScope.of(context)
|
||||||
|
.hasFocus) {
|
||||||
|
FocusScope.of(context)
|
||||||
|
.unfocus();
|
||||||
|
await Future<void>.delayed(
|
||||||
|
const Duration(
|
||||||
|
milliseconds: 75));
|
||||||
|
}
|
||||||
|
|
||||||
|
final qrResult =
|
||||||
|
await BarcodeScanner.scan();
|
||||||
|
|
||||||
|
controllers[i].text =
|
||||||
|
qrResult.rawContent;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
fieldIsEmptyFlags[i] =
|
||||||
|
controllers[i]
|
||||||
|
.text
|
||||||
|
.isEmpty;
|
||||||
|
});
|
||||||
|
} on PlatformException catch (e, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"Failed to get camera permissions "
|
||||||
|
"while trying to scan qr code: $e\n$s",
|
||||||
|
level: LogLevel.Warning,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const QrCodeIcon(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (!Util.isDesktop) const Spacer(),
|
||||||
|
const _Div(),
|
||||||
|
PrimaryButton(
|
||||||
|
label: "Continue",
|
||||||
|
enabled: amOutgoingParticipant ||
|
||||||
|
!fieldIsEmptyFlags.reduce((v, e) => v |= e),
|
||||||
|
onPressed: _onPressed,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Div extends StatelessWidget {
|
||||||
|
const _Div({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const SizedBox(
|
||||||
|
height: 12,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,429 @@
|
||||||
|
import 'dart:ffi';
|
||||||
|
|
||||||
|
import 'package:barcode_scan2/barcode_scan2.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:qr_flutter/qr_flutter.dart';
|
||||||
|
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart';
|
||||||
|
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
|
||||||
|
import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart';
|
||||||
|
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||||
|
import 'package:stackwallet/services/frost.dart';
|
||||||
|
import 'package:stackwallet/themes/stack_colors.dart';
|
||||||
|
import 'package:stackwallet/utilities/constants.dart';
|
||||||
|
import 'package:stackwallet/utilities/logger.dart';
|
||||||
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
|
import 'package:stackwallet/utilities/util.dart';
|
||||||
|
import 'package:stackwallet/widgets/background.dart';
|
||||||
|
import 'package:stackwallet/widgets/conditional_parent.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/detail_item.dart';
|
||||||
|
import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart';
|
||||||
|
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
|
||||||
|
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
|
||||||
|
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
|
||||||
|
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||||
|
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||||
|
import 'package:stackwallet/widgets/textfield_icon_button.dart';
|
||||||
|
|
||||||
|
class ContinueResharingView extends ConsumerStatefulWidget {
|
||||||
|
const ContinueResharingView({
|
||||||
|
super.key,
|
||||||
|
required this.walletId,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const String routeName = "/continueResharingView";
|
||||||
|
|
||||||
|
final String walletId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<ContinueResharingView> createState() =>
|
||||||
|
_ContinueResharingViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ContinueResharingViewState extends ConsumerState<ContinueResharingView> {
|
||||||
|
final List<TextEditingController> controllers = [];
|
||||||
|
final List<FocusNode> focusNodes = [];
|
||||||
|
|
||||||
|
late final List<String> newParticipants;
|
||||||
|
late final int myIndex;
|
||||||
|
late final String? myEncryptionKey;
|
||||||
|
late final bool amOutgoingParticipant;
|
||||||
|
|
||||||
|
final List<bool> fieldIsEmptyFlags = [];
|
||||||
|
|
||||||
|
bool _buttonLock = false;
|
||||||
|
Future<void> _onPressed() async {
|
||||||
|
if (_buttonLock) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_buttonLock = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// collect encryptionKeys strings and insert my own at the correct index
|
||||||
|
final encryptionKeys = controllers.map((e) => e.text).toList();
|
||||||
|
if (!amOutgoingParticipant) {
|
||||||
|
encryptionKeys.insert(myIndex, myEncryptionKey!);
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = Frost.finishResharer(
|
||||||
|
machine: ref.read(pFrostResharingData).startResharerData!.machine.ref,
|
||||||
|
encryptionKeysOfResharedTo: encryptionKeys,
|
||||||
|
);
|
||||||
|
|
||||||
|
ref.read(pFrostResharingData).resharerComplete = result;
|
||||||
|
|
||||||
|
await Navigator.of(context).pushNamed(
|
||||||
|
FinishResharingView.routeName,
|
||||||
|
arguments: widget.walletId,
|
||||||
|
);
|
||||||
|
} catch (e, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"$e\n$s",
|
||||||
|
level: LogLevel.Fatal,
|
||||||
|
);
|
||||||
|
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => StackOkDialog(
|
||||||
|
title: "Error",
|
||||||
|
message: e.toString(),
|
||||||
|
desktopPopRootNavigator: Util.isDesktop,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
_buttonLock = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
myEncryptionKey =
|
||||||
|
ref.read(pFrostResharingData).startResharedData?.resharedStart;
|
||||||
|
|
||||||
|
newParticipants = ref.read(pFrostResharingData).configData!.newParticipants;
|
||||||
|
myIndex = newParticipants.indexOf(ref.read(pFrostResharingData).myName!);
|
||||||
|
|
||||||
|
if (myIndex >= 0) {
|
||||||
|
// remove my name for now as we don't need a text field for it
|
||||||
|
newParticipants.removeAt(myIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (myEncryptionKey == null && myIndex == -1) {
|
||||||
|
amOutgoingParticipant = true;
|
||||||
|
} else if (myEncryptionKey != null && myIndex >= 0) {
|
||||||
|
amOutgoingParticipant = false;
|
||||||
|
} else {
|
||||||
|
throw Exception("Invalid resharing state");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < newParticipants.length; i++) {
|
||||||
|
controllers.add(TextEditingController());
|
||||||
|
focusNodes.add(FocusNode());
|
||||||
|
fieldIsEmptyFlags.add(true);
|
||||||
|
}
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
for (int i = 0; i < controllers.length; i++) {
|
||||||
|
controllers[i].dispose();
|
||||||
|
}
|
||||||
|
for (int i = 0; i < focusNodes.length; i++) {
|
||||||
|
focusNodes[i].dispose();
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return WillPopScope(
|
||||||
|
onWillPop: () async {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => FrostInterruptionDialog(
|
||||||
|
type: FrostInterruptionDialogType.resharing,
|
||||||
|
popUntilOnYesRouteName: Util.isDesktop
|
||||||
|
? DesktopWalletView.routeName
|
||||||
|
: WalletView.routeName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
child: ConditionalParent(
|
||||||
|
condition: Util.isDesktop,
|
||||||
|
builder: (child) => DesktopScaffold(
|
||||||
|
background: Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: DesktopAppBar(
|
||||||
|
isCompactHeight: false,
|
||||||
|
leading: AppBarBackButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => const FrostInterruptionDialog(
|
||||||
|
type: FrostInterruptionDialogType.resharing,
|
||||||
|
popUntilOnYesRouteName: DesktopWalletView.routeName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SizedBox(
|
||||||
|
width: 480,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ConditionalParent(
|
||||||
|
condition: !Util.isDesktop,
|
||||||
|
builder: (child) => Background(
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: AppBarBackButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => const FrostInterruptionDialog(
|
||||||
|
type: FrostInterruptionDialogType.resharing,
|
||||||
|
popUntilOnYesRouteName: WalletView.routeName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
"Encryption keys",
|
||||||
|
style: STextStyles.navBarTitle(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: constraints.maxHeight,
|
||||||
|
),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
if (!amOutgoingParticipant)
|
||||||
|
SizedBox(
|
||||||
|
height: 220,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
QrImageView(
|
||||||
|
data: myEncryptionKey!,
|
||||||
|
size: 220,
|
||||||
|
backgroundColor: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.background,
|
||||||
|
foregroundColor: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.accentColorDark,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!amOutgoingParticipant) const _Div(),
|
||||||
|
if (!amOutgoingParticipant)
|
||||||
|
DetailItem(
|
||||||
|
title: "My encryption key",
|
||||||
|
detail: myEncryptionKey!,
|
||||||
|
button: Util.isDesktop
|
||||||
|
? IconCopyButton(
|
||||||
|
data: myEncryptionKey!,
|
||||||
|
)
|
||||||
|
: SimpleCopyButton(
|
||||||
|
data: myEncryptionKey!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!amOutgoingParticipant) const _Div(),
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
for (int i = 0; i < newParticipants.length; i++)
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
Constants.size.circularBorderRadius,
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
key: Key("frostEncryptionKeyTextFieldKey_$i"),
|
||||||
|
controller: controllers[i],
|
||||||
|
focusNode: focusNodes[i],
|
||||||
|
readOnly: false,
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
style: STextStyles.field(context),
|
||||||
|
onChanged: (_) {
|
||||||
|
setState(() {
|
||||||
|
fieldIsEmptyFlags[i] =
|
||||||
|
controllers[i].text.isEmpty;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
decoration: standardInputDecoration(
|
||||||
|
"Enter "
|
||||||
|
"${newParticipants[i]}"
|
||||||
|
"'s encryption key",
|
||||||
|
focusNodes[i],
|
||||||
|
context,
|
||||||
|
).copyWith(
|
||||||
|
contentPadding: const EdgeInsets.only(
|
||||||
|
left: 16,
|
||||||
|
top: 6,
|
||||||
|
bottom: 8,
|
||||||
|
right: 5,
|
||||||
|
),
|
||||||
|
suffixIcon: Padding(
|
||||||
|
padding: fieldIsEmptyFlags[i]
|
||||||
|
? const EdgeInsets.only(right: 8)
|
||||||
|
: const EdgeInsets.only(right: 0),
|
||||||
|
child: UnconstrainedBox(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
!fieldIsEmptyFlags[i]
|
||||||
|
? TextFieldIconButton(
|
||||||
|
semanticsLabel:
|
||||||
|
"Clear Button. Clears The Encryption Key Field Input.",
|
||||||
|
key: Key(
|
||||||
|
"frostEncryptionKeyClearButtonKey_$i"),
|
||||||
|
onTap: () {
|
||||||
|
controllers[i].text = "";
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
fieldIsEmptyFlags[i] = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const XIcon(),
|
||||||
|
)
|
||||||
|
: TextFieldIconButton(
|
||||||
|
semanticsLabel:
|
||||||
|
"Paste Button. Pastes From Clipboard To Encryption Key Field Input.",
|
||||||
|
key: Key(
|
||||||
|
"frostEncryptionKeyPasteButtonKey_$i"),
|
||||||
|
onTap: () async {
|
||||||
|
final ClipboardData? data =
|
||||||
|
await Clipboard.getData(
|
||||||
|
Clipboard.kTextPlain);
|
||||||
|
if (data?.text != null &&
|
||||||
|
data!.text!.isNotEmpty) {
|
||||||
|
controllers[i].text =
|
||||||
|
data.text!.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
fieldIsEmptyFlags[i] =
|
||||||
|
controllers[i]
|
||||||
|
.text
|
||||||
|
.isEmpty;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: fieldIsEmptyFlags[i]
|
||||||
|
? const ClipboardIcon()
|
||||||
|
: const XIcon(),
|
||||||
|
),
|
||||||
|
if (fieldIsEmptyFlags[i])
|
||||||
|
TextFieldIconButton(
|
||||||
|
semanticsLabel: "Scan QR Button. "
|
||||||
|
"Opens Camera For Scanning QR Code.",
|
||||||
|
key: Key(
|
||||||
|
"frostCommitmentsScanQrButtonKey_$i"),
|
||||||
|
onTap: () async {
|
||||||
|
try {
|
||||||
|
if (FocusScope.of(context)
|
||||||
|
.hasFocus) {
|
||||||
|
FocusScope.of(context)
|
||||||
|
.unfocus();
|
||||||
|
await Future<void>.delayed(
|
||||||
|
const Duration(
|
||||||
|
milliseconds: 75));
|
||||||
|
}
|
||||||
|
|
||||||
|
final qrResult =
|
||||||
|
await BarcodeScanner.scan();
|
||||||
|
|
||||||
|
controllers[i].text =
|
||||||
|
qrResult.rawContent;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
fieldIsEmptyFlags[i] =
|
||||||
|
controllers[i]
|
||||||
|
.text
|
||||||
|
.isEmpty;
|
||||||
|
});
|
||||||
|
} on PlatformException catch (e, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"Failed to get camera permissions "
|
||||||
|
"while trying to scan qr code: $e\n$s",
|
||||||
|
level: LogLevel.Warning,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const QrCodeIcon(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (!Util.isDesktop) const Spacer(),
|
||||||
|
const _Div(),
|
||||||
|
PrimaryButton(
|
||||||
|
label: "Continue",
|
||||||
|
enabled: !fieldIsEmptyFlags.reduce((v, e) => v |= e),
|
||||||
|
onPressed: _onPressed,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Div extends StatelessWidget {
|
||||||
|
const _Div({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const SizedBox(
|
||||||
|
height: 12,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,204 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:qr_flutter/qr_flutter.dart';
|
||||||
|
import 'package:stackwallet/pages/home_view/home_view.dart';
|
||||||
|
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart';
|
||||||
|
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
|
||||||
|
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||||
|
import 'package:stackwallet/themes/stack_colors.dart';
|
||||||
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
|
import 'package:stackwallet/utilities/util.dart';
|
||||||
|
import 'package:stackwallet/widgets/background.dart';
|
||||||
|
import 'package:stackwallet/widgets/conditional_parent.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/detail_item.dart';
|
||||||
|
import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart';
|
||||||
|
|
||||||
|
class NewContinueSharingView extends ConsumerStatefulWidget {
|
||||||
|
const NewContinueSharingView({
|
||||||
|
super.key,
|
||||||
|
required this.walletId,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const String routeName = "/NewContinueSharingView";
|
||||||
|
|
||||||
|
final String walletId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<NewContinueSharingView> createState() =>
|
||||||
|
_NewContinueSharingViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NewContinueSharingViewState
|
||||||
|
extends ConsumerState<NewContinueSharingView> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return WillPopScope(
|
||||||
|
onWillPop: () async {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => FrostInterruptionDialog(
|
||||||
|
type: FrostInterruptionDialogType.resharing,
|
||||||
|
popUntilOnYesRouteName:
|
||||||
|
Util.isDesktop ? DesktopHomeView.routeName : HomeView.routeName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
child: ConditionalParent(
|
||||||
|
condition: Util.isDesktop,
|
||||||
|
builder: (child) => DesktopScaffold(
|
||||||
|
background: Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: DesktopAppBar(
|
||||||
|
isCompactHeight: false,
|
||||||
|
leading: AppBarBackButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => const FrostInterruptionDialog(
|
||||||
|
type: FrostInterruptionDialogType.resharing,
|
||||||
|
popUntilOnYesRouteName: DesktopHomeView.routeName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
trailing: ExitToMyStackButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => const FrostInterruptionDialog(
|
||||||
|
type: FrostInterruptionDialogType.resharing,
|
||||||
|
popUntilOnYesRouteName: DesktopHomeView.routeName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SizedBox(
|
||||||
|
width: 480,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ConditionalParent(
|
||||||
|
condition: !Util.isDesktop,
|
||||||
|
builder: (child) => Background(
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: AppBarBackButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => const FrostInterruptionDialog(
|
||||||
|
type: FrostInterruptionDialogType.resharing,
|
||||||
|
popUntilOnYesRouteName: HomeView.routeName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
"Encryption keys",
|
||||||
|
style: STextStyles.navBarTitle(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: constraints.maxHeight,
|
||||||
|
),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 220,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
QrImageView(
|
||||||
|
data: ref
|
||||||
|
.watch(pFrostResharingData)
|
||||||
|
.startResharedData!
|
||||||
|
.resharedStart,
|
||||||
|
size: 220,
|
||||||
|
backgroundColor: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.background,
|
||||||
|
foregroundColor: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.accentColorDark,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const _Div(),
|
||||||
|
DetailItem(
|
||||||
|
title: "My encryption key",
|
||||||
|
detail: ref
|
||||||
|
.watch(pFrostResharingData)
|
||||||
|
.startResharedData!
|
||||||
|
.resharedStart,
|
||||||
|
button: Util.isDesktop
|
||||||
|
? IconCopyButton(
|
||||||
|
data: ref
|
||||||
|
.watch(pFrostResharingData)
|
||||||
|
.startResharedData!
|
||||||
|
.resharedStart,
|
||||||
|
)
|
||||||
|
: SimpleCopyButton(
|
||||||
|
data: ref
|
||||||
|
.watch(pFrostResharingData)
|
||||||
|
.startResharedData!
|
||||||
|
.resharedStart,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!Util.isDesktop) const Spacer(),
|
||||||
|
const _Div(),
|
||||||
|
PrimaryButton(
|
||||||
|
label: "Continue",
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pushNamed(
|
||||||
|
FinishResharingView.routeName,
|
||||||
|
arguments: widget.walletId,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Div extends StatelessWidget {
|
||||||
|
const _Div({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const SizedBox(
|
||||||
|
height: 12,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,426 @@
|
||||||
|
import 'package:barcode_scan2/barcode_scan2.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
|
||||||
|
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||||
|
import 'package:stackwallet/themes/stack_colors.dart';
|
||||||
|
import 'package:stackwallet/utilities/constants.dart';
|
||||||
|
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||||
|
import 'package:stackwallet/utilities/logger.dart';
|
||||||
|
import 'package:stackwallet/utilities/show_loading.dart';
|
||||||
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
|
import 'package:stackwallet/utilities/util.dart';
|
||||||
|
import 'package:stackwallet/wallets/isar/models/wallet_info.dart';
|
||||||
|
import 'package:stackwallet/wallets/models/incomplete_frost_wallet.dart';
|
||||||
|
import 'package:stackwallet/widgets/background.dart';
|
||||||
|
import 'package:stackwallet/widgets/conditional_parent.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
|
||||||
|
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
|
||||||
|
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
|
||||||
|
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||||
|
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||||
|
import 'package:stackwallet/widgets/textfield_icon_button.dart';
|
||||||
|
|
||||||
|
class NewImportResharerConfigView extends ConsumerStatefulWidget {
|
||||||
|
const NewImportResharerConfigView({
|
||||||
|
super.key,
|
||||||
|
required this.walletName,
|
||||||
|
required this.coin,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const String routeName = "/newImportResharerConfigView";
|
||||||
|
|
||||||
|
final String walletName;
|
||||||
|
final Coin coin;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<NewImportResharerConfigView> createState() =>
|
||||||
|
_NewImportResharerConfigViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NewImportResharerConfigViewState
|
||||||
|
extends ConsumerState<NewImportResharerConfigView> {
|
||||||
|
late final TextEditingController myNameFieldController, configFieldController;
|
||||||
|
late final FocusNode myNameFocusNode, configFocusNode;
|
||||||
|
|
||||||
|
bool _nameEmpty = true, _configEmpty = true;
|
||||||
|
|
||||||
|
bool _buttonLock = false;
|
||||||
|
|
||||||
|
Future<IncompleteFrostWallet> _createWallet() async {
|
||||||
|
final info = WalletInfo.createNew(
|
||||||
|
name: widget.walletName,
|
||||||
|
coin: widget.coin,
|
||||||
|
);
|
||||||
|
|
||||||
|
final wallet = IncompleteFrostWallet();
|
||||||
|
wallet.info = info;
|
||||||
|
|
||||||
|
return wallet;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
myNameFieldController = TextEditingController();
|
||||||
|
configFieldController = TextEditingController();
|
||||||
|
myNameFocusNode = FocusNode();
|
||||||
|
configFocusNode = FocusNode();
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
myNameFieldController.dispose();
|
||||||
|
configFieldController.dispose();
|
||||||
|
myNameFocusNode.dispose();
|
||||||
|
configFocusNode.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ConditionalParent(
|
||||||
|
condition: Util.isDesktop,
|
||||||
|
builder: (child) => DesktopScaffold(
|
||||||
|
background: Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: const DesktopAppBar(
|
||||||
|
isCompactHeight: false,
|
||||||
|
leading: AppBarBackButton(),
|
||||||
|
trailing: ExitToMyStackButton(),
|
||||||
|
),
|
||||||
|
body: SizedBox(
|
||||||
|
width: 480,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ConditionalParent(
|
||||||
|
condition: !Util.isDesktop,
|
||||||
|
builder: (child) => Background(
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: AppBarBackButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
"Import FROST reshare config",
|
||||||
|
style: STextStyles.navBarTitle(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: constraints.maxHeight,
|
||||||
|
),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
Constants.size.circularBorderRadius,
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
key: const Key("frMyNameTextFieldKey"),
|
||||||
|
controller: myNameFieldController,
|
||||||
|
onChanged: (_) {
|
||||||
|
setState(() {
|
||||||
|
_nameEmpty = myNameFieldController.text.isEmpty;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
focusNode: myNameFocusNode,
|
||||||
|
readOnly: false,
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
style: STextStyles.field(context),
|
||||||
|
decoration: standardInputDecoration(
|
||||||
|
"My name",
|
||||||
|
myNameFocusNode,
|
||||||
|
context,
|
||||||
|
).copyWith(
|
||||||
|
contentPadding: const EdgeInsets.only(
|
||||||
|
left: 16,
|
||||||
|
top: 6,
|
||||||
|
bottom: 8,
|
||||||
|
right: 5,
|
||||||
|
),
|
||||||
|
suffixIcon: Padding(
|
||||||
|
padding: _nameEmpty
|
||||||
|
? const EdgeInsets.only(right: 8)
|
||||||
|
: const EdgeInsets.only(right: 0),
|
||||||
|
child: UnconstrainedBox(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
!_nameEmpty
|
||||||
|
? TextFieldIconButton(
|
||||||
|
semanticsLabel:
|
||||||
|
"Clear Button. Clears The Config Field.",
|
||||||
|
key: const Key("frMyNameClearButtonKey"),
|
||||||
|
onTap: () {
|
||||||
|
myNameFieldController.text = "";
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_nameEmpty = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const XIcon(),
|
||||||
|
)
|
||||||
|
: TextFieldIconButton(
|
||||||
|
semanticsLabel:
|
||||||
|
"Paste Button. Pastes From Clipboard To Name Field.",
|
||||||
|
key: const Key("frMyNamePasteButtonKey"),
|
||||||
|
onTap: () async {
|
||||||
|
final ClipboardData? data =
|
||||||
|
await Clipboard.getData(
|
||||||
|
Clipboard.kTextPlain);
|
||||||
|
if (data?.text != null &&
|
||||||
|
data!.text!.isNotEmpty) {
|
||||||
|
myNameFieldController.text =
|
||||||
|
data.text!.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_nameEmpty =
|
||||||
|
myNameFieldController.text.isEmpty;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: _nameEmpty
|
||||||
|
? const ClipboardIcon()
|
||||||
|
: const XIcon(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
Constants.size.circularBorderRadius,
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
key: const Key("frConfigTextFieldKey"),
|
||||||
|
controller: configFieldController,
|
||||||
|
onChanged: (_) {
|
||||||
|
setState(() {
|
||||||
|
_configEmpty = configFieldController.text.isEmpty;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
focusNode: configFocusNode,
|
||||||
|
readOnly: false,
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
style: STextStyles.field(context),
|
||||||
|
decoration: standardInputDecoration(
|
||||||
|
"Enter config",
|
||||||
|
configFocusNode,
|
||||||
|
context,
|
||||||
|
).copyWith(
|
||||||
|
contentPadding: const EdgeInsets.only(
|
||||||
|
left: 16,
|
||||||
|
top: 6,
|
||||||
|
bottom: 8,
|
||||||
|
right: 5,
|
||||||
|
),
|
||||||
|
suffixIcon: Padding(
|
||||||
|
padding: _configEmpty
|
||||||
|
? const EdgeInsets.only(right: 8)
|
||||||
|
: const EdgeInsets.only(right: 0),
|
||||||
|
child: UnconstrainedBox(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
!_configEmpty
|
||||||
|
? TextFieldIconButton(
|
||||||
|
semanticsLabel:
|
||||||
|
"Clear Button. Clears The Config Field.",
|
||||||
|
key: const Key("frConfigClearButtonKey"),
|
||||||
|
onTap: () {
|
||||||
|
configFieldController.text = "";
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_configEmpty = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const XIcon(),
|
||||||
|
)
|
||||||
|
: TextFieldIconButton(
|
||||||
|
semanticsLabel:
|
||||||
|
"Paste Button. Pastes From Clipboard To Config Field Input.",
|
||||||
|
key: const Key("frConfigPasteButtonKey"),
|
||||||
|
onTap: () async {
|
||||||
|
final ClipboardData? data =
|
||||||
|
await Clipboard.getData(
|
||||||
|
Clipboard.kTextPlain);
|
||||||
|
if (data?.text != null &&
|
||||||
|
data!.text!.isNotEmpty) {
|
||||||
|
configFieldController.text =
|
||||||
|
data.text!.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_configEmpty =
|
||||||
|
configFieldController.text.isEmpty;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: _configEmpty
|
||||||
|
? const ClipboardIcon()
|
||||||
|
: const XIcon(),
|
||||||
|
),
|
||||||
|
if (_configEmpty)
|
||||||
|
TextFieldIconButton(
|
||||||
|
semanticsLabel:
|
||||||
|
"Scan QR Button. Opens Camera For Scanning QR Code.",
|
||||||
|
key: const Key("frConfigScanQrButtonKey"),
|
||||||
|
onTap: () async {
|
||||||
|
try {
|
||||||
|
if (FocusScope.of(context).hasFocus) {
|
||||||
|
FocusScope.of(context).unfocus();
|
||||||
|
await Future<void>.delayed(
|
||||||
|
const Duration(milliseconds: 75));
|
||||||
|
}
|
||||||
|
|
||||||
|
final qrResult = await BarcodeScanner.scan();
|
||||||
|
|
||||||
|
configFieldController.text =
|
||||||
|
qrResult.rawContent;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_configEmpty =
|
||||||
|
configFieldController.text.isEmpty;
|
||||||
|
});
|
||||||
|
} on PlatformException catch (e, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"Failed to get camera permissions while trying to scan qr code: $e\n$s",
|
||||||
|
level: LogLevel.Warning,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const QrCodeIcon(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
if (!Util.isDesktop) const Spacer(),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
PrimaryButton(
|
||||||
|
label: "Start",
|
||||||
|
enabled: !_nameEmpty && !_configEmpty,
|
||||||
|
onPressed: () async {
|
||||||
|
if (FocusScope.of(context).hasFocus) {
|
||||||
|
FocusScope.of(context).unfocus();
|
||||||
|
}
|
||||||
|
if (_buttonLock) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_buttonLock = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
ref.read(pFrostResharingData).reset();
|
||||||
|
ref.read(pFrostResharingData).myName =
|
||||||
|
myNameFieldController.text;
|
||||||
|
ref.read(pFrostResharingData).resharerConfig =
|
||||||
|
configFieldController.text;
|
||||||
|
|
||||||
|
if (!ref
|
||||||
|
.read(pFrostResharingData)
|
||||||
|
.configData!
|
||||||
|
.newParticipants
|
||||||
|
.contains(ref.read(pFrostResharingData).myName!)) {
|
||||||
|
ref.read(pFrostResharingData).reset();
|
||||||
|
return await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => StackOkDialog(
|
||||||
|
title: "My name not found in config participants",
|
||||||
|
desktopPopRootNavigator: Util.isDesktop,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Exception? ex;
|
||||||
|
final wallet = await showLoading(
|
||||||
|
whileFuture: _createWallet(),
|
||||||
|
context: context,
|
||||||
|
message: "Setting up wallet",
|
||||||
|
isDesktop: Util.isDesktop,
|
||||||
|
onException: (e) => ex = e,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ex != null) {
|
||||||
|
throw ex!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
ref.read(pFrostResharingData).incompleteWallet = wallet!;
|
||||||
|
await Navigator.of(context).pushNamed(
|
||||||
|
NewStartResharingView.routeName,
|
||||||
|
arguments: wallet.walletId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"$e\n$s",
|
||||||
|
level: LogLevel.Fatal,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => StackOkDialog(
|
||||||
|
title: e.toString(),
|
||||||
|
desktopPopRootNavigator: Util.isDesktop,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_buttonLock = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,379 @@
|
||||||
|
import 'package:barcode_scan2/barcode_scan2.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:stackwallet/pages/home_view/home_view.dart';
|
||||||
|
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
|
||||||
|
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||||
|
import 'package:stackwallet/services/frost.dart';
|
||||||
|
import 'package:stackwallet/themes/stack_colors.dart';
|
||||||
|
import 'package:stackwallet/utilities/constants.dart';
|
||||||
|
import 'package:stackwallet/utilities/logger.dart';
|
||||||
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
|
import 'package:stackwallet/utilities/util.dart';
|
||||||
|
import 'package:stackwallet/widgets/background.dart';
|
||||||
|
import 'package:stackwallet/widgets/conditional_parent.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart';
|
||||||
|
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
|
||||||
|
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
|
||||||
|
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
|
||||||
|
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||||
|
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||||
|
import 'package:stackwallet/widgets/textfield_icon_button.dart';
|
||||||
|
|
||||||
|
class NewStartResharingView extends ConsumerStatefulWidget {
|
||||||
|
const NewStartResharingView({
|
||||||
|
super.key,
|
||||||
|
required this.walletId,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const String routeName = "/newStartResharingView";
|
||||||
|
|
||||||
|
final String walletId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<NewStartResharingView> createState() =>
|
||||||
|
_NewStartResharingViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NewStartResharingViewState extends ConsumerState<NewStartResharingView> {
|
||||||
|
final List<TextEditingController> controllers = [];
|
||||||
|
final List<FocusNode> focusNodes = [];
|
||||||
|
|
||||||
|
late final List<int> resharerIndexes;
|
||||||
|
|
||||||
|
final List<bool> fieldIsEmptyFlags = [];
|
||||||
|
|
||||||
|
bool _buttonLock = false;
|
||||||
|
Future<void> _onPressed() async {
|
||||||
|
if (_buttonLock) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_buttonLock = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// collect resharer strings
|
||||||
|
final resharerStarts = controllers.map((e) => e.text).toList();
|
||||||
|
|
||||||
|
final result = Frost.beginReshared(
|
||||||
|
myName: ref.read(pFrostResharingData).myName!,
|
||||||
|
resharerConfig: ref.read(pFrostResharingData).resharerConfig!,
|
||||||
|
resharerStarts: resharerStarts,
|
||||||
|
);
|
||||||
|
|
||||||
|
ref.read(pFrostResharingData).startResharedData = result;
|
||||||
|
|
||||||
|
await Navigator.of(context).pushNamed(
|
||||||
|
NewContinueSharingView.routeName,
|
||||||
|
arguments: widget.walletId,
|
||||||
|
);
|
||||||
|
} catch (e, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"$e\n$s",
|
||||||
|
level: LogLevel.Fatal,
|
||||||
|
);
|
||||||
|
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => StackOkDialog(
|
||||||
|
title: "Error",
|
||||||
|
message: e.toString(),
|
||||||
|
desktopPopRootNavigator: Util.isDesktop,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
_buttonLock = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
resharerIndexes = ref.read(pFrostResharingData).configData!.resharers;
|
||||||
|
|
||||||
|
for (int i = 0; i < resharerIndexes.length; i++) {
|
||||||
|
controllers.add(TextEditingController());
|
||||||
|
focusNodes.add(FocusNode());
|
||||||
|
fieldIsEmptyFlags.add(true);
|
||||||
|
}
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
for (int i = 0; i < controllers.length; i++) {
|
||||||
|
controllers[i].dispose();
|
||||||
|
}
|
||||||
|
for (int i = 0; i < focusNodes.length; i++) {
|
||||||
|
focusNodes[i].dispose();
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return WillPopScope(
|
||||||
|
onWillPop: () async {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => FrostInterruptionDialog(
|
||||||
|
type: FrostInterruptionDialogType.resharing,
|
||||||
|
popUntilOnYesRouteName:
|
||||||
|
Util.isDesktop ? DesktopHomeView.routeName : HomeView.routeName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
child: ConditionalParent(
|
||||||
|
condition: Util.isDesktop,
|
||||||
|
builder: (child) => DesktopScaffold(
|
||||||
|
background: Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: DesktopAppBar(
|
||||||
|
isCompactHeight: false,
|
||||||
|
leading: AppBarBackButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => const FrostInterruptionDialog(
|
||||||
|
type: FrostInterruptionDialogType.resharing,
|
||||||
|
popUntilOnYesRouteName: DesktopHomeView.routeName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
trailing: ExitToMyStackButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => const FrostInterruptionDialog(
|
||||||
|
type: FrostInterruptionDialogType.resharing,
|
||||||
|
popUntilOnYesRouteName: DesktopHomeView.routeName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SizedBox(
|
||||||
|
width: 480,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ConditionalParent(
|
||||||
|
condition: !Util.isDesktop,
|
||||||
|
builder: (child) => Background(
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: AppBarBackButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => const FrostInterruptionDialog(
|
||||||
|
type: FrostInterruptionDialogType.resharing,
|
||||||
|
popUntilOnYesRouteName: HomeView.routeName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
"Resharers",
|
||||||
|
style: STextStyles.navBarTitle(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: constraints.maxHeight,
|
||||||
|
),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
for (int i = 0; i < resharerIndexes.length; i++)
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
Constants.size.circularBorderRadius,
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
key: Key("frostResharerTextFieldKey_$i"),
|
||||||
|
controller: controllers[i],
|
||||||
|
focusNode: focusNodes[i],
|
||||||
|
readOnly: false,
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
style: STextStyles.field(context),
|
||||||
|
onChanged: (_) {
|
||||||
|
setState(() {
|
||||||
|
fieldIsEmptyFlags[i] =
|
||||||
|
controllers[i].text.isEmpty;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
decoration: standardInputDecoration(
|
||||||
|
"Enter index "
|
||||||
|
"${resharerIndexes[i]}"
|
||||||
|
"'s resharer",
|
||||||
|
focusNodes[i],
|
||||||
|
context,
|
||||||
|
).copyWith(
|
||||||
|
contentPadding: const EdgeInsets.only(
|
||||||
|
left: 16,
|
||||||
|
top: 6,
|
||||||
|
bottom: 8,
|
||||||
|
right: 5,
|
||||||
|
),
|
||||||
|
suffixIcon: Padding(
|
||||||
|
padding: fieldIsEmptyFlags[i]
|
||||||
|
? const EdgeInsets.only(right: 8)
|
||||||
|
: const EdgeInsets.only(right: 0),
|
||||||
|
child: UnconstrainedBox(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
!fieldIsEmptyFlags[i]
|
||||||
|
? TextFieldIconButton(
|
||||||
|
semanticsLabel:
|
||||||
|
"Clear Button. Clears The Resharer Field Input.",
|
||||||
|
key: Key(
|
||||||
|
"frostResharerClearButtonKey_$i"),
|
||||||
|
onTap: () {
|
||||||
|
controllers[i].text = "";
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
fieldIsEmptyFlags[i] = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const XIcon(),
|
||||||
|
)
|
||||||
|
: TextFieldIconButton(
|
||||||
|
semanticsLabel:
|
||||||
|
"Paste Button. Pastes From Clipboard To Resharer Field Input.",
|
||||||
|
key: Key(
|
||||||
|
"frostResharerPasteButtonKey_$i"),
|
||||||
|
onTap: () async {
|
||||||
|
final ClipboardData? data =
|
||||||
|
await Clipboard.getData(
|
||||||
|
Clipboard.kTextPlain);
|
||||||
|
if (data?.text != null &&
|
||||||
|
data!.text!.isNotEmpty) {
|
||||||
|
controllers[i].text =
|
||||||
|
data.text!.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
fieldIsEmptyFlags[i] =
|
||||||
|
controllers[i]
|
||||||
|
.text
|
||||||
|
.isEmpty;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: fieldIsEmptyFlags[i]
|
||||||
|
? const ClipboardIcon()
|
||||||
|
: const XIcon(),
|
||||||
|
),
|
||||||
|
if (fieldIsEmptyFlags[i])
|
||||||
|
TextFieldIconButton(
|
||||||
|
semanticsLabel: "Scan QR Button. "
|
||||||
|
"Opens Camera For Scanning QR Code.",
|
||||||
|
key: Key(
|
||||||
|
"frostCommitmentsScanQrButtonKey_$i"),
|
||||||
|
onTap: () async {
|
||||||
|
try {
|
||||||
|
if (FocusScope.of(context)
|
||||||
|
.hasFocus) {
|
||||||
|
FocusScope.of(context)
|
||||||
|
.unfocus();
|
||||||
|
await Future<void>.delayed(
|
||||||
|
const Duration(
|
||||||
|
milliseconds: 75));
|
||||||
|
}
|
||||||
|
|
||||||
|
final qrResult =
|
||||||
|
await BarcodeScanner.scan();
|
||||||
|
|
||||||
|
controllers[i].text =
|
||||||
|
qrResult.rawContent;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
fieldIsEmptyFlags[i] =
|
||||||
|
controllers[i]
|
||||||
|
.text
|
||||||
|
.isEmpty;
|
||||||
|
});
|
||||||
|
} on PlatformException catch (e, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"Failed to get camera permissions "
|
||||||
|
"while trying to scan qr code: $e\n$s",
|
||||||
|
level: LogLevel.Warning,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const QrCodeIcon(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (!Util.isDesktop) const Spacer(),
|
||||||
|
const _Div(),
|
||||||
|
PrimaryButton(
|
||||||
|
label: "Continue",
|
||||||
|
enabled: !fieldIsEmptyFlags.reduce((v, e) => v |= e),
|
||||||
|
onPressed: _onPressed,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Div extends StatelessWidget {
|
||||||
|
const _Div({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const SizedBox(
|
||||||
|
height: 12,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,315 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:stackwallet/pages/home_view/home_view.dart';
|
||||||
|
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
|
||||||
|
import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
|
||||||
|
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart';
|
||||||
|
import 'package:stackwallet/providers/db/main_db_provider.dart';
|
||||||
|
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||||
|
import 'package:stackwallet/providers/global/node_service_provider.dart';
|
||||||
|
import 'package:stackwallet/providers/global/prefs_provider.dart';
|
||||||
|
import 'package:stackwallet/providers/global/secure_store_provider.dart';
|
||||||
|
import 'package:stackwallet/providers/global/wallets_provider.dart';
|
||||||
|
import 'package:stackwallet/themes/stack_colors.dart';
|
||||||
|
import 'package:stackwallet/utilities/logger.dart';
|
||||||
|
import 'package:stackwallet/utilities/show_loading.dart';
|
||||||
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
|
import 'package:stackwallet/utilities/util.dart';
|
||||||
|
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
|
||||||
|
import 'package:stackwallet/widgets/background.dart';
|
||||||
|
import 'package:stackwallet/widgets/conditional_parent.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/detail_item.dart';
|
||||||
|
import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart';
|
||||||
|
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||||
|
|
||||||
|
class VerifyUpdatedWalletView extends ConsumerStatefulWidget {
|
||||||
|
const VerifyUpdatedWalletView({
|
||||||
|
super.key,
|
||||||
|
required this.walletId,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const String routeName = "/verifyUpdatedWalletView";
|
||||||
|
|
||||||
|
final String walletId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<VerifyUpdatedWalletView> createState() =>
|
||||||
|
_VerifyUpdatedWalletViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VerifyUpdatedWalletViewState
|
||||||
|
extends ConsumerState<VerifyUpdatedWalletView> {
|
||||||
|
late final String config;
|
||||||
|
late final String serializedKeys;
|
||||||
|
late final String reshareId;
|
||||||
|
|
||||||
|
late final bool isNew;
|
||||||
|
|
||||||
|
bool _buttonLock = false;
|
||||||
|
Future<void> _onPressed() async {
|
||||||
|
if (_buttonLock) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_buttonLock = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Exception? ex;
|
||||||
|
|
||||||
|
final BitcoinFrostWallet wallet;
|
||||||
|
|
||||||
|
if (isNew) {
|
||||||
|
wallet = await ref
|
||||||
|
.read(pFrostResharingData)
|
||||||
|
.incompleteWallet!
|
||||||
|
.toBitcoinFrostWallet(
|
||||||
|
mainDB: ref.read(mainDBProvider),
|
||||||
|
secureStorageInterface: ref.read(secureStoreProvider),
|
||||||
|
nodeService: ref.read(nodeServiceChangeNotifierProvider),
|
||||||
|
prefs: ref.read(prefsChangeNotifierProvider),
|
||||||
|
);
|
||||||
|
|
||||||
|
await wallet.info.setMnemonicVerified(
|
||||||
|
isar: ref.read(mainDBProvider).isar,
|
||||||
|
);
|
||||||
|
|
||||||
|
ref.read(pWallets).addWallet(wallet);
|
||||||
|
} else {
|
||||||
|
wallet =
|
||||||
|
ref.read(pWallets).getWallet(widget.walletId) as BitcoinFrostWallet;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
await showLoading(
|
||||||
|
whileFuture: wallet.updateWithResharedData(
|
||||||
|
serializedKeys: serializedKeys,
|
||||||
|
multisigConfig: config,
|
||||||
|
isNewWallet: isNew,
|
||||||
|
),
|
||||||
|
context: context,
|
||||||
|
message: isNew ? "Creating wallet" : "Updating wallet data",
|
||||||
|
isDesktop: Util.isDesktop,
|
||||||
|
onException: (e) => ex = e,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ex != null) {
|
||||||
|
throw ex!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
ref.read(pFrostResharingData).reset();
|
||||||
|
|
||||||
|
Navigator.of(context).popUntil(
|
||||||
|
ModalRoute.withName(
|
||||||
|
_popUntilPath,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"$e\n$s",
|
||||||
|
level: LogLevel.Fatal,
|
||||||
|
);
|
||||||
|
if (mounted) {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => StackOkDialog(
|
||||||
|
title: "Error",
|
||||||
|
message: e.toString(),
|
||||||
|
desktopPopRootNavigator: Util.isDesktop,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_buttonLock = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String get _popUntilPath => isNew
|
||||||
|
? Util.isDesktop
|
||||||
|
? DesktopHomeView.routeName
|
||||||
|
: HomeView.routeName
|
||||||
|
: Util.isDesktop
|
||||||
|
? DesktopWalletView.routeName
|
||||||
|
: WalletView.routeName;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
config = ref.read(pFrostResharingData).newWalletData!.multisigConfig;
|
||||||
|
serializedKeys =
|
||||||
|
ref.read(pFrostResharingData).newWalletData!.serializedKeys;
|
||||||
|
reshareId = ref.read(pFrostResharingData).newWalletData!.resharedId;
|
||||||
|
|
||||||
|
isNew = ref.read(pFrostResharingData).incompleteWallet != null &&
|
||||||
|
ref.read(pFrostResharingData).incompleteWallet!.walletId ==
|
||||||
|
widget.walletId;
|
||||||
|
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return WillPopScope(
|
||||||
|
onWillPop: () async {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => FrostInterruptionDialog(
|
||||||
|
type: FrostInterruptionDialogType.resharing,
|
||||||
|
popUntilOnYesRouteName: _popUntilPath,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
child: ConditionalParent(
|
||||||
|
condition: Util.isDesktop,
|
||||||
|
builder: (child) => DesktopScaffold(
|
||||||
|
background: Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: DesktopAppBar(
|
||||||
|
isCompactHeight: false,
|
||||||
|
leading: AppBarBackButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => FrostInterruptionDialog(
|
||||||
|
type: FrostInterruptionDialogType.resharing,
|
||||||
|
popUntilOnYesRouteName: _popUntilPath,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
trailing: ExitToMyStackButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => FrostInterruptionDialog(
|
||||||
|
type: FrostInterruptionDialogType.resharing,
|
||||||
|
popUntilOnYesRouteName: _popUntilPath,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SizedBox(
|
||||||
|
width: 480,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ConditionalParent(
|
||||||
|
condition: !Util.isDesktop,
|
||||||
|
builder: (child) => Background(
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: AppBarBackButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => FrostInterruptionDialog(
|
||||||
|
type: FrostInterruptionDialogType.resharing,
|
||||||
|
popUntilOnYesRouteName: _popUntilPath,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: constraints.maxHeight,
|
||||||
|
),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Ensure your reshare ID matches that of each other participant",
|
||||||
|
style: STextStyles.pageTitleH2(context),
|
||||||
|
),
|
||||||
|
const _Div(),
|
||||||
|
DetailItem(
|
||||||
|
title: "ID",
|
||||||
|
detail: reshareId,
|
||||||
|
button: Util.isDesktop
|
||||||
|
? IconCopyButton(
|
||||||
|
data: reshareId,
|
||||||
|
)
|
||||||
|
: SimpleCopyButton(
|
||||||
|
data: reshareId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const _Div(),
|
||||||
|
const _Div(),
|
||||||
|
Text(
|
||||||
|
"Back up your keys and config",
|
||||||
|
style: STextStyles.pageTitleH2(context),
|
||||||
|
),
|
||||||
|
const _Div(),
|
||||||
|
DetailItem(
|
||||||
|
title: "Config",
|
||||||
|
detail: config,
|
||||||
|
button: Util.isDesktop
|
||||||
|
? IconCopyButton(
|
||||||
|
data: config,
|
||||||
|
)
|
||||||
|
: SimpleCopyButton(
|
||||||
|
data: config,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const _Div(),
|
||||||
|
DetailItem(
|
||||||
|
title: "Keys",
|
||||||
|
detail: serializedKeys,
|
||||||
|
button: Util.isDesktop
|
||||||
|
? IconCopyButton(
|
||||||
|
data: serializedKeys,
|
||||||
|
)
|
||||||
|
: SimpleCopyButton(
|
||||||
|
data: serializedKeys,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!Util.isDesktop) const Spacer(),
|
||||||
|
const _Div(),
|
||||||
|
PrimaryButton(
|
||||||
|
label: "Confirm",
|
||||||
|
onPressed: _onPressed,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Div extends StatelessWidget {
|
||||||
|
const _Div({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const SizedBox(
|
||||||
|
height: 12,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
103
lib/providers/frost_wallet/frost_wallet_providers.dart
Normal file
103
lib/providers/frost_wallet/frost_wallet_providers.dart
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
import 'dart:ffi';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:frostdart/frostdart_bindings_generated.dart';
|
||||||
|
import 'package:stackwallet/services/frost.dart';
|
||||||
|
import 'package:stackwallet/wallets/models/incomplete_frost_wallet.dart';
|
||||||
|
import 'package:stackwallet/wallets/models/tx_data.dart';
|
||||||
|
|
||||||
|
// =================== wallet creation =========================================
|
||||||
|
final pFrostMultisigConfig = StateProvider<String?>((ref) => null);
|
||||||
|
final pFrostMyName = StateProvider<String?>((ref) => null);
|
||||||
|
|
||||||
|
final pFrostStartKeyGenData = StateProvider<
|
||||||
|
({
|
||||||
|
String seed,
|
||||||
|
String commitments,
|
||||||
|
Pointer<MultisigConfigWithName> multisigConfigWithNamePtr,
|
||||||
|
Pointer<SecretShareMachineWrapper> secretShareMachineWrapperPtr,
|
||||||
|
})?>((_) => null);
|
||||||
|
|
||||||
|
final pFrostSecretSharesData = StateProvider<
|
||||||
|
({
|
||||||
|
String share,
|
||||||
|
Pointer<SecretSharesRes> secretSharesResPtr,
|
||||||
|
})?>((ref) => null);
|
||||||
|
|
||||||
|
final pFrostCompletedKeyGenData = StateProvider<
|
||||||
|
({
|
||||||
|
Uint8List multisigId,
|
||||||
|
String recoveryString,
|
||||||
|
String serializedKeys,
|
||||||
|
})?>((ref) => null);
|
||||||
|
|
||||||
|
// ================= transaction creation ======================================
|
||||||
|
final pFrostTxData = StateProvider<TxData?>((ref) => null);
|
||||||
|
|
||||||
|
final pFrostAttemptSignData = StateProvider<
|
||||||
|
({
|
||||||
|
Pointer<TransactionSignMachineWrapper> machinePtr,
|
||||||
|
String preprocess,
|
||||||
|
})?>((ref) => null);
|
||||||
|
|
||||||
|
final pFrostContinueSignData = StateProvider<
|
||||||
|
({
|
||||||
|
Pointer<TransactionSignatureMachineWrapper> machinePtr,
|
||||||
|
String share,
|
||||||
|
})?>((ref) => null);
|
||||||
|
|
||||||
|
// ===================== shared/util ===========================================
|
||||||
|
final pFrostSelectParticipantsUnordered =
|
||||||
|
StateProvider<List<String>?>((ref) => null);
|
||||||
|
|
||||||
|
// ========================= resharing =========================================
|
||||||
|
final pFrostResharingData = Provider((ref) => _ResharingData());
|
||||||
|
|
||||||
|
class _ResharingData {
|
||||||
|
String? myName;
|
||||||
|
|
||||||
|
IncompleteFrostWallet? incompleteWallet;
|
||||||
|
|
||||||
|
// resharer encoded config string
|
||||||
|
String? resharerConfig;
|
||||||
|
({
|
||||||
|
int newThreshold,
|
||||||
|
List<int> resharers,
|
||||||
|
List<String> newParticipants,
|
||||||
|
})? get configData => resharerConfig != null
|
||||||
|
? Frost.extractResharerConfigData(resharerConfig: resharerConfig!)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// resharer start string (for sharing) and machine
|
||||||
|
({
|
||||||
|
String resharerStart,
|
||||||
|
Pointer<StartResharerRes> machine,
|
||||||
|
})? startResharerData;
|
||||||
|
|
||||||
|
// reshared start string (for sharing) and machine
|
||||||
|
({
|
||||||
|
String resharedStart,
|
||||||
|
Pointer<StartResharedRes> prior,
|
||||||
|
})? startResharedData;
|
||||||
|
|
||||||
|
// resharer complete string (for sharing)
|
||||||
|
String? resharerComplete;
|
||||||
|
|
||||||
|
// new keys and config with an ID
|
||||||
|
({
|
||||||
|
String multisigConfig,
|
||||||
|
String serializedKeys,
|
||||||
|
String resharedId,
|
||||||
|
})? newWalletData;
|
||||||
|
|
||||||
|
// reset/clear all data
|
||||||
|
void reset() {
|
||||||
|
resharerConfig = null;
|
||||||
|
startResharerData = null;
|
||||||
|
startResharedData = null;
|
||||||
|
resharerComplete = null;
|
||||||
|
newWalletData = null;
|
||||||
|
incompleteWallet = null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,6 +26,13 @@ import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_custom_tok
|
||||||
import 'package:stackwallet/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart';
|
import 'package:stackwallet/pages/add_wallet_views/add_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/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/create_or_restore_wallet_view/create_or_restore_wallet_view.dart';
|
||||||
|
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart';
|
||||||
|
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart';
|
||||||
|
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart';
|
||||||
|
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart';
|
||||||
|
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart';
|
||||||
|
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart';
|
||||||
|
import 'package:stackwallet/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart';
|
||||||
import 'package:stackwallet/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart';
|
import 'package:stackwallet/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart';
|
||||||
import 'package:stackwallet/pages/add_wallet_views/new_wallet_options/new_wallet_options_view.dart';
|
import 'package:stackwallet/pages/add_wallet_views/new_wallet_options/new_wallet_options_view.dart';
|
||||||
import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart';
|
import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart';
|
||||||
|
@ -113,6 +120,19 @@ import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_pr
|
||||||
import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart';
|
import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart';
|
||||||
import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart';
|
import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart';
|
||||||
import 'package:stackwallet/pages/settings_views/global_settings_view/tor_settings/tor_settings_view.dart';
|
import 'package:stackwallet/pages/settings_views/global_settings_view/tor_settings/tor_settings_view.dart';
|
||||||
|
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart';
|
||||||
|
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart';
|
||||||
|
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart';
|
||||||
|
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/begin_reshare_config_view.dart';
|
||||||
|
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/complete_reshare_config_view.dart';
|
||||||
|
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/display_reshare_config_view.dart';
|
||||||
|
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1b/import_reshare_config_view.dart';
|
||||||
|
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_view.dart';
|
||||||
|
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/continue_resharing_view.dart';
|
||||||
|
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart';
|
||||||
|
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart';
|
||||||
|
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart';
|
||||||
|
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/verify_updated_wallet_view.dart';
|
||||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart';
|
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart';
|
||||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart';
|
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart';
|
||||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_view.dart';
|
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_view.dart';
|
||||||
|
@ -423,6 +443,319 @@ class RouteGenerator {
|
||||||
}
|
}
|
||||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||||
|
|
||||||
|
case CreateNewFrostMsWalletView.routeName:
|
||||||
|
if (args is ({
|
||||||
|
String walletName,
|
||||||
|
Coin coin,
|
||||||
|
})) {
|
||||||
|
return getRoute(
|
||||||
|
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||||
|
builder: (_) => CreateNewFrostMsWalletView(
|
||||||
|
walletName: args.walletName,
|
||||||
|
coin: args.coin,
|
||||||
|
),
|
||||||
|
settings: RouteSettings(
|
||||||
|
name: settings.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||||
|
|
||||||
|
case RestoreFrostMsWalletView.routeName:
|
||||||
|
if (args is ({
|
||||||
|
String walletName,
|
||||||
|
Coin coin,
|
||||||
|
})) {
|
||||||
|
return getRoute(
|
||||||
|
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||||
|
builder: (_) => RestoreFrostMsWalletView(
|
||||||
|
walletName: args.walletName,
|
||||||
|
coin: args.coin,
|
||||||
|
),
|
||||||
|
settings: RouteSettings(
|
||||||
|
name: settings.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||||
|
|
||||||
|
case ShareNewMultisigConfigView.routeName:
|
||||||
|
if (args is ({
|
||||||
|
String walletName,
|
||||||
|
Coin coin,
|
||||||
|
})) {
|
||||||
|
return getRoute(
|
||||||
|
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||||
|
builder: (_) => ShareNewMultisigConfigView(
|
||||||
|
walletName: args.walletName,
|
||||||
|
coin: args.coin,
|
||||||
|
),
|
||||||
|
settings: RouteSettings(
|
||||||
|
name: settings.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||||
|
|
||||||
|
case ImportNewFrostMsWalletView.routeName:
|
||||||
|
if (args is ({
|
||||||
|
String walletName,
|
||||||
|
Coin coin,
|
||||||
|
})) {
|
||||||
|
return getRoute(
|
||||||
|
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||||
|
builder: (_) => ImportNewFrostMsWalletView(
|
||||||
|
walletName: args.walletName,
|
||||||
|
coin: args.coin,
|
||||||
|
),
|
||||||
|
settings: RouteSettings(
|
||||||
|
name: settings.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||||
|
|
||||||
|
case NewImportResharerConfigView.routeName:
|
||||||
|
if (args is ({
|
||||||
|
String walletName,
|
||||||
|
Coin coin,
|
||||||
|
})) {
|
||||||
|
return getRoute(
|
||||||
|
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||||
|
builder: (_) => NewImportResharerConfigView(
|
||||||
|
walletName: args.walletName,
|
||||||
|
coin: args.coin,
|
||||||
|
),
|
||||||
|
settings: RouteSettings(
|
||||||
|
name: settings.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||||
|
|
||||||
|
case NewStartResharingView.routeName:
|
||||||
|
if (args is String) {
|
||||||
|
return getRoute(
|
||||||
|
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||||
|
builder: (_) => NewStartResharingView(
|
||||||
|
walletId: args,
|
||||||
|
),
|
||||||
|
settings: RouteSettings(
|
||||||
|
name: settings.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||||
|
|
||||||
|
case NewContinueSharingView.routeName:
|
||||||
|
if (args is String) {
|
||||||
|
return getRoute(
|
||||||
|
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||||
|
builder: (_) => NewContinueSharingView(
|
||||||
|
walletId: args,
|
||||||
|
),
|
||||||
|
settings: RouteSettings(
|
||||||
|
name: settings.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||||
|
|
||||||
|
case FrostShareCommitmentsView.routeName:
|
||||||
|
if (args is ({
|
||||||
|
String walletName,
|
||||||
|
Coin coin,
|
||||||
|
})) {
|
||||||
|
return getRoute(
|
||||||
|
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||||
|
builder: (_) => FrostShareCommitmentsView(
|
||||||
|
walletName: args.walletName,
|
||||||
|
coin: args.coin,
|
||||||
|
),
|
||||||
|
settings: RouteSettings(
|
||||||
|
name: settings.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||||
|
|
||||||
|
case FrostShareSharesView.routeName:
|
||||||
|
if (args is ({
|
||||||
|
String walletName,
|
||||||
|
Coin coin,
|
||||||
|
})) {
|
||||||
|
return getRoute(
|
||||||
|
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||||
|
builder: (_) => FrostShareSharesView(
|
||||||
|
walletName: args.walletName,
|
||||||
|
coin: args.coin,
|
||||||
|
),
|
||||||
|
settings: RouteSettings(
|
||||||
|
name: settings.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||||
|
|
||||||
|
case ConfirmNewFrostMSWalletCreationView.routeName:
|
||||||
|
if (args is ({
|
||||||
|
String walletName,
|
||||||
|
Coin coin,
|
||||||
|
})) {
|
||||||
|
return getRoute(
|
||||||
|
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||||
|
builder: (_) => ConfirmNewFrostMSWalletCreationView(
|
||||||
|
walletName: args.walletName,
|
||||||
|
coin: args.coin,
|
||||||
|
),
|
||||||
|
settings: RouteSettings(
|
||||||
|
name: settings.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||||
|
|
||||||
|
case FrostMSWalletOptionsView.routeName:
|
||||||
|
if (args is String) {
|
||||||
|
return getRoute(
|
||||||
|
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||||
|
builder: (_) => FrostMSWalletOptionsView(
|
||||||
|
walletId: args,
|
||||||
|
),
|
||||||
|
settings: RouteSettings(
|
||||||
|
name: settings.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||||
|
|
||||||
|
case FrostParticipantsView.routeName:
|
||||||
|
if (args is String) {
|
||||||
|
return getRoute(
|
||||||
|
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||||
|
builder: (_) => FrostParticipantsView(
|
||||||
|
walletId: args,
|
||||||
|
),
|
||||||
|
settings: RouteSettings(
|
||||||
|
name: settings.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||||
|
|
||||||
|
case ImportReshareConfigView.routeName:
|
||||||
|
if (args is String) {
|
||||||
|
return getRoute(
|
||||||
|
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||||
|
builder: (_) => ImportReshareConfigView(
|
||||||
|
walletId: args,
|
||||||
|
),
|
||||||
|
settings: RouteSettings(
|
||||||
|
name: settings.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||||
|
|
||||||
|
case BeginReshareConfigView.routeName:
|
||||||
|
if (args is String) {
|
||||||
|
return getRoute(
|
||||||
|
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||||
|
builder: (_) => BeginReshareConfigView(
|
||||||
|
walletId: args,
|
||||||
|
),
|
||||||
|
settings: RouteSettings(
|
||||||
|
name: settings.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||||
|
|
||||||
|
case CompleteReshareConfigView.routeName:
|
||||||
|
if (args is ({String walletId, List<int> resharers})) {
|
||||||
|
return getRoute(
|
||||||
|
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||||
|
builder: (_) => CompleteReshareConfigView(
|
||||||
|
walletId: args.walletId,
|
||||||
|
resharers: args.resharers,
|
||||||
|
),
|
||||||
|
settings: RouteSettings(
|
||||||
|
name: settings.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||||
|
|
||||||
|
case DisplayReshareConfigView.routeName:
|
||||||
|
if (args is String) {
|
||||||
|
return getRoute(
|
||||||
|
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||||
|
builder: (_) => DisplayReshareConfigView(
|
||||||
|
walletId: args,
|
||||||
|
),
|
||||||
|
settings: RouteSettings(
|
||||||
|
name: settings.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||||
|
|
||||||
|
case BeginResharingView.routeName:
|
||||||
|
if (args is String) {
|
||||||
|
return getRoute(
|
||||||
|
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||||
|
builder: (_) => BeginResharingView(
|
||||||
|
walletId: args,
|
||||||
|
),
|
||||||
|
settings: RouteSettings(
|
||||||
|
name: settings.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||||
|
|
||||||
|
case ContinueResharingView.routeName:
|
||||||
|
if (args is String) {
|
||||||
|
return getRoute(
|
||||||
|
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||||
|
builder: (_) => ContinueResharingView(
|
||||||
|
walletId: args,
|
||||||
|
),
|
||||||
|
settings: RouteSettings(
|
||||||
|
name: settings.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||||
|
|
||||||
|
case FinishResharingView.routeName:
|
||||||
|
if (args is String) {
|
||||||
|
return getRoute(
|
||||||
|
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||||
|
builder: (_) => FinishResharingView(
|
||||||
|
walletId: args,
|
||||||
|
),
|
||||||
|
settings: RouteSettings(
|
||||||
|
name: settings.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||||
|
|
||||||
|
case VerifyUpdatedWalletView.routeName:
|
||||||
|
if (args is String) {
|
||||||
|
return getRoute(
|
||||||
|
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||||
|
builder: (_) => VerifyUpdatedWalletView(
|
||||||
|
walletId: args,
|
||||||
|
),
|
||||||
|
settings: RouteSettings(
|
||||||
|
name: settings.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||||
|
|
||||||
// case MonkeyLoadedView.routeName:
|
// case MonkeyLoadedView.routeName:
|
||||||
// if (args is Tuple2<String, ChangeNotifierProvider<Manager>>) {
|
// if (args is Tuple2<String, ChangeNotifierProvider<Manager>>) {
|
||||||
// return getRoute(
|
// return getRoute(
|
||||||
|
|
|
@ -16,6 +16,13 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||||
final coinCardProvider = Provider.family<String?, Coin>((ref, coin) {
|
final coinCardProvider = Provider.family<String?, Coin>((ref, coin) {
|
||||||
final assets = ref.watch(themeAssetsProvider);
|
final assets = ref.watch(themeAssetsProvider);
|
||||||
|
|
||||||
|
// TODO: handle this differently by adding proper frost assets to themes
|
||||||
|
if (coin == Coin.bitcoinFrost) {
|
||||||
|
coin = Coin.bitcoin;
|
||||||
|
} else if (coin == Coin.bitcoinFrostTestNet) {
|
||||||
|
coin = Coin.bitcoinTestNet;
|
||||||
|
}
|
||||||
|
|
||||||
if (assets is ThemeAssetsV3) {
|
if (assets is ThemeAssetsV3) {
|
||||||
return assets.coinCardImages?[coin.mainNetVersion];
|
return assets.coinCardImages?[coin.mainNetVersion];
|
||||||
} else {
|
} else {
|
||||||
|
@ -26,6 +33,13 @@ final coinCardProvider = Provider.family<String?, Coin>((ref, coin) {
|
||||||
final coinCardFavoritesProvider = Provider.family<String?, Coin>((ref, coin) {
|
final coinCardFavoritesProvider = Provider.family<String?, Coin>((ref, coin) {
|
||||||
final assets = ref.watch(themeAssetsProvider);
|
final assets = ref.watch(themeAssetsProvider);
|
||||||
|
|
||||||
|
// TODO: handle this differently by adding proper frost assets to themes
|
||||||
|
if (coin == Coin.bitcoinFrost) {
|
||||||
|
coin = Coin.bitcoin;
|
||||||
|
} else if (coin == Coin.bitcoinFrostTestNet) {
|
||||||
|
coin = Coin.bitcoinTestNet;
|
||||||
|
}
|
||||||
|
|
||||||
if (assets is ThemeAssetsV3) {
|
if (assets is ThemeAssetsV3) {
|
||||||
return assets.coinCardFavoritesImages?[coin.mainNetVersion] ??
|
return assets.coinCardFavoritesImages?[coin.mainNetVersion] ??
|
||||||
assets.coinCardImages?[coin.mainNetVersion];
|
assets.coinCardImages?[coin.mainNetVersion];
|
||||||
|
|
|
@ -16,6 +16,13 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||||
final coinIconProvider = Provider.family<String, Coin>((ref, coin) {
|
final coinIconProvider = Provider.family<String, Coin>((ref, coin) {
|
||||||
final assets = ref.watch(themeAssetsProvider);
|
final assets = ref.watch(themeAssetsProvider);
|
||||||
|
|
||||||
|
// TODO: handle this differently by adding proper frost assets to themes
|
||||||
|
if (coin == Coin.bitcoinFrost) {
|
||||||
|
coin = Coin.bitcoin;
|
||||||
|
} else if (coin == Coin.bitcoinFrostTestNet) {
|
||||||
|
coin = Coin.bitcoinTestNet;
|
||||||
|
}
|
||||||
|
|
||||||
if (assets is ThemeAssets) {
|
if (assets is ThemeAssets) {
|
||||||
switch (coin) {
|
switch (coin) {
|
||||||
case Coin.bitcoin:
|
case Coin.bitcoin:
|
||||||
|
|
|
@ -16,6 +16,13 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||||
final coinImageProvider = Provider.family<String, Coin>((ref, coin) {
|
final coinImageProvider = Provider.family<String, Coin>((ref, coin) {
|
||||||
final assets = ref.watch(themeAssetsProvider);
|
final assets = ref.watch(themeAssetsProvider);
|
||||||
|
|
||||||
|
// TODO: handle this differently by adding proper frost assets to themes
|
||||||
|
if (coin == Coin.bitcoinFrost) {
|
||||||
|
coin = Coin.bitcoin;
|
||||||
|
} else if (coin == Coin.bitcoinFrostTestNet) {
|
||||||
|
coin = Coin.bitcoinTestNet;
|
||||||
|
}
|
||||||
|
|
||||||
if (assets is ThemeAssets) {
|
if (assets is ThemeAssets) {
|
||||||
switch (coin) {
|
switch (coin) {
|
||||||
case Coin.bitcoin:
|
case Coin.bitcoin:
|
||||||
|
@ -64,6 +71,13 @@ final coinImageProvider = Provider.family<String, Coin>((ref, coin) {
|
||||||
final coinImageSecondaryProvider = Provider.family<String, Coin>((ref, coin) {
|
final coinImageSecondaryProvider = Provider.family<String, Coin>((ref, coin) {
|
||||||
final assets = ref.watch(themeAssetsProvider);
|
final assets = ref.watch(themeAssetsProvider);
|
||||||
|
|
||||||
|
// TODO: handle this differently by adding proper frost assets to themes
|
||||||
|
if (coin == Coin.bitcoinFrost) {
|
||||||
|
coin = Coin.bitcoin;
|
||||||
|
} else if (coin == Coin.bitcoinFrostTestNet) {
|
||||||
|
coin = Coin.bitcoinTestNet;
|
||||||
|
}
|
||||||
|
|
||||||
if (assets is ThemeAssets) {
|
if (assets is ThemeAssets) {
|
||||||
switch (coin) {
|
switch (coin) {
|
||||||
case Coin.bitcoin:
|
case Coin.bitcoin:
|
||||||
|
|
|
@ -297,6 +297,17 @@ extension CoinExt on Coin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get isFrost {
|
||||||
|
switch (this) {
|
||||||
|
case Coin.bitcoinFrost:
|
||||||
|
case Coin.bitcoinFrostTestNet:
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Coin get mainNetVersion {
|
Coin get mainNetVersion {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case Coin.bitcoin:
|
case Coin.bitcoin:
|
||||||
|
@ -495,6 +506,15 @@ Coin coinFromPrettyName(String name) {
|
||||||
case "tStellar":
|
case "tStellar":
|
||||||
return Coin.stellarTestnet;
|
return Coin.stellarTestnet;
|
||||||
|
|
||||||
|
case "Bitcoin Frost":
|
||||||
|
case "bitcoinFrost":
|
||||||
|
return Coin.bitcoinFrost;
|
||||||
|
|
||||||
|
case "Bitcoin Frost Testnet":
|
||||||
|
case "tBitcoin Frost":
|
||||||
|
case "bitcoinFrostTestNet":
|
||||||
|
return Coin.bitcoinFrostTestNet;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw ArgumentError.value(
|
throw ArgumentError.value(
|
||||||
name,
|
name,
|
||||||
|
|
42
lib/wallets/models/incomplete_frost_wallet.dart
Normal file
42
lib/wallets/models/incomplete_frost_wallet.dart
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import 'package:stackwallet/db/isar/main_db.dart';
|
||||||
|
import 'package:stackwallet/services/node_service.dart';
|
||||||
|
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
|
||||||
|
import 'package:stackwallet/utilities/prefs.dart';
|
||||||
|
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart';
|
||||||
|
import 'package:stackwallet/wallets/isar/models/wallet_info.dart';
|
||||||
|
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
|
||||||
|
import 'package:stackwallet/wallets/wallet/wallet.dart';
|
||||||
|
|
||||||
|
class IncompleteFrostWallet {
|
||||||
|
WalletInfo? info;
|
||||||
|
|
||||||
|
String? get walletId => info?.walletId;
|
||||||
|
|
||||||
|
Future<BitcoinFrostWallet> toBitcoinFrostWallet({
|
||||||
|
required MainDB mainDB,
|
||||||
|
required SecureStorageInterface secureStorageInterface,
|
||||||
|
required NodeService nodeService,
|
||||||
|
required Prefs prefs,
|
||||||
|
}) async {
|
||||||
|
final wallet = await Wallet.create(
|
||||||
|
walletInfo: info!,
|
||||||
|
mainDB: mainDB,
|
||||||
|
secureStorageInterface: secureStorageInterface,
|
||||||
|
nodeService: nodeService,
|
||||||
|
prefs: prefs,
|
||||||
|
);
|
||||||
|
|
||||||
|
// dummy entry so updaters work when `wallet.updateWithResharedData` is called
|
||||||
|
final frostInfo = FrostWalletInfo(
|
||||||
|
walletId: info!.walletId,
|
||||||
|
knownSalts: [],
|
||||||
|
participants: [],
|
||||||
|
myName: "",
|
||||||
|
threshold: -1,
|
||||||
|
);
|
||||||
|
|
||||||
|
await mainDB.isar.frostWalletInfo.put(frostInfo);
|
||||||
|
|
||||||
|
return wallet as BitcoinFrostWallet;
|
||||||
|
}
|
||||||
|
}
|
|
@ -171,7 +171,7 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final serializedKeys = await _getSerializedKeys();
|
final serializedKeys = await getSerializedKeys();
|
||||||
final keys = frost.deserializeKeys(keys: serializedKeys!);
|
final keys = frost.deserializeKeys(keys: serializedKeys!);
|
||||||
|
|
||||||
final int network = cryptoCurrency.network == CryptoCurrencyNetwork.main
|
final int network = cryptoCurrency.network == CryptoCurrencyNetwork.main
|
||||||
|
@ -213,7 +213,7 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
|
||||||
final int network = cryptoCurrency.network == CryptoCurrencyNetwork.main
|
final int network = cryptoCurrency.network == CryptoCurrencyNetwork.main
|
||||||
? Network.Mainnet
|
? Network.Mainnet
|
||||||
: Network.Testnet;
|
: Network.Testnet;
|
||||||
final serializedKeys = await _getSerializedKeys();
|
final serializedKeys = await getSerializedKeys();
|
||||||
|
|
||||||
return Frost.attemptSignConfig(
|
return Frost.attemptSignConfig(
|
||||||
network: network,
|
network: network,
|
||||||
|
@ -859,7 +859,7 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
|
||||||
|
|
||||||
// =================== Secure storage ========================================
|
// =================== Secure storage ========================================
|
||||||
|
|
||||||
Future<String?> _getSerializedKeys() async =>
|
Future<String?> getSerializedKeys() async =>
|
||||||
await secureStorageInterface.read(
|
await secureStorageInterface.read(
|
||||||
key: "{$walletId}_serializedFROSTKeys",
|
key: "{$walletId}_serializedFROSTKeys",
|
||||||
);
|
);
|
||||||
|
@ -867,7 +867,7 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
|
||||||
Future<void> _saveSerializedKeys(
|
Future<void> _saveSerializedKeys(
|
||||||
String keys,
|
String keys,
|
||||||
) async {
|
) async {
|
||||||
final current = await _getSerializedKeys();
|
final current = await getSerializedKeys();
|
||||||
|
|
||||||
if (current == null) {
|
if (current == null) {
|
||||||
// do nothing
|
// do nothing
|
||||||
|
|
90
lib/widgets/detail_item.dart
Normal file
90
lib/widgets/detail_item.dart
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stackwallet/themes/stack_colors.dart';
|
||||||
|
import 'package:stackwallet/utilities/text_styles.dart';
|
||||||
|
import 'package:stackwallet/utilities/util.dart';
|
||||||
|
import 'package:stackwallet/widgets/conditional_parent.dart';
|
||||||
|
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
||||||
|
|
||||||
|
class DetailItem extends StatelessWidget {
|
||||||
|
const DetailItem({
|
||||||
|
Key? key,
|
||||||
|
required this.title,
|
||||||
|
required this.detail,
|
||||||
|
this.button,
|
||||||
|
this.showEmptyDetail = true,
|
||||||
|
this.disableSelectableText = false,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final String detail;
|
||||||
|
final Widget? button;
|
||||||
|
final bool showEmptyDetail;
|
||||||
|
final bool disableSelectableText;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ConditionalParent(
|
||||||
|
condition: !Util.isDesktop,
|
||||||
|
builder: (child) => RoundedWhiteContainer(
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
child: ConditionalParent(
|
||||||
|
condition: Util.isDesktop,
|
||||||
|
builder: (child) => Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
disableSelectableText
|
||||||
|
? Text(
|
||||||
|
title,
|
||||||
|
style: STextStyles.itemSubtitle(context),
|
||||||
|
)
|
||||||
|
: SelectableText(
|
||||||
|
title,
|
||||||
|
style: STextStyles.itemSubtitle(context),
|
||||||
|
),
|
||||||
|
button ?? Container(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 5,
|
||||||
|
),
|
||||||
|
detail.isEmpty && showEmptyDetail
|
||||||
|
? disableSelectableText
|
||||||
|
? Text(
|
||||||
|
"$title will appear here",
|
||||||
|
style: STextStyles.w500_14(context).copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.textSubtitle3,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: SelectableText(
|
||||||
|
"$title will appear here",
|
||||||
|
style: STextStyles.w500_14(context).copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.textSubtitle3,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: disableSelectableText
|
||||||
|
? Text(
|
||||||
|
detail,
|
||||||
|
style: STextStyles.w500_14(context),
|
||||||
|
)
|
||||||
|
: SelectableText(
|
||||||
|
detail,
|
||||||
|
style: STextStyles.w500_14(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
70
lib/widgets/dialogs/frost_interruption_dialog.dart
Normal file
70
lib/widgets/dialogs/frost_interruption_dialog.dart
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stackwallet/utilities/util.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
|
||||||
|
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||||
|
|
||||||
|
enum FrostInterruptionDialogType {
|
||||||
|
walletCreation,
|
||||||
|
resharing,
|
||||||
|
transactionCreation;
|
||||||
|
}
|
||||||
|
|
||||||
|
class FrostInterruptionDialog extends StatelessWidget {
|
||||||
|
const FrostInterruptionDialog({
|
||||||
|
super.key,
|
||||||
|
required this.type,
|
||||||
|
required this.popUntilOnYesRouteName,
|
||||||
|
this.onNoPressedOverride,
|
||||||
|
this.onYesPressedOverride,
|
||||||
|
});
|
||||||
|
|
||||||
|
final FrostInterruptionDialogType type;
|
||||||
|
final String popUntilOnYesRouteName;
|
||||||
|
final VoidCallback? onNoPressedOverride;
|
||||||
|
final VoidCallback? onYesPressedOverride;
|
||||||
|
|
||||||
|
String get message {
|
||||||
|
switch (type) {
|
||||||
|
case FrostInterruptionDialogType.walletCreation:
|
||||||
|
return "wallet creation";
|
||||||
|
case FrostInterruptionDialogType.resharing:
|
||||||
|
return "resharing";
|
||||||
|
case FrostInterruptionDialogType.transactionCreation:
|
||||||
|
return "transaction signing";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return StackDialog(
|
||||||
|
title: "Cancel $message process",
|
||||||
|
message: "Are you sure you want to cancel the $message process?",
|
||||||
|
leftButton: SecondaryButton(
|
||||||
|
label: "No",
|
||||||
|
onPressed: onNoPressedOverride ??
|
||||||
|
Navigator.of(
|
||||||
|
context,
|
||||||
|
rootNavigator: Util.isDesktop,
|
||||||
|
).pop,
|
||||||
|
),
|
||||||
|
rightButton: PrimaryButton(
|
||||||
|
label: "Yes",
|
||||||
|
onPressed: onYesPressedOverride ??
|
||||||
|
() {
|
||||||
|
// pop dialog
|
||||||
|
Navigator.of(
|
||||||
|
context,
|
||||||
|
rootNavigator: Util.isDesktop,
|
||||||
|
).pop();
|
||||||
|
|
||||||
|
Navigator.of(context).popUntil(
|
||||||
|
ModalRoute.withName(
|
||||||
|
popUntilOnYesRouteName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -147,8 +147,10 @@ class StackOkDialog extends StatelessWidget {
|
||||||
this.icon,
|
this.icon,
|
||||||
required this.title,
|
required this.title,
|
||||||
this.message,
|
this.message,
|
||||||
|
this.desktopPopRootNavigator = false,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final bool desktopPopRootNavigator;
|
||||||
final Widget? leftButton;
|
final Widget? leftButton;
|
||||||
final void Function(String)? onOkPressed;
|
final void Function(String)? onOkPressed;
|
||||||
|
|
||||||
|
@ -208,9 +210,13 @@ class StackOkDialog extends StatelessWidget {
|
||||||
onOkPressed?.call("OK");
|
onOkPressed?.call("OK");
|
||||||
}
|
}
|
||||||
: () {
|
: () {
|
||||||
|
if (desktopPopRootNavigator) {
|
||||||
|
Navigator.of(context, rootNavigator: true).pop();
|
||||||
|
} else {
|
||||||
int count = 0;
|
int count = 0;
|
||||||
Navigator.of(context).popUntil((_) => count++ >= 2);
|
Navigator.of(context).popUntil((_) => count++ >= 2);
|
||||||
// onOkPressed?.call("OK");
|
// onOkPressed?.call("OK");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.extension<StackColors>()!
|
.extension<StackColors>()!
|
||||||
|
|
Loading…
Reference in a new issue