ui view only wallet changes

This commit is contained in:
julian 2024-11-12 09:29:12 -06:00 committed by julian-CStack
parent 53eb6ac8d1
commit 1f0ee995b9
17 changed files with 1501 additions and 430 deletions

View file

@ -12,8 +12,6 @@ import 'dart:async';
import 'dart:convert';
import 'package:bip39/bip39.dart' as bip39;
import 'package:blockchain_utils/bip/bip/bip39/bip39_mnemonic.dart';
import 'package:blockchain_utils/bip/bip/bip39/bip39_mnemonic_generator.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';

View file

@ -23,6 +23,7 @@ import '../../../../utilities/format.dart';
import '../../../../utilities/text_styles.dart';
import '../../../../utilities/util.dart';
import '../../../../wallets/crypto_currency/crypto_currency.dart';
import '../../../../wallets/crypto_currency/interfaces/view_only_option_currency_interface.dart';
import '../../../../widgets/conditional_parent.dart';
import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../../../widgets/custom_buttons/checkbox_text_button.dart';
@ -32,7 +33,9 @@ import '../../../../widgets/desktop/desktop_scaffold.dart';
import '../../../../widgets/expandable.dart';
import '../../../../widgets/rounded_white_container.dart';
import '../../../../widgets/stack_text_field.dart';
import '../../../../widgets/toggle.dart';
import '../../create_or_restore_wallet_view/sub_widgets/coin_image.dart';
import '../restore_view_only_wallet_view.dart';
import '../restore_wallet_view.dart';
import '../sub_widgets/mnemonic_word_count_select_sheet.dart';
import 'sub_widgets/mobile_mnemonic_length_selector.dart';
@ -69,7 +72,6 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> {
final bool _nextEnabled = true;
DateTime? _restoreFromDate;
bool hidePassword = true;
bool _expandedAdavnced = false;
bool get supportsMnemonicPassphrase => coin.hasMnemonicPassphraseSupport;
@ -99,27 +101,46 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> {
super.dispose();
}
bool _nextLock = false;
Future<void> nextPressed() async {
if (!isDesktop) {
// hide keyboard if has focus
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 75));
if (_nextLock) return;
_nextLock = true;
try {
if (!isDesktop) {
// hide keyboard if has focus
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 75));
}
}
}
if (mounted) {
await Navigator.of(context).pushNamed(
RestoreWalletView.routeName,
arguments: Tuple6(
walletName,
coin,
ref.read(mnemonicWordCountStateProvider.state).state,
_restoreFromDate,
passwordController.text,
enableLelantusScanning,
),
);
if (mounted) {
if (!_showViewOnlyOption) {
await Navigator.of(context).pushNamed(
RestoreWalletView.routeName,
arguments: Tuple6(
walletName,
coin,
ref.read(mnemonicWordCountStateProvider.state).state,
_restoreFromDate,
passwordController.text,
enableLelantusScanning,
),
);
} else {
await Navigator.of(context).pushNamed(
RestoreViewOnlyWalletView.routeName,
arguments: (
walletName: walletName,
coin: coin,
restoreFromDate: _restoreFromDate,
enableLelantusScanning: enableLelantusScanning,
),
);
}
}
} finally {
_nextLock = false;
}
}
@ -164,17 +185,12 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> {
);
}
bool _showViewOnlyOption = false;
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType with ${coin.identifier} $walletName");
final lengths = coin.possibleMnemonicLengths;
final isMoneroAnd25 = coin is Monero &&
ref.watch(mnemonicWordCountStateProvider.state).state == 25;
final isWowneroAnd25 = coin is Wownero &&
ref.watch(mnemonicWordCountStateProvider.state).state == 25;
return MasterScaffold(
isDesktop: isDesktop,
appBar: isDesktop
@ -227,288 +243,57 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> {
SizedBox(
height: isDesktop ? 40 : 24,
),
if (isMoneroAnd25 || coin is Epiccash || isWowneroAnd25)
Text(
"Choose start date",
style: isDesktop
? STextStyles.desktopTextExtraSmall(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
)
: STextStyles.smallMed12(context),
textAlign: TextAlign.left,
),
if (isMoneroAnd25 || coin is Epiccash || isWowneroAnd25)
if (coin is ViewOnlyOptionCurrencyInterface)
SizedBox(
height: isDesktop ? 16 : 8,
),
if (isMoneroAnd25 || coin is Epiccash || isWowneroAnd25)
if (!isDesktop)
RestoreFromDatePicker(
onTap: chooseDate,
controller: _dateController,
),
if (isMoneroAnd25 || coin is Epiccash || isWowneroAnd25)
if (isDesktop)
// TODO desktop date picker
RestoreFromDatePicker(
onTap: chooseDesktopDate,
controller: _dateController,
),
if (isMoneroAnd25 || coin is Epiccash || isWowneroAnd25)
const SizedBox(
height: 8,
),
if (isMoneroAnd25 || coin is Epiccash || isWowneroAnd25)
RoundedWhiteContainer(
child: Center(
child: Text(
"Choose the date you made the wallet (approximate is fine)",
style: isDesktop
? STextStyles.desktopTextExtraSmall(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
)
: STextStyles.smallMed12(context).copyWith(
fontSize: 10,
),
),
),
),
if (isMoneroAnd25 || coin is Epiccash || isWowneroAnd25)
SizedBox(
height: isDesktop ? 24 : 16,
),
Text(
"Choose recovery phrase length",
style: isDesktop
? STextStyles.desktopTextExtraSmall(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
)
: STextStyles.smallMed12(context),
textAlign: TextAlign.left,
),
SizedBox(
height: isDesktop ? 16 : 8,
),
if (isDesktop)
DropdownButtonHideUnderline(
child: DropdownButton2<int>(
value:
ref.watch(mnemonicWordCountStateProvider.state).state,
items: [
...lengths.map(
(e) => DropdownMenuItem(
value: e,
child: Text(
"$e words",
style: STextStyles.desktopTextMedium(context),
),
),
),
],
onChanged: (value) {
if (value is int) {
ref.read(mnemonicWordCountStateProvider.state).state =
value;
}
height: isDesktop ? 56 : 48,
width: isDesktop ? 490 : null,
child: Toggle(
key: UniqueKey(),
onText: "Seed",
offText: "View Only",
onColor:
Theme.of(context).extension<StackColors>()!.popupBG,
offColor: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
isOn: _showViewOnlyOption,
onValueChanged: (value) {
setState(() {
_showViewOnlyOption = value;
});
},
isExpanded: true,
iconStyleData: IconStyleData(
icon: SvgPicture.asset(
Assets.svg.chevronDown,
width: 12,
height: 6,
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveSearchIconRight,
),
),
dropdownStyleData: DropdownStyleData(
offset: const Offset(0, -10),
elevation: 0,
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
),
menuItemStyleData: const MenuItemStyleData(
padding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
),
),
if (!isDesktop)
MobileMnemonicLengthSelector(
chooseMnemonicLength: chooseMnemonicLength,
),
if (supportsMnemonicPassphrase)
if (coin is ViewOnlyOptionCurrencyInterface)
SizedBox(
height: isDesktop ? 24 : 16,
height: isDesktop ? 40 : 24,
),
if (supportsMnemonicPassphrase)
Expandable(
onExpandChanged: (state) {
setState(() {
_expandedAdavnced = state == ExpandableState.expanded;
});
},
header: Container(
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.only(
top: 8.0,
bottom: 8.0,
right: 10,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Advanced",
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(
context,
).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
)
: STextStyles.smallMed12(context),
textAlign: TextAlign.left,
),
SvgPicture.asset(
_expandedAdavnced
? Assets.svg.chevronUp
: Assets.svg.chevronDown,
width: 12,
height: 6,
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveSearchIconRight,
),
],
),
_showViewOnlyOption
? ViewOnlyRestoreOption(
coin: coin,
dateController: _dateController,
dateChooserFunction:
isDesktop ? chooseDesktopDate : chooseDate,
)
: SeedRestoreOption(
coin: coin,
dateController: _dateController,
pwController: passwordController,
pwFocusNode: passwordFocusNode,
supportsMnemonicPassphrase: supportsMnemonicPassphrase,
dateChooserFunction:
isDesktop ? chooseDesktopDate : chooseDate,
chooseMnemonicLength: chooseMnemonicLength,
lelScanChanged: (value) {
enableLelantusScanning = value;
},
),
),
body: Container(
color: Colors.transparent,
child: Column(
children: [
if (coin is Firo)
CheckboxTextButton(
label: "Scan for Lelantus transactions",
onChanged: (newValue) {
setState(() {
enableLelantusScanning = newValue ?? true;
});
},
),
if (coin is Firo)
const SizedBox(
height: 8,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("mnemonicPassphraseFieldKey1"),
focusNode: passwordFocusNode,
controller: passwordController,
style: isDesktop
? STextStyles.desktopTextMedium(context)
.copyWith(
height: 2,
)
: STextStyles.field(context),
obscureText: hidePassword,
enableSuggestions: false,
autocorrect: false,
decoration: standardInputDecoration(
"BIP39 passphrase",
passwordFocusNode,
context,
).copyWith(
suffixIcon: UnconstrainedBox(
child: ConditionalParent(
condition: isDesktop,
builder: (child) => SizedBox(
height: 70,
child: child,
),
child: Row(
children: [
SizedBox(
width: isDesktop ? 24 : 16,
),
GestureDetector(
key: const Key(
"mnemonicPassphraseFieldShowPasswordButtonKey",
),
onTap: () async {
setState(() {
hidePassword = !hidePassword;
});
},
child: SvgPicture.asset(
hidePassword
? Assets.svg.eye
: Assets.svg.eyeSlash,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: isDesktop ? 24 : 16,
height: isDesktop ? 24 : 16,
),
),
const SizedBox(
width: 12,
),
],
),
),
),
),
),
),
const SizedBox(
height: 8,
),
RoundedWhiteContainer(
child: Center(
child: Text(
"If the recovery phrase you are about to restore "
"was created with an optional BIP39 passphrase "
"you can enter it here.",
style: isDesktop
? STextStyles.desktopTextExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
)
: STextStyles.itemSubtitle(context),
),
),
),
const SizedBox(
height: 16,
),
],
),
),
),
if (!isDesktop)
const Spacer(
flex: 3,
@ -532,3 +317,394 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> {
);
}
}
class SeedRestoreOption extends ConsumerStatefulWidget {
const SeedRestoreOption({
super.key,
required this.coin,
required this.dateController,
required this.pwController,
required this.pwFocusNode,
required this.supportsMnemonicPassphrase,
required this.dateChooserFunction,
required this.chooseMnemonicLength,
required this.lelScanChanged,
});
final CryptoCurrency coin;
final TextEditingController dateController;
final TextEditingController pwController;
final FocusNode pwFocusNode;
final bool supportsMnemonicPassphrase;
final Future<void> Function() dateChooserFunction;
final Future<void> Function() chooseMnemonicLength;
final void Function(bool) lelScanChanged;
@override
ConsumerState<SeedRestoreOption> createState() => _SeedRestoreOptionState();
}
class _SeedRestoreOptionState extends ConsumerState<SeedRestoreOption> {
bool _hidePassword = true;
bool _expandedAdvanced = false;
bool _enableLelantusScanning = false;
@override
Widget build(BuildContext context) {
final lengths = widget.coin.possibleMnemonicLengths;
final isMoneroAnd25 = widget.coin is Monero &&
ref.watch(mnemonicWordCountStateProvider.state).state == 25;
final isWowneroAnd25 = widget.coin is Wownero &&
ref.watch(mnemonicWordCountStateProvider.state).state == 25;
return Column(
children: [
if (isMoneroAnd25 || widget.coin is Epiccash || isWowneroAnd25)
Text(
"Choose start date",
style: Util.isDesktop
? STextStyles.desktopTextExtraSmall(context).copyWith(
color:
Theme.of(context).extension<StackColors>()!.textDark3,
)
: STextStyles.smallMed12(context),
textAlign: TextAlign.left,
),
if (isMoneroAnd25 || widget.coin is Epiccash || isWowneroAnd25)
SizedBox(
height: Util.isDesktop ? 16 : 8,
),
if (isMoneroAnd25 || widget.coin is Epiccash || isWowneroAnd25)
RestoreFromDatePicker(
onTap: widget.dateChooserFunction,
controller: widget.dateController,
),
if (isMoneroAnd25 || widget.coin is Epiccash || isWowneroAnd25)
const SizedBox(
height: 8,
),
if (isMoneroAnd25 || widget.coin is Epiccash || isWowneroAnd25)
RoundedWhiteContainer(
child: Center(
child: Text(
"Choose the date you made the wallet (approximate is fine)",
style: Util.isDesktop
? STextStyles.desktopTextExtraSmall(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
)
: STextStyles.smallMed12(context).copyWith(
fontSize: 10,
),
),
),
),
if (isMoneroAnd25 || widget.coin is Epiccash || isWowneroAnd25)
SizedBox(
height: Util.isDesktop ? 24 : 16,
),
Text(
"Choose recovery phrase length",
style: Util.isDesktop
? STextStyles.desktopTextExtraSmall(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark3,
)
: STextStyles.smallMed12(context),
textAlign: TextAlign.left,
),
SizedBox(
height: Util.isDesktop ? 16 : 8,
),
if (Util.isDesktop)
DropdownButtonHideUnderline(
child: DropdownButton2<int>(
value: ref.watch(mnemonicWordCountStateProvider.state).state,
items: [
...lengths.map(
(e) => DropdownMenuItem(
value: e,
child: Text(
"$e words",
style: STextStyles.desktopTextMedium(context),
),
),
),
],
onChanged: (value) {
if (value is int) {
ref.read(mnemonicWordCountStateProvider.state).state = value;
}
},
isExpanded: true,
iconStyleData: IconStyleData(
icon: SvgPicture.asset(
Assets.svg.chevronDown,
width: 12,
height: 6,
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveSearchIconRight,
),
),
dropdownStyleData: DropdownStyleData(
offset: const Offset(0, -10),
elevation: 0,
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
),
menuItemStyleData: const MenuItemStyleData(
padding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
),
),
),
if (!Util.isDesktop)
MobileMnemonicLengthSelector(
chooseMnemonicLength: widget.chooseMnemonicLength,
),
if (widget.supportsMnemonicPassphrase)
SizedBox(
height: Util.isDesktop ? 24 : 16,
),
if (widget.supportsMnemonicPassphrase)
Expandable(
onExpandChanged: (state) {
setState(() {
_expandedAdvanced = state == ExpandableState.expanded;
});
},
header: Container(
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.only(
top: 8.0,
bottom: 8.0,
right: 10,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Advanced",
style: Util.isDesktop
? STextStyles.desktopTextExtraExtraSmall(
context,
).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
)
: STextStyles.smallMed12(context),
textAlign: TextAlign.left,
),
SvgPicture.asset(
_expandedAdvanced
? Assets.svg.chevronUp
: Assets.svg.chevronDown,
width: 12,
height: 6,
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveSearchIconRight,
),
],
),
),
),
body: Container(
color: Colors.transparent,
child: Column(
children: [
if (widget.coin is Firo)
CheckboxTextButton(
label: "Scan for Lelantus transactions",
onChanged: (newValue) {
setState(() {
_enableLelantusScanning = newValue ?? true;
});
widget.lelScanChanged(_enableLelantusScanning);
},
),
if (widget.coin is Firo)
const SizedBox(
height: 8,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("mnemonicPassphraseFieldKey1"),
focusNode: widget.pwFocusNode,
controller: widget.pwController,
style: Util.isDesktop
? STextStyles.desktopTextMedium(context).copyWith(
height: 2,
)
: STextStyles.field(context),
obscureText: _hidePassword,
enableSuggestions: false,
autocorrect: false,
decoration: standardInputDecoration(
"BIP39 passphrase",
widget.pwFocusNode,
context,
).copyWith(
suffixIcon: UnconstrainedBox(
child: ConditionalParent(
condition: Util.isDesktop,
builder: (child) => SizedBox(
height: 70,
child: child,
),
child: Row(
children: [
SizedBox(
width: Util.isDesktop ? 24 : 16,
),
GestureDetector(
key: const Key(
"mnemonicPassphraseFieldShowPasswordButtonKey",
),
onTap: () async {
setState(() {
_hidePassword = !_hidePassword;
});
},
child: SvgPicture.asset(
_hidePassword
? Assets.svg.eye
: Assets.svg.eyeSlash,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: Util.isDesktop ? 24 : 16,
height: Util.isDesktop ? 24 : 16,
),
),
const SizedBox(
width: 12,
),
],
),
),
),
),
),
),
const SizedBox(
height: 8,
),
RoundedWhiteContainer(
child: Center(
child: Text(
"If the recovery phrase you are about to restore "
"was created with an optional BIP39 passphrase "
"you can enter it here.",
style: Util.isDesktop
? STextStyles.desktopTextExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
)
: STextStyles.itemSubtitle(context),
),
),
),
const SizedBox(
height: 16,
),
],
),
),
),
],
);
}
}
class ViewOnlyRestoreOption extends StatefulWidget {
const ViewOnlyRestoreOption({
super.key,
required this.coin,
required this.dateController,
required this.dateChooserFunction,
});
final CryptoCurrency coin;
final TextEditingController dateController;
final Future<void> Function() dateChooserFunction;
@override
State<ViewOnlyRestoreOption> createState() => _ViewOnlyRestoreOptionState();
}
class _ViewOnlyRestoreOptionState extends State<ViewOnlyRestoreOption> {
@override
Widget build(BuildContext context) {
final showDateOption = widget.coin is ViewOnlyOptionCurrencyInterface;
return Column(
children: [
if (showDateOption)
Text(
"Choose start date",
style: Util.isDesktop
? STextStyles.desktopTextExtraSmall(context).copyWith(
color:
Theme.of(context).extension<StackColors>()!.textDark3,
)
: STextStyles.smallMed12(context),
textAlign: TextAlign.left,
),
if (showDateOption)
SizedBox(
height: Util.isDesktop ? 16 : 8,
),
if (showDateOption)
RestoreFromDatePicker(
onTap: widget.dateChooserFunction,
controller: widget.dateController,
),
if (showDateOption)
const SizedBox(
height: 8,
),
if (showDateOption)
RoundedWhiteContainer(
child: Center(
child: Text(
"Choose the date you made the wallet (approximate is fine)",
style: Util.isDesktop
? STextStyles.desktopTextExtraSmall(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
)
: STextStyles.smallMed12(context).copyWith(
fontSize: 10,
),
),
),
),
if (showDateOption)
SizedBox(
height: Util.isDesktop ? 24 : 16,
),
],
);
}
}

