CW-894-Improve-UX-for-restore-from-seeds-keys-page (#1986)

* Update wallet_restore_page.dart

* Fix conflicts with main [skip ci]

---------

Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>
This commit is contained in:
Serhii
2025-02-17 23:38:08 +02:00
committed by GitHub
co-authored by OmarHatem
parent 9ae533cfbd
commit 7c7ea03946
+304 -223
View File
@@ -20,10 +20,20 @@ import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:keyboard_actions/keyboard_actions.dart';
import 'package:mobx/mobx.dart';
import 'package:smooth_page_indicator/smooth_page_indicator.dart';
class WalletRestorePage extends BasePage {
WalletRestorePage(this.walletRestoreViewModel, this.seedSettingsViewModel);
WalletRestorePage(this.walletRestoreViewModel, this.seedSettingsViewModel)
: walletRestoreFromSeedFormKey = GlobalKey<WalletRestoreFromSeedFormState>(),
walletRestoreFromKeysFormKey = GlobalKey<WalletRestoreFromKeysFromState>(),
_blockHeightFocusNode = FocusNode();
final WalletRestoreViewModel walletRestoreViewModel;
final SeedSettingsViewModel seedSettingsViewModel;
final GlobalKey<WalletRestoreFromSeedFormState> walletRestoreFromSeedFormKey;
final GlobalKey<WalletRestoreFromKeysFromState> walletRestoreFromKeysFormKey;
final FocusNode _blockHeightFocusNode;
bool _formProcessing = false;
@override
Widget middle(BuildContext context) => Observer(
@@ -38,8 +48,9 @@ class WalletRestorePage extends BasePage {
color: titleColor(context)),
));
final WalletRestoreViewModel walletRestoreViewModel;
final SeedSettingsViewModel seedSettingsViewModel;
// DerivationType derivationType = DerivationType.unknown;
// String? derivationPath = null;
DerivationInfo? derivationInfo;
@override
Function(BuildContext)? get pushToNextWidget => (context) {
@@ -51,137 +62,6 @@ class WalletRestorePage extends BasePage {
@override
Widget body(BuildContext context) {
return WalletRestorePageBody(walletRestoreViewModel, seedSettingsViewModel);
}
}
class WalletRestorePageBody extends StatefulWidget {
WalletRestorePageBody(this.walletRestoreViewModel, this.seedSettingsViewModel);
final WalletRestoreViewModel walletRestoreViewModel;
final SeedSettingsViewModel seedSettingsViewModel;
@override
State<WalletRestorePageBody> createState() =>
_WalletRestorePageBodyState(walletRestoreViewModel, seedSettingsViewModel);
}
class _WalletRestorePageBodyState extends State<WalletRestorePageBody> {
_WalletRestorePageBodyState(this.walletRestoreViewModel, this.seedSettingsViewModel)
: walletRestoreFromSeedFormKey = GlobalKey<WalletRestoreFromSeedFormState>(),
walletRestoreFromKeysFormKey = GlobalKey<WalletRestoreFromKeysFromState>(),
_pages = [],
_blockHeightFocusNode = FocusNode(),
_controller = PageController(initialPage: 0) {
walletRestoreViewModel.availableModes.forEach((mode) {
switch (mode) {
case WalletRestoreMode.seed:
_pages.add(WalletRestoreFromSeedForm(
seedSettingsViewModel: seedSettingsViewModel,
displayBlockHeightSelector:
walletRestoreViewModel.hasBlockchainHeightLanguageSelector,
displayLanguageSelector: walletRestoreViewModel.hasSeedLanguageSelector,
type: walletRestoreViewModel.type,
key: walletRestoreFromSeedFormKey,
blockHeightFocusNode: _blockHeightFocusNode,
onHeightOrDateEntered: (value) {
if (_isValidSeed()) {
walletRestoreViewModel.isButtonEnabled = value;
}
},
onSeedChange: (String seed) {
final isPolyseed = walletRestoreViewModel.isPolyseed(seed);
_validateOnChange(isPolyseed: isPolyseed);
},
onLanguageChange: (String language) {
final isPolyseed = language.startsWith("POLYSEED_");
_validateOnChange(isPolyseed: isPolyseed);
},
displayWalletPassword: walletRestoreViewModel.hasWalletPassword,
onPasswordChange: (String password) =>
walletRestoreViewModel.walletPassword = password,
onRepeatedPasswordChange: (String repeatedPassword) =>
walletRestoreViewModel.repeatedWalletPassword = repeatedPassword));
break;
case WalletRestoreMode.keys:
_pages.add(WalletRestoreFromKeysFrom(
key: walletRestoreFromKeysFormKey,
walletRestoreViewModel: walletRestoreViewModel,
onPrivateKeyChange: (String seed) {
if (walletRestoreViewModel.type == WalletType.nano ||
walletRestoreViewModel.type == WalletType.banano) {
walletRestoreViewModel.isButtonEnabled = _isValidSeedKey();
}
},
displayPrivateKeyField: walletRestoreViewModel.hasRestoreFromPrivateKey,
displayWalletPassword: walletRestoreViewModel.hasWalletPassword,
onPasswordChange: (String password) =>
walletRestoreViewModel.walletPassword = password,
onRepeatedPasswordChange: (String repeatedPassword) =>
walletRestoreViewModel.repeatedWalletPassword = repeatedPassword,
onHeightOrDateEntered: (value) => walletRestoreViewModel.isButtonEnabled = value));
break;
default:
break;
}
});
}
final WalletRestoreViewModel walletRestoreViewModel;
final SeedSettingsViewModel seedSettingsViewModel;
final PageController _controller;
final List<Widget> _pages;
final GlobalKey<WalletRestoreFromSeedFormState> walletRestoreFromSeedFormKey;
final GlobalKey<WalletRestoreFromKeysFromState> walletRestoreFromKeysFormKey;
final FocusNode _blockHeightFocusNode;
bool _formProcessing = false;
// DerivationType derivationType = DerivationType.unknown;
// String? derivationPath = null;
DerivationInfo? derivationInfo;
@override
void initState() {
super.initState();
reaction((_) => walletRestoreViewModel.state, (ExecutionState state) {
if (state is FailureState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showPopUp<void>(
context: context,
builder: (_) {
return AlertWithOneAction(
alertTitle: S.current.new_wallet,
alertContent: state.error,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop());
});
});
}
});
reaction((_) => walletRestoreViewModel.mode, (WalletRestoreMode mode) {
walletRestoreViewModel.isButtonEnabled = false;
walletRestoreViewModel.walletPassword = null;
walletRestoreViewModel.repeatedWalletPassword = null;
walletRestoreFromSeedFormKey
.currentState!.blockchainHeightKey.currentState!.restoreHeightController.text = '';
walletRestoreFromSeedFormKey
.currentState!.blockchainHeightKey.currentState!.dateController.text = '';
walletRestoreFromSeedFormKey.currentState!.nameTextEditingController.text = '';
walletRestoreFromKeysFormKey
.currentState!.blockchainHeightKey.currentState!.restoreHeightController.text = '';
walletRestoreFromKeysFormKey
.currentState!.blockchainHeightKey.currentState!.dateController.text = '';
walletRestoreFromKeysFormKey.currentState!.nameTextEditingController.text = '';
});
}
@override
Widget build(BuildContext context) {
return KeyboardActions(
config: KeyboardActionsConfig(
keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
@@ -202,41 +82,18 @@ class _WalletRestorePageBodyState extends State<WalletRestorePageBody> {
constraints:
BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: PageView.builder(
onPageChanged: (page) {
walletRestoreViewModel.mode =
page == 0 ? WalletRestoreMode.seed : WalletRestoreMode.keys;
},
controller: _controller,
itemCount: _pages.length,
itemBuilder: (_, index) => SingleChildScrollView(child: _pages[index]),
child: _WalletRestorePageBody(
walletRestoreViewModel: walletRestoreViewModel,
seedSettingsViewModel: seedSettingsViewModel,
walletRestoreFromSeedFormKey: walletRestoreFromSeedFormKey,
walletRestoreFromKeysFormKey: walletRestoreFromKeysFormKey,
blockHeightFocusNode: _blockHeightFocusNode,
derivationInfo: derivationInfo,
onDerivationInfoChanged: (info) => derivationInfo = info,
),
),
if (_pages.length > 1)
Padding(
padding: EdgeInsets.only(top: 10),
child: Semantics(
button: false,
label: 'Page Indicator',
hint: 'Swipe to change restore mode',
excludeSemantics: true,
child: SmoothPageIndicator(
controller: _controller,
count: _pages.length,
effect: ColorTransitionEffect(
spacing: 6.0,
radius: 6.0,
dotWidth: 6.0,
dotHeight: 6.0,
dotColor: Theme.of(context).hintColor.withOpacity(0.5),
activeDotColor: Theme.of(context).hintColor,
),
),
),
),
Padding(
padding: EdgeInsets.only(top: 20, bottom: 24, left: 24, right: 24),
child: Column(
@@ -245,9 +102,7 @@ class _WalletRestorePageBodyState extends State<WalletRestorePageBody> {
builder: (context) {
return LoadingPrimaryButton(
key: ValueKey('wallet_restore_seed_or_key_restore_button_key'),
onPressed: () async {
await _confirmForm(context);
},
onPressed: () async => await _confirmForm(context),
text: S.of(context).restore_recover,
color: Theme.of(context)
.extension<WalletListTheme>()!
@@ -285,60 +140,6 @@ class _WalletRestorePageBodyState extends State<WalletRestorePageBody> {
);
}
void _validateOnChange({bool isPolyseed = false}) {
if (!isPolyseed && walletRestoreViewModel.hasBlockchainHeightLanguageSelector) {
final hasHeight = walletRestoreFromSeedFormKey
.currentState?.blockchainHeightKey.currentState?.restoreHeightController.text.isNotEmpty;
if (hasHeight == true) {
walletRestoreViewModel.isButtonEnabled = _isValidSeed();
}
} else {
walletRestoreViewModel.isButtonEnabled = _isValidSeed();
}
}
bool _isValidSeed() {
final seedPhrase =
walletRestoreFromSeedFormKey.currentState!.seedWidgetStateKey.currentState!.text;
if (walletRestoreViewModel.isPolyseed(seedPhrase)) return true;
final seedWords = seedPhrase.split(' ');
if (seedWords.length == 14 && walletRestoreViewModel.type == WalletType.wownero) return true;
if (seedWords.length == 26 && walletRestoreViewModel.type == WalletType.zano) return true;
if ((walletRestoreViewModel.type == WalletType.monero ||
walletRestoreViewModel.type == WalletType.wownero ||
walletRestoreViewModel.type == WalletType.haven) &&
seedWords.length != WalletRestoreViewModelBase.moneroSeedMnemonicLength) {
return false;
}
// bip39:
final validBip39SeedLengths = [12, 18, 24];
final nonBip39WalletTypes = [WalletType.monero, WalletType.wownero, WalletType.haven];
// if it's a bip39 wallet and the length is not valid return false
if (!nonBip39WalletTypes.contains(walletRestoreViewModel.type) &&
!(validBip39SeedLengths.contains(seedWords.length))) {
return false;
}
final words =
walletRestoreFromSeedFormKey.currentState!.seedWidgetStateKey.currentState!.words.toSet();
return seedWords.toSet().difference(words).toSet().isEmpty;
}
bool _isValidSeedKey() {
final seedKey = walletRestoreFromKeysFormKey.currentState!.privateKeyController.text;
if (seedKey.length != 64 && seedKey.length != 128) {
return false;
}
return true;
}
Map<String, dynamic> _credentials() {
final credentials = <String, dynamic>{};
@@ -466,3 +267,283 @@ class _WalletRestorePageBodyState extends State<WalletRestorePageBody> {
});
}
}
class _WalletRestorePageBody extends StatefulWidget {
const _WalletRestorePageBody({
Key? key,
required this.walletRestoreViewModel,
required this.seedSettingsViewModel,
required this.walletRestoreFromSeedFormKey,
required this.walletRestoreFromKeysFormKey,
required this.blockHeightFocusNode,
required this.derivationInfo,
required this.onDerivationInfoChanged,
}) : super(key: key);
final WalletRestoreViewModel walletRestoreViewModel;
final SeedSettingsViewModel seedSettingsViewModel;
final GlobalKey<WalletRestoreFromSeedFormState> walletRestoreFromSeedFormKey;
final GlobalKey<WalletRestoreFromKeysFromState> walletRestoreFromKeysFormKey;
final FocusNode blockHeightFocusNode;
final DerivationInfo? derivationInfo;
final void Function(DerivationInfo?) onDerivationInfoChanged;
@override
State<_WalletRestorePageBody> createState() => _WalletRestorePageBodyState(
walletRestoreViewModel: walletRestoreViewModel,
seedSettingsViewModel: seedSettingsViewModel,
walletRestoreFromSeedFormKey: walletRestoreFromSeedFormKey,
walletRestoreFromKeysFormKey: walletRestoreFromKeysFormKey,
blockHeightFocusNode: blockHeightFocusNode,
derivationInfo: derivationInfo);
}
class _WalletRestorePageBodyState extends State<_WalletRestorePageBody>
with SingleTickerProviderStateMixin {
_WalletRestorePageBodyState(
{required this.walletRestoreViewModel,
required this.seedSettingsViewModel,
required this.walletRestoreFromSeedFormKey,
required this.walletRestoreFromKeysFormKey,
required this.blockHeightFocusNode,
required this.derivationInfo});
final WalletRestoreViewModel walletRestoreViewModel;
final SeedSettingsViewModel seedSettingsViewModel;
final GlobalKey<WalletRestoreFromSeedFormState> walletRestoreFromSeedFormKey;
final GlobalKey<WalletRestoreFromKeysFromState> walletRestoreFromKeysFormKey;
final FocusNode blockHeightFocusNode;
DerivationInfo? derivationInfo;
late TabController _tabController;
late bool _hasKeysTab;
@override
void initState() {
super.initState();
_hasKeysTab = widget.walletRestoreViewModel.availableModes.contains(WalletRestoreMode.keys);
final tabCount = _hasKeysTab ? 2 : 1;
_tabController = TabController(length: tabCount, vsync: this);
_tabController.addListener(() {
if (!_tabController.indexIsChanging) {
widget.walletRestoreViewModel.mode =
_tabController.index == 0 ? WalletRestoreMode.seed : WalletRestoreMode.keys;
}
});
reaction<WalletRestoreMode>(
(_) => widget.walletRestoreViewModel.mode,
(mode) {
final index = mode == WalletRestoreMode.seed ? 0 : 1;
if (_tabController.index != index) {
_tabController.animateTo(index);
}
},
);
reaction((_) => walletRestoreViewModel.state, (ExecutionState state) {
if (state is FailureState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showPopUp<void>(
context: context,
builder: (_) {
return AlertWithOneAction(
alertTitle: S.current.new_wallet,
alertContent: state.error,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop());
});
});
}
});
reaction((_) => walletRestoreViewModel.mode, (WalletRestoreMode mode) {
walletRestoreViewModel.isButtonEnabled = false;
walletRestoreViewModel.walletPassword = null;
walletRestoreViewModel.repeatedWalletPassword = null;
walletRestoreFromSeedFormKey
.currentState!.blockchainHeightKey.currentState!.restoreHeightController.text = '';
walletRestoreFromSeedFormKey
.currentState!.blockchainHeightKey.currentState!.dateController.text = '';
walletRestoreFromSeedFormKey.currentState!.nameTextEditingController.text = '';
walletRestoreFromKeysFormKey
.currentState!.blockchainHeightKey.currentState!.restoreHeightController.text = '';
walletRestoreFromKeysFormKey
.currentState!.blockchainHeightKey.currentState!.dateController.text = '';
walletRestoreFromKeysFormKey.currentState!.nameTextEditingController.text = '';
});
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 22),
child: TabBar(
controller: _tabController,
splashFactory: NoSplash.splashFactory,
indicatorSize: TabBarIndicatorSize.label,
isScrollable: true,
labelStyle: TextStyle(
fontSize: 18,
fontFamily: 'Lato',
fontWeight: FontWeight.w600,
color: Theme.of(context).appBarTheme.titleTextStyle!.color),
unselectedLabelStyle: TextStyle(
fontSize: 18,
fontFamily: 'Lato',
fontWeight: FontWeight.w600,
color: Theme.of(context).appBarTheme.titleTextStyle!.color?.withOpacity(0.5)),
labelColor: Theme.of(context).appBarTheme.titleTextStyle!.color,
indicatorColor: Theme.of(context).appBarTheme.titleTextStyle!.color,
indicatorPadding: EdgeInsets.zero,
labelPadding: const EdgeInsets.only(right: 24),
tabAlignment: TabAlignment.start,
dividerColor: Colors.transparent,
padding: EdgeInsets.zero,
tabs: [
Tab(text: S.of(context).widgets_seed),
if (_hasKeysTab) Tab(text: S.of(context).keys),
],
),
),
const SizedBox(height: 20),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: _buildWalletRestoreFromSeedTab(),
),
if (_hasKeysTab)
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: _buildWalletRestoreFromKeysTab(),
),
],
),
),
],
),
);
}
WalletRestoreFromKeysFrom _buildWalletRestoreFromKeysTab() {
return WalletRestoreFromKeysFrom(
key: widget.walletRestoreFromKeysFormKey,
walletRestoreViewModel: widget.walletRestoreViewModel,
displayPrivateKeyField: widget.walletRestoreViewModel.hasRestoreFromPrivateKey,
displayWalletPassword: widget.walletRestoreViewModel.hasWalletPassword,
onPrivateKeyChange: (String seed) {
// For nano/banano, set button state if valid seed key
if (widget.walletRestoreViewModel.type == WalletType.nano ||
widget.walletRestoreViewModel.type == WalletType.banano) {
widget.walletRestoreViewModel.isButtonEnabled = _isValidSeedKey();
}
},
onPasswordChange: (String password) =>
widget.walletRestoreViewModel.walletPassword = password,
onRepeatedPasswordChange: (String repeatedPassword) =>
widget.walletRestoreViewModel.repeatedWalletPassword = repeatedPassword,
onHeightOrDateEntered: (value) => widget.walletRestoreViewModel.isButtonEnabled = value,
);
}
WalletRestoreFromSeedForm _buildWalletRestoreFromSeedTab() {
return WalletRestoreFromSeedForm(
key: widget.walletRestoreFromSeedFormKey,
seedSettingsViewModel: widget.seedSettingsViewModel,
displayBlockHeightSelector: widget.walletRestoreViewModel.hasBlockchainHeightLanguageSelector,
displayLanguageSelector: widget.walletRestoreViewModel.hasSeedLanguageSelector,
type: widget.walletRestoreViewModel.type,
blockHeightFocusNode: widget.blockHeightFocusNode,
onHeightOrDateEntered: (value) {
// set button state
if (_isValidSeed()) {
widget.walletRestoreViewModel.isButtonEnabled = value;
}
},
onSeedChange: (String seed) {
final isPolyseed = widget.walletRestoreViewModel.isPolyseed(seed);
_validateOnChange(isPolyseed: isPolyseed);
},
onLanguageChange: (String language) {
final isPolyseed = language.startsWith("POLYSEED_");
_validateOnChange(isPolyseed: isPolyseed);
},
displayWalletPassword: widget.walletRestoreViewModel.hasWalletPassword,
onPasswordChange: (String password) =>
widget.walletRestoreViewModel.walletPassword = password,
onRepeatedPasswordChange: (String repeatedPassword) =>
widget.walletRestoreViewModel.repeatedWalletPassword = repeatedPassword,
);
}
void _validateOnChange({bool isPolyseed = false}) {
if (!isPolyseed && walletRestoreViewModel.hasBlockchainHeightLanguageSelector) {
final hasHeight = walletRestoreFromSeedFormKey
.currentState?.blockchainHeightKey.currentState?.restoreHeightController.text.isNotEmpty;
if (hasHeight == true) {
walletRestoreViewModel.isButtonEnabled = _isValidSeed();
}
} else {
walletRestoreViewModel.isButtonEnabled = _isValidSeed();
}
}
bool _isValidSeed() {
final seedPhrase =
walletRestoreFromSeedFormKey.currentState!.seedWidgetStateKey.currentState!.text;
if (walletRestoreViewModel.isPolyseed(seedPhrase)) return true;
final seedWords = seedPhrase.split(' ');
if (seedWords.length == 14 && walletRestoreViewModel.type == WalletType.wownero) return true;
if (seedWords.length == 26 && walletRestoreViewModel.type == WalletType.zano) return true;
if ((walletRestoreViewModel.type == WalletType.monero ||
walletRestoreViewModel.type == WalletType.wownero ||
walletRestoreViewModel.type == WalletType.haven) &&
seedWords.length != WalletRestoreViewModelBase.moneroSeedMnemonicLength) {
return false;
}
// bip39:
final validBip39SeedLengths = [12, 18, 24];
final nonBip39WalletTypes = [WalletType.monero, WalletType.wownero, WalletType.haven];
// if it's a bip39 wallet and the length is not valid return false
if (!nonBip39WalletTypes.contains(walletRestoreViewModel.type) &&
!(validBip39SeedLengths.contains(seedWords.length))) {
return false;
}
final words =
walletRestoreFromSeedFormKey.currentState!.seedWidgetStateKey.currentState!.words.toSet();
return seedWords.toSet().difference(words).toSet().isEmpty;
}
bool _isValidSeedKey() {
final seedKey = walletRestoreFromKeysFormKey.currentState!.privateKeyController.text;
if (seedKey.length != 64 && seedKey.length != 128) {
return false;
}
return true;
}
}