From 7c7ea039469c35906c48e235e37cdd94195c0af4 Mon Sep 17 00:00:00 2001 From: Serhii Date: Mon, 17 Feb 2025 23:38:08 +0200 Subject: [PATCH] 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 --- .../screens/restore/wallet_restore_page.dart | 527 ++++++++++-------- 1 file changed, 304 insertions(+), 223 deletions(-) diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index 7f34e9f0c..dcaf2f4c7 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -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(), + walletRestoreFromKeysFormKey = GlobalKey(), + _blockHeightFocusNode = FocusNode(); + + final WalletRestoreViewModel walletRestoreViewModel; + final SeedSettingsViewModel seedSettingsViewModel; + final GlobalKey walletRestoreFromSeedFormKey; + final GlobalKey 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 createState() => - _WalletRestorePageBodyState(walletRestoreViewModel, seedSettingsViewModel); -} - -class _WalletRestorePageBodyState extends State { - _WalletRestorePageBodyState(this.walletRestoreViewModel, this.seedSettingsViewModel) - : walletRestoreFromSeedFormKey = GlobalKey(), - walletRestoreFromKeysFormKey = GlobalKey(), - _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 _pages; - final GlobalKey walletRestoreFromSeedFormKey; - final GlobalKey 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( - 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 { 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 { 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()! @@ -285,60 +140,6 @@ class _WalletRestorePageBodyState extends State { ); } - 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 _credentials() { final credentials = {}; @@ -466,3 +267,283 @@ class _WalletRestorePageBodyState extends State { }); } } + +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 walletRestoreFromSeedFormKey; + final GlobalKey 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 walletRestoreFromSeedFormKey; + final GlobalKey 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( + (_) => 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( + 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; + } +}