View file

@ -0,0 +1,393 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:cs_monero/src/deprecated/get_height_by_date.dart'
as cs_monero_deprecated;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import '../../../pages_desktop_specific/desktop_home_view.dart';
import '../../../pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
import '../../../providers/db/main_db_provider.dart';
import '../../../providers/global/secure_store_provider.dart';
import '../../../providers/providers.dart';
import '../../../themes/stack_colors.dart';
import '../../../utilities/barcode_scanner_interface.dart';
import '../../../utilities/clipboard_interface.dart';
import '../../../utilities/text_styles.dart';
import '../../../utilities/util.dart';
import '../../../wallets/crypto_currency/crypto_currency.dart';
import '../../../wallets/isar/models/wallet_info.dart';
import '../../../wallets/wallet/impl/epiccash_wallet.dart';
import '../../../wallets/wallet/impl/monero_wallet.dart';
import '../../../wallets/wallet/impl/wownero_wallet.dart';
import '../../../wallets/wallet/wallet.dart';
import '../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
import '../../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../../widgets/desktop/desktop_app_bar.dart';
import '../../../widgets/desktop/desktop_scaffold.dart';
import '../../../widgets/desktop/primary_button.dart';
import '../../../widgets/stack_text_field.dart';
import '../../home_view/home_view.dart';
import 'confirm_recovery_dialog.dart';
import 'sub_widgets/restore_failed_dialog.dart';
import 'sub_widgets/restore_succeeded_dialog.dart';
import 'sub_widgets/restoring_dialog.dart';
class RestoreViewOnlyWalletView extends ConsumerStatefulWidget {
const RestoreViewOnlyWalletView({
super.key,
required this.walletName,
required this.coin,
required this.restoreFromDate,
this.enableLelantusScanning = false,
this.barcodeScanner = const BarcodeScannerWrapper(),
this.clipboard = const ClipboardWrapper(),
});
static const routeName = "/restoreViewOnlyWallet";
final String walletName;
final CryptoCurrency coin;
final DateTime? restoreFromDate;
final bool enableLelantusScanning;
final BarcodeScannerInterface barcodeScanner;
final ClipboardInterface clipboard;
@override
ConsumerState<RestoreViewOnlyWalletView> createState() =>
_RestoreViewOnlyWalletViewState();
}
class _RestoreViewOnlyWalletViewState
extends ConsumerState<RestoreViewOnlyWalletView> {
late final TextEditingController addressController;
late final TextEditingController viewKeyController;
bool _enableRestoreButton = false;
bool _buttonLock = false;
Future<void> _requestRestore() async {
if (_buttonLock) return;
_buttonLock = true;
try {
if (!Util.isDesktop) {
// wait for keyboard to disappear
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(milliseconds: 100),
);
}
if (mounted) {
await showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return ConfirmRecoveryDialog(
onConfirm: _attemptRestore,
);
},
);
}
} finally {
_buttonLock = false;
}
}
Future<void> _attemptRestore() async {
int height = 0;
final Map<String, dynamic> otherDataJson = {
WalletInfoKeys.isViewOnlyKey: true,
};
if (widget.restoreFromDate != null) {
if (widget.coin is Monero) {
height = cs_monero_deprecated.getMoneroHeightByDate(
date: widget.restoreFromDate!,
);
}
if (widget.coin is Wownero) {
height = cs_monero_deprecated.getWowneroHeightByDate(
date: widget.restoreFromDate!,
);
}
if (height < 0) {
height = 0;
}
}
if (widget.coin is Firo) {
otherDataJson.addAll(
{
WalletInfoKeys.lelantusCoinIsarRescanRequired: false,
WalletInfoKeys.enableLelantusScanning: widget.enableLelantusScanning,
},
);
}
if (!Platform.isLinux && !Util.isDesktop) await WakelockPlus.enable();
try {
final info = WalletInfo.createNew(
coin: widget.coin,
name: widget.walletName,
restoreHeight: height,
otherDataJsonString: jsonEncode(otherDataJson),
);
bool isRestoring = true;
// show restoring in progress
if (mounted) {
unawaited(
showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: false,
builder: (context) {
return RestoringDialog(
onCancel: () async {
isRestoring = false;
await ref.read(pWallets).deleteWallet(
info,
ref.read(secureStoreProvider),
);
},
);
},
),
);
}
var node = ref
.read(nodeServiceChangeNotifierProvider)
.getPrimaryNodeFor(currency: widget.coin);
if (node == null) {
node = widget.coin.defaultNode;
await ref.read(nodeServiceChangeNotifierProvider).setPrimaryNodeFor(
coin: widget.coin,
node: node,
);
}
try {
final wallet = await Wallet.create(
walletInfo: info,
mainDB: ref.read(mainDBProvider),
secureStorageInterface: ref.read(secureStoreProvider),
nodeService: ref.read(nodeServiceChangeNotifierProvider),
prefs: ref.read(prefsChangeNotifierProvider),
viewOnlyData: ViewOnlyWalletData(
address: addressController.text,
privateViewKey: viewKeyController.text,
),
);
// TODO: extract interface with isRestore param
switch (wallet.runtimeType) {
case const (EpiccashWallet):
await (wallet as EpiccashWallet).init(isRestore: true);
break;
case const (MoneroWallet):
await (wallet as MoneroWallet).init(isRestore: true);
break;
case const (WowneroWallet):
await (wallet as WowneroWallet).init(isRestore: true);
break;
default:
await wallet.init();
}
await wallet.recover(isRescan: false);
// check if state is still active before continuing
if (mounted) {
// don't remove this setMnemonicVerified thing
await wallet.info.setMnemonicVerified(
isar: ref.read(mainDBProvider).isar,
);
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,
),
);
}
await showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return const RestoreSucceededDialog();
},
);
}
}
} catch (e) {
// check if state is still active and restore wasn't cancelled
// before continuing
if (mounted && isRestoring) {
// pop waiting dialog
Navigator.pop(context);
// show restoring wallet failed dialog
await showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return RestoreFailedDialog(
errorMessage: e.toString(),
walletId: info.walletId,
walletName: info.name,
);
},
);
}
}
} finally {
if (!Platform.isLinux && !Util.isDesktop) await WakelockPlus.disable();
}
}
@override
void initState() {
super.initState();
addressController = TextEditingController();
viewKeyController = TextEditingController();
}
@override
Widget build(BuildContext context) {
final isDesktop = Util.isDesktop;
return MasterScaffold(
isDesktop: isDesktop,
appBar: isDesktop
? const DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(),
trailing: ExitToMyStackButton(),
)
: AppBar(
leading: AppBarBackButton(
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(milliseconds: 50),
);
}
if (context.mounted) {
Navigator.of(context).pop();
}
},
),
),
body: Container(
color: Theme.of(context).extension<StackColors>()!.background,
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
maxWidth: isDesktop ? 480 : double.infinity,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
if (isDesktop)
const Spacer(
flex: 10,
),
if (!isDesktop)
Text(
widget.walletName,
style: STextStyles.itemSubtitle(context),
),
SizedBox(
height: isDesktop ? 0 : 4,
),
Text(
"Enter view only details",
style: isDesktop
? STextStyles.desktopH2(context)
: STextStyles.pageTitleH1(context),
),
SizedBox(
height: isDesktop ? 24 : 16,
),
FullTextField(
label: "Address",
controller: addressController,
onChanged: (newValue) {
setState(() {
_enableRestoreButton = newValue.isNotEmpty &&
viewKeyController.text.isNotEmpty;
});
},
),
SizedBox(
height: isDesktop ? 16 : 12,
),
FullTextField(
label: "View Key",
controller: viewKeyController,
onChanged: (value) {
setState(() {
_enableRestoreButton = value.isNotEmpty &&
addressController.text.isNotEmpty;
});
},
),
if (!isDesktop) const Spacer(),
SizedBox(
height: isDesktop ? 24 : 16,
),
PrimaryButton(
enabled: _enableRestoreButton,
onPressed: _requestRestore,
width: isDesktop ? 480 : null,
label: "Restore",
),
if (isDesktop)
const Spacer(
flex: 15,
),
],
),
),
),
),
);
},
),
),
);
}
}

