diff --git a/assets/translations/en.json b/assets/translations/en.json index 4cfd98b685..1f27bc3c7f 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -201,6 +201,7 @@ "customSeedWarningText": "Custom seed phrases are generally less secure and easier to crack than a generated BIP39-compliant seed phrase. To confirm you understand and are aware of the risk, type \"I understand\" in the box below.", "customSeedIUnderstand": "i understand", "walletCreationBip39SeedError": "BIP39 seed validation failed, try again or select 'Allow custom seed'", + "walletCreationHdBip39SeedError": "Your input seed is not BIP39 compliant, and can not be used in multi-address wallet mode. Please try again, or disable multi-address wallet mode and select 'Allow custom seed' to proceed.", "walletPageNoSuchAsset": "No assets match search criteria", "swapCoin": "Swap", "fiatBalance": "Fiat balance", diff --git a/lib/bloc/auth_bloc/auth_bloc.dart b/lib/bloc/auth_bloc/auth_bloc.dart index 5ca71be1f4..95f6ccbab8 100644 --- a/lib/bloc/auth_bloc/auth_bloc.dart +++ b/lib/bloc/auth_bloc/auth_bloc.dart @@ -21,6 +21,7 @@ class AuthBloc extends Bloc { AuthBloc(this._kdfSdk, this._walletsRepository) : super(AuthBlocState.initial()) { on(_onAuthChanged); + on(_onClearState); on(_onLogout); on(_onLogIn); on(_onRegister); @@ -31,11 +32,11 @@ class AuthBloc extends Bloc { final KomodoDefiSdk _kdfSdk; final WalletsRepository _walletsRepository; - StreamSubscription? _authorizationSubscription; + StreamSubscription? _authChangesSubscription; @override Future close() async { - await _authorizationSubscription?.cancel(); + await _authChangesSubscription?.cancel(); await super.close(); } @@ -43,17 +44,11 @@ class AuthBloc extends Bloc { AuthSignOutRequested event, Emitter emit, ) async { - log( - 'Logging out from a wallet', - path: 'auth_bloc => _logOut', - ).ignore(); - + log('Logging out from a wallet', path: 'auth_bloc => _logOut').ignore(); + emit(AuthBlocState.loading()); await _kdfSdk.auth.signOut(); - log( - 'Logged out from a wallet', - path: 'auth_bloc => _logOut', - ).ignore(); - emit(const AuthBlocState(mode: AuthorizeMode.noLogin, currentUser: null)); + await _authChangesSubscription?.cancel(); + emit(AuthBlocState.initial()); } Future _onLogIn( @@ -72,6 +67,7 @@ class AuthBloc extends Bloc { } log('login from a wallet', path: 'auth_bloc => _reLogin').ignore(); + emit(AuthBlocState.loading()); await _kdfSdk.auth.signIn( walletName: event.wallet.name, password: event.password, @@ -81,22 +77,20 @@ class AuthBloc extends Bloc { : DerivationMethod.iguana, ), ); + final KdfUser? currentUser = await _kdfSdk.auth.currentUser; + if (currentUser == null) { + return emit(AuthBlocState.error('Failed to login')); + } + log('logged in from a wallet', path: 'auth_bloc => _reLogin').ignore(); - emit( - AuthBlocState( - mode: AuthorizeMode.logIn, - currentUser: await _kdfSdk.auth.currentUser, - ), - ); + emit(AuthBlocState.loggedIn(currentUser)); _listenToAuthStateChanges(); } catch (e, s) { - log( - 'Failed to login wallet ${event.wallet.name}', - isError: true, - trace: s, - path: 'auth_bloc -> onLogin', - ).ignore(); - emit(const AuthBlocState(mode: AuthorizeMode.noLogin)); + final error = 'Failed to login wallet ${event.wallet.name}'; + log(error, isError: true, trace: s, path: 'auth_bloc -> onLogin') + .ignore(); + emit(AuthBlocState.error(error)); + await _authChangesSubscription?.cancel(); } } @@ -107,20 +101,21 @@ class AuthBloc extends Bloc { emit(AuthBlocState(mode: event.mode, currentUser: event.currentUser)); } + Future _onClearState( + AuthStateClearRequested event, + Emitter emit, + ) async { + await _authChangesSubscription?.cancel(); + emit(AuthBlocState.initial()); + } + Future _onRegister( AuthRegisterRequested event, Emitter emit, ) async { try { - final existingWallets = await _kdfSdk.auth.getUsers(); - final walletExists = existingWallets - .any((KdfUser user) => user.walletId.name == event.wallet.name); - if (walletExists) { - add( - AuthSignInRequested(wallet: event.wallet, password: event.password), - ); - log('Wallet ${event.wallet.name} already exist, attempting sign-in') - .ignore(); + emit(AuthBlocState.loading()); + if (await _didSignInExistingWallet(event.wallet, event.password)) { return; } @@ -134,27 +129,26 @@ class AuthBloc extends Bloc { : DerivationMethod.iguana, ), ); + if (!await _kdfSdk.auth.isSignedIn()) { throw Exception('Registration failed: user is not signed in'); } + log('registered from a wallet', path: 'auth_bloc => _register').ignore(); await _kdfSdk.setWalletType(event.wallet.config.type); await _kdfSdk.confirmSeedBackup(hasBackup: false); - emit( - AuthBlocState( - mode: AuthorizeMode.logIn, - currentUser: await _kdfSdk.auth.currentUser, - ), - ); + final currentUser = await _kdfSdk.auth.currentUser; + if (currentUser == null) { + throw Exception('Registration failed: user is not signed in'); + } + emit(AuthBlocState.loggedIn(currentUser)); _listenToAuthStateChanges(); } catch (e, s) { - log( - 'Failed to register wallet ${event.wallet.name}', - isError: true, - trace: s, - path: 'auth_bloc -> onRegister', - ).ignore(); - emit(const AuthBlocState(mode: AuthorizeMode.noLogin)); + final error = 'Failed to register wallet ${event.wallet.name}'; + log(error, isError: true, trace: s, path: 'auth_bloc -> onRegister') + .ignore(); + emit(AuthBlocState.error(error)); + await _authChangesSubscription?.cancel(); } } @@ -163,15 +157,8 @@ class AuthBloc extends Bloc { Emitter emit, ) async { try { - final existingWallets = await _kdfSdk.auth.getUsers(); - final walletExists = existingWallets - .any((KdfUser user) => user.walletId.name == event.wallet.name); - if (walletExists) { - add( - AuthSignInRequested(wallet: event.wallet, password: event.password), - ); - log('Wallet ${event.wallet.name} already exist, attempting sign-in') - .ignore(); + emit(AuthBlocState.loading()); + if (await _didSignInExistingWallet(event.wallet, event.password)) { return; } @@ -186,20 +173,19 @@ class AuthBloc extends Bloc { : DerivationMethod.iguana, ), ); + if (!await _kdfSdk.auth.isSignedIn()) { throw Exception('Registration failed: user is not signed in'); } - log('restored from a wallet', path: 'auth_bloc => _restore').ignore(); + log('restored from a wallet', path: 'auth_bloc => _restore').ignore(); await _kdfSdk.setWalletType(event.wallet.config.type); await _kdfSdk.confirmSeedBackup(hasBackup: event.wallet.config.hasBackup); - - emit( - AuthBlocState( - mode: AuthorizeMode.logIn, - currentUser: await _kdfSdk.auth.currentUser, - ), - ); + final currentUser = await _kdfSdk.auth.currentUser; + if (currentUser == null) { + throw Exception('Registration failed: user is not signed in'); + } + emit(AuthBlocState.loggedIn(currentUser)); // Delete legacy wallet on successful restoration & login to avoid // duplicates in the wallet list @@ -210,16 +196,30 @@ class AuthBloc extends Bloc { _listenToAuthStateChanges(); } catch (e, s) { - log( - 'Failed to restore existing wallet ${event.wallet.name}', - isError: true, - trace: s, - path: 'auth_bloc -> onRestore', - ).ignore(); - emit(const AuthBlocState(mode: AuthorizeMode.noLogin)); + final error = 'Failed to restore existing wallet ${event.wallet.name}'; + log(error, isError: true, trace: s, path: 'auth_bloc -> onRestore') + .ignore(); + emit(AuthBlocState.error(error)); + await _authChangesSubscription?.cancel(); } } + Future _didSignInExistingWallet( + Wallet wallet, + String password, + ) async { + final existingWallets = await _kdfSdk.auth.getUsers(); + final walletExists = existingWallets + .any((KdfUser user) => user.walletId.name == wallet.name); + if (walletExists) { + add(AuthSignInRequested(wallet: wallet, password: password)); + log('Wallet ${wallet.name} already exist, attempting sign-in').ignore(); + return true; + } + + return false; + } + Future _onSeedBackupConfirmed( AuthSeedBackupConfirmed event, Emitter emit, @@ -254,8 +254,8 @@ class AuthBloc extends Bloc { } void _listenToAuthStateChanges() { - _authorizationSubscription?.cancel(); - _authorizationSubscription = _kdfSdk.auth.authStateChanges.listen((user) { + _authChangesSubscription?.cancel(); + _authChangesSubscription = _kdfSdk.auth.authStateChanges.listen((user) { final AuthorizeMode event = user != null ? AuthorizeMode.logIn : AuthorizeMode.noLogin; add(AuthModeChanged(mode: event, currentUser: user)); diff --git a/lib/bloc/auth_bloc/auth_bloc_event.dart b/lib/bloc/auth_bloc/auth_bloc_event.dart index df403bbe81..7692a49668 100644 --- a/lib/bloc/auth_bloc/auth_bloc_event.dart +++ b/lib/bloc/auth_bloc/auth_bloc_event.dart @@ -11,6 +11,10 @@ class AuthModeChanged extends AuthBlocEvent { final KdfUser? currentUser; } +class AuthStateClearRequested extends AuthBlocEvent { + const AuthStateClearRequested(); +} + class AuthSignOutRequested extends AuthBlocEvent { const AuthSignOutRequested(); } diff --git a/lib/bloc/auth_bloc/auth_bloc_state.dart b/lib/bloc/auth_bloc/auth_bloc_state.dart index be5e392811..88864e2531 100644 --- a/lib/bloc/auth_bloc/auth_bloc_state.dart +++ b/lib/bloc/auth_bloc/auth_bloc_state.dart @@ -1,16 +1,41 @@ part of 'auth_bloc.dart'; +enum AuthStatus { initial, loading, success, failure } + class AuthBlocState extends Equatable { - const AuthBlocState({required this.mode, this.currentUser}); + const AuthBlocState({ + required this.mode, + this.currentUser, + this.status = AuthStatus.initial, + this.errorMessage, + }); factory AuthBlocState.initial() => const AuthBlocState(mode: AuthorizeMode.noLogin); + factory AuthBlocState.loading() => const AuthBlocState( + mode: AuthorizeMode.noLogin, + status: AuthStatus.loading, + ); + factory AuthBlocState.error(String errorMessage) => AuthBlocState( + mode: AuthorizeMode.noLogin, + status: AuthStatus.failure, + errorMessage: errorMessage, + ); + factory AuthBlocState.loggedIn(KdfUser user) => AuthBlocState( + mode: AuthorizeMode.logIn, + status: AuthStatus.success, + currentUser: user, + ); final KdfUser? currentUser; final AuthorizeMode mode; + final AuthStatus status; + final String? errorMessage; bool get isSignedIn => currentUser != null; + bool get isLoading => status == AuthStatus.loading; + bool get isError => status == AuthStatus.failure; @override - List get props => [mode, currentUser]; + List get props => [mode, currentUser, status, errorMessage]; } diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index af25874caf..5f940f893e 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -1,5 +1,7 @@ // DO NOT EDIT. This is code generated via package:easy_localization/generate.dart +// ignore_for_file: constant_identifier_names + abstract class LocaleKeys { static const plsActivateKmd = 'plsActivateKmd'; static const rewardClaiming = 'rewardClaiming'; @@ -196,6 +198,7 @@ abstract class LocaleKeys { static const customSeedWarningText = 'customSeedWarningText'; static const customSeedIUnderstand = 'customSeedIUnderstand'; static const walletCreationBip39SeedError = 'walletCreationBip39SeedError'; + static const walletCreationHdBip39SeedError = 'walletCreationHdBip39SeedError'; static const walletPageNoSuchAsset = 'walletPageNoSuchAsset'; static const swapCoin = 'swapCoin'; static const fiatBalance = 'fiatBalance'; @@ -606,8 +609,8 @@ abstract class LocaleKeys { static const importTokenWarning = 'importTokenWarning'; static const importToken = 'importToken'; static const selectNetwork = 'selectNetwork'; - static const tokenNotFound = 'tokenNotFound'; static const tokenContractAddress = 'tokenContractAddress'; + static const tokenNotFound = 'tokenNotFound'; static const decimals = 'decimals'; static const onlySendToThisAddress = 'onlySendToThisAddress'; static const scanTheQrCode = 'scanTheQrCode'; diff --git a/lib/views/common/wallet_password_dialog/password_dialog_content.dart b/lib/views/common/wallet_password_dialog/password_dialog_content.dart index 26dea60251..96050f1793 100644 --- a/lib/views/common/wallet_password_dialog/password_dialog_content.dart +++ b/lib/views/common/wallet_password_dialog/password_dialog_content.dart @@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; import 'package:web_dex/common/screen.dart'; @@ -9,15 +10,17 @@ import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/wallet.dart'; import 'package:web_dex/shared/widgets/password_visibility_control.dart'; +// TODO: refactor this widget, and other seed viewing/backup related widgets +// to use a dedicated bloc for seed access attempts (view and download) class PasswordDialogContent extends StatefulWidget { const PasswordDialogContent({ - Key? key, required this.onSuccess, required this.onCancel, + super.key, this.wallet, - }) : super(key: key); + }); - final Function(String) onSuccess; + final void Function(String) onSuccess; final VoidCallback onCancel; final Wallet? wallet; @@ -54,14 +57,12 @@ class _PasswordDialogContentState extends State { Container( padding: const EdgeInsets.only(top: 24), child: Column( - mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ UiTextFormField( key: const Key('confirmation-showing-seed-phrase'), controller: _passwordController, autocorrect: false, - enableInteractiveSelection: true, obscureText: _isObscured, inputFormatters: [LengthLimitingTextInputFormatter(40)], errorMaxLines: 6, @@ -89,7 +90,7 @@ class _PasswordDialogContentState extends State { text: LocaleKeys.cancel.tr(), onPressed: widget.onCancel, ), - ) + ), ], ), ), @@ -106,9 +107,28 @@ class _PasswordDialogContentState extends State { setState(() => _inProgress = true); WidgetsBinding.instance.addPostFrameCallback((_) async { + final sdk = RepositoryProvider.of(context); + try { + final seed = await sdk.auth.getMnemonicPlainText(password); + if (seed.plaintextMnemonic?.isEmpty ?? true) { + _setInvalidPasswordState(); + return; + } + } catch (_) { + _setInvalidPasswordState(); + return; + } + widget.onSuccess(password); if (mounted) setState(() => _inProgress = false); }); } + + void _setInvalidPasswordState() { + setState(() { + _error = LocaleKeys.invalidPasswordError.tr(); + _inProgress = false; + }); + } } diff --git a/lib/views/wallets_manager/widgets/iguana_wallets_manager.dart b/lib/views/wallets_manager/widgets/iguana_wallets_manager.dart index c136e26edb..a23529b269 100644 --- a/lib/views/wallets_manager/widgets/iguana_wallets_manager.dart +++ b/lib/views/wallets_manager/widgets/iguana_wallets_manager.dart @@ -38,7 +38,6 @@ class IguanaWalletsManager extends StatefulWidget { class _IguanaWalletsManagerState extends State { bool _isLoading = false; WalletsManagerAction _action = WalletsManagerAction.none; - String? _errorText; Wallet? _selectedWallet; WalletsManagerExistWalletAction _existWalletAction = WalletsManagerExistWalletAction.none; @@ -117,7 +116,6 @@ class _IguanaWalletsManagerState extends State { wallet: selectedWallet, onLogin: _logInToWallet, onCancel: _cancel, - errorText: _errorText, ); } } @@ -179,11 +177,12 @@ class _IguanaWalletsManagerState extends State { void _cancel() { setState(() { - _errorText = null; _selectedWallet = null; _action = WalletsManagerAction.none; _existWalletAction = WalletsManagerExistWalletAction.none; }); + + context.read().add(const AuthStateClearRequested()); } void _createWallet({ @@ -225,7 +224,6 @@ class _IguanaWalletsManagerState extends State { Future _logInToWallet(String password, Wallet wallet) async { setState(() { _isLoading = true; - _errorText = null; }); final AnalyticsBloc analyticsBloc = context.read(); diff --git a/lib/views/wallets_manager/widgets/wallet_login.dart b/lib/views/wallets_manager/widgets/wallet_login.dart index 94fe9f65fc..ea55373f54 100644 --- a/lib/views/wallets_manager/widgets/wallet_login.dart +++ b/lib/views/wallets_manager/widgets/wallet_login.dart @@ -1,10 +1,13 @@ +import 'dart:async'; + +import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:collection/collection.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; +import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/wallet.dart'; @@ -13,35 +16,30 @@ import 'package:web_dex/views/wallets_manager/widgets/hdwallet_mode_switch.dart' class WalletLogIn extends StatefulWidget { const WalletLogIn({ - Key? key, required this.wallet, required this.onLogin, required this.onCancel, - this.errorText, - }) : super(key: key); + super.key, + }); final Wallet wallet; final void Function(String, Wallet) onLogin; final void Function() onCancel; - final String? errorText; @override State createState() => _WalletLogInState(); } class _WalletLogInState extends State { - bool _isPasswordObscured = true; - bool _errorDisplay = false; final _backKeyButton = GlobalKey(); final TextEditingController _passwordController = TextEditingController(); - bool _inProgress = false; bool _isHdMode = true; KdfUser? _user; @override void initState() { super.initState(); - _fetchKdfUser(); + unawaited(_fetchKdfUser()); } Future _fetchKdfUser() async { @@ -63,12 +61,7 @@ class _WalletLogInState extends State { super.dispose(); } - void _submitLogin() async { - setState(() { - _errorDisplay = true; - _inProgress = true; - }); - + void _submitLogin() { WidgetsBinding.instance.addPostFrameCallback((_) { widget.wallet.config.type = _isHdMode && _user != null && _user!.isBip39Seed == true @@ -79,101 +72,123 @@ class _WalletLogInState extends State { _passwordController.text, widget.wallet, ); - - if (mounted) setState(() => _inProgress = false); }); } @override Widget build(BuildContext context) { - return Column( - mainAxisSize: isMobile ? MainAxisSize.max : MainAxisSize.min, - children: [ - Text(LocaleKeys.walletLogInTitle.tr(), - style: - Theme.of(context).textTheme.titleLarge?.copyWith(fontSize: 18)), - const SizedBox(height: 40), - _buildWalletField(), - const SizedBox( - height: 20, - ), - _buildPasswordField(), - const SizedBox(height: 20), - if (_user != null && _user!.isBip39Seed == true) - HDWalletModeSwitch( - value: _isHdMode, - onChanged: (value) { - setState(() => _isHdMode = value); - }, - ), - const SizedBox(height: 20), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 2.0), - child: UiPrimaryButton( - height: 50, - text: _inProgress - ? '${LocaleKeys.pleaseWait.tr()}...' - : LocaleKeys.logIn.tr(), - textStyle: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w700, + return BlocBuilder( + builder: (context, state) { + // TODO: expand to parse SDK errors and show more specific messages + final errorMessage = state.errorMessage != null + ? LocaleKeys.invalidPasswordError.tr() + : null; + + return Column( + mainAxisSize: isMobile ? MainAxisSize.max : MainAxisSize.min, + children: [ + Text( + LocaleKeys.walletLogInTitle.tr(), + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith(fontSize: 18), ), - onPressed: _inProgress ? null : _submitLogin, - ), - ), - const SizedBox(height: 20), - UiUnderlineTextButton( - key: _backKeyButton, - onPressed: () { - widget.onCancel(); - }, - text: LocaleKeys.cancel.tr(), - ), - ], + const SizedBox(height: 40), + UiTextFormField( + key: const Key('wallet-field'), + initialValue: widget.wallet.name, + readOnly: true, + autocorrect: false, + ), + const SizedBox( + height: 20, + ), + PasswordTextField( + onFieldSubmitted: state.isLoading ? null : _submitLogin, + controller: _passwordController, + errorText: errorMessage, + ), + const SizedBox(height: 20), + if (_user != null && _user!.isBip39Seed == true) + HDWalletModeSwitch( + value: _isHdMode, + onChanged: (value) { + setState(() => _isHdMode = value); + }, + ), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 2.0), + child: UiPrimaryButton( + height: 50, + text: state.isLoading + ? '${LocaleKeys.pleaseWait.tr()}...' + : LocaleKeys.logIn.tr(), + textStyle: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + ), + onPressed: state.isLoading ? null : _submitLogin, + ), + ), + const SizedBox(height: 20), + UiUnderlineTextButton( + key: _backKeyButton, + onPressed: widget.onCancel, + text: LocaleKeys.cancel.tr(), + ), + ], + ); + }, ); } +} - Widget _buildWalletField() { - return UiTextFormField( - key: const Key('wallet-field'), - initialValue: widget.wallet.name, - readOnly: true, - autocorrect: false, - enableInteractiveSelection: true, - ); - } +class PasswordTextField extends StatefulWidget { + const PasswordTextField({ + required this.onFieldSubmitted, + required this.controller, + super.key, + this.errorText, + }); + + final String? errorText; + final TextEditingController controller; + final void Function()? onFieldSubmitted; + + @override + State createState() => _PasswordTextFieldState(); +} - Widget _buildPasswordField() { +class _PasswordTextFieldState extends State { + bool _isPasswordObscured = true; + + @override + Widget build(BuildContext context) { return Stack( children: [ UiTextFormField( key: const Key('create-password-field'), - controller: _passwordController, textInputAction: TextInputAction.next, autocorrect: false, - enableInteractiveSelection: true, + controller: widget.controller, obscureText: _isPasswordObscured, - errorText: !_inProgress && _errorDisplay ? widget.errorText : null, + errorText: widget.errorText, hintText: LocaleKeys.walletCreationPasswordHint.tr(), - onChanged: (text) { - if (text == '') { - setState(() { - _errorDisplay = false; - }); - } - }, suffixIcon: PasswordVisibilityControl( - onVisibilityChange: (bool isPasswordObscured) { - setState(() { - _isPasswordObscured = isPasswordObscured; - }); - }, + onVisibilityChange: onVisibilityChange, ), - onFieldSubmitted: (text) { - if (!_inProgress) _submitLogin(); - }, + onFieldSubmitted: (_) => widget.onFieldSubmitted?.call(), ), ], ); } + + // ignore: avoid_positional_boolean_parameters + void onVisibilityChange(bool isPasswordObscured) { + setState(() { + _isPasswordObscured = isPasswordObscured; + }); + } } diff --git a/lib/views/wallets_manager/widgets/wallet_simple_import.dart b/lib/views/wallets_manager/widgets/wallet_simple_import.dart index 26223fc24a..cffd5c0584 100644 --- a/lib/views/wallets_manager/widgets/wallet_simple_import.dart +++ b/lib/views/wallets_manager/widgets/wallet_simple_import.dart @@ -60,7 +60,9 @@ class _WalletImportWrapperState extends State { bool _isHdMode = true; bool get _isButtonEnabled { - return _eulaAndTosChecked && !_inProgress; + final isFormValid = _refreshFormValidationState(); + + return _eulaAndTosChecked && !_inProgress && isFormValid; } @override @@ -128,14 +130,22 @@ class _WalletImportWrapperState extends State { _allowCustomSeed = value; }); - if (_seedController.text.isNotEmpty && - _nameController.text.isNotEmpty) { - _formKey.currentState!.validate(); - } + _refreshFormValidationState(); }, ); } + bool _refreshFormValidationState() { + final nameHasValue = _nameController.text.isNotEmpty; + final seedHasValue = _seedController.text.isNotEmpty; + + if (seedHasValue && nameHasValue) { + return _formKey.currentState!.validate(); + } + + return false; + } + Widget _buildFields() { switch (_step) { case WalletSimpleImportSteps.nameAndSeed: @@ -186,7 +196,12 @@ class _WalletImportWrapperState extends State { HDWalletModeSwitch( value: _isHdMode, onChanged: (value) { - setState(() => _isHdMode = value); + setState(() { + _isHdMode = value; + _allowCustomSeed = false; + }); + + _refreshFormValidationState(); }, ), const SizedBox(height: 20), @@ -293,6 +308,10 @@ class _WalletImportWrapperState extends State { } String? _validateSeed(String? seed) { + if (_allowCustomSeed) { + return null; + } + final maybeFailedReason = context.read().mnemonicValidator.validateMnemonic( seed ?? '', @@ -309,15 +328,18 @@ class _WalletImportWrapperState extends State { return switch (maybeFailedReason) { MnemonicFailedReason.empty => LocaleKeys.walletCreationEmptySeedError.tr(), - MnemonicFailedReason.customNotSupportedForHd => - LocaleKeys.walletCreationBip39SeedError.tr(), + MnemonicFailedReason.customNotSupportedForHd => _isHdMode + ? LocaleKeys.walletCreationHdBip39SeedError.tr() + : LocaleKeys.walletCreationBip39SeedError.tr(), MnemonicFailedReason.customNotAllowed => LocaleKeys.customSeedWarningText.tr(), MnemonicFailedReason.invalidLength => // TODO: Add this string has placeholders for min/max counts, which we // specify as "12" and "24" // LocaleKeys.seedPhraseCheckingEnterWord.tr(args: ['12', '24']), - LocaleKeys.walletCreationBip39SeedError.tr(), + _isHdMode + ? LocaleKeys.walletCreationHdBip39SeedError.tr() + : LocaleKeys.walletCreationBip39SeedError.tr(), }; } } diff --git a/packages/komodo_ui_kit/lib/src/inputs/ui_text_form_field.dart b/packages/komodo_ui_kit/lib/src/inputs/ui_text_form_field.dart index 3a0dc26b60..9dadb543ba 100644 --- a/packages/komodo_ui_kit/lib/src/inputs/ui_text_form_field.dart +++ b/packages/komodo_ui_kit/lib/src/inputs/ui_text_form_field.dart @@ -130,7 +130,8 @@ class _UiTextFormFieldState extends State { void didUpdateWidget(covariant UiTextFormField oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.errorText != oldWidget.errorText) { + final error = widget.validator?.call(_controller?.text) ?? widget.errorText; + if (error != oldWidget.errorText) { _errorText = widget.errorText; _displayedErrorText = widget.errorText; if (_errorText?.isNotEmpty == true) {