From 7746d08759ad9ee84797c5e88da3f0f0c891ec7a Mon Sep 17 00:00:00 2001 From: JulianAlcala Date: Tue, 21 Apr 2026 20:35:18 +0200 Subject: [PATCH] refactor(legacy-account): migrate change_password to AsyncNotifier --- .../domain/password_validator.dart | 34 +++ .../presentation/change_password_screen.dart | 279 +++++++++-------- .../providers/change_password_controller.dart | 34 +++ .../change_password_controller.g.dart | 56 ++++ .../change_password_visibility_provider.dart | 32 ++ ...change_password_visibility_provider.g.dart | 72 +++++ .../state/change_password_view_model.dart | 152 --------- .../state/change_password_view_state.dart | 16 - .../change_password_view_state.freezed.dart | 289 ------------------ .../change_password_controller_test.dart | 116 +++++++ ...nge_password_visibility_provider_test.dart | 51 ++++ .../password_validator_test.dart | 88 ++++++ packages/sf_localizations/assets/l10n/de.json | 1 + packages/sf_localizations/assets/l10n/en.json | 1 + packages/sf_localizations/assets/l10n/es.json | 1 + packages/sf_localizations/assets/l10n/fr.json | 1 + packages/sf_localizations/assets/l10n/it.json | 1 + packages/sf_localizations/assets/l10n/pt.json | 1 + .../lib/src/generated/i18n.dart | 1 + 19 files changed, 637 insertions(+), 589 deletions(-) create mode 100644 modules/legacy/modules/account/lib/src/features/change_password/domain/password_validator.dart create mode 100644 modules/legacy/modules/account/lib/src/features/change_password/presentation/providers/change_password_controller.dart create mode 100644 modules/legacy/modules/account/lib/src/features/change_password/presentation/providers/change_password_controller.g.dart create mode 100644 modules/legacy/modules/account/lib/src/features/change_password/presentation/providers/change_password_visibility_provider.dart create mode 100644 modules/legacy/modules/account/lib/src/features/change_password/presentation/providers/change_password_visibility_provider.g.dart delete mode 100644 modules/legacy/modules/account/lib/src/features/change_password/presentation/state/change_password_view_model.dart delete mode 100644 modules/legacy/modules/account/lib/src/features/change_password/presentation/state/change_password_view_state.dart delete mode 100644 modules/legacy/modules/account/lib/src/features/change_password/presentation/state/change_password_view_state.freezed.dart create mode 100644 modules/legacy/modules/account/test/features/change_password/change_password_controller_test.dart create mode 100644 modules/legacy/modules/account/test/features/change_password/change_password_visibility_provider_test.dart create mode 100644 modules/legacy/modules/account/test/features/change_password/password_validator_test.dart diff --git a/modules/legacy/modules/account/lib/src/features/change_password/domain/password_validator.dart b/modules/legacy/modules/account/lib/src/features/change_password/domain/password_validator.dart new file mode 100644 index 00000000..5f6a91de --- /dev/null +++ b/modules/legacy/modules/account/lib/src/features/change_password/domain/password_validator.dart @@ -0,0 +1,34 @@ +import 'package:sf_localizations/sf_localizations.dart'; + +class PasswordValidator { + const PasswordValidator._(); + + static final _upperRegex = RegExp(r'[A-Z]'); + static final _digitRegex = RegExp(r'[0-9]'); + static final _specialRegex = RegExp(r'[!@#$%^&*(),.?":{}|<>\-_+=\[\]\\\/~`]'); + + static const int minLength = 8; + + static String? validate({ + required String password, + required String repeat, + }) { + final trimmed = password.trim(); + final trimmedRepeat = repeat.trim(); + + if (trimmed.isEmpty) return I18n.errorPasswordRequired; + if (trimmedRepeat.isEmpty) return I18n.errorPasswordRequired; + if (trimmed != trimmedRepeat) return I18n.errorMessageUnequalPasswords; + if (trimmed.length < minLength) return I18n.errorMessagePasswordTooShort; + if (!_upperRegex.hasMatch(trimmed)) { + return I18n.errorMessagePasswordNoCapitals; + } + if (!_digitRegex.hasMatch(trimmed)) { + return I18n.errorMessagePasswordNoNumbers; + } + if (!_specialRegex.hasMatch(trimmed)) { + return I18n.errorMessagePasswordNoSpecialChars; + } + return null; + } +} 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 d108c664..f1acf9af 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 @@ -1,23 +1,73 @@ -import 'package:account/src/features/change_password/presentation/state/change_password_view_model.dart'; -import 'package:legacy_theme/legacy_theme.dart'; +import 'package:account/src/features/change_password/domain/password_validator.dart'; +import 'package:account/src/features/change_password/presentation/providers/change_password_controller.dart'; +import 'package:account/src/features/change_password/presentation/providers/change_password_visibility_provider.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:legacy_theme/legacy_theme.dart'; import 'package:legacy_ui/legacy_ui.dart'; import 'package:navigation/navigation.dart'; import 'package:sf_localizations/sf_localizations.dart'; +import 'package:sf_shared/sf_shared.dart'; import 'package:utils/utils.dart'; -class ChangePasswordScreen extends ConsumerWidget { +class ChangePasswordScreen extends ConsumerStatefulWidget { final NavigationContract navigationContract; const ChangePasswordScreen({super.key, required this.navigationContract}); @override - Widget build(BuildContext context, WidgetRef ref) { - final password = ref.watch( - changePasswordViewModelProvider.select((s) => s.newPassword), + ConsumerState createState() => + _ChangePasswordScreenState(); +} + +class _ChangePasswordScreenState extends ConsumerState { + late final TextEditingController _newPasswordController; + late final TextEditingController _repeatPasswordController; + String? _localError; + + @override + void initState() { + super.initState(); + _newPasswordController = TextEditingController(); + _repeatPasswordController = TextEditingController(); + } + + @override + void dispose() { + _newPasswordController.dispose(); + _repeatPasswordController.dispose(); + super.dispose(); + } + + void _onSubmit() { + final error = PasswordValidator.validate( + password: _newPasswordController.text, + repeat: _repeatPasswordController.text, ); + if (error != null) { + setState(() => _localError = error); + return; + } + setState(() => _localError = null); + ref + .read(changePasswordControllerProvider.notifier) + .submit(password: _newPasswordController.text.trim()); + } + + @override + Widget build(BuildContext context) { + ref.listen(changePasswordControllerProvider, (prev, next) { + next.showErrorOn(context); + if (prev != null && + prev.isLoading && + !next.isLoading && + !next.hasError) { + widget.navigationContract.goBack(); + } + }); + + final submitState = ref.watch(changePasswordControllerProvider); return LegacyPageLayout( title: context.translate(I18n.changePassword), @@ -30,65 +80,90 @@ class ChangePasswordScreen extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - const _NewPasswordSection(), + _NewPasswordField(controller: _newPasswordController), SizedBox(height: SizeUtils.getByScreen(small: 24, big: 22)), - const _RepeatPasswordSection(), + _RepeatPasswordField(controller: _repeatPasswordController), SizedBox(height: SizeUtils.getByScreen(small: 24, big: 22)), - _PasswordCriteriaList(password: password), - const _ErrorMessageSection(), + _PasswordCriteriaList( + newController: _newPasswordController, + repeatController: _repeatPasswordController, + ), + if (_localError != null) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + context.translate(_localError!), + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + fontSize: 12, + ), + ), + ), ], ), ), ), - footer: _SaveSection(navigationContract: navigationContract), + footer: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10), + child: PrimaryButton( + onPressed: submitState.isLoading ? null : _onSubmit, + text: context.translate(I18n.save), + color: context.sfColors.legacyPrimary, + ), + ), ); } } -class _NewPasswordSection extends ConsumerWidget { - const _NewPasswordSection(); +class _NewPasswordField extends ConsumerWidget { + final TextEditingController controller; + + const _NewPasswordField({required this.controller}); @override Widget build(BuildContext context, WidgetRef ref) { - final vm = ref.read(changePasswordViewModelProvider.notifier); - final showPassword = ref.watch( - changePasswordViewModelProvider.select((s) => s.showNewPassword), - ); - + final visibility = ref.watch(changePasswordVisibilityProvider); return CustomTextField( - controller: vm.newPasswordController, + controller: controller, hint: '********', label: context.translate(I18n.newPassword), - showPassword: showPassword, - onVisibilityChanged: vm.toggleNewPasswordVisibility, + showPassword: visibility.showNew, + onVisibilityChanged: ref + .read(changePasswordVisibilityProvider.notifier) + .toggleNew, ); } } -class _RepeatPasswordSection extends ConsumerWidget { - const _RepeatPasswordSection(); +class _RepeatPasswordField extends ConsumerWidget { + final TextEditingController controller; + + const _RepeatPasswordField({required this.controller}); @override Widget build(BuildContext context, WidgetRef ref) { - final vm = ref.read(changePasswordViewModelProvider.notifier); - final showPassword = ref.watch( - changePasswordViewModelProvider.select((s) => s.showRepeatedPassword), - ); - + final visibility = ref.watch(changePasswordVisibilityProvider); return CustomTextField( - controller: vm.repeatPasswordController, + controller: controller, hint: '********', label: context.translate(I18n.repeatPassword), - showPassword: showPassword, - onVisibilityChanged: vm.toggleRepeatedPasswordVisibility, + showPassword: visibility.showRepeated, + onVisibilityChanged: ref + .read(changePasswordVisibilityProvider.notifier) + .toggleRepeated, ); } } class _PasswordCriteriaList extends StatelessWidget { - final String password; + final TextEditingController newController; + final TextEditingController repeatController; - const _PasswordCriteriaList({required this.password}); + const _PasswordCriteriaList({ + required this.newController, + required this.repeatController, + }); static final _upperRegex = RegExp(r'[A-Z]'); static final _digitRegex = RegExp(r'[0-9]'); @@ -96,33 +171,45 @@ class _PasswordCriteriaList extends StatelessWidget { @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, - ), - ], + return ListenableBuilder( + listenable: Listenable.merge([newController, repeatController]), + builder: (_, __) { + final password = newController.text; + final repeat = repeatController.text; + final hasInput = password.isNotEmpty; + final hasRepeatInput = repeat.isNotEmpty; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 6, + children: [ + _CriteriaRow( + label: context.translate(I18n.passwordLength), + met: password.length >= PasswordValidator.minLength, + 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, + ), + _CriteriaRow( + label: context.translate(I18n.passwordMatch), + met: password.isNotEmpty && password == repeat, + hasInput: hasInput && hasRepeatInput, + ), + ], + ); + }, ); } } @@ -163,75 +250,3 @@ class _CriteriaRow extends StatelessWidget { ); } } - -class _ErrorMessageSection extends ConsumerWidget { - const _ErrorMessageSection(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final errorMessage = ref.watch( - changePasswordViewModelProvider.select((s) => s.errorMessage), - ); - - if (errorMessage.isNotEmpty) { - return Column( - children: [ - const SizedBox(height: 8), - Text( - errorMessage, - textAlign: TextAlign.center, - style: TextStyle( - color: Theme.of(context).colorScheme.error, - fontSize: 12, - ), - ), - ], - ); - } else { - return SizedBox.shrink(); - } - } -} - -class _SaveSection extends ConsumerWidget { - final NavigationContract navigationContract; - - const _SaveSection({required this.navigationContract}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - - final vm = ref.read(changePasswordViewModelProvider.notifier); - - return Padding( - padding: EdgeInsets.symmetric(horizontal: 24, vertical: 10), - child: PrimaryButton( - onPressed: () async { - await vm.submit(); - if (!context.mounted) return; - - final errorMessage = ref.read( - changePasswordViewModelProvider.select((s) => s.errorMessage), - ); - if (errorMessage.isNotEmpty) { - showTopSnackbar( - context, - message: errorMessage, - type: MessageType.error, - ); - return; - } - - final isComplete = ref.read( - changePasswordViewModelProvider.select((s) => s.isComplete), - ); - if (isComplete) { - navigationContract.goTo(AppRoutes.legacyLogin); - } - }, - text: context.translate(I18n.save), - color: context.sfColors.legacyPrimary, - ), - ); - } -} diff --git a/modules/legacy/modules/account/lib/src/features/change_password/presentation/providers/change_password_controller.dart b/modules/legacy/modules/account/lib/src/features/change_password/presentation/providers/change_password_controller.dart new file mode 100644 index 00000000..38eea895 --- /dev/null +++ b/modules/legacy/modules/account/lib/src/features/change_password/presentation/providers/change_password_controller.dart @@ -0,0 +1,34 @@ +import 'dart:async'; + +import 'package:account/src/core/providers/change_password_repository_provider.dart'; +import 'package:account/src/features/change_password/domain/models/entities/change_password_request_entity.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:sf_shared/sf_shared.dart'; +import 'package:sf_tracking/sf_tracking.dart'; + +part 'change_password_controller.g.dart'; + +@riverpod +class ChangePasswordController extends _$ChangePasswordController { + @override + FutureOr build() {} + + Future submit({required String password}) async { + state = const AsyncLoading(); + state = await AsyncValue.guard(() async { + final user = await ref.read(userInfoProvider.future); + await ref.read(changePasswordRepositoryProvider).changePassword( + userId: user.id, + request: ChangePasswordRequestEntity(password: password), + ); + unawaited(ref.read(sfTrackingProvider).legacyAccountPasswordChanged()); + }); + if (state.hasError) { + unawaited( + ref + .read(sfTrackingProvider) + .legacyAccountPasswordChangeFailed(state.error.toString()), + ); + } + } +} diff --git a/modules/legacy/modules/account/lib/src/features/change_password/presentation/providers/change_password_controller.g.dart b/modules/legacy/modules/account/lib/src/features/change_password/presentation/providers/change_password_controller.g.dart new file mode 100644 index 00000000..f769750b --- /dev/null +++ b/modules/legacy/modules/account/lib/src/features/change_password/presentation/providers/change_password_controller.g.dart @@ -0,0 +1,56 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'change_password_controller.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(ChangePasswordController) +const changePasswordControllerProvider = ChangePasswordControllerProvider._(); + +final class ChangePasswordControllerProvider + extends $AsyncNotifierProvider { + const ChangePasswordControllerProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'changePasswordControllerProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$changePasswordControllerHash(); + + @$internal + @override + ChangePasswordController create() => ChangePasswordController(); +} + +String _$changePasswordControllerHash() => + r'45645f3f5759459d1f048b0e8e553b9908e659d9'; + +abstract class _$ChangePasswordController extends $AsyncNotifier { + FutureOr build(); + @$mustCallSuper + @override + void runBuild() { + build(); + final ref = this.ref as $Ref, void>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, void>, + AsyncValue, + Object?, + Object? + >; + element.handleValue(ref, null); + } +} diff --git a/modules/legacy/modules/account/lib/src/features/change_password/presentation/providers/change_password_visibility_provider.dart b/modules/legacy/modules/account/lib/src/features/change_password/presentation/providers/change_password_visibility_provider.dart new file mode 100644 index 00000000..745cc6a7 --- /dev/null +++ b/modules/legacy/modules/account/lib/src/features/change_password/presentation/providers/change_password_visibility_provider.dart @@ -0,0 +1,32 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'change_password_visibility_provider.g.dart'; + +class ChangePasswordVisibility { + const ChangePasswordVisibility({ + this.showNew = false, + this.showRepeated = false, + }); + + final bool showNew; + final bool showRepeated; + + ChangePasswordVisibility copyWith({bool? showNew, bool? showRepeated}) { + return ChangePasswordVisibility( + showNew: showNew ?? this.showNew, + showRepeated: showRepeated ?? this.showRepeated, + ); + } +} + +@riverpod +class ChangePasswordVisibilityNotifier + extends _$ChangePasswordVisibilityNotifier { + @override + ChangePasswordVisibility build() => const ChangePasswordVisibility(); + + void toggleNew() => state = state.copyWith(showNew: !state.showNew); + + void toggleRepeated() => + state = state.copyWith(showRepeated: !state.showRepeated); +} diff --git a/modules/legacy/modules/account/lib/src/features/change_password/presentation/providers/change_password_visibility_provider.g.dart b/modules/legacy/modules/account/lib/src/features/change_password/presentation/providers/change_password_visibility_provider.g.dart new file mode 100644 index 00000000..4614d14c --- /dev/null +++ b/modules/legacy/modules/account/lib/src/features/change_password/presentation/providers/change_password_visibility_provider.g.dart @@ -0,0 +1,72 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'change_password_visibility_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(ChangePasswordVisibilityNotifier) +const changePasswordVisibilityProvider = + ChangePasswordVisibilityNotifierProvider._(); + +final class ChangePasswordVisibilityNotifierProvider + extends + $NotifierProvider< + ChangePasswordVisibilityNotifier, + ChangePasswordVisibility + > { + const ChangePasswordVisibilityNotifierProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'changePasswordVisibilityProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$changePasswordVisibilityNotifierHash(); + + @$internal + @override + ChangePasswordVisibilityNotifier create() => + ChangePasswordVisibilityNotifier(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(ChangePasswordVisibility value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$changePasswordVisibilityNotifierHash() => + r'8b4a81d152e0982675608d0ba3dbd53c93257d17'; + +abstract class _$ChangePasswordVisibilityNotifier + extends $Notifier { + ChangePasswordVisibility build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = + this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + ChangePasswordVisibility, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} 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 deleted file mode 100644 index 631fb9f3..00000000 --- a/modules/legacy/modules/account/lib/src/features/change_password/presentation/state/change_password_view_model.dart +++ /dev/null @@ -1,152 +0,0 @@ -import 'dart:async'; - -import 'package:account/src/core/domain/repositories/change_password_repository.dart'; -import 'package:account/src/core/providers/change_password_repository_provider.dart'; -import 'package:account/src/features/change_password/domain/models/entities/change_password_request_entity.dart'; -import 'package:account/src/features/change_password/presentation/state/change_password_view_state.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:sf_shared/sf_shared.dart'; -import 'package:sf_tracking/sf_tracking.dart'; - -final changePasswordViewModelProvider = - NotifierProvider.autoDispose< - ChangePasswordViewModel, - ChangePasswordViewState - >(ChangePasswordViewModel.new); - -class ChangePasswordViewModel extends Notifier { - late final ChangePasswordRepository _changePasswordRepository; - late final SfTrackingRepository _tracking; - - late final TextEditingController newPasswordController; - late final TextEditingController repeatPasswordController; - late final TextEditingController passwordController; - - @override - ChangePasswordViewState build() { - _changePasswordRepository = ref.read(changePasswordRepositoryProvider); - _tracking = ref.read(sfTrackingProvider); - - _initControllers(); - - return const ChangePasswordViewState(); - } - - void _initControllers() { - newPasswordController = TextEditingController(); - newPasswordController.addListener(_onNewPasswordChanged); - - repeatPasswordController = TextEditingController(); - repeatPasswordController.addListener(_onRepeatPasswordChanged); - - ref.onDispose(disposeControllers); - } - - void toggleNewPasswordVisibility() { - state = state.copyWith(showNewPassword: !state.showNewPassword); - } - - void toggleRepeatedPasswordVisibility() { - state = state.copyWith(showRepeatedPassword: !state.showRepeatedPassword); - } - - void _onNewPasswordChanged() { - final value = newPasswordController.text; - - if (value == state.newPassword) return; - - state = state.copyWith(newPassword: value, errorMessage: ''); - } - - void _onRepeatPasswordChanged() { - final value = repeatPasswordController.text; - - if (value == state.repeatPassword) return; - - state = state.copyWith(repeatPassword: value, errorMessage: ''); - } - - bool _validateForm() { - 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; - } - if (state.repeatPassword.trim().isEmpty) { - state = state.copyWith(errorMessage: 'errorMessageRepeatPasswordIsEmpty'); - return false; - } - 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; - } - - ChangePasswordRequestEntity _toRequest() { - return ChangePasswordRequestEntity(password: state.newPassword.trim()); - } - - Future submit() async { - if (state.isLoading) return; - if (!_validateForm()) return; - - try { - state = state.copyWith(isLoading: true, isComplete: false); - - final user = await ref.read(userInfoProvider.future); - - final request = _toRequest(); - - await _changePasswordRepository.changePassword( - userId: user.id, - request: request, - ); - unawaited(_tracking.legacyAccountPasswordChanged()); - state = state.copyWith(isLoading: false, isComplete: true); - } catch (e) { - if (!ref.mounted) return; - _finishWithError(message: e.toString()); - return; - } - } - - void _finishWithError({required String message}) { - unawaited(_tracking.legacyAccountPasswordChangeFailed(message)); - state = state.copyWith( - isLoading: false, - isComplete: false, - errorMessage: message, - ); - } - - void disposeControllers() { - newPasswordController.removeListener(_onNewPasswordChanged); - newPasswordController.dispose(); - - repeatPasswordController.removeListener(_onRepeatPasswordChanged); - repeatPasswordController.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 deleted file mode 100644 index 8800ac73..00000000 --- a/modules/legacy/modules/account/lib/src/features/change_password/presentation/state/change_password_view_state.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'change_password_view_state.freezed.dart'; - -@freezed -abstract class ChangePasswordViewState with _$ChangePasswordViewState { - const factory ChangePasswordViewState({ - @Default(false) bool isLoading, - @Default(false) bool isComplete, - @Default(false) bool showNewPassword, - @Default(false) bool showRepeatedPassword, - @Default('') String newPassword, - @Default('') String repeatPassword, - @Default('') String errorMessage, - }) = _ChangePasswordViewState; -} diff --git a/modules/legacy/modules/account/lib/src/features/change_password/presentation/state/change_password_view_state.freezed.dart b/modules/legacy/modules/account/lib/src/features/change_password/presentation/state/change_password_view_state.freezed.dart deleted file mode 100644 index 3bf82465..00000000 --- a/modules/legacy/modules/account/lib/src/features/change_password/presentation/state/change_password_view_state.freezed.dart +++ /dev/null @@ -1,289 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND -// coverage:ignore-file -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'change_password_view_state.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -// dart format off -T _$identity(T value) => value; -/// @nodoc -mixin _$ChangePasswordViewState { - - bool get isLoading; bool get isComplete; bool get showNewPassword; bool get showRepeatedPassword; String get newPassword; String get repeatPassword; String get errorMessage; -/// Create a copy of ChangePasswordViewState -/// with the given fields replaced by the non-null parameter values. -@JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -$ChangePasswordViewStateCopyWith get copyWith => _$ChangePasswordViewStateCopyWithImpl(this as ChangePasswordViewState, _$identity); - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is ChangePasswordViewState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isComplete, isComplete) || other.isComplete == isComplete)&&(identical(other.showNewPassword, showNewPassword) || other.showNewPassword == showNewPassword)&&(identical(other.showRepeatedPassword, showRepeatedPassword) || other.showRepeatedPassword == showRepeatedPassword)&&(identical(other.newPassword, newPassword) || other.newPassword == newPassword)&&(identical(other.repeatPassword, repeatPassword) || other.repeatPassword == repeatPassword)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)); -} - - -@override -int get hashCode => Object.hash(runtimeType,isLoading,isComplete,showNewPassword,showRepeatedPassword,newPassword,repeatPassword,errorMessage); - -@override -String toString() { - return 'ChangePasswordViewState(isLoading: $isLoading, isComplete: $isComplete, showNewPassword: $showNewPassword, showRepeatedPassword: $showRepeatedPassword, newPassword: $newPassword, repeatPassword: $repeatPassword, errorMessage: $errorMessage)'; -} - - -} - -/// @nodoc -abstract mixin class $ChangePasswordViewStateCopyWith<$Res> { - factory $ChangePasswordViewStateCopyWith(ChangePasswordViewState value, $Res Function(ChangePasswordViewState) _then) = _$ChangePasswordViewStateCopyWithImpl; -@useResult -$Res call({ - bool isLoading, bool isComplete, bool showNewPassword, bool showRepeatedPassword, String newPassword, String repeatPassword, String errorMessage -}); - - - - -} -/// @nodoc -class _$ChangePasswordViewStateCopyWithImpl<$Res> - implements $ChangePasswordViewStateCopyWith<$Res> { - _$ChangePasswordViewStateCopyWithImpl(this._self, this._then); - - final ChangePasswordViewState _self; - final $Res Function(ChangePasswordViewState) _then; - -/// Create a copy of ChangePasswordViewState -/// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? isLoading = null,Object? isComplete = null,Object? showNewPassword = null,Object? showRepeatedPassword = null,Object? newPassword = null,Object? repeatPassword = null,Object? errorMessage = null,}) { - return _then(_self.copyWith( -isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable -as bool,isComplete: null == isComplete ? _self.isComplete : isComplete // ignore: cast_nullable_to_non_nullable -as bool,showNewPassword: null == showNewPassword ? _self.showNewPassword : showNewPassword // ignore: cast_nullable_to_non_nullable -as bool,showRepeatedPassword: null == showRepeatedPassword ? _self.showRepeatedPassword : showRepeatedPassword // ignore: cast_nullable_to_non_nullable -as bool,newPassword: null == newPassword ? _self.newPassword : newPassword // ignore: cast_nullable_to_non_nullable -as String,repeatPassword: null == repeatPassword ? _self.repeatPassword : repeatPassword // ignore: cast_nullable_to_non_nullable -as String,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable -as String, - )); -} - -} - - -/// Adds pattern-matching-related methods to [ChangePasswordViewState]. -extension ChangePasswordViewStatePatterns on ChangePasswordViewState { -/// A variant of `map` that fallback to returning `orElse`. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case _: -/// return orElse(); -/// } -/// ``` - -@optionalTypeArgs TResult maybeMap(TResult Function( _ChangePasswordViewState value)? $default,{required TResult orElse(),}){ -final _that = this; -switch (_that) { -case _ChangePasswordViewState() when $default != null: -return $default(_that);case _: - return orElse(); - -} -} -/// A `switch`-like method, using callbacks. -/// -/// Callbacks receives the raw object, upcasted. -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case final Subclass2 value: -/// return ...; -/// } -/// ``` - -@optionalTypeArgs TResult map(TResult Function( _ChangePasswordViewState value) $default,){ -final _that = this; -switch (_that) { -case _ChangePasswordViewState(): -return $default(_that);case _: - throw StateError('Unexpected subclass'); - -} -} -/// A variant of `map` that fallback to returning `null`. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case _: -/// return null; -/// } -/// ``` - -@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ChangePasswordViewState value)? $default,){ -final _that = this; -switch (_that) { -case _ChangePasswordViewState() when $default != null: -return $default(_that);case _: - return null; - -} -} -/// A variant of `when` that fallback to an `orElse` callback. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case _: -/// return orElse(); -/// } -/// ``` - -@optionalTypeArgs TResult maybeWhen(TResult Function( bool isLoading, bool isComplete, bool showNewPassword, bool showRepeatedPassword, String newPassword, String repeatPassword, String errorMessage)? $default,{required TResult orElse(),}) {final _that = this; -switch (_that) { -case _ChangePasswordViewState() when $default != null: -return $default(_that.isLoading,_that.isComplete,_that.showNewPassword,_that.showRepeatedPassword,_that.newPassword,_that.repeatPassword,_that.errorMessage);case _: - return orElse(); - -} -} -/// A `switch`-like method, using callbacks. -/// -/// As opposed to `map`, this offers destructuring. -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case Subclass2(:final field2): -/// return ...; -/// } -/// ``` - -@optionalTypeArgs TResult when(TResult Function( bool isLoading, bool isComplete, bool showNewPassword, bool showRepeatedPassword, String newPassword, String repeatPassword, String errorMessage) $default,) {final _that = this; -switch (_that) { -case _ChangePasswordViewState(): -return $default(_that.isLoading,_that.isComplete,_that.showNewPassword,_that.showRepeatedPassword,_that.newPassword,_that.repeatPassword,_that.errorMessage);case _: - throw StateError('Unexpected subclass'); - -} -} -/// A variant of `when` that fallback to returning `null` -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case _: -/// return null; -/// } -/// ``` - -@optionalTypeArgs TResult? whenOrNull(TResult? Function( bool isLoading, bool isComplete, bool showNewPassword, bool showRepeatedPassword, String newPassword, String repeatPassword, String errorMessage)? $default,) {final _that = this; -switch (_that) { -case _ChangePasswordViewState() when $default != null: -return $default(_that.isLoading,_that.isComplete,_that.showNewPassword,_that.showRepeatedPassword,_that.newPassword,_that.repeatPassword,_that.errorMessage);case _: - return null; - -} -} - -} - -/// @nodoc - - -class _ChangePasswordViewState implements ChangePasswordViewState { - const _ChangePasswordViewState({this.isLoading = false, this.isComplete = false, this.showNewPassword = false, this.showRepeatedPassword = false, this.newPassword = '', this.repeatPassword = '', this.errorMessage = ''}); - - -@override@JsonKey() final bool isLoading; -@override@JsonKey() final bool isComplete; -@override@JsonKey() final bool showNewPassword; -@override@JsonKey() final bool showRepeatedPassword; -@override@JsonKey() final String newPassword; -@override@JsonKey() final String repeatPassword; -@override@JsonKey() final String errorMessage; - -/// Create a copy of ChangePasswordViewState -/// with the given fields replaced by the non-null parameter values. -@override @JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -_$ChangePasswordViewStateCopyWith<_ChangePasswordViewState> get copyWith => __$ChangePasswordViewStateCopyWithImpl<_ChangePasswordViewState>(this, _$identity); - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _ChangePasswordViewState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isComplete, isComplete) || other.isComplete == isComplete)&&(identical(other.showNewPassword, showNewPassword) || other.showNewPassword == showNewPassword)&&(identical(other.showRepeatedPassword, showRepeatedPassword) || other.showRepeatedPassword == showRepeatedPassword)&&(identical(other.newPassword, newPassword) || other.newPassword == newPassword)&&(identical(other.repeatPassword, repeatPassword) || other.repeatPassword == repeatPassword)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)); -} - - -@override -int get hashCode => Object.hash(runtimeType,isLoading,isComplete,showNewPassword,showRepeatedPassword,newPassword,repeatPassword,errorMessage); - -@override -String toString() { - return 'ChangePasswordViewState(isLoading: $isLoading, isComplete: $isComplete, showNewPassword: $showNewPassword, showRepeatedPassword: $showRepeatedPassword, newPassword: $newPassword, repeatPassword: $repeatPassword, errorMessage: $errorMessage)'; -} - - -} - -/// @nodoc -abstract mixin class _$ChangePasswordViewStateCopyWith<$Res> implements $ChangePasswordViewStateCopyWith<$Res> { - factory _$ChangePasswordViewStateCopyWith(_ChangePasswordViewState value, $Res Function(_ChangePasswordViewState) _then) = __$ChangePasswordViewStateCopyWithImpl; -@override @useResult -$Res call({ - bool isLoading, bool isComplete, bool showNewPassword, bool showRepeatedPassword, String newPassword, String repeatPassword, String errorMessage -}); - - - - -} -/// @nodoc -class __$ChangePasswordViewStateCopyWithImpl<$Res> - implements _$ChangePasswordViewStateCopyWith<$Res> { - __$ChangePasswordViewStateCopyWithImpl(this._self, this._then); - - final _ChangePasswordViewState _self; - final $Res Function(_ChangePasswordViewState) _then; - -/// Create a copy of ChangePasswordViewState -/// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? isLoading = null,Object? isComplete = null,Object? showNewPassword = null,Object? showRepeatedPassword = null,Object? newPassword = null,Object? repeatPassword = null,Object? errorMessage = null,}) { - return _then(_ChangePasswordViewState( -isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable -as bool,isComplete: null == isComplete ? _self.isComplete : isComplete // ignore: cast_nullable_to_non_nullable -as bool,showNewPassword: null == showNewPassword ? _self.showNewPassword : showNewPassword // ignore: cast_nullable_to_non_nullable -as bool,showRepeatedPassword: null == showRepeatedPassword ? _self.showRepeatedPassword : showRepeatedPassword // ignore: cast_nullable_to_non_nullable -as bool,newPassword: null == newPassword ? _self.newPassword : newPassword // ignore: cast_nullable_to_non_nullable -as String,repeatPassword: null == repeatPassword ? _self.repeatPassword : repeatPassword // ignore: cast_nullable_to_non_nullable -as String,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable -as String, - )); -} - - -} - -// dart format on diff --git a/modules/legacy/modules/account/test/features/change_password/change_password_controller_test.dart b/modules/legacy/modules/account/test/features/change_password/change_password_controller_test.dart new file mode 100644 index 00000000..9d313264 --- /dev/null +++ b/modules/legacy/modules/account/test/features/change_password/change_password_controller_test.dart @@ -0,0 +1,116 @@ +import 'package:account/src/core/domain/repositories/change_password_repository.dart'; +import 'package:account/src/core/providers/change_password_repository_provider.dart'; +import 'package:account/src/features/change_password/domain/models/entities/change_password_request_entity.dart'; +import 'package:account/src/features/change_password/presentation/providers/change_password_controller.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:sf_infrastructure/sf_infrastructure.dart'; +import 'package:sf_shared/sf_shared.dart'; +import 'package:sf_shared/testing.dart'; +import 'package:sf_tracking/sf_tracking.dart'; + +class MockChangePasswordRepository extends Mock + implements ChangePasswordRepository {} + +class _FakeUserInfoNotifier extends UserInfoNotifier { + _FakeUserInfoNotifier(this._user); + final UserEntity _user; + + @override + Future build() async => _user; +} + +const _user = UserEntity( + id: 'user-1', + email: 'user1@test.com', + createdAt: 0, + status: 'active', + role: 'parent', + lastLogin: 0, + currentLogin: 0, + language: 'es', + firstName: 'Owner', + lastName: 'Doe', + hasApiKey: false, + phone: '', +); + +void main() { + setUpAll(() { + registerFallbackValue(const ChangePasswordRequestEntity(password: '')); + }); + + group('ChangePasswordController.submit', () { + test('transitions from loading to AsyncData on success', () async { + final repo = MockChangePasswordRepository(); + when( + () => repo.changePassword( + userId: any(named: 'userId'), + request: any(named: 'request'), + ), + ).thenAnswer((_) async {}); + + final container = makeContainer( + overrides: [ + userInfoProvider.overrideWith(() => _FakeUserInfoNotifier(_user)), + changePasswordRepositoryProvider.overrideWithValue(repo), + sfTrackingProvider.overrideWithValue( + SfTrackingRepository(clients: const []), + ), + ], + ); + addTearDown(container.dispose); + + expect( + container.read(changePasswordControllerProvider).isLoading, + isFalse, + ); + + await container + .read(changePasswordControllerProvider.notifier) + .submit(password: 'Abcd1234!'); + + final state = container.read(changePasswordControllerProvider); + expect(state, isA>()); + expect(state.isLoading, isFalse); + expect(state.error, isNull); + + verify( + () => repo.changePassword( + userId: _user.id, + request: const ChangePasswordRequestEntity(password: 'Abcd1234!'), + ), + ).called(1); + }); + + test('exposes AsyncError when the repository fails', () async { + final repo = MockChangePasswordRepository(); + when( + () => repo.changePassword( + userId: any(named: 'userId'), + request: any(named: 'request'), + ), + ).thenThrow(const ApiException(message: 'boom', isNetworkError: true)); + + final container = makeContainer( + overrides: [ + userInfoProvider.overrideWith(() => _FakeUserInfoNotifier(_user)), + changePasswordRepositoryProvider.overrideWithValue(repo), + sfTrackingProvider.overrideWithValue( + SfTrackingRepository(clients: const []), + ), + ], + ); + addTearDown(container.dispose); + + await container + .read(changePasswordControllerProvider.notifier) + .submit(password: 'Abcd1234!'); + + final state = container.read(changePasswordControllerProvider); + expect(state, isA>()); + expect(state.error, isA()); + }); + }); +} diff --git a/modules/legacy/modules/account/test/features/change_password/change_password_visibility_provider_test.dart b/modules/legacy/modules/account/test/features/change_password/change_password_visibility_provider_test.dart new file mode 100644 index 00000000..8f85ca46 --- /dev/null +++ b/modules/legacy/modules/account/test/features/change_password/change_password_visibility_provider_test.dart @@ -0,0 +1,51 @@ +import 'package:account/src/features/change_password/presentation/providers/change_password_visibility_provider.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sf_shared/testing.dart'; + +void main() { + group('ChangePasswordVisibilityNotifier', () { + test('defaults to both flags false', () { + final container = makeContainer(); + addTearDown(container.dispose); + + final state = container.read(changePasswordVisibilityProvider); + expect(state.showNew, isFalse); + expect(state.showRepeated, isFalse); + }); + + test('toggleNew flips showNew only', () { + final container = makeContainer(); + addTearDown(container.dispose); + + final notifier = container.read( + changePasswordVisibilityProvider.notifier, + ); + + notifier.toggleNew(); + expect(container.read(changePasswordVisibilityProvider).showNew, isTrue); + expect( + container.read(changePasswordVisibilityProvider).showRepeated, + isFalse, + ); + + notifier.toggleNew(); + expect(container.read(changePasswordVisibilityProvider).showNew, isFalse); + }); + + test('toggleRepeated flips showRepeated only', () { + final container = makeContainer(); + addTearDown(container.dispose); + + final notifier = container.read( + changePasswordVisibilityProvider.notifier, + ); + + notifier.toggleRepeated(); + expect( + container.read(changePasswordVisibilityProvider).showRepeated, + isTrue, + ); + expect(container.read(changePasswordVisibilityProvider).showNew, isFalse); + }); + }); +} diff --git a/modules/legacy/modules/account/test/features/change_password/password_validator_test.dart b/modules/legacy/modules/account/test/features/change_password/password_validator_test.dart new file mode 100644 index 00000000..8a7d7838 --- /dev/null +++ b/modules/legacy/modules/account/test/features/change_password/password_validator_test.dart @@ -0,0 +1,88 @@ +import 'package:account/src/features/change_password/domain/password_validator.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sf_localizations/sf_localizations.dart'; + +void main() { + group('PasswordValidator.validate', () { + test('returns errorPasswordRequired when password is empty', () { + expect( + PasswordValidator.validate(password: '', repeat: 'Abcd1234!'), + I18n.errorPasswordRequired, + ); + }); + + test('returns errorPasswordRequired when repeat is empty', () { + expect( + PasswordValidator.validate(password: 'Abcd1234!', repeat: ''), + I18n.errorPasswordRequired, + ); + }); + + test('returns errorMessageUnequalPasswords when they differ', () { + expect( + PasswordValidator.validate( + password: 'Abcd1234!', + repeat: 'Abcd1234@', + ), + I18n.errorMessageUnequalPasswords, + ); + }); + + test('returns errorMessagePasswordTooShort when under minLength', () { + expect( + PasswordValidator.validate(password: 'Ab1!', repeat: 'Ab1!'), + I18n.errorMessagePasswordTooShort, + ); + }); + + test('returns errorMessagePasswordNoCapitals when missing uppercase', () { + expect( + PasswordValidator.validate( + password: 'abcd1234!', + repeat: 'abcd1234!', + ), + I18n.errorMessagePasswordNoCapitals, + ); + }); + + test('returns errorMessagePasswordNoNumbers when missing digit', () { + expect( + PasswordValidator.validate( + password: 'Abcdefgh!', + repeat: 'Abcdefgh!', + ), + I18n.errorMessagePasswordNoNumbers, + ); + }); + + test('returns errorMessagePasswordNoSpecialChars when missing special', () { + expect( + PasswordValidator.validate( + password: 'Abcd1234', + repeat: 'Abcd1234', + ), + I18n.errorMessagePasswordNoSpecialChars, + ); + }); + + test('returns null when password meets all criteria', () { + expect( + PasswordValidator.validate( + password: 'Abcd1234!', + repeat: 'Abcd1234!', + ), + isNull, + ); + }); + + test('trims whitespace before validating', () { + expect( + PasswordValidator.validate( + password: ' Abcd1234! ', + repeat: ' Abcd1234! ', + ), + isNull, + ); + }); + }); +} diff --git a/packages/sf_localizations/assets/l10n/de.json b/packages/sf_localizations/assets/l10n/de.json index 3582c11a..23281f64 100644 --- a/packages/sf_localizations/assets/l10n/de.json +++ b/packages/sf_localizations/assets/l10n/de.json @@ -54,6 +54,7 @@ "passwordCapital": "ein Großbuchstabe", "passwordNumber": "eine Zahl", "passwordSpecial": "Ein Sonderzeichen enthalten", + "passwordMatch": "Passwörter stimmen überein", "accept": "Akzeptieren", "errorMessageUnequalPasswords": "Passwörter stimmen nicht überein. versuchen Sie es erneut", "errorMessagePasswordTooShort": "Das Passwort muss mindestens 8 Zeichen lang sein", diff --git a/packages/sf_localizations/assets/l10n/en.json b/packages/sf_localizations/assets/l10n/en.json index d09d6d48..3624ff92 100755 --- a/packages/sf_localizations/assets/l10n/en.json +++ b/packages/sf_localizations/assets/l10n/en.json @@ -54,6 +54,7 @@ "passwordCapital": "One capital letter", "passwordNumber": "One number", "passwordSpecial": "One special character", + "passwordMatch": "Passwords match", "accept": "Accept", "errorMessageUnequalPasswords": "Passwords don't match. Try again", "errorMessagePasswordTooShort": "Password must include at least 8 characters", diff --git a/packages/sf_localizations/assets/l10n/es.json b/packages/sf_localizations/assets/l10n/es.json index c89f8b78..c4bbdf60 100644 --- a/packages/sf_localizations/assets/l10n/es.json +++ b/packages/sf_localizations/assets/l10n/es.json @@ -54,6 +54,7 @@ "passwordCapital": "Una mayúscula", "passwordNumber": "Un número", "passwordSpecial": "Una carácter especial", + "passwordMatch": "Las contraseñas coinciden", "accept": "Aceptar", "errorMessageUnequalPasswords": "Las contraseñas no coinciden. Inténtalo de nuevo", "errorMessagePasswordTooShort": "La contraseña debe tener al menos 8 caracteres", diff --git a/packages/sf_localizations/assets/l10n/fr.json b/packages/sf_localizations/assets/l10n/fr.json index 98a6d949..993516e0 100644 --- a/packages/sf_localizations/assets/l10n/fr.json +++ b/packages/sf_localizations/assets/l10n/fr.json @@ -54,6 +54,7 @@ "passwordCapital": "une majuscule", "passwordNumber": "un numéro", "passwordSpecial": "Un caractère particulier", + "passwordMatch": "Les mots de passe correspondent", "accept": "Accepter", "errorMessageUnequalPasswords": "Les mots de passe ne correspondent pas. essayer à nouveau", "errorMessagePasswordTooShort": "Le mot de passe doit contenir au moins 8 caractères", diff --git a/packages/sf_localizations/assets/l10n/it.json b/packages/sf_localizations/assets/l10n/it.json index 9ab82926..169bf131 100644 --- a/packages/sf_localizations/assets/l10n/it.json +++ b/packages/sf_localizations/assets/l10n/it.json @@ -54,6 +54,7 @@ "passwordCapital": "una lettera maiuscola", "passwordNumber": "un numero", "passwordSpecial": "Un carattere speciale", + "passwordMatch": "Le password corrispondono", "accept": "Accettare", "errorMessageUnequalPasswords": "Le password non corrispondono. riprova", "errorMessagePasswordTooShort": "La password deve contenere almeno 8 caratteri", diff --git a/packages/sf_localizations/assets/l10n/pt.json b/packages/sf_localizations/assets/l10n/pt.json index fb739767..1c645b04 100644 --- a/packages/sf_localizations/assets/l10n/pt.json +++ b/packages/sf_localizations/assets/l10n/pt.json @@ -54,6 +54,7 @@ "passwordCapital": "Una mayúscula", "passwordNumber": "Um número", "passwordSpecial": "Um caráter especial", + "passwordMatch": "As palavras-passe coincidem", "accept": "Aceitar", "errorMessageUnequalPasswords": "Las contraseñas não é coincidência.", "errorMessagePasswordTooShort": "A senha deve ter pelo menos 8 caracteres", diff --git a/packages/sf_localizations/lib/src/generated/i18n.dart b/packages/sf_localizations/lib/src/generated/i18n.dart index c1641f5d..456d8451 100755 --- a/packages/sf_localizations/lib/src/generated/i18n.dart +++ b/packages/sf_localizations/lib/src/generated/i18n.dart @@ -646,6 +646,7 @@ class I18n { static const String passwordCapital = 'passwordCapital'; static const String passwordLabel = 'passwordLabel'; static const String passwordLength = 'passwordLength'; + static const String passwordMatch = 'passwordMatch'; static const String passwordNumber = 'passwordNumber'; static const String passwordRulesSubtitle = 'passwordRulesSubtitle'; static const String passwordSpecial = 'passwordSpecial';