View file

@ -655,16 +655,18 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
const Duration(milliseconds: 100),
);
await showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return ConfirmRecoveryDialog(
onConfirm: attemptRestore,
);
},
);
if (mounted) {
await showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return ConfirmRecoveryDialog(
onConfirm: attemptRestore,
);
},
);
}
}
@override

View file

@ -39,6 +39,7 @@ import '../../../wallets/wallet/impl/epiccash_wallet.dart';
import '../../../wallets/wallet/intermediate/lib_monero_wallet.dart';
import '../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
import '../../../wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart';
import '../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
import '../../../widgets/background.dart';
import '../../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../../widgets/desktop/secondary_button.dart';
@ -273,6 +274,7 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
String keys
})? prevGen,
})? frostWalletData;
ViewOnlyWalletData? voData;
if (wallet is BitcoinFrostWallet) {
final futures = [
wallet.getSerializedKeys(),
@ -298,10 +300,17 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
),
);
}
} else if (wallet
is MnemonicInterface) {
mnemonic =
await wallet.getMnemonicAsWords();
} else {
if (wallet
is ViewOnlyOptionInterface &&
wallet.isViewOnly) {
voData = await wallet
.getViewOnlyWalletData();
} else if (wallet
is MnemonicInterface) {
mnemonic = await wallet
.getMnemonicAsWords();
}
}
KeyDataInterface? keyData;
@ -312,36 +321,68 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
}
if (context.mounted) {
await Navigator.push(
context,
RouteGenerator.getRoute(
shouldUseMaterialRoute:
RouteGenerator
.useMaterialPageRoute,
builder: (_) => LockscreenView(
routeOnSuccessArguments: (
walletId: walletId,
mnemonic: mnemonic ?? [],
frostWalletData:
frostWalletData,
keyData: keyData,
if (voData != null) {
await Navigator.push(
context,
RouteGenerator.getRoute(
shouldUseMaterialRoute:
RouteGenerator
.useMaterialPageRoute,
builder: (_) => LockscreenView(
routeOnSuccessArguments: (
walletId: walletId,
keyData: keyData,
),
showBackButton: true,
routeOnSuccess:
MobileKeyDataView
.routeName,
biometricsCancelButtonString:
"CANCEL",
biometricsLocalizedReason:
"Authenticate to view recovery data",
biometricsAuthenticationTitle:
"View recovery data",
),
settings: const RouteSettings(
name:
"/viewRecoveryDataLockscreen",
),
showBackButton: true,
routeOnSuccess:
WalletBackupView.routeName,
biometricsCancelButtonString:
"CANCEL",
biometricsLocalizedReason:
"Authenticate to view recovery phrase",
biometricsAuthenticationTitle:
"View recovery phrase",
),
settings: const RouteSettings(
name:
"/viewRecoverPhraseLockscreen",
);
} else {
await Navigator.push(
context,
RouteGenerator.getRoute(
shouldUseMaterialRoute:
RouteGenerator
.useMaterialPageRoute,
builder: (_) => LockscreenView(
routeOnSuccessArguments: (
walletId: walletId,
mnemonic: mnemonic ?? [],
frostWalletData:
frostWalletData,
keyData: keyData,
),
showBackButton: true,
routeOnSuccess:
WalletBackupView
.routeName,
biometricsCancelButtonString:
"CANCEL",
biometricsLocalizedReason:
"Authenticate to view recovery phrase",
biometricsAuthenticationTitle:
"View recovery phrase",
),
settings: const RouteSettings(
name:
"/viewRecoverPhraseLockscreen",
),
),
),
);
);
}
}
},
);

View file

@ -0,0 +1,204 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../app_config.dart';
import '../../../../pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart';
import '../../../../providers/global/secure_store_provider.dart';
import '../../../../providers/global/wallets_provider.dart';
import '../../../../route_generator.dart';
import '../../../../themes/stack_colors.dart';
import '../../../../utilities/text_styles.dart';
import '../../../../utilities/util.dart';
import '../../../../wallets/isar/providers/wallet_info_provider.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
import '../../../../widgets/conditional_parent.dart';
import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../../../widgets/custom_buttons/simple_copy_button.dart';
import '../../../../widgets/desktop/primary_button.dart';
import '../../../../widgets/detail_item.dart';
import '../../../../widgets/rounded_white_container.dart';
import '../../../../widgets/stack_dialog.dart';
import '../../../home_view/home_view.dart';
import '../../../wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart';
class DeleteViewOnlyWalletKeysView extends ConsumerStatefulWidget {
const DeleteViewOnlyWalletKeysView({
super.key,
required this.walletId,
required this.data,
});
static const routeName = "/deleteWalletViewOnlyData";
final String walletId;
final ViewOnlyWalletData data;
@override
ConsumerState<DeleteViewOnlyWalletKeysView> createState() =>
_DeleteViewOnlyWalletKeysViewState();
}
class _DeleteViewOnlyWalletKeysViewState
extends ConsumerState<DeleteViewOnlyWalletKeysView> {
bool _lock = false;
void _continuePressed() async {
if (_lock) {
return;
}
_lock = true;
try {
if (Util.isDesktop) {
await Navigator.of(context).push(
RouteGenerator.getRoute(
builder: (context) {
return ConfirmDelete(
walletId: widget.walletId,
);
},
settings: const RouteSettings(
name: "/desktopConfirmDelete",
),
),
);
} else {
await showDialog<dynamic>(
barrierDismissible: true,
context: context,
builder: (_) => StackDialog(
title: "Thanks! Your wallet will be deleted.",
leftButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonStyle(context),
onPressed: () {
Navigator.pop(context);
},
child: Text(
"Cancel",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
),
),
rightButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonStyle(context),
onPressed: () async {
await ref.read(pWallets).deleteWallet(
ref.read(pWalletInfo(widget.walletId)),
ref.read(secureStoreProvider),
);
if (mounted) {
Navigator.of(context).popUntil(
ModalRoute.withName(HomeView.routeName),
);
}
},
child: Text(
"Ok",
style: STextStyles.button(context),
),
),
),
);
}
} finally {
_lock = false;
}
}
@override
Widget build(BuildContext context) {
return ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, cons) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: cons.maxHeight),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
);
},
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
RoundedWhiteContainer(
child: Text(
"Please write down your backup data. Keep it safe and "
"never share it with anyone. "
"Your backup data is the only way you can access your "
"wallet if you forget your PIN, lose your phone, etc."
"\n\n"
"${AppConfig.appName} does not keep nor is able to restore "
"your backup data. "
"Only you have access to your wallet.",
style: STextStyles.label(context),
),
),
const SizedBox(
height: 24,
),
if (widget.data.address != null)
DetailItem(
title: "Address",
detail: widget.data.address!,
button: Util.isDesktop
? IconCopyButton(
data: widget.data.address!,
)
: SimpleCopyButton(
data: widget.data.address!,
),
),
if (widget.data.address != null)
const SizedBox(
height: 16,
),
if (widget.data.privateViewKey != null)
DetailItem(
title: "Private view key",
detail: widget.data.privateViewKey!,
button: Util.isDesktop
? IconCopyButton(
data: widget.data.privateViewKey!,
)
: SimpleCopyButton(
data: widget.data.privateViewKey!,
),
),
if (!Util.isDesktop) const Spacer(),
const SizedBox(
height: 16,
),
PrimaryButton(
label: "Continue",
onPressed: _continuePressed,
),
],
),
);
}
}

