refactor(legacy_auth): migrate recover_password to Riverpod

This commit is contained in:
2026-04-22 23:14:29 +02:00
parent c1e498b1ab
commit 3065b78779
8 changed files with 455 additions and 398 deletions

View File

@@ -1,25 +1,69 @@
import 'package:legacy_auth/src/features/recover_password/presentation/state/recover_password_view_model.dart';
import 'package:legacy_theme/legacy_theme.dart';
import 'package:legacy_auth/src/features/recover_password/presentation/state/recover_password_view_state.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:get_it/get_it.dart';
import 'package:legacy_auth/src/features/recover_password/presentation/providers/recover_password_controller.dart';
import 'package:legacy_auth/src/features/recover_password/presentation/providers/recover_password_state.dart';
import 'package:legacy_theme/legacy_theme.dart';
import 'package:navigation/navigation.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:utils/utils.dart';
class LegacyNewPasswordScreen extends ConsumerWidget {
class LegacyNewPasswordScreen extends ConsumerStatefulWidget {
final NavigationContract navigationContract;
const LegacyNewPasswordScreen({super.key, required this.navigationContract});
const LegacyNewPasswordScreen({
super.key,
required this.navigationContract,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final NavigationContract navigationContract = GetIt.I<NavigationContract>();
ConsumerState<LegacyNewPasswordScreen> createState() =>
_LegacyNewPasswordScreenState();
}
final viewModel = ref.read(legacyRecoverPasswordViewModelProvider.notifier);
final viewState = ref.watch(legacyRecoverPasswordViewModelProvider);
class _LegacyNewPasswordScreenState
extends ConsumerState<LegacyNewPasswordScreen> {
late final TextEditingController _passwordController;
late final TextEditingController _repeatedPasswordController;
@override
void initState() {
super.initState();
final initial = ref.read(recoverPasswordControllerProvider);
_passwordController = TextEditingController(text: initial.password);
_repeatedPasswordController =
TextEditingController(text: initial.repeatedPassword);
_passwordController.addListener(_onPasswordChanged);
_repeatedPasswordController.addListener(_onRepeatedPasswordChanged);
}
@override
void dispose() {
_passwordController.removeListener(_onPasswordChanged);
_repeatedPasswordController.removeListener(_onRepeatedPasswordChanged);
_passwordController.dispose();
_repeatedPasswordController.dispose();
super.dispose();
}
void _onPasswordChanged() {
ref
.read(recoverPasswordControllerProvider.notifier)
.setPassword(_passwordController.text);
}
void _onRepeatedPasswordChanged() {
ref
.read(recoverPasswordControllerProvider.notifier)
.setRepeatedPassword(_repeatedPasswordController.text);
}
@override
Widget build(BuildContext context) {
final navigationContract = GetIt.I<NavigationContract>();
final viewState = ref.watch(recoverPasswordControllerProvider);
final notifier = ref.read(recoverPasswordControllerProvider.notifier);
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
@@ -35,7 +79,8 @@ class LegacyNewPasswordScreen extends ConsumerWidget {
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: SizeUtils.getByScreen(small: 30, big: 30, xl: 26),
fontSize:
SizeUtils.getByScreen(small: 30, big: 30, xl: 26),
letterSpacing: 0,
),
),
@@ -43,23 +88,22 @@ class LegacyNewPasswordScreen extends ConsumerWidget {
CustomTextField(
showPassword: viewState.passwordVisible,
label: context.translate(I18n.newPassword),
labelSize: SizeUtils.getByScreen(small: 14, big: 14, xl: 12),
labelSize:
SizeUtils.getByScreen(small: 14, big: 14, xl: 12),
hint: '********',
controller: viewModel.passwordController,
// onVisibilityChanged: viewModel.togglePasswordVisible,
controller: _passwordController,
),
SizedBox(height: 16),
const SizedBox(height: 16),
CustomTextField(
showPassword: viewState.passwordVisible,
label: context.translate(I18n.repeatPassword),
labelSize: SizeUtils.getByScreen(small: 14, big: 14, xl: 12),
labelSize:
SizeUtils.getByScreen(small: 14, big: 14, xl: 12),
hint: '********',
controller: viewModel.repeatedPasswordController,
// onVisibilityChanged: viewModel.togglePasswordVisible,
// color: viewState.equalPasswords ? Theme.of(context).colorScheme.onSurface : Theme.of(context).colorScheme.error,
controller: _repeatedPasswordController,
),
if (!viewState.equalPasswords) ...[
SizedBox(height: 4),
const SizedBox(height: 4),
Row(
spacing: 8,
children: [
@@ -76,100 +120,32 @@ class LegacyNewPasswordScreen extends ConsumerWidget {
fontSize: 10,
),
),
Spacer(),
const Spacer(),
],
),
],
SizedBox(height: 12),
Row(
spacing: 8,
children: [
Icon(
Icons.check,
color: viewState.securityChecks['min']!
? context.sfColors.legacyPrimary
: Theme.of(context).colorScheme.secondary,
),
Text(
context.translate(I18n.passwordLength),
style: TextStyle(
fontSize: SizeUtils.getByScreen(
small: 14,
big: 14,
xl: 12,
),
),
),
],
const SizedBox(height: 12),
_Check(
label: context.translate(I18n.passwordLength),
passed: viewState.securityChecks['min']!,
),
SizedBox(height: SizeUtils.getByScreen(small: 2, big: 4)),
Row(
spacing: 8,
children: [
Icon(
Icons.check,
color: viewState.securityChecks['capital']!
? context.sfColors.legacyPrimary
: Theme.of(context).colorScheme.secondary,
),
Text(
context.translate(I18n.passwordCapital),
style: TextStyle(
fontSize: SizeUtils.getByScreen(
small: 14,
big: 14,
xl: 12,
),
),
),
],
_Check(
label: context.translate(I18n.passwordCapital),
passed: viewState.securityChecks['capital']!,
),
SizedBox(height: SizeUtils.getByScreen(small: 2, big: 4)),
Row(
spacing: 8,
children: [
Icon(
Icons.check,
color: viewState.securityChecks['number']!
? context.sfColors.legacyPrimary
: Theme.of(context).colorScheme.secondary,
),
Text(
context.translate(I18n.passwordNumber),
style: TextStyle(
fontSize: SizeUtils.getByScreen(
small: 14,
big: 14,
xl: 12,
),
),
),
],
_Check(
label: context.translate(I18n.passwordNumber),
passed: viewState.securityChecks['number']!,
),
SizedBox(height: SizeUtils.getByScreen(small: 2, big: 4)),
Row(
spacing: 8,
children: [
Icon(
Icons.check,
color: viewState.securityChecks['special']!
? context.sfColors.legacyPrimary
: Theme.of(context).colorScheme.secondary,
),
Text(
context.translate(I18n.passwordSpecial),
style: TextStyle(
fontSize: SizeUtils.getByScreen(
small: 14,
big: 14,
xl: 12,
),
),
),
],
_Check(
label: context.translate(I18n.passwordSpecial),
passed: viewState.securityChecks['special']!,
),
if (viewState.displayErrorKey != null) ...[
SizedBox(height: 10),
const SizedBox(height: 10),
Text(
context.translate(viewState.displayErrorKey!),
textAlign: TextAlign.center,
@@ -179,14 +155,13 @@ class LegacyNewPasswordScreen extends ConsumerWidget {
),
),
],
SizedBox(height: 56),
const SizedBox(height: 56),
PrimaryButton(
onPressed: () async {
await viewModel.recoverPassword();
final updatedState = ref.read(
legacyRecoverPasswordViewModelProvider,
);
if (updatedState.passwordChanged) {
await notifier.recoverPassword();
final updated =
ref.read(recoverPasswordControllerProvider);
if (updated.passwordChanged) {
navigationContract.goTo(AppRoutes.controlPanel);
}
},
@@ -201,3 +176,31 @@ class LegacyNewPasswordScreen extends ConsumerWidget {
);
}
}
class _Check extends StatelessWidget {
final String label;
final bool passed;
const _Check({required this.label, required this.passed});
@override
Widget build(BuildContext context) {
return Row(
spacing: 8,
children: [
Icon(
Icons.check,
color: passed
? context.sfColors.legacyPrimary
: Theme.of(context).colorScheme.secondary,
),
Text(
label,
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 14, big: 14, xl: 12),
),
),
],
);
}
}

View File

@@ -0,0 +1,186 @@
import 'dart:async';
import 'package:legacy_auth/src/core/providers/auth_repository_provider.dart';
import 'package:legacy_auth/src/features/recover_password/domain/entities/legacy_recover_password_error_event.dart';
import 'package:legacy_auth/src/features/recover_password/presentation/providers/recover_password_state.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:sf_tracking/sf_tracking.dart';
part 'recover_password_controller.g.dart';
@Riverpod(keepAlive: true)
class RecoverPasswordController extends _$RecoverPasswordController {
@override
RecoverPasswordState build() => const RecoverPasswordState();
void setEmail(String value) {
if (value == state.email) return;
state = state.copyWith(
email: value,
validationErrorKey: '',
apiErrorEvent: null,
recoveryRequested: false,
);
}
void setPassword(String value) {
if (value == state.password) return;
final equalPasswords = value == state.repeatedPassword;
state = state.copyWith(
password: value,
validationErrorKey: '',
apiErrorEvent: null,
equalPasswords: equalPasswords,
securityChecks: {
'min': value.length >= 8,
'capital': RegExp(r'[A-Z]').hasMatch(value),
'number': RegExp(r'[0-9]').hasMatch(value),
'special': RegExp(r'[^A-Za-z0-9]').hasMatch(value),
},
);
}
void setRepeatedPassword(String value) {
if (value == state.repeatedPassword) return;
state = state.copyWith(
repeatedPassword: value,
validationErrorKey: '',
apiErrorEvent: null,
equalPasswords: value == state.password,
);
}
void togglePasswordVisible() {
state = state.copyWith(passwordVisible: !state.passwordVisible);
}
void clearApiError() {
if (state.apiErrorEvent != null) {
state = state.copyWith(apiErrorEvent: null);
}
}
void clearValidationError() {
if (state.validationErrorKey.isNotEmpty) {
state = state.copyWith(validationErrorKey: '');
}
}
Future<void> requestRecovery() async {
final email = state.email.trim();
if (email.isEmpty) {
state =
state.copyWith(validationErrorKey: 'errorMessageContactIsEmpty');
return;
}
state = state.copyWith(
isLoading: true,
validationErrorKey: '',
apiErrorEvent: null,
recoveryRequested: false,
);
final tracking = ref.read(sfTrackingProvider);
unawaited(tracking.legacyAuthPasswordResetRequested());
try {
final token = await ref
.read(legacyAuthRepositoryProvider)
.requestPasswordReset(email: email);
unawaited(tracking.legacyAuthPasswordResetEmailSent());
state = state.copyWith(
isLoading: false,
recoveryRequested: true,
token: token,
);
} catch (e) {
final mappedError = mapResetPasswordError(e);
if (mappedError == null) {
unawaited(tracking.legacyAuthPasswordResetEmailSent());
state = state.copyWith(isLoading: false, recoveryRequested: true);
return;
}
state = state.copyWith(
isLoading: false,
apiErrorEvent: mappedError,
recoveryRequested: false,
passwordChanged: false,
);
}
}
Future<void> recoverPassword() async {
final tracking = ref.read(sfTrackingProvider);
if (!state.equalPasswords) {
unawaited(tracking.legacyAuthPasswordResetFailed('unequal_passwords'));
state = state.copyWith(
validationErrorKey: 'errorMessageUnequalPasswords',
passwordChanged: false,
);
return;
}
if (!state.securityChecks['min']!) {
unawaited(tracking.legacyAuthPasswordResetFailed('too_short'));
state = state.copyWith(
validationErrorKey: 'errorMessagePasswordTooShort',
passwordChanged: false,
);
return;
}
if (!state.securityChecks['capital']!) {
unawaited(tracking.legacyAuthPasswordResetFailed('no_capitals'));
state = state.copyWith(
validationErrorKey: 'errorMessagePasswordNoCapitals',
passwordChanged: false,
);
return;
}
if (!state.securityChecks['number']!) {
unawaited(tracking.legacyAuthPasswordResetFailed('no_numbers'));
state = state.copyWith(
validationErrorKey: 'errorMessagePasswordNoNumbers',
passwordChanged: false,
);
return;
}
if (!state.securityChecks['special']!) {
unawaited(tracking.legacyAuthPasswordResetFailed('no_special_chars'));
state = state.copyWith(
validationErrorKey: 'errorMessagePasswordNoSpecialChars',
passwordChanged: false,
);
return;
}
state = state.copyWith(
isLoading: true,
apiErrorEvent: null,
passwordChanged: false,
);
try {
await ref.read(legacyAuthRepositoryProvider).recoverPassword(
newPassword: state.password,
token: state.token,
);
unawaited(tracking.legacyAuthPasswordResetCompleted());
state = state.copyWith(isLoading: false, passwordChanged: true);
} catch (error) {
unawaited(tracking.legacyAuthPasswordResetFailed(error.toString()));
state = state.copyWith(
apiErrorEvent: mapRecoveryPasswordError(error),
isLoading: false,
passwordChanged: false,
);
}
}
}

View File

@@ -0,0 +1,65 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'recover_password_controller.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(RecoverPasswordController)
const recoverPasswordControllerProvider = RecoverPasswordControllerProvider._();
final class RecoverPasswordControllerProvider
extends $NotifierProvider<RecoverPasswordController, RecoverPasswordState> {
const RecoverPasswordControllerProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'recoverPasswordControllerProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$recoverPasswordControllerHash();
@$internal
@override
RecoverPasswordController create() => RecoverPasswordController();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(RecoverPasswordState value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<RecoverPasswordState>(value),
);
}
}
String _$recoverPasswordControllerHash() =>
r'1047baf34df2dc5c35a3816e0e3cac05d45d882c';
abstract class _$RecoverPasswordController
extends $Notifier<RecoverPasswordState> {
RecoverPasswordState build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<RecoverPasswordState, RecoverPasswordState>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<RecoverPasswordState, RecoverPasswordState>,
RecoverPasswordState,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}

View File

@@ -2,12 +2,11 @@ import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:legacy_auth/src/features/recover_password/domain/entities/legacy_recover_password_error_event.dart';
import 'package:sf_localizations/sf_localizations.dart';
part 'recover_password_view_state.freezed.dart';
part 'recover_password_state.freezed.dart';
@freezed
abstract class LegacyRecoverPasswordViewState
with _$LegacyRecoverPasswordViewState {
const factory LegacyRecoverPasswordViewState({
abstract class RecoverPasswordState with _$RecoverPasswordState {
const factory RecoverPasswordState({
@Default('') String email,
@Default('') String validationErrorKey,
LegacyRecoverPasswordErrorEvent? apiErrorEvent,
@@ -26,11 +25,10 @@ abstract class LegacyRecoverPasswordViewState
'special': false,
})
Map<String, bool> securityChecks,
}) = _LegacyRecoverPasswordViewState;
}) = _RecoverPasswordState;
}
extension LegacyRecoverPasswordViewStateDisplay
on LegacyRecoverPasswordViewState {
extension RecoverPasswordStateDisplay on RecoverPasswordState {
String? get displayErrorKey {
if (validationErrorKey.isNotEmpty) return validationErrorKey;
final event = apiErrorEvent;

View File

@@ -3,7 +3,7 @@
// 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 'recover_password_view_state.dart';
part of 'recover_password_state.dart';
// **************************************************************************
// FreezedGenerator
@@ -12,20 +12,20 @@ part of 'recover_password_view_state.dart';
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$LegacyRecoverPasswordViewState {
mixin _$RecoverPasswordState {
String get email; String get validationErrorKey; LegacyRecoverPasswordErrorEvent? get apiErrorEvent; bool get isLoading; bool get recoveryRequested; bool get passwordChanged; String get token; String get password; String get repeatedPassword; bool get passwordVisible; bool get equalPasswords; Map<String, bool> get securityChecks;
/// Create a copy of LegacyRecoverPasswordViewState
/// Create a copy of RecoverPasswordState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$LegacyRecoverPasswordViewStateCopyWith<LegacyRecoverPasswordViewState> get copyWith => _$LegacyRecoverPasswordViewStateCopyWithImpl<LegacyRecoverPasswordViewState>(this as LegacyRecoverPasswordViewState, _$identity);
$RecoverPasswordStateCopyWith<RecoverPasswordState> get copyWith => _$RecoverPasswordStateCopyWithImpl<RecoverPasswordState>(this as RecoverPasswordState, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is LegacyRecoverPasswordViewState&&(identical(other.email, email) || other.email == email)&&(identical(other.validationErrorKey, validationErrorKey) || other.validationErrorKey == validationErrorKey)&&(identical(other.apiErrorEvent, apiErrorEvent) || other.apiErrorEvent == apiErrorEvent)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.recoveryRequested, recoveryRequested) || other.recoveryRequested == recoveryRequested)&&(identical(other.passwordChanged, passwordChanged) || other.passwordChanged == passwordChanged)&&(identical(other.token, token) || other.token == token)&&(identical(other.password, password) || other.password == password)&&(identical(other.repeatedPassword, repeatedPassword) || other.repeatedPassword == repeatedPassword)&&(identical(other.passwordVisible, passwordVisible) || other.passwordVisible == passwordVisible)&&(identical(other.equalPasswords, equalPasswords) || other.equalPasswords == equalPasswords)&&const DeepCollectionEquality().equals(other.securityChecks, securityChecks));
return identical(this, other) || (other.runtimeType == runtimeType&&other is RecoverPasswordState&&(identical(other.email, email) || other.email == email)&&(identical(other.validationErrorKey, validationErrorKey) || other.validationErrorKey == validationErrorKey)&&(identical(other.apiErrorEvent, apiErrorEvent) || other.apiErrorEvent == apiErrorEvent)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.recoveryRequested, recoveryRequested) || other.recoveryRequested == recoveryRequested)&&(identical(other.passwordChanged, passwordChanged) || other.passwordChanged == passwordChanged)&&(identical(other.token, token) || other.token == token)&&(identical(other.password, password) || other.password == password)&&(identical(other.repeatedPassword, repeatedPassword) || other.repeatedPassword == repeatedPassword)&&(identical(other.passwordVisible, passwordVisible) || other.passwordVisible == passwordVisible)&&(identical(other.equalPasswords, equalPasswords) || other.equalPasswords == equalPasswords)&&const DeepCollectionEquality().equals(other.securityChecks, securityChecks));
}
@@ -34,15 +34,15 @@ int get hashCode => Object.hash(runtimeType,email,validationErrorKey,apiErrorEve
@override
String toString() {
return 'LegacyRecoverPasswordViewState(email: $email, validationErrorKey: $validationErrorKey, apiErrorEvent: $apiErrorEvent, isLoading: $isLoading, recoveryRequested: $recoveryRequested, passwordChanged: $passwordChanged, token: $token, password: $password, repeatedPassword: $repeatedPassword, passwordVisible: $passwordVisible, equalPasswords: $equalPasswords, securityChecks: $securityChecks)';
return 'RecoverPasswordState(email: $email, validationErrorKey: $validationErrorKey, apiErrorEvent: $apiErrorEvent, isLoading: $isLoading, recoveryRequested: $recoveryRequested, passwordChanged: $passwordChanged, token: $token, password: $password, repeatedPassword: $repeatedPassword, passwordVisible: $passwordVisible, equalPasswords: $equalPasswords, securityChecks: $securityChecks)';
}
}
/// @nodoc
abstract mixin class $LegacyRecoverPasswordViewStateCopyWith<$Res> {
factory $LegacyRecoverPasswordViewStateCopyWith(LegacyRecoverPasswordViewState value, $Res Function(LegacyRecoverPasswordViewState) _then) = _$LegacyRecoverPasswordViewStateCopyWithImpl;
abstract mixin class $RecoverPasswordStateCopyWith<$Res> {
factory $RecoverPasswordStateCopyWith(RecoverPasswordState value, $Res Function(RecoverPasswordState) _then) = _$RecoverPasswordStateCopyWithImpl;
@useResult
$Res call({
String email, String validationErrorKey, LegacyRecoverPasswordErrorEvent? apiErrorEvent, bool isLoading, bool recoveryRequested, bool passwordChanged, String token, String password, String repeatedPassword, bool passwordVisible, bool equalPasswords, Map<String, bool> securityChecks
@@ -53,14 +53,14 @@ $Res call({
}
/// @nodoc
class _$LegacyRecoverPasswordViewStateCopyWithImpl<$Res>
implements $LegacyRecoverPasswordViewStateCopyWith<$Res> {
_$LegacyRecoverPasswordViewStateCopyWithImpl(this._self, this._then);
class _$RecoverPasswordStateCopyWithImpl<$Res>
implements $RecoverPasswordStateCopyWith<$Res> {
_$RecoverPasswordStateCopyWithImpl(this._self, this._then);
final LegacyRecoverPasswordViewState _self;
final $Res Function(LegacyRecoverPasswordViewState) _then;
final RecoverPasswordState _self;
final $Res Function(RecoverPasswordState) _then;
/// Create a copy of LegacyRecoverPasswordViewState
/// Create a copy of RecoverPasswordState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? email = null,Object? validationErrorKey = null,Object? apiErrorEvent = freezed,Object? isLoading = null,Object? recoveryRequested = null,Object? passwordChanged = null,Object? token = null,Object? password = null,Object? repeatedPassword = null,Object? passwordVisible = null,Object? equalPasswords = null,Object? securityChecks = null,}) {
return _then(_self.copyWith(
@@ -83,8 +83,8 @@ as Map<String, bool>,
}
/// Adds pattern-matching-related methods to [LegacyRecoverPasswordViewState].
extension LegacyRecoverPasswordViewStatePatterns on LegacyRecoverPasswordViewState {
/// Adds pattern-matching-related methods to [RecoverPasswordState].
extension RecoverPasswordStatePatterns on RecoverPasswordState {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
@@ -97,10 +97,10 @@ extension LegacyRecoverPasswordViewStatePatterns on LegacyRecoverPasswordViewSta
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _LegacyRecoverPasswordViewState value)? $default,{required TResult orElse(),}){
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _RecoverPasswordState value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _LegacyRecoverPasswordViewState() when $default != null:
case _RecoverPasswordState() when $default != null:
return $default(_that);case _:
return orElse();
@@ -119,10 +119,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _LegacyRecoverPasswordViewState value) $default,){
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _RecoverPasswordState value) $default,){
final _that = this;
switch (_that) {
case _LegacyRecoverPasswordViewState():
case _RecoverPasswordState():
return $default(_that);case _:
throw StateError('Unexpected subclass');
@@ -140,10 +140,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _LegacyRecoverPasswordViewState value)? $default,){
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _RecoverPasswordState value)? $default,){
final _that = this;
switch (_that) {
case _LegacyRecoverPasswordViewState() when $default != null:
case _RecoverPasswordState() when $default != null:
return $default(_that);case _:
return null;
@@ -163,7 +163,7 @@ return $default(_that);case _:
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String email, String validationErrorKey, LegacyRecoverPasswordErrorEvent? apiErrorEvent, bool isLoading, bool recoveryRequested, bool passwordChanged, String token, String password, String repeatedPassword, bool passwordVisible, bool equalPasswords, Map<String, bool> securityChecks)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _LegacyRecoverPasswordViewState() when $default != null:
case _RecoverPasswordState() when $default != null:
return $default(_that.email,_that.validationErrorKey,_that.apiErrorEvent,_that.isLoading,_that.recoveryRequested,_that.passwordChanged,_that.token,_that.password,_that.repeatedPassword,_that.passwordVisible,_that.equalPasswords,_that.securityChecks);case _:
return orElse();
@@ -184,7 +184,7 @@ return $default(_that.email,_that.validationErrorKey,_that.apiErrorEvent,_that.i
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String email, String validationErrorKey, LegacyRecoverPasswordErrorEvent? apiErrorEvent, bool isLoading, bool recoveryRequested, bool passwordChanged, String token, String password, String repeatedPassword, bool passwordVisible, bool equalPasswords, Map<String, bool> securityChecks) $default,) {final _that = this;
switch (_that) {
case _LegacyRecoverPasswordViewState():
case _RecoverPasswordState():
return $default(_that.email,_that.validationErrorKey,_that.apiErrorEvent,_that.isLoading,_that.recoveryRequested,_that.passwordChanged,_that.token,_that.password,_that.repeatedPassword,_that.passwordVisible,_that.equalPasswords,_that.securityChecks);case _:
throw StateError('Unexpected subclass');
@@ -204,7 +204,7 @@ return $default(_that.email,_that.validationErrorKey,_that.apiErrorEvent,_that.i
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String email, String validationErrorKey, LegacyRecoverPasswordErrorEvent? apiErrorEvent, bool isLoading, bool recoveryRequested, bool passwordChanged, String token, String password, String repeatedPassword, bool passwordVisible, bool equalPasswords, Map<String, bool> securityChecks)? $default,) {final _that = this;
switch (_that) {
case _LegacyRecoverPasswordViewState() when $default != null:
case _RecoverPasswordState() when $default != null:
return $default(_that.email,_that.validationErrorKey,_that.apiErrorEvent,_that.isLoading,_that.recoveryRequested,_that.passwordChanged,_that.token,_that.password,_that.repeatedPassword,_that.passwordVisible,_that.equalPasswords,_that.securityChecks);case _:
return null;
@@ -216,8 +216,8 @@ return $default(_that.email,_that.validationErrorKey,_that.apiErrorEvent,_that.i
/// @nodoc
class _LegacyRecoverPasswordViewState implements LegacyRecoverPasswordViewState {
const _LegacyRecoverPasswordViewState({this.email = '', this.validationErrorKey = '', this.apiErrorEvent, this.isLoading = false, this.recoveryRequested = false, this.passwordChanged = false, this.token = '', this.password = '', this.repeatedPassword = '', this.passwordVisible = false, this.equalPasswords = true, final Map<String, bool> securityChecks = const {'min' : false, 'capital' : false, 'number' : false, 'special' : false}}): _securityChecks = securityChecks;
class _RecoverPasswordState implements RecoverPasswordState {
const _RecoverPasswordState({this.email = '', this.validationErrorKey = '', this.apiErrorEvent, this.isLoading = false, this.recoveryRequested = false, this.passwordChanged = false, this.token = '', this.password = '', this.repeatedPassword = '', this.passwordVisible = false, this.equalPasswords = true, final Map<String, bool> securityChecks = const {'min' : false, 'capital' : false, 'number' : false, 'special' : false}}): _securityChecks = securityChecks;
@override@JsonKey() final String email;
@@ -239,17 +239,17 @@ class _LegacyRecoverPasswordViewState implements LegacyRecoverPasswordViewState
}
/// Create a copy of LegacyRecoverPasswordViewState
/// Create a copy of RecoverPasswordState
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$LegacyRecoverPasswordViewStateCopyWith<_LegacyRecoverPasswordViewState> get copyWith => __$LegacyRecoverPasswordViewStateCopyWithImpl<_LegacyRecoverPasswordViewState>(this, _$identity);
_$RecoverPasswordStateCopyWith<_RecoverPasswordState> get copyWith => __$RecoverPasswordStateCopyWithImpl<_RecoverPasswordState>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LegacyRecoverPasswordViewState&&(identical(other.email, email) || other.email == email)&&(identical(other.validationErrorKey, validationErrorKey) || other.validationErrorKey == validationErrorKey)&&(identical(other.apiErrorEvent, apiErrorEvent) || other.apiErrorEvent == apiErrorEvent)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.recoveryRequested, recoveryRequested) || other.recoveryRequested == recoveryRequested)&&(identical(other.passwordChanged, passwordChanged) || other.passwordChanged == passwordChanged)&&(identical(other.token, token) || other.token == token)&&(identical(other.password, password) || other.password == password)&&(identical(other.repeatedPassword, repeatedPassword) || other.repeatedPassword == repeatedPassword)&&(identical(other.passwordVisible, passwordVisible) || other.passwordVisible == passwordVisible)&&(identical(other.equalPasswords, equalPasswords) || other.equalPasswords == equalPasswords)&&const DeepCollectionEquality().equals(other._securityChecks, _securityChecks));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _RecoverPasswordState&&(identical(other.email, email) || other.email == email)&&(identical(other.validationErrorKey, validationErrorKey) || other.validationErrorKey == validationErrorKey)&&(identical(other.apiErrorEvent, apiErrorEvent) || other.apiErrorEvent == apiErrorEvent)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.recoveryRequested, recoveryRequested) || other.recoveryRequested == recoveryRequested)&&(identical(other.passwordChanged, passwordChanged) || other.passwordChanged == passwordChanged)&&(identical(other.token, token) || other.token == token)&&(identical(other.password, password) || other.password == password)&&(identical(other.repeatedPassword, repeatedPassword) || other.repeatedPassword == repeatedPassword)&&(identical(other.passwordVisible, passwordVisible) || other.passwordVisible == passwordVisible)&&(identical(other.equalPasswords, equalPasswords) || other.equalPasswords == equalPasswords)&&const DeepCollectionEquality().equals(other._securityChecks, _securityChecks));
}
@@ -258,15 +258,15 @@ int get hashCode => Object.hash(runtimeType,email,validationErrorKey,apiErrorEve
@override
String toString() {
return 'LegacyRecoverPasswordViewState(email: $email, validationErrorKey: $validationErrorKey, apiErrorEvent: $apiErrorEvent, isLoading: $isLoading, recoveryRequested: $recoveryRequested, passwordChanged: $passwordChanged, token: $token, password: $password, repeatedPassword: $repeatedPassword, passwordVisible: $passwordVisible, equalPasswords: $equalPasswords, securityChecks: $securityChecks)';
return 'RecoverPasswordState(email: $email, validationErrorKey: $validationErrorKey, apiErrorEvent: $apiErrorEvent, isLoading: $isLoading, recoveryRequested: $recoveryRequested, passwordChanged: $passwordChanged, token: $token, password: $password, repeatedPassword: $repeatedPassword, passwordVisible: $passwordVisible, equalPasswords: $equalPasswords, securityChecks: $securityChecks)';
}
}
/// @nodoc
abstract mixin class _$LegacyRecoverPasswordViewStateCopyWith<$Res> implements $LegacyRecoverPasswordViewStateCopyWith<$Res> {
factory _$LegacyRecoverPasswordViewStateCopyWith(_LegacyRecoverPasswordViewState value, $Res Function(_LegacyRecoverPasswordViewState) _then) = __$LegacyRecoverPasswordViewStateCopyWithImpl;
abstract mixin class _$RecoverPasswordStateCopyWith<$Res> implements $RecoverPasswordStateCopyWith<$Res> {
factory _$RecoverPasswordStateCopyWith(_RecoverPasswordState value, $Res Function(_RecoverPasswordState) _then) = __$RecoverPasswordStateCopyWithImpl;
@override @useResult
$Res call({
String email, String validationErrorKey, LegacyRecoverPasswordErrorEvent? apiErrorEvent, bool isLoading, bool recoveryRequested, bool passwordChanged, String token, String password, String repeatedPassword, bool passwordVisible, bool equalPasswords, Map<String, bool> securityChecks
@@ -277,17 +277,17 @@ $Res call({
}
/// @nodoc
class __$LegacyRecoverPasswordViewStateCopyWithImpl<$Res>
implements _$LegacyRecoverPasswordViewStateCopyWith<$Res> {
__$LegacyRecoverPasswordViewStateCopyWithImpl(this._self, this._then);
class __$RecoverPasswordStateCopyWithImpl<$Res>
implements _$RecoverPasswordStateCopyWith<$Res> {
__$RecoverPasswordStateCopyWithImpl(this._self, this._then);
final _LegacyRecoverPasswordViewState _self;
final $Res Function(_LegacyRecoverPasswordViewState) _then;
final _RecoverPasswordState _self;
final $Res Function(_RecoverPasswordState) _then;
/// Create a copy of LegacyRecoverPasswordViewState
/// Create a copy of RecoverPasswordState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? email = null,Object? validationErrorKey = null,Object? apiErrorEvent = freezed,Object? isLoading = null,Object? recoveryRequested = null,Object? passwordChanged = null,Object? token = null,Object? password = null,Object? repeatedPassword = null,Object? passwordVisible = null,Object? equalPasswords = null,Object? securityChecks = null,}) {
return _then(_LegacyRecoverPasswordViewState(
return _then(_RecoverPasswordState(
email: null == email ? _self.email : email // ignore: cast_nullable_to_non_nullable
as String,validationErrorKey: null == validationErrorKey ? _self.validationErrorKey : validationErrorKey // ignore: cast_nullable_to_non_nullable
as String,apiErrorEvent: freezed == apiErrorEvent ? _self.apiErrorEvent : apiErrorEvent // ignore: cast_nullable_to_non_nullable

View File

@@ -1,15 +1,14 @@
import 'package:legacy_auth/src/features/recover_password/presentation/sent/sent_screen.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:navigation/navigation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:legacy_auth/src/features/recover_password/presentation/providers/recover_password_controller.dart';
import 'package:legacy_auth/src/features/recover_password/presentation/providers/recover_password_state.dart';
import 'package:legacy_auth/src/features/recover_password/presentation/sent/sent_screen.dart';
import 'package:navigation/navigation.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:utils/utils.dart';
import '../state/recover_password_view_model.dart';
import '../state/recover_password_view_state.dart';
class LegacyRequestRecoveryScreen extends ConsumerWidget {
class LegacyRequestRecoveryScreen extends ConsumerStatefulWidget {
final NavigationContract navigationContract;
const LegacyRequestRecoveryScreen({
@@ -18,10 +17,39 @@ class LegacyRequestRecoveryScreen extends ConsumerWidget {
});
@override
Widget build(BuildContext context, WidgetRef ref) {
ConsumerState<LegacyRequestRecoveryScreen> createState() =>
_LegacyRequestRecoveryScreenState();
}
final viewModel = ref.read(legacyRecoverPasswordViewModelProvider.notifier);
final viewState = ref.watch(legacyRecoverPasswordViewModelProvider);
class _LegacyRequestRecoveryScreenState
extends ConsumerState<LegacyRequestRecoveryScreen> {
late final TextEditingController _emailController;
@override
void initState() {
super.initState();
final initial = ref.read(recoverPasswordControllerProvider).email;
_emailController = TextEditingController(text: initial);
_emailController.addListener(_onEmailChanged);
}
@override
void dispose() {
_emailController.removeListener(_onEmailChanged);
_emailController.dispose();
super.dispose();
}
void _onEmailChanged() {
ref
.read(recoverPasswordControllerProvider.notifier)
.setEmail(_emailController.text);
}
@override
Widget build(BuildContext context) {
final viewState = ref.watch(recoverPasswordControllerProvider);
final notifier = ref.read(recoverPasswordControllerProvider.notifier);
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
@@ -53,7 +81,7 @@ class LegacyRequestRecoveryScreen extends ConsumerWidget {
CustomTextField(
label: context.translate(I18n.email),
hint: context.translate(I18n.email),
controller: viewModel.emailController,
controller: _emailController,
),
SizedBox(
height: SizeUtils.getByScreen(small: 40, big: 40, xl: 28),
@@ -67,13 +95,13 @@ class LegacyRequestRecoveryScreen extends ConsumerWidget {
fontSize: 12,
),
),
SizedBox(height: 40),
const SizedBox(height: 40),
],
Row(
children: [
Expanded(
child: SecondaryButton(
onPressed: () => {Navigator.pop(context)},
onPressed: () => Navigator.pop(context),
text: context.translate(I18n.back),
size: SizeUtils.getByScreen(small: 16, big: 16, xl: 14),
),
@@ -84,17 +112,17 @@ class LegacyRequestRecoveryScreen extends ConsumerWidget {
Expanded(
child: PrimaryButton(
onPressed: () async {
await viewModel.requestRecovery();
await notifier.requestRecovery();
if (!context.mounted) return;
final updatedState = ref.read(
legacyRecoverPasswordViewModelProvider,
);
if (updatedState.recoveryRequested) {
final updated =
ref.read(recoverPasswordControllerProvider);
if (updated.recoveryRequested) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => LegacySentScreen(
navigationContract: navigationContract,
navigationContract:
widget.navigationContract,
),
),
);

View File

@@ -7,7 +7,7 @@ import 'package:navigation/navigation.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:utils/utils.dart';
import '../state/recover_password_view_model.dart';
import '../providers/recover_password_controller.dart';
class LegacySentScreen extends ConsumerWidget {
final NavigationContract navigationContract;
@@ -17,7 +17,7 @@ class LegacySentScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final viewModel = ref.read(legacyRecoverPasswordViewModelProvider.notifier);
final viewModel = ref.read(recoverPasswordControllerProvider.notifier);
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,

View File

@@ -1,223 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:legacy_auth/src/core/domain/repositories/auth_repository.dart';
import 'package:legacy_auth/src/core/providers/auth_repository_provider.dart';
import 'package:legacy_auth/src/features/recover_password/domain/entities/legacy_recover_password_error_event.dart';
import 'package:legacy_auth/src/features/recover_password/presentation/state/recover_password_view_state.dart';
import 'package:sf_tracking/sf_tracking.dart';
final legacyRecoverPasswordViewModelProvider =
NotifierProvider.autoDispose<
LegacyRecoverPasswordViewModel,
LegacyRecoverPasswordViewState
>(LegacyRecoverPasswordViewModel.new);
class LegacyRecoverPasswordViewModel
extends Notifier<LegacyRecoverPasswordViewState> {
late final LegacyAuthRepository _authRepository;
late final SfTrackingRepository _tracking;
late final TextEditingController emailController;
late final TextEditingController passwordController;
late final TextEditingController repeatedPasswordController;
@override
LegacyRecoverPasswordViewState build() {
_authRepository = ref.read(legacyAuthRepositoryProvider);
_tracking = ref.read(sfTrackingProvider);
emailController = TextEditingController();
emailController.addListener(_onEmailChanged);
passwordController = TextEditingController();
passwordController.addListener(_onPasswordChanged);
repeatedPasswordController = TextEditingController();
repeatedPasswordController.addListener(_onRepeatedPasswordChanged);
ref.onDispose(disposeControllers);
return const LegacyRecoverPasswordViewState();
}
void _onEmailChanged() {
state = state.copyWith(
email: emailController.text,
validationErrorKey: '',
apiErrorEvent: null,
recoveryRequested: false,
);
}
void _onPasswordChanged() {
final raw = passwordController.text;
final equalPasswords = raw == repeatedPasswordController.text;
state = state.copyWith(
password: raw,
validationErrorKey: '',
apiErrorEvent: null,
equalPasswords: equalPasswords,
securityChecks: {
'min': raw.length >= 8,
'capital': RegExp(r'[A-Z]').hasMatch(raw),
'number': RegExp(r'[0-9]').hasMatch(raw),
'special': RegExp(r'[^A-Za-z0-9]').hasMatch(raw),
},
);
}
void _onRepeatedPasswordChanged() {
final raw = repeatedPasswordController.text;
state = state.copyWith(
repeatedPassword: raw,
validationErrorKey: '',
apiErrorEvent: null,
equalPasswords: raw == passwordController.text,
);
}
void togglePasswordVisible() {
state = state.copyWith(passwordVisible: !state.passwordVisible);
}
void clearApiError() {
if (state.apiErrorEvent != null) state = state.copyWith(apiErrorEvent: null);
}
void clearValidationError() {
if (state.validationErrorKey.isNotEmpty) {
state = state.copyWith(validationErrorKey: '');
}
}
Future<void> requestRecovery() async {
final email = state.email.trim();
if (email.isEmpty) {
state = state.copyWith(validationErrorKey: 'errorMessageContactIsEmpty');
return;
}
state = state.copyWith(
isLoading: true,
validationErrorKey: '',
apiErrorEvent: null,
recoveryRequested: false,
);
unawaited(_tracking.legacyAuthPasswordResetRequested());
try {
final token = await _authRepository.requestPasswordReset(email: email);
if (!ref.mounted) return;
unawaited(_tracking.legacyAuthPasswordResetEmailSent());
state = state.copyWith(
isLoading: false,
recoveryRequested: true,
token: token,
);
} catch (e) {
if (!ref.mounted) return;
final mappedError = mapResetPasswordError(e);
if (mappedError == null) {
unawaited(_tracking.legacyAuthPasswordResetEmailSent());
state = state.copyWith(
isLoading: false,
recoveryRequested: true,
);
return;
}
state = state.copyWith(
isLoading: false,
apiErrorEvent: mappedError,
recoveryRequested: false,
passwordChanged: false,
);
}
}
Future<void> recoverPassword() async {
if (!state.equalPasswords) {
unawaited(_tracking.legacyAuthPasswordResetFailed('unequal_passwords'));
state = state.copyWith(
validationErrorKey: 'errorMessageUnequalPasswords',
passwordChanged: false,
);
return;
}
if (!state.securityChecks['min']!) {
unawaited(_tracking.legacyAuthPasswordResetFailed('too_short'));
state = state.copyWith(
validationErrorKey: 'errorMessagePasswordTooShort',
passwordChanged: false,
);
return;
}
if (!state.securityChecks['capital']!) {
unawaited(_tracking.legacyAuthPasswordResetFailed('no_capitals'));
state = state.copyWith(
validationErrorKey: 'errorMessagePasswordNoCapitals',
passwordChanged: false,
);
return;
}
if (!state.securityChecks['number']!) {
unawaited(_tracking.legacyAuthPasswordResetFailed('no_numbers'));
state = state.copyWith(
validationErrorKey: 'errorMessagePasswordNoNumbers',
passwordChanged: false,
);
return;
}
if (!state.securityChecks['special']!) {
unawaited(_tracking.legacyAuthPasswordResetFailed('no_special_chars'));
state = state.copyWith(
validationErrorKey: 'errorMessagePasswordNoSpecialChars',
passwordChanged: false,
);
return;
}
state = state.copyWith(
isLoading: true,
apiErrorEvent: null,
passwordChanged: false,
);
try {
await _authRepository.recoverPassword(
newPassword: state.password,
token: state.token,
);
unawaited(_tracking.legacyAuthPasswordResetCompleted());
state = state.copyWith(isLoading: false, passwordChanged: true);
} catch (error) {
unawaited(_tracking.legacyAuthPasswordResetFailed(error.toString()));
state = state.copyWith(
apiErrorEvent: mapRecoveryPasswordError(error),
isLoading: false,
passwordChanged: false,
);
}
}
void disposeControllers() {
emailController.removeListener(_onEmailChanged);
emailController.dispose();
passwordController.removeListener(_onPasswordChanged);
passwordController.dispose();
repeatedPasswordController.removeListener(_onRepeatedPasswordChanged);
repeatedPasswordController.dispose();
}
}