From 4e7d4d6e53b003f274018ed8b411d9fd4d58417a Mon Sep 17 00:00:00 2001 From: Damian Molinski Date: Wed, 2 Oct 2024 11:41:42 +0200 Subject: [PATCH 1/8] chore: wip --- .../create_keychain_panel.dart | 4 ++-- .../stage/seed_phrase_check_panel.dart | 2 +- .../stage/unlock_password_panel.dart | 21 +++++++++++++++++++ .../registration/registration_info_panel.dart | 7 +++++-- .../catalyst_voices_localizations.dart | 12 +++++++++++ .../catalyst_voices_localizations_en.dart | 6 ++++++ .../catalyst_voices_localizations_es.dart | 6 ++++++ .../lib/l10n/intl_en.arb | 4 +++- 8 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 catalyst_voices/lib/pages/registration/create_keychain/stage/unlock_password_panel.dart diff --git a/catalyst_voices/lib/pages/registration/create_keychain/create_keychain_panel.dart b/catalyst_voices/lib/pages/registration/create_keychain/create_keychain_panel.dart index 7a52d1d9050..dedce824af8 100644 --- a/catalyst_voices/lib/pages/registration/create_keychain/create_keychain_panel.dart +++ b/catalyst_voices/lib/pages/registration/create_keychain/create_keychain_panel.dart @@ -5,7 +5,7 @@ import 'package:catalyst_voices/pages/registration/create_keychain/stage/seed_ph import 'package:catalyst_voices/pages/registration/create_keychain/stage/seed_phrase_panel.dart'; import 'package:catalyst_voices/pages/registration/create_keychain/stage/splash_panel.dart'; import 'package:catalyst_voices/pages/registration/create_keychain/stage/unlock_password_instructions_panel.dart'; -import 'package:catalyst_voices/pages/registration/placeholder_panel.dart'; +import 'package:catalyst_voices/pages/registration/create_keychain/stage/unlock_password_panel.dart'; import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:flutter/material.dart'; @@ -40,7 +40,7 @@ class CreateKeychainPanel extends StatelessWidget { ), CreateKeychainStage.unlockPasswordInstructions => const UnlockPasswordInstructionsPanel(), - CreateKeychainStage.unlockPasswordCreate => const PlaceholderPanel(), + CreateKeychainStage.unlockPasswordCreate => const UnlockPasswordPanel(), }; } } diff --git a/catalyst_voices/lib/pages/registration/create_keychain/stage/seed_phrase_check_panel.dart b/catalyst_voices/lib/pages/registration/create_keychain/stage/seed_phrase_check_panel.dart index 81a2933a92e..c4751e516b1 100644 --- a/catalyst_voices/lib/pages/registration/create_keychain/stage/seed_phrase_check_panel.dart +++ b/catalyst_voices/lib/pages/registration/create_keychain/stage/seed_phrase_check_panel.dart @@ -38,7 +38,7 @@ class _SeedPhraseCheckPanelState extends State { super.initState(); _updateSeedPhraseWords(); - _updateUserWords(); + _updateUserWords(_seedPhraseWords); } @override 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 new file mode 100644 index 00000000000..c82026de1e7 --- /dev/null +++ b/catalyst_voices/lib/pages/registration/create_keychain/stage/unlock_password_panel.dart @@ -0,0 +1,21 @@ +import 'package:catalyst_voices/pages/registration/registration_stage_navigation.dart'; +import 'package:flutter/material.dart'; + +class UnlockPasswordPanel extends StatelessWidget { + const UnlockPasswordPanel({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return const Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox(height: 24), + Spacer(), + SizedBox(height: 10), + RegistrationBackNextNavigation(isNextEnabled: false), + ], + ); + } +} diff --git a/catalyst_voices/lib/pages/registration/registration_info_panel.dart b/catalyst_voices/lib/pages/registration/registration_info_panel.dart index 05ff377caa4..5910295145a 100644 --- a/catalyst_voices/lib/pages/registration/registration_info_panel.dart +++ b/catalyst_voices/lib/pages/registration/registration_info_panel.dart @@ -70,8 +70,11 @@ class RegistrationInfoPanel extends StatelessWidget { CreateKeychainStage.checkSeedPhraseResult || CreateKeychainStage.unlockPasswordInstructions => _HeaderStrings(title: context.l10n.catalystKeychain), - CreateKeychainStage.unlockPasswordCreate => - _HeaderStrings(title: 'TODO'), + CreateKeychainStage.unlockPasswordCreate => _HeaderStrings( + title: context.l10n.catalystKeychain, + subtitle: context.l10n.createKeychainUnlockPasswordIntoSubtitle, + body: context.l10n.createKeychainUnlockPasswordIntoBody, + ), }; } 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 3d45330bd03..e530fbdf5b7 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 @@ -1125,6 +1125,18 @@ abstract class VoicesLocalizations { /// 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 createKeychainUnlockPasswordInstructionsSubtitle; + + /// No description provided for @createKeychainUnlockPasswordIntoSubtitle. + /// + /// In en, this message translates to: + /// **'Catalyst unlock password'** + String get createKeychainUnlockPasswordIntoSubtitle; + + /// No description provided for @createKeychainUnlockPasswordIntoBody. + /// + /// In en, this message translates to: + /// **'Please provide a password for your Catalyst Keychain.'** + String get createKeychainUnlockPasswordIntoBody; } 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 4b95551381b..a10ea5c1904 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 @@ -578,4 +578,10 @@ class VoicesLocalizationsEn extends VoicesLocalizations { @override String get createKeychainUnlockPasswordInstructionsSubtitle => '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.'; + + @override + String get createKeychainUnlockPasswordIntoSubtitle => 'Catalyst unlock password'; + + @override + String get createKeychainUnlockPasswordIntoBody => 'Please provide a password for your Catalyst Keychain.'; } 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 87b8823f5d8..3bf8c6ce850 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 @@ -578,4 +578,10 @@ class VoicesLocalizationsEs extends VoicesLocalizations { @override String get createKeychainUnlockPasswordInstructionsSubtitle => '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.'; + + @override + String get createKeychainUnlockPasswordIntoSubtitle => 'Catalyst unlock password'; + + @override + String get createKeychainUnlockPasswordIntoBody => 'Please provide a password for your Catalyst Keychain.'; } 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 323b35c44fb..a511c6a447e 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 @@ -651,5 +651,7 @@ "yourNextStep": "Your next step", "createKeychainSeedPhraseCheckSuccessNextStep": "Now let’s set your Unlock password for this device!", "createKeychainUnlockPasswordInstructionsTitle": "Set your Catalyst unlock password \u2028for this device", - "createKeychainUnlockPasswordInstructionsSubtitle": "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." + "createKeychainUnlockPasswordInstructionsSubtitle": "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.", + "createKeychainUnlockPasswordIntoSubtitle": "Catalyst unlock password", + "createKeychainUnlockPasswordIntoBody": "Please provide a password for your Catalyst Keychain." } \ No newline at end of file From 659f16f20c4e9beb0d24ed62ead306a778e77c35 Mon Sep 17 00:00:00 2001 From: Damian Molinski Date: Wed, 2 Oct 2024 16:01:54 +0200 Subject: [PATCH 2/8] chore: wip --- .../common/formatters/input_formatters.dart | 6 + .../login/login_password_text_field.dart | 9 ++ .../create_keychain_panel.dart | 6 +- .../stage/unlock_password_panel.dart | 124 ++++++++++++++++-- .../registration/registration_dialog.dart | 9 +- .../registration/registration_info_panel.dart | 2 +- .../voices_password_strength_indicator.dart | 4 +- .../voices_password_text_field.dart | 37 ++++-- .../widgets/text_field/voices_text_field.dart | 16 +++ .../cubits/keychain_creation_cubit.dart | 64 +++++++-- .../lib/src/registration/registration.dart | 1 + .../src/registration/registration_cubit.dart | 8 ++ .../src/registration/registration_state.dart | 16 ++- .../registration/unlock_password_state.dart | 43 ++++++ .../catalyst_voices_localizations.dart | 24 ++++ .../catalyst_voices_localizations_en.dart | 14 ++ .../catalyst_voices_localizations_es.dart | 14 ++ .../lib/l10n/intl_en.arb | 16 ++- .../lib/src/auth/password_strength.dart | 4 +- .../formatters/input_formatters_test.dart | 38 ++++++ 20 files changed, 408 insertions(+), 47 deletions(-) create mode 100644 catalyst_voices/lib/common/formatters/input_formatters.dart create mode 100644 catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/unlock_password_state.dart create mode 100644 catalyst_voices/test/common/formatters/input_formatters_test.dart diff --git a/catalyst_voices/lib/common/formatters/input_formatters.dart b/catalyst_voices/lib/common/formatters/input_formatters.dart new file mode 100644 index 00000000000..43e6a347284 --- /dev/null +++ b/catalyst_voices/lib/common/formatters/input_formatters.dart @@ -0,0 +1,6 @@ +import 'package:flutter/services.dart'; + +/// Removes any whitespaces. +final class NoWhitespacesFormatter extends FilteringTextInputFormatter { + NoWhitespacesFormatter() : super.deny(RegExp(r'\s+')); +} diff --git a/catalyst_voices/lib/pages/login/login_password_text_field.dart b/catalyst_voices/lib/pages/login/login_password_text_field.dart index aa165e2d109..bf4f1b7ecce 100644 --- a/catalyst_voices/lib/pages/login/login_password_text_field.dart +++ b/catalyst_voices/lib/pages/login/login_password_text_field.dart @@ -1,5 +1,6 @@ import 'package:catalyst_voices/widgets/widgets.dart'; import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -15,9 +16,17 @@ final class LoginPasswordTextField extends StatelessWidget { return BlocBuilder( buildWhen: (previous, current) => previous.password != current.password, builder: (context, state) { + final l10n = context.l10n; + return VoicesPasswordTextField( key: passwordInputKey, onChanged: (password) => _onPasswordChanged(context, password), + decoration: VoicesTextFieldDecoration( + errorMaxLines: 2, + labelText: l10n.passwordLabelText, + hintText: l10n.passwordHintText, + errorText: l10n.passwordErrorText, + ), ); }, ); diff --git a/catalyst_voices/lib/pages/registration/create_keychain/create_keychain_panel.dart b/catalyst_voices/lib/pages/registration/create_keychain/create_keychain_panel.dart index dedce824af8..fdf43f7a9d5 100644 --- a/catalyst_voices/lib/pages/registration/create_keychain/create_keychain_panel.dart +++ b/catalyst_voices/lib/pages/registration/create_keychain/create_keychain_panel.dart @@ -13,11 +13,13 @@ import 'package:flutter/material.dart'; class CreateKeychainPanel extends StatelessWidget { final CreateKeychainStage stage; final SeedPhraseState seedPhraseState; + final UnlockPasswordState unlockPasswordState; const CreateKeychainPanel({ super.key, required this.stage, required this.seedPhraseState, + required this.unlockPasswordState, }); @override @@ -40,7 +42,9 @@ class CreateKeychainPanel extends StatelessWidget { ), CreateKeychainStage.unlockPasswordInstructions => const UnlockPasswordInstructionsPanel(), - CreateKeychainStage.unlockPasswordCreate => const UnlockPasswordPanel(), + CreateKeychainStage.unlockPasswordCreate => UnlockPasswordPanel( + data: unlockPasswordState, + ), }; } } 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 c82026de1e7..2f342e541bc 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,21 +1,127 @@ import 'package:catalyst_voices/pages/registration/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_blocs/catalyst_voices_blocs.dart'; +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; import 'package:flutter/material.dart'; -class UnlockPasswordPanel extends StatelessWidget { +class UnlockPasswordPanel extends StatefulWidget { + final UnlockPasswordState data; + const UnlockPasswordPanel({ super.key, + required this.data, + }); + + @override + State createState() => _UnlockPasswordPanelState(); +} + +class _UnlockPasswordPanelState extends State { + late final TextEditingController _passwordController; + late final TextEditingController _confirmPasswordController; + + @override + void initState() { + super.initState(); + + _passwordController = TextEditingController() + ..addListener(_onPasswordChanged); + _confirmPasswordController = TextEditingController() + ..addListener(_onConfirmPasswordChanged); + } + + @override + void dispose() { + _passwordController.dispose(); + _confirmPasswordController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AutofillGroup( + onDisposeAction: AutofillContextAction.cancel, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 24), + const SizedBox(height: 12), + _EnterPasswordTextField( + controller: _passwordController, + ), + const SizedBox(height: 12), + _ConfirmPasswordTextField( + controller: _confirmPasswordController, + showError: widget.data.showPasswordMisMatch, + minimumLength: widget.data.minPasswordLength, + ), + const Spacer(), + const SizedBox(height: 22), + if (widget.data.showPasswordStrength) + VoicesPasswordStrengthIndicator( + passwordStrength: widget.data.passwordStrength, + ), + const SizedBox(height: 22), + RegistrationBackNextNavigation( + isNextEnabled: widget.data.isNextEnabled, + ), + ], + ), + ); + } + + void _onPasswordChanged() { + final password = _passwordController.text; + RegistrationCubit.of(context).setPassword(password); + } + + void _onConfirmPasswordChanged() { + final confirmPassword = _confirmPasswordController.text; + RegistrationCubit.of(context).setConfirmPassword(confirmPassword); + } +} + +class _EnterPasswordTextField extends StatelessWidget { + final TextEditingController controller; + + const _EnterPasswordTextField({ + required this.controller, + }); + + @override + Widget build(BuildContext context) { + return VoicesPasswordTextField( + controller: controller, + textInputAction: TextInputAction.next, + decoration: VoicesTextFieldDecoration( + labelText: context.l10n.enterPassword, + ), + ); + } +} + +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 const Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - SizedBox(height: 24), - Spacer(), - SizedBox(height: 10), - RegistrationBackNextNavigation(isNextEnabled: false), - ], + return VoicesPasswordTextField( + controller: controller, + decoration: VoicesTextFieldDecoration( + labelText: context.l10n.confirmPassword, + helperText: context.l10n.xCharactersMinimum(minimumLength), + errorText: showError ? context.l10n.passwordDoNotMatch : null, + ), ); } } diff --git a/catalyst_voices/lib/pages/registration/registration_dialog.dart b/catalyst_voices/lib/pages/registration/registration_dialog.dart index 33ef92bda13..2b5b8d4ea35 100644 --- a/catalyst_voices/lib/pages/registration/registration_dialog.dart +++ b/catalyst_voices/lib/pages/registration/registration_dialog.dart @@ -47,10 +47,15 @@ class _RegistrationDialog extends StatelessWidget { GetStarted() => const GetStartedPanel(), FinishAccountCreation() => const PlaceholderPanel(), Recover() => const Placeholder(), - CreateKeychain(:final stage, :final seedPhraseState) => + CreateKeychain( + :final stage, + :final seedPhrase, + :final unlockPassword, + ) => CreateKeychainPanel( stage: stage, - seedPhraseState: seedPhraseState, + seedPhraseState: seedPhrase, + unlockPasswordState: unlockPassword, ), WalletLink(:final stage, :final stateData) => WalletLinkPanel( stage: stage, diff --git a/catalyst_voices/lib/pages/registration/registration_info_panel.dart b/catalyst_voices/lib/pages/registration/registration_info_panel.dart index 5910295145a..58d5231d9e6 100644 --- a/catalyst_voices/lib/pages/registration/registration_info_panel.dart +++ b/catalyst_voices/lib/pages/registration/registration_info_panel.dart @@ -162,7 +162,7 @@ class _RegistrationPicture extends StatelessWidget { GetStarted() => const KeychainPicture(), FinishAccountCreation() => const KeychainPicture(), Recover() => const KeychainPicture(), - CreateKeychain(:final stage, :final seedPhraseState) => + CreateKeychain(:final stage, seedPhrase: final seedPhraseState) => buildKeychainStagePicture( stage, isSeedPhraseCheckConfirmed: seedPhraseState.isCheckConfirmed, diff --git a/catalyst_voices/lib/widgets/indicators/voices_password_strength_indicator.dart b/catalyst_voices/lib/widgets/indicators/voices_password_strength_indicator.dart index 68333b325d4..41be4d15bd4 100644 --- a/catalyst_voices/lib/widgets/indicators/voices_password_strength_indicator.dart +++ b/catalyst_voices/lib/widgets/indicators/voices_password_strength_indicator.dart @@ -42,7 +42,9 @@ class _Label extends StatelessWidget { PasswordStrength.normal => context.l10n.normalPasswordStrength, PasswordStrength.strong => context.l10n.goodPasswordStrength, }, - style: Theme.of(context).textTheme.bodySmall, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + fontWeight: FontWeight.w700, + ), ); } } diff --git a/catalyst_voices/lib/widgets/text_field/voices_password_text_field.dart b/catalyst_voices/lib/widgets/text_field/voices_password_text_field.dart index e2588d06205..e2313703226 100644 --- a/catalyst_voices/lib/widgets/text_field/voices_password_text_field.dart +++ b/catalyst_voices/lib/widgets/text_field/voices_password_text_field.dart @@ -1,33 +1,42 @@ +import 'package:catalyst_voices/common/formatters/input_formatters.dart'; import 'package:catalyst_voices/widgets/text_field/voices_text_field.dart'; -import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; final class VoicesPasswordTextField extends StatelessWidget { - /// Emits new value when widget input changes + /// [VoicesTextField.controller]. + final TextEditingController? controller; + + /// [VoicesTextField.textInputAction]. + final TextInputAction textInputAction; + + /// Emits new value when widget input changes. final ValueChanged? onChanged; + /// Optional decoration. See [VoicesTextField] for more details. + final VoicesTextFieldDecoration? decoration; + const VoicesPasswordTextField({ super.key, + this.controller, + this.textInputAction = TextInputAction.done, this.onChanged, + this.decoration, }); @override Widget build(BuildContext context) { - final l10n = context.l10n; return VoicesTextField( - keyboardType: TextInputType.multiline, + controller: controller, + keyboardType: TextInputType.visiblePassword, obscureText: true, - textInputAction: TextInputAction.done, + textInputAction: textInputAction, onChanged: onChanged, - decoration: VoicesTextFieldDecoration( - errorMaxLines: 2, - labelText: l10n.passwordLabelText, - hintText: l10n.passwordHintText, - errorText: l10n.passwordErrorText, - ), - style: const TextStyle( - fontWeight: FontWeight.w500, - ), + decoration: decoration, + inputFormatters: [ + FilteringTextInputFormatter.singleLineFormatter, + NoWhitespacesFormatter(), + ], ); } } diff --git a/catalyst_voices/lib/widgets/text_field/voices_text_field.dart b/catalyst_voices/lib/widgets/text_field/voices_text_field.dart index d1a46ca5c62..0c244f87f3a 100644 --- a/catalyst_voices/lib/widgets/text_field/voices_text_field.dart +++ b/catalyst_voices/lib/widgets/text_field/voices_text_field.dart @@ -4,6 +4,7 @@ import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; /// A replacement for [TextField] and [TextFormField] /// that is pre-configured to use Project Catalyst theming. @@ -58,6 +59,15 @@ class VoicesTextField extends StatefulWidget { /// [TextField.onChanged] final ValueChanged? onChanged; + /// [TextField.onSubmitted] + final ValueChanged? onFieldSubmitted; + + /// [FormField.onSaved] + final FormFieldSetter? onSaved; + + /// [TextField.inputFormatters] + final List? inputFormatters; + const VoicesTextField({ super.key, this.controller, @@ -75,6 +85,9 @@ class VoicesTextField extends StatefulWidget { this.validator, this.onChanged, this.resizable, + this.onFieldSubmitted, + this.onSaved, + this.inputFormatters, }); @override @@ -163,6 +176,9 @@ class _VoicesTextFieldState extends State { expands: resizable, controller: _obtainController(), focusNode: widget.focusNode, + onFieldSubmitted: widget.onFieldSubmitted, + onSaved: widget.onSaved, + inputFormatters: widget.inputFormatters, decoration: InputDecoration( filled: widget.decoration?.filled, fillColor: widget.decoration?.fillColor, 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 bf2718e9b81..9062335562f 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 @@ -13,16 +13,25 @@ final _logger = Logger('KeychainCreationCubit'); final class KeychainCreationCubit extends Cubit { final Downloader _downloader; + String _password = ''; + String _confirmPassword = ''; + KeychainCreationCubit({ required Downloader downloader, }) : _downloader = downloader, super(const CreateKeychain()); - set _seedPhraseState(SeedPhraseState newValue) { - emit(state.copyWith(seedPhraseState: newValue)); + set _seedPhrase(SeedPhraseState newValue) { + emit(state.copyWith(seedPhrase: newValue)); + } + + SeedPhraseState get _seedPhrase => state.seedPhrase; + + set _unlockPassword(UnlockPasswordState newValue) { + emit(state.copyWith(unlockPassword: newValue)); } - SeedPhraseState get _seedPhraseState => state.seedPhraseState; + UnlockPasswordState get _unlockPassword => state.unlockPassword; void changeStage(CreateKeychainStage newValue) { if (state.stage != newValue) { @@ -32,34 +41,33 @@ final class KeychainCreationCubit extends Cubit { void buildSeedPhrase() { final seedPhrase = SeedPhrase(); - _seedPhraseState = _seedPhraseState.copyWith( + _seedPhrase = _seedPhrase.copyWith( seedPhrase: Optional.of(seedPhrase), ); } void ensureSeedPhraseCreated() { - if (_seedPhraseState.seedPhrase == null) { + if (_seedPhrase.seedPhrase == null) { buildSeedPhrase(); } } void setSeedPhraseStoredConfirmed(bool newValue) { - if (_seedPhraseState.isStoredConfirmed != newValue) { - _seedPhraseState = _seedPhraseState.copyWith(isStoredConfirmed: newValue); + if (_seedPhrase.isStoredConfirmed != newValue) { + _seedPhrase = _seedPhrase.copyWith(isStoredConfirmed: newValue); } } void setSeedPhraseCheckConfirmed({ required bool isConfirmed, }) { - if (_seedPhraseState.isCheckConfirmed != isConfirmed) { - _seedPhraseState = - _seedPhraseState.copyWith(isCheckConfirmed: isConfirmed); + if (_seedPhrase.isCheckConfirmed != isConfirmed) { + _seedPhrase = _seedPhrase.copyWith(isCheckConfirmed: isConfirmed); } } Future downloadSeedPhrase() async { - final mnemonic = _seedPhraseState.seedPhrase?.mnemonic; + final mnemonic = _seedPhrase.seedPhrase?.mnemonic; if (mnemonic == null) { throw StateError('SeedPhrase is not generated. Make sure it exits first'); } @@ -90,6 +98,40 @@ final class KeychainCreationCubit extends Cubit { } } + void setPassword(String newValue) { + if (_password != newValue) { + _password = newValue; + _updateUnlockPasswordState(); + } + } + + void setConfirmPassword(String newValue) { + if (_confirmPassword != newValue) { + _confirmPassword = newValue; + _updateUnlockPasswordState(); + } + } + + void _updateUnlockPasswordState() { + final password = _password; + final confirmPassword = _confirmPassword; + + const minimumLength = PasswordStrength.minimumLength; + + final passwordStrength = PasswordStrength.calculate(password); + final correctLength = password.length >= minimumLength; + final matching = password == confirmPassword; + final hasConfirmPassword = confirmPassword.isNotEmpty; + + _unlockPassword = _unlockPassword.copyWith( + passwordStrength: passwordStrength, + showPasswordStrength: password.isNotEmpty, + minPasswordLength: minimumLength, + showPasswordMisMatch: correctLength && hasConfirmPassword && !matching, + isNextEnabled: correctLength && matching, + ); + } + CreateKeychainStep? nextStep() { final currentStageIndex = CreateKeychainStage.values.indexOf(state.stage); final isLast = currentStageIndex == CreateKeychainStage.values.length - 1; diff --git a/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/registration.dart b/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/registration.dart index f8082d11844..f3bdc91a2a1 100644 --- a/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/registration.dart +++ b/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/registration.dart @@ -1,4 +1,5 @@ export 'registration_cubit.dart'; export 'registration_state.dart'; export 'seed_phrase_state.dart'; +export 'unlock_password_state.dart'; export 'wallet_link_state_data.dart'; diff --git a/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/registration_cubit.dart b/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/registration_cubit.dart index a8fae37063f..e31c916e762 100644 --- a/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/registration_cubit.dart +++ b/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/registration_cubit.dart @@ -88,6 +88,14 @@ final class RegistrationCubit extends Cubit { _keychainCreationCubit.setSeedPhraseStoredConfirmed(confirmed); } + void setPassword(String newValue) { + _keychainCreationCubit.setPassword(newValue); + } + + void setConfirmPassword(String newValue) { + _keychainCreationCubit.setConfirmPassword(newValue); + } + void refreshWallets() { unawaited(_walletLinkCubit.refreshWallets()); } diff --git a/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/registration_state.dart b/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/registration_state.dart index f4e547427c2..29c0efa6e54 100644 --- a/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/registration_state.dart +++ b/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/registration_state.dart @@ -1,4 +1,5 @@ import 'package:catalyst_voices_blocs/src/registration/seed_phrase_state.dart'; +import 'package:catalyst_voices_blocs/src/registration/unlock_password_state.dart'; import 'package:catalyst_voices_blocs/src/registration/wallet_link_state_data.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:equatable/equatable.dart'; @@ -56,11 +57,13 @@ sealed class CreateNew extends RegistrationState { /// Building up information for creating new Keychain. final class CreateKeychain extends CreateNew { final CreateKeychainStage stage; - final SeedPhraseState seedPhraseState; + final SeedPhraseState seedPhrase; + final UnlockPasswordState unlockPassword; const CreateKeychain({ this.stage = CreateKeychainStage.splash, - this.seedPhraseState = const SeedPhraseState(), + this.seedPhrase = const SeedPhraseState(), + this.unlockPassword = const UnlockPasswordState(), }); @override @@ -78,16 +81,19 @@ final class CreateKeychain extends CreateNew { super.props + [ stage, - seedPhraseState, + seedPhrase, + unlockPassword, ]; CreateKeychain copyWith({ CreateKeychainStage? stage, - SeedPhraseState? seedPhraseState, + SeedPhraseState? seedPhrase, + UnlockPasswordState? unlockPassword, }) { return CreateKeychain( stage: stage ?? this.stage, - seedPhraseState: seedPhraseState ?? this.seedPhraseState, + seedPhrase: seedPhrase ?? this.seedPhrase, + unlockPassword: unlockPassword ?? this.unlockPassword, ); } } diff --git a/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/unlock_password_state.dart b/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/unlock_password_state.dart new file mode 100644 index 00000000000..650e93db100 --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/unlock_password_state.dart @@ -0,0 +1,43 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:equatable/equatable.dart'; + +final class UnlockPasswordState extends Equatable { + final PasswordStrength passwordStrength; + final bool showPasswordStrength; + final int minPasswordLength; + final bool showPasswordMisMatch; + final bool isNextEnabled; + + const UnlockPasswordState({ + this.passwordStrength = PasswordStrength.weak, + this.showPasswordStrength = false, + this.minPasswordLength = PasswordStrength.minimumLength, + this.showPasswordMisMatch = false, + this.isNextEnabled = false, + }); + + UnlockPasswordState copyWith({ + PasswordStrength? passwordStrength, + bool? showPasswordStrength, + int? minPasswordLength, + bool? showPasswordMisMatch, + bool? isNextEnabled, + }) { + return UnlockPasswordState( + passwordStrength: passwordStrength ?? this.passwordStrength, + showPasswordStrength: showPasswordStrength ?? this.showPasswordStrength, + minPasswordLength: minPasswordLength ?? this.minPasswordLength, + showPasswordMisMatch: showPasswordMisMatch ?? this.showPasswordMisMatch, + isNextEnabled: isNextEnabled ?? this.isNextEnabled, + ); + } + + @override + List get props => [ + passwordStrength, + showPasswordStrength, + minPasswordLength, + showPasswordMisMatch, + isNextEnabled, + ]; +} 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 e530fbdf5b7..347cd360ff6 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 @@ -1137,6 +1137,30 @@ abstract class VoicesLocalizations { /// In en, this message translates to: /// **'Please provide a password for your Catalyst Keychain.'** String get createKeychainUnlockPasswordIntoBody; + + /// No description provided for @enterPassword. + /// + /// In en, this message translates to: + /// **'Enter password'** + String get enterPassword; + + /// No description provided for @confirmPassword. + /// + /// In en, this message translates to: + /// **'Confirm password'** + String get confirmPassword; + + /// No description provided for @xCharactersMinimum. + /// + /// In en, this message translates to: + /// **'{number} characters minimum length'** + String xCharactersMinimum(int number); + + /// When user confirms password but it does not match original one. + /// + /// In en, this message translates to: + /// **'Passwords do not match, please correct'** + String get passwordDoNotMatch; } 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 a10ea5c1904..42999539f4d 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 @@ -584,4 +584,18 @@ class VoicesLocalizationsEn extends VoicesLocalizations { @override String get createKeychainUnlockPasswordIntoBody => 'Please provide a password for your Catalyst Keychain.'; + + @override + String get enterPassword => 'Enter password'; + + @override + String get confirmPassword => 'Confirm password'; + + @override + String xCharactersMinimum(int number) { + return '$number characters minimum length'; + } + + @override + String get passwordDoNotMatch => 'Passwords do not match, please correct'; } 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 3bf8c6ce850..db72cb36db2 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 @@ -584,4 +584,18 @@ class VoicesLocalizationsEs extends VoicesLocalizations { @override String get createKeychainUnlockPasswordIntoBody => 'Please provide a password for your Catalyst Keychain.'; + + @override + String get enterPassword => 'Enter password'; + + @override + String get confirmPassword => 'Confirm password'; + + @override + String xCharactersMinimum(int number) { + return '$number characters minimum length'; + } + + @override + String get passwordDoNotMatch => 'Passwords do not match, please correct'; } 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 a511c6a447e..88655929b7b 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 @@ -653,5 +653,19 @@ "createKeychainUnlockPasswordInstructionsTitle": "Set your Catalyst unlock password \u2028for this device", "createKeychainUnlockPasswordInstructionsSubtitle": "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.", "createKeychainUnlockPasswordIntoSubtitle": "Catalyst unlock password", - "createKeychainUnlockPasswordIntoBody": "Please provide a password for your Catalyst Keychain." + "createKeychainUnlockPasswordIntoBody": "Please provide a password for your Catalyst Keychain.", + "enterPassword": "Enter password", + "confirmPassword": "Confirm password", + "xCharactersMinimum": "{number} characters minimum length", + "@xCharactersMinimum": { + "placeholders": { + "number": { + "type": "int" + } + } + }, + "passwordDoNotMatch": "Passwords do not match, please correct", + "@passwordDoNotMatch": { + "description": "When user confirms password but it does not match original one." + } } \ No newline at end of file diff --git a/catalyst_voices/packages/catalyst_voices_models/lib/src/auth/password_strength.dart b/catalyst_voices/packages/catalyst_voices_models/lib/src/auth/password_strength.dart index 5f5c1205b3f..d8a5472c92e 100644 --- a/catalyst_voices/packages/catalyst_voices_models/lib/src/auth/password_strength.dart +++ b/catalyst_voices/packages/catalyst_voices_models/lib/src/auth/password_strength.dart @@ -14,10 +14,10 @@ enum PasswordStrength { strong; /// The minimum length of accepted password. - static const int minimumPasswordLength = 8; + static const int minimumLength = 8; factory PasswordStrength.calculate(String text) { - if (text.length < minimumPasswordLength) return PasswordStrength.weak; + if (text.length < minimumLength) return PasswordStrength.weak; final strength = ps.estimatePasswordStrength(text); if (strength <= 0.33) return PasswordStrength.weak; diff --git a/catalyst_voices/test/common/formatters/input_formatters_test.dart b/catalyst_voices/test/common/formatters/input_formatters_test.dart new file mode 100644 index 00000000000..85feb5985f2 --- /dev/null +++ b/catalyst_voices/test/common/formatters/input_formatters_test.dart @@ -0,0 +1,38 @@ +import 'package:catalyst_voices/common/formatters/input_formatters.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group(NoWhitespacesFormatter, () { + test('spaces are not allowed', () { + // Given + const oldValue = TextEditingValue(text: 'qqq'); + const newValue = TextEditingValue(text: 'qqq '); + + const expectedValue = TextEditingValue(text: 'qqq'); + + final formatter = NoWhitespacesFormatter(); + + // When + final value = formatter.formatEditUpdate(oldValue, newValue); + + // Then + expect(value, expectedValue); + }); + + test('upper case is allowed', () { + // Given + const oldValue = TextEditingValue(text: 'qqq'); + const newValue = TextEditingValue(text: 'QQQ'); + + const expectedValue = TextEditingValue(text: 'QQQ'); + + final formatter = NoWhitespacesFormatter(); + + // When + final value = formatter.formatEditUpdate(oldValue, newValue); + + // Then + expect(value, expectedValue); + }); + }); +} From 3a0b7754b748db0cccdc7639b41e8215b93f5d05 Mon Sep 17 00:00:00 2001 From: Damian Molinski Date: Thu, 3 Oct 2024 07:42:56 +0200 Subject: [PATCH 3/8] feat: mocked create keychain method --- .../create_keychain/stage/unlock_password_panel.dart | 8 ++++++++ .../pages/registration/get_started/get_started_panel.dart | 4 ++-- .../pages/registration/registration_stage_navigation.dart | 4 +++- .../src/registration/cubits/keychain_creation_cubit.dart | 3 +++ .../lib/src/registration/registration_cubit.dart | 6 ++++-- 5 files changed, 20 insertions(+), 5 deletions(-) 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 2f342e541bc..bbeadf00198 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 @@ -66,6 +66,7 @@ class _UnlockPasswordPanelState extends State { const SizedBox(height: 22), RegistrationBackNextNavigation( isNextEnabled: widget.data.isNextEnabled, + onNextTap: _createKeychain, ), ], ), @@ -81,6 +82,13 @@ class _UnlockPasswordPanelState extends State { final confirmPassword = _confirmPasswordController.text; RegistrationCubit.of(context).setConfirmPassword(confirmPassword); } + + Future _createKeychain() async { + final registrationCubit = RegistrationCubit.of(context); + + await registrationCubit.createKeychain(); + registrationCubit.nextStep(); + } } class _EnterPasswordTextField extends StatelessWidget { diff --git a/catalyst_voices/lib/pages/registration/get_started/get_started_panel.dart b/catalyst_voices/lib/pages/registration/get_started/get_started_panel.dart index 3e119694c86..4d70e68b74a 100644 --- a/catalyst_voices/lib/pages/registration/get_started/get_started_panel.dart +++ b/catalyst_voices/lib/pages/registration/get_started/get_started_panel.dart @@ -42,9 +42,9 @@ class GetStartedPanel extends StatelessWidget { onTap: () { switch (type) { case CreateAccountType.createNew: - RegistrationCubit.of(context).createNewKeychain(); + RegistrationCubit.of(context).createNewKeychainStep(); case CreateAccountType.recover: - RegistrationCubit.of(context).recoverKeychain(); + RegistrationCubit.of(context).recoverKeychainStep(); } }, ); diff --git a/catalyst_voices/lib/pages/registration/registration_stage_navigation.dart b/catalyst_voices/lib/pages/registration/registration_stage_navigation.dart index b70ee7a4e85..e86fd7dc6b8 100644 --- a/catalyst_voices/lib/pages/registration/registration_stage_navigation.dart +++ b/catalyst_voices/lib/pages/registration/registration_stage_navigation.dart @@ -6,11 +6,13 @@ import 'package:flutter/material.dart'; class RegistrationBackNextNavigation extends StatelessWidget { final bool isBackEnabled; final bool isNextEnabled; + final VoidCallback? onNextTap; const RegistrationBackNextNavigation({ super.key, this.isBackEnabled = true, this.isNextEnabled = true, + this.onNextTap, }); @override @@ -26,7 +28,7 @@ class RegistrationBackNextNavigation extends StatelessWidget { ), VoicesNextButton( onTap: isNextEnabled - ? () => RegistrationCubit.of(context).nextStep() + ? onNextTap ?? () => RegistrationCubit.of(context).nextStep() : null, ), ], 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 9062335562f..2eb550d3236 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 @@ -112,6 +112,9 @@ final class KeychainCreationCubit extends Cubit { } } + // TODO(damian-molinski): implement + Future createKeychain() async {} + void _updateUnlockPasswordState() { final password = _password; final confirmPassword = _confirmPassword; diff --git a/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/registration_cubit.dart b/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/registration_cubit.dart index e31c916e762..6f4b4644dba 100644 --- a/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/registration_cubit.dart +++ b/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/registration_cubit.dart @@ -44,14 +44,14 @@ final class RegistrationCubit extends Cubit { return super.close(); } - void createNewKeychain() { + void createNewKeychainStep() { final nextStep = _nextStep(from: const CreateKeychainStep()); if (nextStep != null) { _goToStep(nextStep); } } - void recoverKeychain() { + void recoverKeychainStep() { final nextStep = _nextStep(from: const RecoverStep()); if (nextStep != null) { _goToStep(nextStep); @@ -96,6 +96,8 @@ final class RegistrationCubit extends Cubit { _keychainCreationCubit.setConfirmPassword(newValue); } + Future createKeychain() => _keychainCreationCubit.createKeychain(); + void refreshWallets() { unawaited(_walletLinkCubit.refreshWallets()); } From 72a357ac683f8aca5e63ad67aaa26a730a48e765 Mon Sep 17 00:00:00 2001 From: Damian Molinski Date: Thu, 3 Oct 2024 07:53:11 +0200 Subject: [PATCH 4/8] feat: info panel password picture types --- .../pictures/password_picture.dart | 9 +++++-- .../registration/registration_info_panel.dart | 26 ++++++++++++++----- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/catalyst_voices/lib/pages/registration/pictures/password_picture.dart b/catalyst_voices/lib/pages/registration/pictures/password_picture.dart index 552fdd22209..163c0310a51 100644 --- a/catalyst_voices/lib/pages/registration/pictures/password_picture.dart +++ b/catalyst_voices/lib/pages/registration/pictures/password_picture.dart @@ -3,13 +3,18 @@ import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; import 'package:flutter/material.dart'; class PasswordPicture extends StatelessWidget { - const PasswordPicture({super.key}); + final TaskPictureType type; + + const PasswordPicture({ + super.key, + this.type = TaskPictureType.normal, + }); @override Widget build(BuildContext context) { return TaskPicture( child: TaskPictureIconBox( - type: TaskPictureType.error, + type: type, child: VoicesAssets.icons.lockClosed.buildIcon(size: 48), ), ); diff --git a/catalyst_voices/lib/pages/registration/registration_info_panel.dart b/catalyst_voices/lib/pages/registration/registration_info_panel.dart index 58d5231d9e6..618a88f1311 100644 --- a/catalyst_voices/lib/pages/registration/registration_info_panel.dart +++ b/catalyst_voices/lib/pages/registration/registration_info_panel.dart @@ -123,6 +123,7 @@ class _RegistrationPicture extends StatelessWidget { Widget buildKeychainStagePicture( CreateKeychainStage stage, { required bool isSeedPhraseCheckConfirmed, + required UnlockPasswordState unlockPassword, }) { return switch (stage) { CreateKeychainStage.splash || @@ -142,7 +143,9 @@ class _RegistrationPicture extends StatelessWidget { ), CreateKeychainStage.unlockPasswordInstructions || CreateKeychainStage.unlockPasswordCreate => - const PasswordPicture(), + PasswordPicture( + type: unlockPassword.pictureType, + ), }; } @@ -162,13 +165,24 @@ class _RegistrationPicture extends StatelessWidget { GetStarted() => const KeychainPicture(), FinishAccountCreation() => const KeychainPicture(), Recover() => const KeychainPicture(), - CreateKeychain(:final stage, seedPhrase: final seedPhraseState) => - buildKeychainStagePicture( - stage, - isSeedPhraseCheckConfirmed: seedPhraseState.isCheckConfirmed, - ), + CreateKeychain(:final stage, :final seedPhrase, :final unlockPassword) => + buildKeychainStagePicture(stage, + isSeedPhraseCheckConfirmed: seedPhrase.isCheckConfirmed, + unlockPassword: unlockPassword), WalletLink(:final stage) => buildWalletLinkStagePicture(stage), AccountCompleted() => const KeychainPicture(), }; } } + +extension _UnlockPasswordStateExt on UnlockPasswordState { + TaskPictureType get pictureType { + if (showPasswordMisMatch) { + return TaskPictureType.error; + } + if (isNextEnabled) { + return TaskPictureType.success; + } + return TaskPictureType.normal; + } +} From ccae72285f891d346962ef7346559740d3c3445a Mon Sep 17 00:00:00 2001 From: Damian Molinski Date: Thu, 3 Oct 2024 08:07:21 +0200 Subject: [PATCH 5/8] chore: remove autofill group --- .../stage/unlock_password_panel.dart | 53 +++++++++---------- 1 file changed, 25 insertions(+), 28 deletions(-) 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 bbeadf00198..202db2be5fd 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 @@ -41,35 +41,32 @@ class _UnlockPasswordPanelState extends State { @override Widget build(BuildContext context) { - return AutofillGroup( - onDisposeAction: AutofillContextAction.cancel, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox(height: 24), - const SizedBox(height: 12), - _EnterPasswordTextField( - controller: _passwordController, + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 24), + const SizedBox(height: 12), + _EnterPasswordTextField( + controller: _passwordController, + ), + const SizedBox(height: 12), + _ConfirmPasswordTextField( + controller: _confirmPasswordController, + showError: widget.data.showPasswordMisMatch, + minimumLength: widget.data.minPasswordLength, + ), + const Spacer(), + const SizedBox(height: 22), + if (widget.data.showPasswordStrength) + VoicesPasswordStrengthIndicator( + passwordStrength: widget.data.passwordStrength, ), - const SizedBox(height: 12), - _ConfirmPasswordTextField( - controller: _confirmPasswordController, - showError: widget.data.showPasswordMisMatch, - minimumLength: widget.data.minPasswordLength, - ), - const Spacer(), - const SizedBox(height: 22), - if (widget.data.showPasswordStrength) - VoicesPasswordStrengthIndicator( - passwordStrength: widget.data.passwordStrength, - ), - const SizedBox(height: 22), - RegistrationBackNextNavigation( - isNextEnabled: widget.data.isNextEnabled, - onNextTap: _createKeychain, - ), - ], - ), + const SizedBox(height: 22), + RegistrationBackNextNavigation( + isNextEnabled: widget.data.isNextEnabled, + onNextTap: _createKeychain, + ), + ], ); } From 22571dc464f8056eb8a3f8fc8efe2efe90f4f264 Mon Sep 17 00:00:00 2001 From: Damian Molinski Date: Thu, 3 Oct 2024 08:29:16 +0200 Subject: [PATCH 6/8] feat: clear password on back --- .../stage/unlock_password_panel.dart | 21 ++++++++++++++-- .../registration_stage_navigation.dart | 4 +++- .../cubits/keychain_creation_cubit.dart | 24 +++++++++---------- .../registration/unlock_password_state.dart | 10 ++++++++ 4 files changed, 44 insertions(+), 15 deletions(-) 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 202db2be5fd..b1ea4cdaf7d 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 @@ -26,9 +26,18 @@ class _UnlockPasswordPanelState extends State { void initState() { super.initState(); - _passwordController = TextEditingController() + final registrationState = RegistrationCubit.of(context).state; + final createKeychain = + registrationState is CreateKeychain ? registrationState : null; + + final unlockPassword = createKeychain?.unlockPassword; + + final password = unlockPassword?.password; + final confirmPassword = unlockPassword?.confirmPassword; + + _passwordController = TextEditingController(text: password) ..addListener(_onPasswordChanged); - _confirmPasswordController = TextEditingController() + _confirmPasswordController = TextEditingController(text: confirmPassword) ..addListener(_onConfirmPasswordChanged); } @@ -65,6 +74,7 @@ class _UnlockPasswordPanelState extends State { RegistrationBackNextNavigation( isNextEnabled: widget.data.isNextEnabled, onNextTap: _createKeychain, + onBackTap: _clearPasswordAndGoBack, ), ], ); @@ -80,6 +90,13 @@ class _UnlockPasswordPanelState extends State { RegistrationCubit.of(context).setConfirmPassword(confirmPassword); } + void _clearPasswordAndGoBack() { + RegistrationCubit.of(context) + ..setPassword('') + ..setConfirmPassword('') + ..previousStep(); + } + Future _createKeychain() async { final registrationCubit = RegistrationCubit.of(context); diff --git a/catalyst_voices/lib/pages/registration/registration_stage_navigation.dart b/catalyst_voices/lib/pages/registration/registration_stage_navigation.dart index e86fd7dc6b8..a2c0fce0e4a 100644 --- a/catalyst_voices/lib/pages/registration/registration_stage_navigation.dart +++ b/catalyst_voices/lib/pages/registration/registration_stage_navigation.dart @@ -7,12 +7,14 @@ class RegistrationBackNextNavigation extends StatelessWidget { final bool isBackEnabled; final bool isNextEnabled; final VoidCallback? onNextTap; + final VoidCallback? onBackTap; const RegistrationBackNextNavigation({ super.key, this.isBackEnabled = true, this.isNextEnabled = true, this.onNextTap, + this.onBackTap, }); @override @@ -23,7 +25,7 @@ class RegistrationBackNextNavigation extends StatelessWidget { children: [ VoicesBackButton( onTap: isBackEnabled - ? () => RegistrationCubit.of(context).previousStep() + ? onBackTap ?? () => RegistrationCubit.of(context).previousStep() : null, ), VoicesNextButton( 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 2eb550d3236..e1984464c7b 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 @@ -13,9 +13,6 @@ final _logger = Logger('KeychainCreationCubit'); final class KeychainCreationCubit extends Cubit { final Downloader _downloader; - String _password = ''; - String _confirmPassword = ''; - KeychainCreationCubit({ required Downloader downloader, }) : _downloader = downloader, @@ -99,25 +96,26 @@ final class KeychainCreationCubit extends Cubit { } void setPassword(String newValue) { - if (_password != newValue) { - _password = newValue; - _updateUnlockPasswordState(); + if (_unlockPassword.password != newValue) { + _updateUnlockPasswordState(password: newValue); } } void setConfirmPassword(String newValue) { - if (_confirmPassword != newValue) { - _confirmPassword = newValue; - _updateUnlockPasswordState(); + if (_unlockPassword.confirmPassword != newValue) { + _updateUnlockPasswordState(confirmPassword: newValue); } } // TODO(damian-molinski): implement Future createKeychain() async {} - void _updateUnlockPasswordState() { - final password = _password; - final confirmPassword = _confirmPassword; + void _updateUnlockPasswordState({ + String? password, + String? confirmPassword, + }) { + password ??= _unlockPassword.password; + confirmPassword ??= _unlockPassword.confirmPassword; const minimumLength = PasswordStrength.minimumLength; @@ -127,6 +125,8 @@ final class KeychainCreationCubit extends Cubit { final hasConfirmPassword = confirmPassword.isNotEmpty; _unlockPassword = _unlockPassword.copyWith( + password: password, + confirmPassword: confirmPassword, passwordStrength: passwordStrength, showPasswordStrength: password.isNotEmpty, minPasswordLength: minimumLength, diff --git a/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/unlock_password_state.dart b/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/unlock_password_state.dart index 650e93db100..54641222a55 100644 --- a/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/unlock_password_state.dart +++ b/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/unlock_password_state.dart @@ -2,6 +2,8 @@ import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:equatable/equatable.dart'; final class UnlockPasswordState extends Equatable { + final String password; + final String confirmPassword; final PasswordStrength passwordStrength; final bool showPasswordStrength; final int minPasswordLength; @@ -9,6 +11,8 @@ final class UnlockPasswordState extends Equatable { final bool isNextEnabled; const UnlockPasswordState({ + this.password = '', + this.confirmPassword = '', this.passwordStrength = PasswordStrength.weak, this.showPasswordStrength = false, this.minPasswordLength = PasswordStrength.minimumLength, @@ -17,6 +21,8 @@ final class UnlockPasswordState extends Equatable { }); UnlockPasswordState copyWith({ + String? password, + String? confirmPassword, PasswordStrength? passwordStrength, bool? showPasswordStrength, int? minPasswordLength, @@ -24,6 +30,8 @@ final class UnlockPasswordState extends Equatable { bool? isNextEnabled, }) { return UnlockPasswordState( + password: password ?? this.password, + confirmPassword: confirmPassword ?? this.confirmPassword, passwordStrength: passwordStrength ?? this.passwordStrength, showPasswordStrength: showPasswordStrength ?? this.showPasswordStrength, minPasswordLength: minPasswordLength ?? this.minPasswordLength, @@ -34,6 +42,8 @@ final class UnlockPasswordState extends Equatable { @override List get props => [ + password, + confirmPassword, passwordStrength, showPasswordStrength, minPasswordLength, From f46021414c3ef003c6d2be13fa6e736c982fbc79 Mon Sep 17 00:00:00 2001 From: Damian Molinski Date: Thu, 3 Oct 2024 08:36:16 +0200 Subject: [PATCH 7/8] fix: formatting --- .../lib/pages/registration/registration_info_panel.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/catalyst_voices/lib/pages/registration/registration_info_panel.dart b/catalyst_voices/lib/pages/registration/registration_info_panel.dart index 618a88f1311..4f8e5901d7a 100644 --- a/catalyst_voices/lib/pages/registration/registration_info_panel.dart +++ b/catalyst_voices/lib/pages/registration/registration_info_panel.dart @@ -166,9 +166,11 @@ class _RegistrationPicture extends StatelessWidget { FinishAccountCreation() => const KeychainPicture(), Recover() => const KeychainPicture(), CreateKeychain(:final stage, :final seedPhrase, :final unlockPassword) => - buildKeychainStagePicture(stage, - isSeedPhraseCheckConfirmed: seedPhrase.isCheckConfirmed, - unlockPassword: unlockPassword), + buildKeychainStagePicture( + stage, + isSeedPhraseCheckConfirmed: seedPhrase.isCheckConfirmed, + unlockPassword: unlockPassword, + ), WalletLink(:final stage) => buildWalletLinkStagePicture(stage), AccountCompleted() => const KeychainPicture(), }; From 0de4357feadbb2041d36359578ac432952f61165 Mon Sep 17 00:00:00 2001 From: Damian Molinski Date: Thu, 3 Oct 2024 10:17:02 +0200 Subject: [PATCH 8/8] refactor: use password from constructor data --- .../create_keychain/stage/unlock_password_panel.dart | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) 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 b1ea4cdaf7d..1646939894f 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 @@ -26,14 +26,8 @@ class _UnlockPasswordPanelState extends State { void initState() { super.initState(); - final registrationState = RegistrationCubit.of(context).state; - final createKeychain = - registrationState is CreateKeychain ? registrationState : null; - - final unlockPassword = createKeychain?.unlockPassword; - - final password = unlockPassword?.password; - final confirmPassword = unlockPassword?.confirmPassword; + final password = widget.data.password; + final confirmPassword = widget.data.confirmPassword; _passwordController = TextEditingController(text: password) ..addListener(_onPasswordChanged);