diff --git a/modules/legacy/modules/account/lib/src/features/change_password/presentation/change_password_screen.dart b/modules/legacy/modules/account/lib/src/features/change_password/presentation/change_password_screen.dart index be45524b..73ff1a4a 100644 --- a/modules/legacy/modules/account/lib/src/features/change_password/presentation/change_password_screen.dart +++ b/modules/legacy/modules/account/lib/src/features/change_password/presentation/change_password_screen.dart @@ -16,6 +16,9 @@ class ChangePasswordScreen extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final theme = ref.watch(themePortProvider); + final password = ref.watch( + changePasswordViewModelProvider.select((s)=>s.newPassword) + ); return LegacyPageLayout( theme: theme, @@ -28,11 +31,11 @@ class ChangePasswordScreen extends ConsumerWidget { child: SingleChildScrollView(child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - const _PasswordSection(), - SizedBox(height: SizeUtils.getByScreen(small: 24, big: 22)), const _NewPasswordSection(), SizedBox(height: SizeUtils.getByScreen(small: 24, big: 22)), const _RepeatPasswordSection(), + SizedBox(height: SizeUtils.getByScreen(small: 24, big: 22)), + _PasswordCriteriaList(password: password), const _ErrorMessageSection() ], )) @@ -42,29 +45,6 @@ class ChangePasswordScreen extends ConsumerWidget { } } -class _PasswordSection extends ConsumerWidget { - - const _PasswordSection(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - - final vm = ref.read(changePasswordViewModelProvider.notifier); - final showPassword = ref.watch( - changePasswordViewModelProvider.select((s)=>s.showCurrentPassword) - ); - - return CustomTextField( - controller: vm.currentPasswordController, - hint: '********', - label: context.translate(I18n.password), - showPassword: showPassword, - onVisibilityChanged: vm.toggleCurrentPasswordVisibility, - ); - } - -} - class _NewPasswordSection extends ConsumerWidget { const _NewPasswordSection(); @@ -111,6 +91,89 @@ class _RepeatPasswordSection extends ConsumerWidget { } +class _PasswordCriteriaList extends StatelessWidget { + final String password; + + const _PasswordCriteriaList({required this.password}); + + static final _upperRegex = RegExp(r'[A-Z]'); + static final _digitRegex = RegExp(r'[0-9]'); + static final _specialRegex = + RegExp(r'[!@#$%^&*(),.?":{}|<>\-_+=\[\]\\\/~`]'); + + @override + Widget build(BuildContext context) { + final hasInput = password.isNotEmpty; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 6, + children: [ + _CriteriaRow( + label: context.translate(I18n.passwordLength), + met: password.length >= 8, + hasInput: hasInput, + ), + _CriteriaRow( + label: context.translate(I18n.passwordCapital), + met: _upperRegex.hasMatch(password), + hasInput: hasInput, + ), + _CriteriaRow( + label: context.translate(I18n.passwordNumber), + met: _digitRegex.hasMatch(password), + hasInput: hasInput, + ), + _CriteriaRow( + label: context.translate(I18n.passwordSpecial), + met: _specialRegex.hasMatch(password), + hasInput: hasInput, + ), + ], + ); + } +} + +class _CriteriaRow extends StatelessWidget { + final String label; + final bool met; + final bool hasInput; + + const _CriteriaRow({ + required this.label, + required this.met, + required this.hasInput, + }); + + @override + Widget build(BuildContext context) { + final Color color; + final IconData icon; + + if (!hasInput) { + color = Colors.grey; + icon = Icons.circle_outlined; + } else if (met) { + color = Colors.green; + icon = Icons.check_circle; + } else { + color = Colors.red.shade400; + icon = Icons.cancel; + } + + return Row( + spacing: 8, + children: [ + Icon(icon, size: 16, color: color), + Text( + label, + style: TextStyle(fontSize: 13, color: color), + ), + ], + ); + } +} + class _ErrorMessageSection extends ConsumerWidget { const _ErrorMessageSection(); @@ -136,7 +199,9 @@ class _ErrorMessageSection extends ConsumerWidget { ), ], ); - } else return SizedBox.shrink(); + } else { + return SizedBox.shrink(); + } } } diff --git a/modules/legacy/modules/account/lib/src/features/change_password/presentation/state/change_password_view_model.dart b/modules/legacy/modules/account/lib/src/features/change_password/presentation/state/change_password_view_model.dart index f5757787..a98f1d10 100644 --- a/modules/legacy/modules/account/lib/src/features/change_password/presentation/state/change_password_view_model.dart +++ b/modules/legacy/modules/account/lib/src/features/change_password/presentation/state/change_password_view_model.dart @@ -14,7 +14,6 @@ NotifierProvider.autoDispose( class ChangePasswordViewModel extends Notifier { late final ChangePasswordUseCase _changePasswordUseCase; - late final TextEditingController currentPasswordController; late final TextEditingController newPasswordController; late final TextEditingController repeatPasswordController; late final TextEditingController passwordController; @@ -29,10 +28,6 @@ class ChangePasswordViewModel extends Notifier { } void _initControllers() { - - currentPasswordController = TextEditingController(); - currentPasswordController.addListener(_onCurrentPasswordChanged); - newPasswordController = TextEditingController(); newPasswordController.addListener(_onNewPasswordChanged); @@ -60,17 +55,6 @@ class ChangePasswordViewModel extends Notifier { ); } - void _onCurrentPasswordChanged() { - final value = currentPasswordController.text; - - if (value == state.currentPassword) return; - - state = state.copyWith( - currentPassword: value, - errorMessage: '' - ); - } - void _onNewPasswordChanged() { final value = newPasswordController.text; @@ -94,11 +78,14 @@ class ChangePasswordViewModel extends Notifier { } bool _validateForm() { - if (state.currentPassword.trim().isEmpty){ - state = state.copyWith(errorMessage: 'errorMessageCurrentPasswordIsEmpty'); - return false; - } - if (state.newPassword.trim().isEmpty){ + final _upperRegex = RegExp(r'[A-Z]'); + final _digitRegex = RegExp(r'[0-9]'); + final _specialRegex = + RegExp(r'[!@#$%^&*(),.?":{}|<>\-_+=\[\]\\\/~`]'); + + final password = state.newPassword.trim(); + + if (password.isEmpty){ state = state.copyWith(errorMessage: 'errorMessageNewPasswordIsEmpty'); return false; } @@ -106,10 +93,26 @@ class ChangePasswordViewModel extends Notifier { state = state.copyWith(errorMessage: 'errorMessageRepeatPasswordIsEmpty'); return false; } - if (state.newPassword.trim() != state.repeatPassword.trim()){ + if (password != state.repeatPassword.trim()){ state = state.copyWith(errorMessage: 'errorMessagePasswordsDontMatch'); return false; } + if (password.length < 8){ + state = state.copyWith(errorMessage: 'errorPasswordMinLength'); + return false; + } + if (!_upperRegex.hasMatch(password)) { + state = state.copyWith(errorMessage: 'errorPasswordUppercase'); + return false; + } + if (!_digitRegex.hasMatch(password)) { + state = state.copyWith(errorMessage: 'errorPasswordDigits'); + return false; + } + if (!_specialRegex.hasMatch(password)) { + state = state.copyWith(errorMessage: 'errorPasswordSpecial'); + return false; + } return true; } @@ -156,9 +159,6 @@ class ChangePasswordViewModel extends Notifier { } void disposeControllers() { - currentPasswordController.removeListener(_onCurrentPasswordChanged); - currentPasswordController.dispose(); - newPasswordController.removeListener(_onNewPasswordChanged); newPasswordController.dispose(); diff --git a/modules/legacy/modules/account/lib/src/features/change_password/presentation/state/change_password_view_state.dart b/modules/legacy/modules/account/lib/src/features/change_password/presentation/state/change_password_view_state.dart index 2316282d..c86b5f6a 100644 --- a/modules/legacy/modules/account/lib/src/features/change_password/presentation/state/change_password_view_state.dart +++ b/modules/legacy/modules/account/lib/src/features/change_password/presentation/state/change_password_view_state.dart @@ -7,10 +7,8 @@ abstract class ChangePasswordViewState with _$ChangePasswordViewState { const factory ChangePasswordViewState({ @Default(false) bool isLoading, @Default(false) bool isComplete, - @Default(false) bool showCurrentPassword, @Default(false) bool showNewPassword, @Default(false) bool showRepeatedPassword, - @Default('') String currentPassword, @Default('') String newPassword, @Default('') String repeatPassword, @Default('') String errorMessage