View file

@ -14,11 +14,9 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import '../../../../app_config.dart';
import '../../../../notifications/show_flush_bar.dart';
import '../../../add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart';
import '../../../home_view/home_view.dart';
import '../../../wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart';
import '../../../../providers/global/secure_store_provider.dart';
import '../../../../providers/global/wallets_provider.dart';
import '../../../../themes/stack_colors.dart';
@ -35,6 +33,9 @@ import '../../../../widgets/desktop/primary_button.dart';
import '../../../../widgets/detail_item.dart';
import '../../../../widgets/rounded_white_container.dart';
import '../../../../widgets/stack_dialog.dart';
import '../../../add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart';
import '../../../home_view/home_view.dart';
import '../../../wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart';
class DeleteWalletRecoveryPhraseView extends ConsumerStatefulWidget {
const DeleteWalletRecoveryPhraseView({
@ -69,7 +70,6 @@ class _DeleteWalletRecoveryPhraseViewState
late ClipboardInterface _clipboardInterface;
bool _lock = false;
void _continuePressed() {
if (_lock) {
return;

View file

@ -17,9 +17,11 @@ import '../../../../themes/stack_colors.dart';
import '../../../../utilities/text_styles.dart';
import '../../../../wallets/wallet/impl/bitcoin_frost_wallet.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
import '../../../../widgets/background.dart';
import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../../../widgets/rounded_container.dart';
import 'delete_view_only_wallet_keys_view.dart';
import 'delete_wallet_recovery_phrase_view.dart';
class DeleteWalletWarningView extends ConsumerWidget {
@ -118,6 +120,7 @@ class DeleteWalletWarningView extends ConsumerWidget {
String keys,
({String config, String keys})? prevGen,
})? frostWalletData;
ViewOnlyWalletData? viewOnlyData;
if (wallet is BitcoinFrostWallet) {
final futures = [
@ -142,18 +145,33 @@ class DeleteWalletWarningView extends ConsumerWidget {
),
);
}
} else if (wallet is MnemonicInterface) {
mnemonic = await wallet.getMnemonicAsWords();
} else {
if (wallet is ViewOnlyOptionInterface &&
wallet.isViewOnly) {
viewOnlyData = await wallet.getViewOnlyWalletData();
} else if (wallet is MnemonicInterface) {
mnemonic = await wallet.getMnemonicAsWords();
}
}
if (context.mounted) {
await Navigator.of(context).pushNamed(
DeleteWalletRecoveryPhraseView.routeName,
arguments: (
walletId: walletId,
mnemonicWords: mnemonic ?? [],
frostWalletData: frostWalletData,
),
);
if (viewOnlyData != null) {
await Navigator.of(context).pushNamed(
DeleteViewOnlyWalletKeysView.routeName,
arguments: (
walletId: walletId,
data: viewOnlyData,
),
);
} else {
await Navigator.of(context).pushNamed(
DeleteWalletRecoveryPhraseView.routeName,
arguments: (
walletId: walletId,
mnemonicWords: mnemonic ?? [],
frostWalletData: frostWalletData,
),
);
}
}
},
child: Text(

View file

@ -56,6 +56,7 @@ import '../../wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart
import '../../wallets/wallet/wallet_mixin_interfaces/ordinals_interface.dart';
import '../../wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart';
import '../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart';
import '../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
import '../../widgets/background.dart';
import '../../widgets/conditional_parent.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
@ -524,6 +525,10 @@ class _WalletViewState extends ConsumerState<WalletView> {
final prefs = ref.watch(prefsChangeNotifierProvider);
final showExchange = prefs.enableExchange;
final wallet = ref.watch(pWallets).getWallet(walletId);
final viewOnly = wallet is ViewOnlyOptionInterface && wallet.isViewOnly;
return ConditionalParent(
condition: _rescanningOnOpen,
builder: (child) {
@ -1026,38 +1031,39 @@ class _WalletViewState extends ConsumerState<WalletView> {
icon: const FrostSignNavIcon(),
onTap: () => _onFrostSignPressed(context),
),
WalletNavigationBarItemData(
label: "Send",
icon: const SendNavIcon(),
onTap: () {
// not sure what this is supposed to accomplish?
// switch (ref
// .read(walletBalanceToggleStateProvider.state)
// .state) {
// case WalletBalanceToggleState.full:
// ref
// .read(publicPrivateBalanceStateProvider.state)
// .state = "Public";
// break;
// case WalletBalanceToggleState.available:
// ref
// .read(publicPrivateBalanceStateProvider.state)
// .state = "Private";
// break;
// }
Navigator.of(context).pushNamed(
ref.read(pWallets).getWallet(walletId)
is BitcoinFrostWallet
? FrostSendView.routeName
: SendView.routeName,
arguments: (
walletId: walletId,
coin: coin,
),
);
},
),
if (Constants.enableExchange &&
if (!viewOnly)
WalletNavigationBarItemData(
label: "Send",
icon: const SendNavIcon(),
onTap: () {
// not sure what this is supposed to accomplish?
// switch (ref
// .read(walletBalanceToggleStateProvider.state)
// .state) {
// case WalletBalanceToggleState.full:
// ref
// .read(publicPrivateBalanceStateProvider.state)
// .state = "Public";
// break;
// case WalletBalanceToggleState.available:
// ref
// .read(publicPrivateBalanceStateProvider.state)
// .state = "Private";
// break;
// }
Navigator.of(context).pushNamed(
wallet is BitcoinFrostWallet
? FrostSendView.routeName
: SendView.routeName,
arguments: (
walletId: walletId,
coin: coin,
),
);
},
),
if (!viewOnly &&
Constants.enableExchange &&
ref.watch(pWalletCoin(walletId)) is! FrostCurrency &&
AppConfig.hasFeature(AppFeature.swap) &&
showExchange)
@ -1113,12 +1119,7 @@ class _WalletViewState extends ConsumerState<WalletView> {
);
},
),
if (ref.watch(
pWallets.select(
(value) => value.getWallet(widget.walletId)
is CoinControlInterface,
),
) &&
if (wallet is CoinControlInterface &&
ref.watch(
prefsChangeNotifierProvider.select(
(value) => value.enableCoinControl,
@ -1137,12 +1138,7 @@ class _WalletViewState extends ConsumerState<WalletView> {
);
},
),
if (ref.watch(
pWallets.select(
(value) =>
value.getWallet(widget.walletId) is PaynymInterface,
),
))
if (wallet is PaynymInterface)
WalletNavigationBarItemData(
label: "PayNym",
icon: const PaynymNavIcon(),
@ -1213,12 +1209,7 @@ class _WalletViewState extends ConsumerState<WalletView> {
);
},
),
if (ref.watch(
pWallets.select(
(value) => value.getWallet(widget.walletId)
is CashFusionInterface,
),
))
if (wallet is CashFusionInterface && !viewOnly)
WalletNavigationBarItemData(
label: "Fusion",
icon: const FusionNavIcon(),
@ -1229,12 +1220,7 @@ class _WalletViewState extends ConsumerState<WalletView> {
);
},
),
if (ref.watch(
pWallets.select(
(value) =>
value.getWallet(widget.walletId) is LibMoneroWallet,
),
))
if (wallet is LibMoneroWallet && !viewOnly)
WalletNavigationBarItemData(
label: "Churn",
icon: const ChurnNavIcon(),

View file

@ -10,18 +10,21 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:tuple/tuple.dart';
import '../../../../app_config.dart';
import 'delete_wallet_keys_popup.dart';
import '../../../../pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_view_only_wallet_keys_view.dart';
import '../../../../providers/global/wallets_provider.dart';
import '../../../../themes/stack_colors.dart';
import '../../../../utilities/text_styles.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
import '../../../../widgets/desktop/desktop_dialog.dart';
import '../../../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../../../widgets/desktop/primary_button.dart';
import '../../../../widgets/desktop/secondary_button.dart';
import '../../../../widgets/rounded_container.dart';
import 'package:tuple/tuple.dart';
import 'delete_wallet_keys_popup.dart';
class DesktopAttentionDeleteWallet extends ConsumerStatefulWidget {
const DesktopAttentionDeleteWallet({
@ -114,6 +117,58 @@ class _DesktopAttentionDeleteWallet
onPressed: () async {
final wallet =
ref.read(pWallets).getWallet(widget.walletId);
if (wallet is ViewOnlyOptionInterface &&
wallet.isViewOnly) {
final data = await wallet.getViewOnlyWalletData();
if (context.mounted) {
await Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (builder) => DesktopDialog(
maxWidth: 614,
maxHeight: double.infinity,
child: Column(
children: [
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(
left: 32,
),
child: Text(
"Wallet keys",
style: STextStyles.desktopH3(
context,
),
),
),
DesktopDialogCloseButton(
onPressedOverride: () {
Navigator.of(
context,
rootNavigator: true,
).pop();
},
),
],
),
Padding(
padding: const EdgeInsets.all(32),
child: DeleteViewOnlyWalletKeysView(
walletId: widget.walletId,
data: data,
),
),
],
),
),
),
);
}
} else
// TODO: [prio=med] handle other types wallet deletion
// All wallets currently are mnemonic based
if (wallet is MnemonicInterface) {

View file

@ -39,6 +39,7 @@ import '../../../../wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface
import '../../../../wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/ordinals_interface.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
import '../../../../widgets/custom_loading_overlay.dart';
import '../../../../widgets/desktop/desktop_dialog.dart';
import '../../../../widgets/desktop/primary_button.dart';
@ -380,9 +381,12 @@ class _DesktopWalletFeaturesState extends ConsumerState<DesktopWalletFeatures> {
wallet is OrdinalsInterface ||
wallet is CashFusionInterface;
final isViewOnly = wallet is ViewOnlyOptionInterface && wallet.isViewOnly;
return Row(
children: [
if (Constants.enableExchange &&
if (!isViewOnly &&
Constants.enableExchange &&
AppConfig.hasFeature(AppFeature.swap) &&
showExchange)
SecondaryButton(

View file

@ -31,6 +31,7 @@ import '../../../../../wallets/wallet/wallet_mixin_interfaces/ordinals_interface
import '../../../../../wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart';
import '../../../../../wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart';
import '../../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart';
import '../../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
import '../../../../../widgets/custom_buttons/draggable_switch_button.dart';
import '../../../../../widgets/desktop/desktop_dialog.dart';
import '../../../../../widgets/desktop/desktop_dialog_close_button.dart';
@ -238,6 +239,8 @@ class _MoreFeaturesDialogState extends ConsumerState<MoreFeaturesDialog> {
),
);
final isViewOnly = wallet is ViewOnlyOptionInterface && wallet.isViewOnly;
return DesktopDialog(
child: Column(
mainAxisSize: MainAxisSize.min,
@ -257,7 +260,7 @@ class _MoreFeaturesDialogState extends ConsumerState<MoreFeaturesDialog> {
const DesktopDialogCloseButton(),
],
),
if (wallet.info.coin is Firo)
if (!isViewOnly && wallet.info.coin is Firo)
_MoreFeaturesItem(
label: "Anonymize funds",
detail: "Anonymize funds",
@ -300,14 +303,14 @@ class _MoreFeaturesDialogState extends ConsumerState<MoreFeaturesDialog> {
iconAsset: Assets.svg.monkey,
onPressed: () async => widget.onMonkeyPressed?.call(),
),
if (wallet is CashFusionInterface)
if (!isViewOnly && wallet is CashFusionInterface)
_MoreFeaturesItem(
label: "Fusion",
detail: "Decentralized mixing protocol",
iconAsset: Assets.svg.cashFusion,
onPressed: () async => widget.onFusionPressed?.call(),
),
if (wallet is LibMoneroWallet)
if (!isViewOnly && wallet is LibMoneroWallet)
_MoreFeaturesItem(
label: "Churn",
detail: "Churning",

View file

@ -10,20 +10,22 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../frost_route_generator.dart';
import '../../../../pages/send_view/frost_ms/frost_send_view.dart';
import '../../../../pages/wallet_view/transaction_views/tx_v2/transaction_v2_list.dart';
import '../../my_stack_view.dart';
import 'desktop_receive.dart';
import 'desktop_send.dart';
import 'desktop_token_send.dart';
import '../../../../providers/global/wallets_provider.dart';
import '../../../../wallets/crypto_currency/crypto_currency.dart';
import '../../../../wallets/wallet/impl/bitcoin_frost_wallet.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
import '../../../../widgets/custom_tab_view.dart';
import '../../../../widgets/desktop/secondary_button.dart';
import '../../../../widgets/frost_scaffold.dart';
import '../../../../widgets/rounded_white_container.dart';
import '../../my_stack_view.dart';
import 'desktop_receive.dart';
import 'desktop_send.dart';
import 'desktop_token_send.dart';
class MyWallet extends ConsumerStatefulWidget {
const MyWallet({
@ -48,6 +50,7 @@ class _MyWalletState extends ConsumerState<MyWallet> {
late final bool isEth;
late final CryptoCurrency coin;
late final bool isFrost;
late final bool isViewOnly;
@override
void initState() {
@ -60,11 +63,34 @@ class _MyWalletState extends ConsumerState<MyWallet> {
titles.add("Transactions");
}
isViewOnly = wallet is ViewOnlyOptionInterface && wallet.isViewOnly;
if (isViewOnly) {
titles.remove("Receive");
}
super.initState();
}
@override
Widget build(BuildContext context) {
if (isViewOnly) {
return ListView(
primary: false,
children: [
RoundedWhiteContainer(
padding: EdgeInsets.zero,
child: Padding(
padding: const EdgeInsets.all(20),
child: DesktopReceive(
walletId: widget.walletId,
contractAddress: widget.contractAddress,
),
),
),
],
);
}
return ListView(
primary: false,
children: [

View file

@ -26,6 +26,7 @@ import '../../../../wallets/wallet/impl/bitcoin_frost_wallet.dart';
import '../../../../wallets/wallet/intermediate/lib_monero_wallet.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
import '../../../../widgets/desktop/desktop_dialog.dart';
import '../../../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../../../widgets/desktop/primary_button.dart';
@ -100,7 +101,11 @@ class _UnlockWalletKeysDesktopState
throw Exception("FIXME ~= see todo in code");
}
} else {
words = await wallet.getMnemonicAsWords();
if (wallet is ViewOnlyOptionInterface) {
// TODO: is something needed here?
} else {
words = await wallet.getMnemonicAsWords();
}
}
KeyDataInterface? keyData;
@ -347,7 +352,11 @@ class _UnlockWalletKeysDesktopState
throw Exception("FIXME ~= see todo in code");
}
} else {
words = await wallet.getMnemonicAsWords();
if (wallet is ViewOnlyOptionInterface) {
// TODO: is something needed here?
} else {
words = await wallet.getMnemonicAsWords();
}
}
KeyDataInterface? keyData;

View file

@ -182,17 +182,18 @@ class WalletKeysDesktopPopup extends ConsumerWidget {
: keyData != null
? CustomTabView(
titles: [
"Mnemonic",
if (words.isNotEmpty) "Mnemonic",
if (keyData is XPrivData) "XPriv(s)",
if (keyData is CWKeyData) "Keys",
],
children: [
Padding(
padding: const EdgeInsets.only(top: 16),
child: _Mnemonic(
words: words,
if (words.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 16),
child: _Mnemonic(
words: words,
),
),
),
if (keyData is XPrivData)
WalletXPrivs(
xprivData: keyData as XPrivData,

View file

@ -37,6 +37,7 @@ import 'pages/add_wallet_views/new_wallet_options/new_wallet_options_view.dart';
import 'pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart';
import 'pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart';
import 'pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart';
import 'pages/add_wallet_views/restore_wallet_view/restore_view_only_wallet_view.dart';
import 'pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart';
import 'pages/add_wallet_views/select_wallet_for_token_view.dart';
import 'pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart';
@ -130,6 +131,7 @@ import 'pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_bac
import 'pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart';
import 'pages/settings_views/wallet_settings_view/wallet_settings_view.dart';
import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart';
import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_view_only_wallet_keys_view.dart';
import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_recovery_phrase_view.dart';
import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart';
import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/lelantus_settings_view.dart';
@ -203,6 +205,7 @@ import 'wallets/crypto_currency/intermediate/frost_currency.dart';
import 'wallets/models/tx_data.dart';
import 'wallets/wallet/wallet.dart';
import 'wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
import 'wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
import 'widgets/choose_coin_view.dart';
import 'widgets/frost_scaffold.dart';
@ -1519,6 +1522,28 @@ class RouteGenerator {
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case RestoreViewOnlyWalletView.routeName:
if (args is ({
String walletName,
CryptoCurrency coin,
DateTime? restoreFromDate,
bool enableLelantusScanning,
})) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => RestoreViewOnlyWalletView(
walletName: args.walletName,
coin: args.coin,
restoreFromDate: args.restoreFromDate,
enableLelantusScanning: args.enableLelantusScanning,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case NewWalletRecoveryPhraseView.routeName:
if (args is Tuple2<Wallet, List<String>>) {
return getRoute(
@ -1927,6 +1952,21 @@ class RouteGenerator {
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case DeleteViewOnlyWalletKeysView.routeName:
if (args is ({String walletId, ViewOnlyWalletData data})) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => DeleteViewOnlyWalletKeysView(
data: args.data,
walletId: args.walletId,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
// exchange steps
case Step1View.routeName:

View file

@ -9,10 +9,15 @@
*/
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../themes/stack_colors.dart';
import '../utilities/constants.dart';
import '../utilities/text_styles.dart';
import '../utilities/util.dart';
import 'icon_widgets/clipboard_icon.dart';
import 'icon_widgets/x_icon.dart';
import 'textfield_icon_button.dart';
InputDecoration standardInputDecoration(
String? labelText,
@ -52,3 +57,113 @@ InputDecoration standardInputDecoration(
focusedErrorBorder: InputBorder.none,
);
}
class FullTextField extends StatefulWidget {
const FullTextField({
super.key,
this.controller,
this.focusNode,
required this.label,
this.onChanged,
});
final String label;
final TextEditingController? controller;
final FocusNode? focusNode;
final void Function(String)? onChanged;
@override
State<FullTextField> createState() => _FullTextFieldState();
}
class _FullTextFieldState extends State<FullTextField> {
late final TextEditingController controller;
late final FocusNode focusNode;
bool _hasValue = false;
@override
void initState() {
super.initState();
controller = widget.controller ?? TextEditingController();
focusNode = widget.focusNode ?? FocusNode();
}
@override
void dispose() {
if (widget.controller == null) {
controller.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
controller: controller,
autocorrect: false,
enableSuggestions: false,
onChanged: (newValue) {
widget.onChanged?.call(newValue);
},
focusNode: focusNode,
style: STextStyles.field(context),
decoration: standardInputDecoration(
widget.label,
focusNode,
context,
).copyWith(
contentPadding: const EdgeInsets.only(
left: 16,
top: 6,
bottom: 8,
right: 5,
),
suffixIcon: Padding(
padding: controller.text.isEmpty
? const EdgeInsets.only(right: 8)
: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
TextFieldIconButton(
onTap: () async {
if (_hasValue) {
controller.text = "";
setState(() {
_hasValue = false;
});
} else {
final data =
await Clipboard.getData(Clipboard.kTextPlain);
if (data?.text != null && data!.text!.isNotEmpty) {
final content = data.text!.trim();
controller.text = content;
setState(() {
_hasValue = content.isNotEmpty;
});
}
}
widget.onChanged?.call(controller.text);
},
child: _hasValue ? const XIcon() : const ClipboardIcon(),
),
],
),
),
),
),
),
);
}
}