diff --git a/catalyst_voices/lib/pages/registration/create_keychain/bloc_unlock_password_builder.dart b/catalyst_voices/lib/pages/registration/bloc_unlock_password_builder.dart similarity index 75% rename from catalyst_voices/lib/pages/registration/create_keychain/bloc_unlock_password_builder.dart rename to catalyst_voices/lib/pages/registration/bloc_unlock_password_builder.dart index 19d8d57f06a..45d6e4cbc8b 100644 --- a/catalyst_voices/lib/pages/registration/create_keychain/bloc_unlock_password_builder.dart +++ b/catalyst_voices/lib/pages/registration/bloc_unlock_password_builder.dart @@ -5,12 +5,14 @@ class BlocUnlockPasswordBuilder extends BlocSelector { BlocUnlockPasswordBuilder({ super.key, + required BlocWidgetSelector + stateSelector, required BlocWidgetSelector selector, required super.builder, super.bloc, }) : super( selector: (state) { - return selector(state.keychainStateData.unlockPasswordState); + return selector(stateSelector(state)); }, ); } diff --git a/catalyst_voices/lib/pages/registration/create_keychain/stage/unlock_password_instructions_panel.dart b/catalyst_voices/lib/pages/registration/create_keychain/stage/unlock_password_instructions_panel.dart index 2be1748cb65..9c0e2f39203 100644 --- a/catalyst_voices/lib/pages/registration/create_keychain/stage/unlock_password_instructions_panel.dart +++ b/catalyst_voices/lib/pages/registration/create_keychain/stage/unlock_password_instructions_panel.dart @@ -20,8 +20,9 @@ class UnlockPasswordInstructionsPanel extends StatelessWidget { child: SingleChildScrollView( child: RegistrationStageMessage( title: Text(l10n.createKeychainUnlockPasswordInstructionsTitle), - subtitle: - Text(l10n.createKeychainUnlockPasswordInstructionsSubtitle), + subtitle: Text( + l10n.createKeychainUnlockPasswordInstructionsSubtitle, + ), ), ), ), diff --git a/catalyst_voices/lib/pages/registration/create_keychain/stage/unlock_password_panel.dart b/catalyst_voices/lib/pages/registration/create_keychain/stage/unlock_password_panel.dart index 3d6cd478c49..993a8008e32 100644 --- a/catalyst_voices/lib/pages/registration/create_keychain/stage/unlock_password_panel.dart +++ b/catalyst_voices/lib/pages/registration/create_keychain/stage/unlock_password_panel.dart @@ -1,10 +1,7 @@ -import 'package:catalyst_voices/pages/registration/create_keychain/bloc_unlock_password_builder.dart'; +import 'package:catalyst_voices/pages/registration/bloc_unlock_password_builder.dart'; import 'package:catalyst_voices/pages/registration/widgets/registration_stage_navigation.dart'; -import 'package:catalyst_voices/widgets/indicators/voices_password_strength_indicator.dart'; -import 'package:catalyst_voices/widgets/text_field/voices_password_text_field.dart'; -import 'package:catalyst_voices/widgets/text_field/voices_text_field.dart'; +import 'package:catalyst_voices/pages/registration/widgets/unlock_password_form.dart'; import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; -import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:flutter/material.dart'; @@ -30,8 +27,8 @@ class _UnlockPasswordPanelState extends State { .keychainStateData .unlockPasswordState; - final password = unlockPasswordState.password; - final confirmPassword = unlockPasswordState.confirmPassword; + final password = unlockPasswordState.password.value; + final confirmPassword = unlockPasswordState.confirmPassword.value; _passwordController = TextEditingController(text: password) ..addListener(_onPasswordChanged); @@ -53,16 +50,12 @@ class _UnlockPasswordPanelState extends State { children: [ const SizedBox(height: 24), const SizedBox(height: 12), - _EnterPasswordTextField( - controller: _passwordController, - ), - const SizedBox(height: 12), - _BlocConfirmPasswordTextField( - controller: _confirmPasswordController, + Expanded( + child: _BlocUnlockPasswordForm( + passwordController: _passwordController, + confirmPasswordController: _confirmPasswordController, + ), ), - const Spacer(), - const SizedBox(height: 22), - const _BlocPasswordStrength(), const SizedBox(height: 22), _BlocNavigation( onNextTap: _createKeychain, @@ -107,90 +100,38 @@ class _UnlockPasswordPanelState extends State { } } -class _EnterPasswordTextField extends StatelessWidget { - final TextEditingController controller; +class _BlocUnlockPasswordForm extends StatelessWidget { + final TextEditingController passwordController; + final TextEditingController confirmPasswordController; - const _EnterPasswordTextField({ - required this.controller, + const _BlocUnlockPasswordForm({ + required this.passwordController, + required this.confirmPasswordController, }); @override Widget build(BuildContext context) { - return VoicesPasswordTextField( - controller: controller, - textInputAction: TextInputAction.next, - decoration: VoicesTextFieldDecoration( - labelText: context.l10n.enterPassword, - ), - ); - } -} - -class _BlocConfirmPasswordTextField extends StatelessWidget { - final TextEditingController controller; - - const _BlocConfirmPasswordTextField({ - required this.controller, - }); - - @override - Widget build(BuildContext context) { - return BlocUnlockPasswordBuilder<({bool showError, int minimumLength})>( - selector: (state) => ( - showError: state.showPasswordMisMatch, - minimumLength: state.minPasswordLength, - ), - builder: (context, state) { - return _ConfirmPasswordTextField( - controller: controller, - showError: state.showError, - minimumLength: state.minimumLength, + return BlocUnlockPasswordBuilder< + ({ + bool showError, + PasswordStrength passwordStrength, + bool showPasswordStrength, + })>( + stateSelector: (state) => state.keychainStateData.unlockPasswordState, + selector: (state) { + return ( + showError: state.showPasswordMisMatch, + passwordStrength: state.passwordStrength, + showPasswordStrength: state.showPasswordStrength, ); }, - ); - } -} - -class _ConfirmPasswordTextField extends StatelessWidget { - final TextEditingController controller; - final bool showError; - final int minimumLength; - - const _ConfirmPasswordTextField({ - required this.controller, - this.showError = false, - required this.minimumLength, - }); - - @override - Widget build(BuildContext context) { - return VoicesPasswordTextField( - controller: controller, - decoration: VoicesTextFieldDecoration( - labelText: context.l10n.confirmPassword, - helperText: context.l10n.xCharactersMinimum(minimumLength), - errorText: showError ? context.l10n.passwordDoNotMatch : null, - ), - ); - } -} - -class _BlocPasswordStrength extends StatelessWidget { - const _BlocPasswordStrength(); - - @override - Widget build(BuildContext context) { - return BlocUnlockPasswordBuilder<({bool show, PasswordStrength strength})>( - selector: (state) => ( - show: state.showPasswordStrength, - strength: state.passwordStrength, - ), builder: (context, state) { - return Offstage( - offstage: !state.show, - child: VoicesPasswordStrengthIndicator( - passwordStrength: state.strength, - ), + return UnlockPasswordForm( + passwordController: passwordController, + confirmPasswordController: confirmPasswordController, + showError: state.showError, + passwordStrength: state.passwordStrength, + showPasswordStrength: state.showPasswordStrength, ); }, ); @@ -209,6 +150,7 @@ class _BlocNavigation extends StatelessWidget { @override Widget build(BuildContext context) { return BlocUnlockPasswordBuilder( + stateSelector: (state) => state.keychainStateData.unlockPasswordState, selector: (state) => state.isNextEnabled, builder: (context, state) { return RegistrationBackNextNavigation( diff --git a/catalyst_voices/lib/pages/registration/recover/recover_seed_phrase_panel.dart b/catalyst_voices/lib/pages/registration/recover/recover_seed_phrase_panel.dart index 58cec36a83d..406e21b8877 100644 --- a/catalyst_voices/lib/pages/registration/recover/recover_seed_phrase_panel.dart +++ b/catalyst_voices/lib/pages/registration/recover/recover_seed_phrase_panel.dart @@ -1,6 +1,8 @@ import 'package:catalyst_voices/pages/registration/recover/seed_phrase/account_details_panel.dart'; import 'package:catalyst_voices/pages/registration/recover/seed_phrase/seed_phrase_input_panel.dart'; import 'package:catalyst_voices/pages/registration/recover/seed_phrase/seed_phrase_instructions_panel.dart'; +import 'package:catalyst_voices/pages/registration/recover/seed_phrase/unlock_password_instructions_panel.dart'; +import 'package:catalyst_voices/pages/registration/recover/seed_phrase/unlock_password_panel.dart'; import 'package:catalyst_voices/pages/registration/widgets/placeholder_panel.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:flutter/material.dart'; @@ -21,8 +23,8 @@ class RecoverSeedPhrasePanel extends StatelessWidget { RecoverSeedPhraseStage.seedPhrase => const SeedPhraseInputPanel(), RecoverSeedPhraseStage.accountDetails => const AccountDetailsPanel(), RecoverSeedPhraseStage.unlockPasswordInstructions => - const PlaceholderPanel(), - RecoverSeedPhraseStage.unlockPassword => const PlaceholderPanel(), + const UnlockPasswordInstructionsPanel(), + RecoverSeedPhraseStage.unlockPassword => const UnlockPasswordPanel(), RecoverSeedPhraseStage.success => const PlaceholderPanel(), }; } diff --git a/catalyst_voices/lib/pages/registration/recover/seed_phrase/unlock_password_instructions_panel.dart b/catalyst_voices/lib/pages/registration/recover/seed_phrase/unlock_password_instructions_panel.dart new file mode 100644 index 00000000000..3c06ec888ea --- /dev/null +++ b/catalyst_voices/lib/pages/registration/recover/seed_phrase/unlock_password_instructions_panel.dart @@ -0,0 +1,34 @@ +import 'package:catalyst_voices/pages/registration/widgets/registration_stage_message.dart'; +import 'package:catalyst_voices/pages/registration/widgets/registration_stage_navigation.dart'; +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:flutter/material.dart'; + +class UnlockPasswordInstructionsPanel extends StatelessWidget { + const UnlockPasswordInstructionsPanel({ + super.key, + }); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 24), + Expanded( + child: SingleChildScrollView( + child: RegistrationStageMessage( + title: Text(l10n.recoveryUnlockPasswordInstructionsTitle), + subtitle: Text( + l10n.recoveryUnlockPasswordInstructionsSubtitle, + ), + ), + ), + ), + const SizedBox(height: 10), + const RegistrationBackNextNavigation(), + ], + ); + } +} diff --git a/catalyst_voices/lib/pages/registration/recover/seed_phrase/unlock_password_panel.dart b/catalyst_voices/lib/pages/registration/recover/seed_phrase/unlock_password_panel.dart new file mode 100644 index 00000000000..da2aa94c11d --- /dev/null +++ b/catalyst_voices/lib/pages/registration/recover/seed_phrase/unlock_password_panel.dart @@ -0,0 +1,160 @@ +import 'package:catalyst_voices/pages/registration/bloc_unlock_password_builder.dart'; +import 'package:catalyst_voices/pages/registration/widgets/registration_stage_navigation.dart'; +import 'package:catalyst_voices/pages/registration/widgets/unlock_password_form.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:flutter/material.dart'; + +class UnlockPasswordPanel extends StatefulWidget { + const UnlockPasswordPanel({super.key}); + + @override + State createState() => _UnlockPasswordPanelState(); +} + +class _UnlockPasswordPanelState extends State { + late final TextEditingController _passwordController; + late final TextEditingController _confirmPasswordController; + + @override + void initState() { + super.initState(); + + final unlockPasswordState = RegistrationCubit.of(context) + .state + .recoverStateData + .unlockPasswordState; + + final password = unlockPasswordState.password.value; + final confirmPassword = unlockPasswordState.confirmPassword.value; + + _passwordController = TextEditingController(text: password) + ..addListener(_onPasswordChanged); + _confirmPasswordController = TextEditingController(text: confirmPassword) + ..addListener(_onConfirmPasswordChanged); + } + + @override + void dispose() { + _passwordController.dispose(); + _confirmPasswordController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 24), + const SizedBox(height: 12), + Expanded( + child: _BlocUnlockPasswordForm( + passwordController: _passwordController, + confirmPasswordController: _confirmPasswordController, + ), + ), + const SizedBox(height: 22), + _BlocNavigation( + onNextTap: _createKeychain, + onBackTap: _clearPasswordAndGoBack, + ), + ], + ); + } + + void _onPasswordChanged() { + final password = _passwordController.text; + + RegistrationCubit.of(context).recover.setPassword(password); + } + + void _onConfirmPasswordChanged() { + final confirmPassword = _confirmPasswordController.text; + + RegistrationCubit.of(context).recover.setConfirmPassword(confirmPassword); + } + + void _clearPasswordAndGoBack() { + final registration = RegistrationCubit.of(context); + + registration.recover + ..setPassword('') + ..setConfirmPassword(''); + + registration.previousStep(); + } + + Future _createKeychain() async { + final registrationCubit = RegistrationCubit.of(context); + + final isSuccess = await registrationCubit.recover.createKeychain(); + + if (isSuccess) { + registrationCubit.nextStep(); + } + } +} + +class _BlocUnlockPasswordForm extends StatelessWidget { + final TextEditingController passwordController; + final TextEditingController confirmPasswordController; + + const _BlocUnlockPasswordForm({ + required this.passwordController, + required this.confirmPasswordController, + }); + + @override + Widget build(BuildContext context) { + return BlocUnlockPasswordBuilder< + ({ + bool showError, + PasswordStrength passwordStrength, + bool showPasswordStrength, + })>( + stateSelector: (state) => state.recoverStateData.unlockPasswordState, + selector: (state) { + return ( + showError: state.showPasswordMisMatch, + passwordStrength: state.passwordStrength, + showPasswordStrength: state.showPasswordStrength, + ); + }, + builder: (context, state) { + return UnlockPasswordForm( + passwordController: passwordController, + confirmPasswordController: confirmPasswordController, + showError: state.showError, + passwordStrength: state.passwordStrength, + showPasswordStrength: state.showPasswordStrength, + ); + }, + ); + } +} + +class _BlocNavigation extends StatelessWidget { + final VoidCallback onNextTap; + final VoidCallback onBackTap; + + const _BlocNavigation({ + required this.onNextTap, + required this.onBackTap, + }); + + @override + Widget build(BuildContext context) { + return BlocUnlockPasswordBuilder( + stateSelector: (state) => state.recoverStateData.unlockPasswordState, + selector: (state) => state.isNextEnabled, + builder: (context, state) { + return RegistrationBackNextNavigation( + isNextEnabled: state, + onNextTap: onNextTap, + onBackTap: onBackTap, + ); + }, + ); + } +} diff --git a/catalyst_voices/lib/pages/registration/registration_info_panel.dart b/catalyst_voices/lib/pages/registration/registration_info_panel.dart index 6c3da3a95b0..a0fe5bf14c1 100644 --- a/catalyst_voices/lib/pages/registration/registration_info_panel.dart +++ b/catalyst_voices/lib/pages/registration/registration_info_panel.dart @@ -1,5 +1,5 @@ +import 'package:catalyst_voices/pages/registration/bloc_unlock_password_builder.dart'; import 'package:catalyst_voices/pages/registration/create_keychain/bloc_seed_phrase_builder.dart'; -import 'package:catalyst_voices/pages/registration/create_keychain/bloc_unlock_password_builder.dart'; import 'package:catalyst_voices/pages/registration/pictures/keychain_picture.dart'; import 'package:catalyst_voices/pages/registration/pictures/keychain_with_password_picture.dart'; import 'package:catalyst_voices/pages/registration/pictures/password_picture.dart'; @@ -158,7 +158,7 @@ class _RegistrationPicture extends StatelessWidget { const _BlocSeedPhraseResultPicture(), CreateKeychainStage.unlockPasswordInstructions || CreateKeychainStage.unlockPasswordCreate => - const _BlocPasswordPicture(), + const _BlocCreationPasswordPicture(), }; } @@ -182,7 +182,7 @@ class _RegistrationPicture extends StatelessWidget { const KeychainPicture(), RecoverSeedPhraseStage.unlockPasswordInstructions || RecoverSeedPhraseStage.unlockPassword => - const PasswordPicture(), + const _BlocRecoveryPasswordPicture(), RecoverSeedPhraseStage.success => const KeychainWithPasswordPicture(), }; } @@ -216,12 +216,26 @@ class _BlocSeedPhraseResultPicture extends StatelessWidget { } } -class _BlocPasswordPicture extends StatelessWidget { - const _BlocPasswordPicture(); +class _BlocCreationPasswordPicture extends StatelessWidget { + const _BlocCreationPasswordPicture(); @override Widget build(BuildContext context) { return BlocUnlockPasswordBuilder( + stateSelector: (state) => state.keychainStateData.unlockPasswordState, + selector: (state) => state.pictureType, + builder: (context, state) => PasswordPicture(type: state), + ); + } +} + +class _BlocRecoveryPasswordPicture extends StatelessWidget { + const _BlocRecoveryPasswordPicture(); + + @override + Widget build(BuildContext context) { + return BlocUnlockPasswordBuilder( + stateSelector: (state) => state.recoverStateData.unlockPasswordState, selector: (state) => state.pictureType, builder: (context, state) => PasswordPicture(type: state), ); diff --git a/catalyst_voices/lib/pages/registration/widgets/unlock_password_form.dart b/catalyst_voices/lib/pages/registration/widgets/unlock_password_form.dart new file mode 100644 index 00000000000..54bbf18a171 --- /dev/null +++ b/catalyst_voices/lib/pages/registration/widgets/unlock_password_form.dart @@ -0,0 +1,109 @@ +import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:flutter/material.dart'; + +class UnlockPasswordForm extends StatelessWidget { + final TextEditingController passwordController; + final TextEditingController confirmPasswordController; + final bool showError; + final PasswordStrength passwordStrength; + final bool showPasswordStrength; + + const UnlockPasswordForm({ + super.key, + required this.passwordController, + required this.confirmPasswordController, + this.showError = false, + this.passwordStrength = PasswordStrength.weak, + this.showPasswordStrength = false, + }); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _UnlockPasswordTextField( + controller: passwordController, + ), + const SizedBox(height: 12), + _ConfirmUnlockPasswordTextField( + controller: confirmPasswordController, + showError: showError, + minimumLength: PasswordStrength.minimumLength, + ), + const Spacer(), + const SizedBox(height: 22), + _PasswordStrength( + strength: passwordStrength, + visible: showPasswordStrength, + ), + ], + ); + } +} + +class _UnlockPasswordTextField extends StatelessWidget { + final TextEditingController controller; + + const _UnlockPasswordTextField({ + required this.controller, + }); + + @override + Widget build(BuildContext context) { + return VoicesPasswordTextField( + controller: controller, + textInputAction: TextInputAction.next, + decoration: VoicesTextFieldDecoration( + labelText: context.l10n.enterPassword, + ), + ); + } +} + +class _ConfirmUnlockPasswordTextField extends StatelessWidget { + final TextEditingController controller; + final bool showError; + final int minimumLength; + + const _ConfirmUnlockPasswordTextField({ + required this.controller, + this.showError = false, + required this.minimumLength, + }); + + @override + Widget build(BuildContext context) { + return VoicesPasswordTextField( + controller: controller, + decoration: VoicesTextFieldDecoration( + labelText: context.l10n.confirmPassword, + helperText: context.l10n.xCharactersMinimum(minimumLength), + errorText: showError ? context.l10n.passwordDoNotMatch : null, + ), + ); + } +} + +class _PasswordStrength extends StatelessWidget { + final PasswordStrength strength; + final bool visible; + + const _PasswordStrength({ + this.strength = PasswordStrength.weak, + this.visible = false, + }); + + @override + Widget build(BuildContext context) { + return Offstage( + offstage: !visible, + child: VoicesPasswordStrengthIndicator( + passwordStrength: strength, + ), + ); + } +} diff --git a/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/cubits/keychain_creation_cubit.dart b/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/cubits/keychain_creation_cubit.dart index c5214446c15..1d280e53b77 100644 --- a/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/cubits/keychain_creation_cubit.dart +++ b/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/cubits/keychain_creation_cubit.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'dart:convert' show utf8; import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; +import 'package:catalyst_voices_blocs/src/registration/cubits/unlock_password_manager.dart'; import 'package:catalyst_voices_blocs/src/registration/state_data/keychain_state_data.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:catalyst_voices_services/catalyst_voices_services.dart'; @@ -13,7 +14,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; final _logger = Logger('KeychainCreationCubit'); -abstract interface class KeychainCreationManager { +abstract interface class KeychainCreationManager + implements UnlockPasswordManager { void buildSeedPhrase({ bool forceRefresh = false, }); @@ -24,15 +26,11 @@ abstract interface class KeychainCreationManager { Future downloadSeedPhrase(); - void setPassword(String value); - - void setConfirmPassword(String value); - Future createKeychain(); } final class KeychainCreationCubit extends Cubit - with BlocErrorEmitterMixin + with BlocErrorEmitterMixin, UnlockPasswordMixin implements KeychainCreationManager { final Downloader _downloader; final RegistrationService _registrationService; @@ -54,16 +52,6 @@ final class KeychainCreationCubit extends Cubit } } - UnlockPasswordState get _unlockPasswordState { - return state.unlockPasswordState; - } - - set _unlockPasswordState(UnlockPasswordState newValue) { - if (state.unlockPasswordState != newValue) { - emit(state.copyWith(unlockPasswordState: newValue)); - } - } - @override void buildSeedPhrase({ bool forceRefresh = false, @@ -131,31 +119,26 @@ final class KeychainCreationCubit extends Cubit } @override - void setPassword(String value) { - _updateUnlockPasswordState(password: value); - } - - @override - void setConfirmPassword(String value) { - _updateUnlockPasswordState(confirmPassword: value); + void onUnlockPasswordStateChanged(UnlockPasswordState data) { + emit(state.copyWith(unlockPasswordState: data)); } @override Future createKeychain() async { try { final seedPhrase = _seedPhraseStateData.seedPhrase; - final password = _unlockPasswordState.password; + final password = this.password; if (seedPhrase == null) { throw const LocalizedRegistrationSeedPhraseNotFoundException(); } - if (password.isEmpty) { + if (password.isNotValid) { throw const LocalizedRegistrationUnlockPasswordNotFoundException(); } await _registrationService.createKeychain( seedPhrase: seedPhrase, - unlockPassword: password, + unlockPassword: password.value, ); return true; @@ -168,31 +151,6 @@ final class KeychainCreationCubit extends Cubit } } - void _updateUnlockPasswordState({ - String? password, - String? confirmPassword, - }) { - password ??= _unlockPasswordState.password; - confirmPassword ??= _unlockPasswordState.confirmPassword; - - const minimumLength = PasswordStrength.minimumLength; - - final passwordStrength = PasswordStrength.calculate(password); - final correctLength = password.length >= minimumLength; - final matching = password == confirmPassword; - final hasConfirmPassword = confirmPassword.isNotEmpty; - - _unlockPasswordState = _unlockPasswordState.copyWith( - password: password, - confirmPassword: confirmPassword, - passwordStrength: passwordStrength, - showPasswordStrength: password.isNotEmpty, - minPasswordLength: minimumLength, - showPasswordMisMatch: correctLength && hasConfirmPassword && !matching, - isNextEnabled: correctLength && matching, - ); - } - void _buildSeedPhrase() { final seedPhrase = SeedPhrase(); diff --git a/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/cubits/recover_cubit.dart b/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/cubits/recover_cubit.dart index d0aca321a02..2e7ffb80e8f 100644 --- a/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/cubits/recover_cubit.dart +++ b/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/cubits/recover_cubit.dart @@ -1,6 +1,7 @@ // ignore_for_file: one_member_abstracts import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; +import 'package:catalyst_voices_blocs/src/registration/cubits/unlock_password_manager.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:catalyst_voices_services/catalyst_voices_services.dart'; import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; @@ -11,16 +12,18 @@ import 'package:result_type/result_type.dart'; final _logger = Logger('RecoverCubit'); -abstract interface class RecoverManager { +abstract interface class RecoverManager implements UnlockPasswordManager { Future checkLocalKeychains(); void setSeedPhraseWords(List words); Future recoverAccount(); + + Future createKeychain(); } final class RecoverCubit extends Cubit - with BlocErrorEmitterMixin + with BlocErrorEmitterMixin, UnlockPasswordMixin implements RecoverManager { final RegistrationService _registrationService; @@ -99,6 +102,39 @@ final class RecoverCubit extends Cubit emit(state.copyWith(accountDetails: Optional(Failure(exception)))); } } + + @override + Future createKeychain() async { + try { + final seedPhrase = _seedPhrase; + final password = this.password; + + if (seedPhrase == null) { + throw const LocalizedRegistrationSeedPhraseNotFoundException(); + } + if (password.isNotValid) { + throw const LocalizedRegistrationUnlockPasswordNotFoundException(); + } + + await _registrationService.createKeychain( + seedPhrase: seedPhrase, + unlockPassword: password.value, + ); + + return true; + } catch (error, stack) { + _logger.severe('Create keychain', error, stack); + + emitError(error); + + return false; + } + } + + @override + void onUnlockPasswordStateChanged(UnlockPasswordState data) { + emit(state.copyWith(unlockPasswordState: data)); + } } const _testWords = [ diff --git a/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/cubits/unlock_password_manager.dart b/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/cubits/unlock_password_manager.dart new file mode 100644 index 00000000000..abcb0697595 --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/cubits/unlock_password_manager.dart @@ -0,0 +1,66 @@ +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; + +abstract interface class UnlockPasswordManager { + void setPassword(String value); + + void setConfirmPassword(String value); +} + +mixin UnlockPasswordMixin implements UnlockPasswordManager { + UnlockPassword get password => _state.password; + + UnlockPasswordState _state = const UnlockPasswordState(); + + void onUnlockPasswordStateChanged(UnlockPasswordState data); + + @override + void setPassword(String value) { + final password = UnlockPassword.dirty(value); + + if (_state.password != password) { + _updateState(password: password); + } + } + + @override + void setConfirmPassword(String value) { + final confirmPassword = UnlockPassword.dirty(value); + + if (_state.confirmPassword != confirmPassword) { + _updateState(confirmPassword: confirmPassword); + } + } + + void _updateState({ + UnlockPassword? password, + UnlockPassword? confirmPassword, + }) { + password ??= _state.password; + confirmPassword ??= _state.confirmPassword; + + const minimumLength = PasswordStrength.minimumLength; + + final passwordStrength = password.strength(); + final isPasswordValid = password.isValid; + + final matching = password.value == confirmPassword.value; + final hasConfirmPassword = confirmPassword.value.isNotEmpty; + + final state = UnlockPasswordState( + password: password, + confirmPassword: confirmPassword, + passwordStrength: passwordStrength, + showPasswordStrength: password.value.isNotEmpty, + minPasswordLength: minimumLength, + showPasswordMisMatch: isPasswordValid && hasConfirmPassword && !matching, + isNextEnabled: isPasswordValid && matching, + ); + + if (_state != state) { + _state = state; + onUnlockPasswordStateChanged(state); + } + } +} diff --git a/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/state_data/recover_state_data.dart b/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/state_data/recover_state_data.dart index 0a8cf6dabd7..da897021ad8 100644 --- a/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/state_data/recover_state_data.dart +++ b/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/state_data/recover_state_data.dart @@ -1,3 +1,4 @@ +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; import 'package:equatable/equatable.dart'; @@ -9,6 +10,7 @@ final class RecoverStateData extends Equatable { final List seedPhraseWords; final bool isSeedPhraseValid; final Result? accountDetails; + final UnlockPasswordState unlockPasswordState; bool get isAccountSummaryNextEnabled => accountDetails?.isSuccess ?? false; @@ -18,6 +20,7 @@ final class RecoverStateData extends Equatable { this.seedPhraseWords = const [], this.isSeedPhraseValid = false, this.accountDetails, + this.unlockPasswordState = const UnlockPasswordState(), }); RecoverStateData copyWith({ @@ -26,6 +29,7 @@ final class RecoverStateData extends Equatable { List? seedPhraseWords, bool? isSeedPhraseValid, Optional>? accountDetails, + UnlockPasswordState? unlockPasswordState, }) { return RecoverStateData( foundKeychain: foundKeychain ?? this.foundKeychain, @@ -33,6 +37,7 @@ final class RecoverStateData extends Equatable { seedPhraseWords: seedPhraseWords ?? this.seedPhraseWords, isSeedPhraseValid: isSeedPhraseValid ?? this.isSeedPhraseValid, accountDetails: accountDetails.dataOr(this.accountDetails), + unlockPasswordState: unlockPasswordState ?? this.unlockPasswordState, ); } @@ -45,6 +50,7 @@ final class RecoverStateData extends Equatable { '${seedPhraseWords.length}, ' '$isSeedPhraseValid, ' '$accountDetails, ' + '$unlockPasswordState, ' ')'; } @@ -55,6 +61,7 @@ final class RecoverStateData extends Equatable { seedPhraseWords, isSeedPhraseValid, accountDetails, + unlockPasswordState, ]; } diff --git a/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/state_data/unlock_password_state.dart b/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/state_data/unlock_password_state.dart index 54641222a55..57090e6d1d7 100644 --- a/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/state_data/unlock_password_state.dart +++ b/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/state_data/unlock_password_state.dart @@ -1,9 +1,10 @@ import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; import 'package:equatable/equatable.dart'; final class UnlockPasswordState extends Equatable { - final String password; - final String confirmPassword; + final UnlockPassword password; + final UnlockPassword confirmPassword; final PasswordStrength passwordStrength; final bool showPasswordStrength; final int minPasswordLength; @@ -11,8 +12,8 @@ final class UnlockPasswordState extends Equatable { final bool isNextEnabled; const UnlockPasswordState({ - this.password = '', - this.confirmPassword = '', + this.password = const UnlockPassword.pure(), + this.confirmPassword = const UnlockPassword.pure(), this.passwordStrength = PasswordStrength.weak, this.showPasswordStrength = false, this.minPasswordLength = PasswordStrength.minimumLength, @@ -21,8 +22,8 @@ final class UnlockPasswordState extends Equatable { }); UnlockPasswordState copyWith({ - String? password, - String? confirmPassword, + UnlockPassword? password, + UnlockPassword? confirmPassword, PasswordStrength? passwordStrength, bool? showPasswordStrength, int? minPasswordLength, diff --git a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart index 1c9bd82310b..cf7738485b6 100644 --- a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart +++ b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart @@ -1647,6 +1647,18 @@ abstract class VoicesLocalizations { /// In en, this message translates to: /// **'Set unlock password for this device'** String get recoveryAccountDetailsAction; + + /// No description provided for @recoveryUnlockPasswordInstructionsTitle. + /// + /// In en, this message translates to: + /// **'Set your Catalyst unlock password f
or this device'** + String get recoveryUnlockPasswordInstructionsTitle; + + /// No description provided for @recoveryUnlockPasswordInstructionsSubtitle. + /// + /// In en, this message translates to: + /// **'With over 300 trillion possible combinations, your 12 word seed phrase is great for keeping your account safe. 

But it can be a bit tedious to enter every single time you want to use the app. 

In this next step, you\'ll set your Unlock Password for your current device. It\'s like a shortcut for proving ownership of your Keychain. 

Whenever you recover your account for the first time on a new device, you\'ll need to use your Catalyst Keychain to get started. Every time after that, you can use your Unlock Password to quickly regain access.'** + String get recoveryUnlockPasswordInstructionsSubtitle; } class _VoicesLocalizationsDelegate extends LocalizationsDelegate { diff --git a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart index 25d2ed58473..f183349f047 100644 --- a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart +++ b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart @@ -864,4 +864,10 @@ class VoicesLocalizationsEn extends VoicesLocalizations { @override String get recoveryAccountDetailsAction => 'Set unlock password for this device'; + + @override + String get recoveryUnlockPasswordInstructionsTitle => 'Set your Catalyst unlock password f
or this device'; + + @override + String get recoveryUnlockPasswordInstructionsSubtitle => 'With over 300 trillion possible combinations, your 12 word seed phrase is great for keeping your account safe. 

But it can be a bit tedious to enter every single time you want to use the app. 

In this next step, you\'ll set your Unlock Password for your current device. It\'s like a shortcut for proving ownership of your Keychain. 

Whenever you recover your account for the first time on a new device, you\'ll need to use your Catalyst Keychain to get started. Every time after that, you can use your Unlock Password to quickly regain access.'; } diff --git a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart index 6dc937016a1..b60bb8d8449 100644 --- a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart +++ b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart @@ -864,4 +864,10 @@ class VoicesLocalizationsEs extends VoicesLocalizations { @override String get recoveryAccountDetailsAction => 'Set unlock password for this device'; + + @override + String get recoveryUnlockPasswordInstructionsTitle => 'Set your Catalyst unlock password f
or this device'; + + @override + String get recoveryUnlockPasswordInstructionsSubtitle => 'With over 300 trillion possible combinations, your 12 word seed phrase is great for keeping your account safe. 

But it can be a bit tedious to enter every single time you want to use the app. 

In this next step, you\'ll set your Unlock Password for your current device. It\'s like a shortcut for proving ownership of your Keychain. 

Whenever you recover your account for the first time on a new device, you\'ll need to use your Catalyst Keychain to get started. Every time after that, you can use your Unlock Password to quickly regain access.'; } diff --git a/catalyst_voices/packages/catalyst_voices_localization/lib/l10n/intl_en.arb b/catalyst_voices/packages/catalyst_voices_localization/lib/l10n/intl_en.arb index 1bb5e4814ba..9e75ac287b4 100644 --- a/catalyst_voices/packages/catalyst_voices_localization/lib/l10n/intl_en.arb +++ b/catalyst_voices/packages/catalyst_voices_localization/lib/l10n/intl_en.arb @@ -879,5 +879,7 @@ "recoverySeedPhraseInputSubtitle": "Enter each word of your Catalyst Key in the right order \u2028to bring your Catalyst account to this device.", "recoveryAccountTitle": "Catalyst account recovery", "recoveryAccountSuccessTitle": "Keychain recovered successfully!", - "recoveryAccountDetailsAction": "Set unlock password for this device" + "recoveryAccountDetailsAction": "Set unlock password for this device", + "recoveryUnlockPasswordInstructionsTitle": "Set your Catalyst unlock password f\u2028or this device", + "recoveryUnlockPasswordInstructionsSubtitle": "With over 300 trillion possible combinations, your 12 word seed phrase is great for keeping your account safe. \u2028\u2028But it can be a bit tedious to enter every single time you want to use the app. \u2028\u2028In this next step, you'll set your Unlock Password for your current device. It's like a shortcut for proving ownership of your Keychain. \u2028\u2028Whenever you recover your account for the first time on a new device, you'll need to use your Catalyst Keychain to get started. Every time after that, you can use your Unlock Password to quickly regain access." } \ No newline at end of file diff --git a/catalyst_voices/packages/catalyst_voices_view_models/lib/src/authentication/authentication.dart b/catalyst_voices/packages/catalyst_voices_view_models/lib/src/authentication/authentication.dart index 31cc892b7de..6206c2555dc 100644 --- a/catalyst_voices/packages/catalyst_voices_view_models/lib/src/authentication/authentication.dart +++ b/catalyst_voices/packages/catalyst_voices_view_models/lib/src/authentication/authentication.dart @@ -1,2 +1,3 @@ export 'email.dart'; export 'password.dart'; +export 'unlock_password.dart'; diff --git a/catalyst_voices/packages/catalyst_voices_view_models/lib/src/authentication/unlock_password.dart b/catalyst_voices/packages/catalyst_voices_view_models/lib/src/authentication/unlock_password.dart new file mode 100644 index 00000000000..9e6a8c816cf --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_view_models/lib/src/authentication/unlock_password.dart @@ -0,0 +1,23 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:formz/formz.dart'; + +final class UnlockPassword extends FormzInput { + const UnlockPassword.dirty([super.value = '']) : super.dirty(); + + const UnlockPassword.pure([super.value = '']) : super.pure(); + + PasswordStrength strength() { + return PasswordStrength.calculate(value); + } + + @override + UnlockPasswordError? validator(String value) { + if (value.length < PasswordStrength.minimumLength) { + return UnlockPasswordError.tooShort; + } + + return null; + } +} + +enum UnlockPasswordError { tooShort }