refactor(legacy-account): migrate change_password to AsyncNotifier
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<ChangePasswordScreen> createState() =>
|
||||
_ChangePasswordScreenState();
|
||||
}
|
||||
|
||||
class _ChangePasswordScreenState extends ConsumerState<ChangePasswordScreen> {
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<void> build() {}
|
||||
|
||||
Future<void> 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()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ChangePasswordController, void> {
|
||||
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<void> {
|
||||
FutureOr<void> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
build();
|
||||
final ref = this.ref as $Ref<AsyncValue<void>, void>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<AsyncValue<void>, void>,
|
||||
AsyncValue<void>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, null);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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<ChangePasswordVisibility>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$changePasswordVisibilityNotifierHash() =>
|
||||
r'8b4a81d152e0982675608d0ba3dbd53c93257d17';
|
||||
|
||||
abstract class _$ChangePasswordVisibilityNotifier
|
||||
extends $Notifier<ChangePasswordVisibility> {
|
||||
ChangePasswordVisibility build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref =
|
||||
this.ref as $Ref<ChangePasswordVisibility, ChangePasswordVisibility>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<ChangePasswordVisibility, ChangePasswordVisibility>,
|
||||
ChangePasswordVisibility,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@@ -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<ChangePasswordViewState> {
|
||||
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<void> 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>(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<ChangePasswordViewState> get copyWith => _$ChangePasswordViewStateCopyWithImpl<ChangePasswordViewState>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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
|
||||
@@ -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<UserEntity> 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<AsyncData<void>>());
|
||||
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<AsyncError<void>>());
|
||||
expect(state.error, isA<ApiException>());
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user