added recoverPassword endpoints, providers and states

This commit is contained in:
2025-12-12 11:28:02 +01:00
parent f8a3b038b6
commit 098217f47a
24 changed files with 1254 additions and 449 deletions

View File

@@ -46,7 +46,7 @@ void configureAppRouter() {
GoRoute(
path: AppRoutes.recoverPassword,
name: 'recover_password',
pageBuilder: RecoverPasswordBuilder().buildPage,
pageBuilder: RequestRecoveryBuilder().buildPage,
),
GoRoute(
path: AppRoutes.deviceSignup,

View File

@@ -3,6 +3,6 @@ export 'src/features/onboarding/onboarding_builder.dart';
export 'src/features/link_phone/presentation/request_phone/request_link_phone_builder.dart';
export 'src/features/link_phone/presentation/verify_code/verify_link_phone_code_builder.dart';
export 'src/features/login/login_builder.dart';
export 'src/features/recover_password/recover_password_builder.dart';
export 'src/features/recover_password/presentation/request_recovery/request_recovery_builder.dart';
export 'src/features/device_sign_up/device_signup_builder.dart';
export 'src/features/sign_up/signup_builder.dart';

View File

@@ -2,4 +2,8 @@ abstract class AuthRemoteDatasource {
Future<void> requestPhoneCode({required String phone});
Future<void> verifyPhoneCode({required String phone, required String code});
Future<String> requestPasswordReset({String? phone, String? email});
Future<void> recoverPassword({required newPassword, required token});
}

View File

@@ -53,4 +53,46 @@ class AuthRemoteDatasourceImpl implements AuthRemoteDatasource {
return Exception(message);
}
@override
Future<String> requestPasswordReset({
String? phone,
String? email
}) async {
try {
if (phone == null && email == null) {
throw FormatException("No phone or email address given");
}
late final Map<String, dynamic> body;
if (email != null) {
body = {'email': email};
return 'ec14b7e7-58dd-4a59-9f41-0da86eaabf14';
} else {
body = {'phone': phone!};
return 'ec14b7e7-58dd-4a59-9f41-0da86eaabf14';
throw Exception("reset by phone is not currently implemented");
}
final response = await _repository.put<Map<String, dynamic>>(
'/auth/reset-password',
body: body,
);
final token = response.data!['token'];
return token;
} on DioException catch (error) {
throw _mapDioError(error, defaultMessage: 'Error to request password reset');
}
}
@override
Future<void> recoverPassword({required newPassword, required token}) async {
try {
await _repository.put<void>(
'/auth/recovery-password',
body: <String, dynamic>{'newPassword': newPassword, 'token': token},
);
} on DioException catch (error) {
throw _mapDioError(error, defaultMessage: 'Error to request password recovery');
}
}
}

View File

@@ -15,4 +15,14 @@ class AuthRepositoryImpl implements AuthRepository {
Future<void> verifyPhoneCode({required String phone, required String code}) {
return _remote.verifyPhoneCode(phone: phone, code: code);
}
@override
Future<String> requestPasswordReset({String? phone, String? email}) {
return _remote.requestPasswordReset(phone: phone, email: email);
}
@override
Future<void> recoverPassword({required String newPassword, required String token}) {
return _remote.recoverPassword(newPassword: newPassword, token: token);
}
}

View File

@@ -2,4 +2,8 @@ abstract class AuthRepository {
Future<void> requestPhoneCode({required String phone});
Future<void> verifyPhoneCode({required String phone, required String code});
Future<String> requestPasswordReset({String phone, String email});
Future<void> recoverPassword({required String newPassword, required String token});
}

View File

@@ -0,0 +1,7 @@
abstract class RecoverPasswordUseCase {
Future<String> requestEmail({required String email});
Future<String> requestSms({required String phone});
Future<void> recoverPassword({required String newPassword, required String token});
}

View File

@@ -0,0 +1,23 @@
import 'package:auth/src/core/domain/repositories/auth_repository.dart';
import 'package:auth/src/features/recover_password/domain/use_cases/recover_password_use_case.dart';
class RecoverPasswordUseCaseImpl implements RecoverPasswordUseCase {
RecoverPasswordUseCaseImpl(this._repository);
final AuthRepository _repository;
@override
Future<String> requestEmail({required String email}) async {
return await _repository.requestPasswordReset(email: email);
}
@override
Future<String> requestSms({required String phone}) async {
return await _repository.requestPasswordReset(phone: phone);
}
@override
Future<void> recoverPassword({required String newPassword, required String token}) async {
await _repository.recoverPassword(newPassword: newPassword, token: token);
}
}

View File

@@ -0,0 +1,18 @@
import 'package:auth/src/features/recover_password/presentation/new_password/new_password_screen.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:get_it/get_it.dart';
import 'package:navigation/navigation.dart';
class NewPasswordBuilder {
const NewPasswordBuilder();
Page<void> buildPage(BuildContext context, GoRouterState state) {
final NavigationContract navigationContract = GetIt.I<NavigationContract>();
return MaterialPage<void>(
key: state.pageKey,
child: NewPasswordScreen(navigationContract: navigationContract),
);
}
}

View File

@@ -0,0 +1,201 @@
import 'package:auth/src/features/recover_password/presentation/state/recover_password_view_model.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:navigation/navigation.dart';
import 'package:sf_localizations/sf_localizations.dart';
class NewPasswordScreen extends ConsumerStatefulWidget {
final NavigationContract navigationContract;
const NewPasswordScreen({super.key, required this.navigationContract});
@override
ConsumerState<NewPasswordScreen> createState() => NewPasswordScreenState();
}
class NewPasswordScreenState extends ConsumerState<NewPasswordScreen> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
final NavigationContract navigationContract = GetIt.I<NavigationContract>();
final theme = ref.watch(themePortProvider);
final viewModel = ref.read(recoverPasswordViewModelProvider.notifier);
final viewState = ref.watch(recoverPasswordViewModelProvider);
return Scaffold(
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
body: Container(
margin: const EdgeInsets.symmetric(horizontal: 24),
child: Center(
child: SingleChildScrollView(child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Recuperar contraseña',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 30/*SizeUtils.getByScreen(small: 30, xl: 26)*/,
letterSpacing: 0
),
),
SizedBox(height: 42/*SizeUtils.getBySize(small: 42, big: 32)*/),
CustomTextField(
showPassword: viewState.passwordVisible,
label: 'Nueva contraseña',
labelSize: 14/*SizeUtils.getByScreen(small: 14, xl: 12)*/,
hint: '********',
controller: viewModel.passwordController,
onVisibilityChanged: viewModel.togglePasswordVisible,
),
SizedBox(height: 16),
CustomTextField(
showPassword: viewState.passwordVisible,
label: 'Repetir contraseña',
labelSize: 14/*SizeUtils.getByScreen(small: 14, xl: 12)*/,
hint: '********',
controller: viewModel.repeatedPasswordController,
onVisibilityChanged: viewModel.togglePasswordVisible,
color: viewState.equalPasswords ? const Color(0xFF4B4B4B) : const Color.fromRGBO(239, 17, 17, 1),
),
if (!viewState.equalPasswords) ...[
SizedBox(height: 4),
const Row(
spacing: 8,
children: [
Icon(
Icons.info_outline_rounded,
color: Color.fromRGBO(239, 17, 17, 1),
size: 16,
),
Text(
'Las contraseñas no coinciden. Inténtalo de nuevo',
textAlign: TextAlign.left,
style: TextStyle(
color: Color.fromRGBO(239, 17, 17, 1),
fontSize: 10,
),
),
Spacer(),
]
),
],
SizedBox(height: 12),
Row(
spacing: 8,
children: [
Icon(
Icons.check,
color: theme.getColorFor(viewState.securityChecks['min']!
? ThemeCode.buttonPrimary
: ThemeCode.buttonSecondary),
),
const Text(
'Al menos 8 caracteres',
style: TextStyle(fontSize: 14/*SizeUtils.getByScreen(small: 14, xl: 12)*/)
),
],
),
SizedBox(height: 2/*SizeUtils.getBySize(small: 2, big: 4)*/),
Row(
spacing: 8,
children: [
Icon(
Icons.check,
color: theme.getColorFor(viewState.securityChecks['capital']!
? ThemeCode.buttonPrimary
: ThemeCode.buttonSecondary),
),
const Text(
'Una mayúscula',
style: TextStyle(fontSize: 14/*SizeUtils.getByScreen(small: 14, xl: 12)*/)
),
],
),
SizedBox(height: 2/*SizeUtils.getBySize(small: 2, big: 4)*/),
Row(
spacing: 8,
children: [
Icon(
Icons.check,
color: theme.getColorFor(viewState.securityChecks['number']!
? ThemeCode.buttonPrimary
: ThemeCode.buttonSecondary),
),
const Text(
'Un número',
style: TextStyle(fontSize: 14/*SizeUtils.getByScreen(small: 14, xl: 12)*/)
),
],
),
SizedBox(height: 2/*SizeUtils.getBySize(small: 2, big: 4)*/),
Row(
spacing: 8,
children: [
Icon(
Icons.check,
color: theme.getColorFor(viewState.securityChecks['special']!
? ThemeCode.buttonPrimary
: ThemeCode.buttonSecondary),
),
const Text(
'Un carácter especial',
style: TextStyle(fontSize: 14/*SizeUtils.getByScreen(small: 14, xl: 12)*/)
),
],
),
SizedBox(height: 32/*SizeUtils.getByScreen(small: 32, xl: 24)*/),
Align(
alignment: Alignment.bottomLeft,
child: const Text(
'Teléfono móvil',
style: TextStyle(fontSize: 14/*SizeUtils.getByScreen(small: 14, xl: 12)*/, letterSpacing: 0),
)
),
SizedBox(height: 8),
Row(
spacing: 8,
children: [
CountryPrefixPicker(
headerText: context.translate(I18n.selectYourCountry),
width: 80,
onChanged: (country) {
viewModel.updateDialCode(
country.dialCode ?? viewState.dialCode,
);
},
),
Expanded(child: CustomTextField(
hint: 'Teléfono',
numeric: true,
controller: viewModel.newPhoneNumberController,
))
]
),
SizedBox(height: 56),
PrimaryButton(
onPressed: () async {
await viewModel.recoverPassword();
final updatedState = ref.read(recoverPasswordViewModelProvider);
if (updatedState.passwordChanged) {
navigationContract.goTo(AppRoutes.dashboardHome);
}
},
text: 'Aceptar',
color: theme.getColorFor(ThemeCode.buttonPrimary)
),
],
),
),
),
));
}
}

View File

@@ -1,193 +0,0 @@
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:navigation/navigation.dart';
class NewPasswordScreen extends ConsumerStatefulWidget {
const NewPasswordScreen({super.key});
@override
ConsumerState<NewPasswordScreen> createState() => NewPasswordScreenState();
}
class NewPasswordScreenState extends ConsumerState<NewPasswordScreen> {
bool passwordVisible = false;
bool equalPasswords = false;
String password = '';
Map<String, bool> securityChecks = {
'min': false,
'capital': false,
'number': false,
'special': false,
};
@override
void initState() {
super.initState();
passwordVisible = false;
equalPasswords = false;
password = '';
securityChecks = {
'min': false,
'capital': false,
'number': false,
'special': false,
};
}
@override
Widget build(BuildContext context) {
final NavigationContract navigationContract = GetIt.I<NavigationContract>();
final theme = ref.watch(themePortProvider);
return Scaffold(
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
body: Container(
margin: const EdgeInsets.all(24),
child: Center(
child: Column(
spacing: 48,
children: [
const Spacer(),
Column(
spacing: 32,
children: [
const Text(
"Recuperar contraseña",
textAlign: TextAlign.center,
style: TextStyle(fontWeight: FontWeight.w500, fontSize: 30, letterSpacing: 0),
),
Column(
spacing: 16,
children: [
CustomTextField(
showPassword: passwordVisible,
label: "Nueva contraseña",
hint: "********",
onChanged: (value) => {
setState(() {
password = value;
securityChecks = checkSecurity(value);
}),
},
),
CustomTextField(
showPassword: passwordVisible,
label: "Repetir contraseña",
hint: "********",
onChanged: (value) => {
setState(() {
equalPasswords = password == value;
}),
},
),
Column(
spacing: 4,
children: [
Row(
spacing: 8,
children: [
Icon(
Icons.check,
color: theme.getColorFor(securityChecks["min"]!
? ThemeCode.buttonPrimary
: ThemeCode.buttonSecondary),
),
const Text("Al menos 8 caracteres", style: TextStyle(fontSize: 14)),
],
),
Row(
spacing: 8,
children: [
Icon(
Icons.check,
color: theme.getColorFor(securityChecks["capital"]!
? ThemeCode.buttonPrimary
: ThemeCode.buttonSecondary),
),
const Text("Una mayúscula", style: TextStyle(fontSize: 14)),
],
),
Row(
spacing: 8,
children: [
Icon(
Icons.check,
color: theme.getColorFor(securityChecks["number"]!
? ThemeCode.buttonPrimary
: ThemeCode.buttonSecondary),
),
const Text("Un número", style: TextStyle(fontSize: 14)),
],
),
Row(
spacing: 8,
children: [
Icon(
Icons.check,
color: theme.getColorFor(securityChecks["special"]!
? ThemeCode.buttonPrimary
: ThemeCode.buttonSecondary),
),
const Text("Un carácter especial", style: TextStyle(fontSize: 14)),
],
),
],
)
],
),
Column(
spacing: 8,
children: [
Align(
alignment: Alignment.bottomLeft,
child: const Text(
"Teléfono móvil",
style: TextStyle(fontSize: 14, letterSpacing: 0),
)
),
Row(
spacing: 8,
children: [
CustomDropdown(
value: 0,
items: [Icon(Icons.outlined_flag), Icon(Icons.outlined_flag), Icon(Icons.outlined_flag)],
onChanged: (value)=> {},
width: 80,
),
Expanded(child: CustomTextField(
hint: "Teléfono",
numeric: true
))
]
),
],
)
],
),
PrimaryButton(
onPressed: ()=>{navigationContract.goTo(AppRoutes.dashboardHome)},
text: "Aceptar",
color: theme.getColorFor(ThemeCode.buttonPrimary)
),
const Spacer(),
],
),
),
),
);
}
//TODO: Extraer de la vista
Map<String, bool> checkSecurity(String value) {
return {
'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),
};
}
}

View File

@@ -0,0 +1,10 @@
import 'package:auth/src/core/providers/auth_repository_provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../domain/use_cases/recover_password_use_case.dart';
import '../../domain/use_cases/recover_password_use_case_impl.dart';
final recoverPasswordUseCaseProvider = Provider.autoDispose<RecoverPasswordUseCase>((ref) {
final authRepository = ref.read(authRepositoryProvider);
return RecoverPasswordUseCaseImpl(authRepository);
});

View File

@@ -0,0 +1,18 @@
import 'package:auth/src/features/recover_password/presentation/request_recovery/request_recovery_screen.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:get_it/get_it.dart';
import 'package:navigation/navigation.dart';
class RequestRecoveryBuilder {
const RequestRecoveryBuilder();
Page<void> buildPage(BuildContext context, GoRouterState state) {
final NavigationContract navigationContract = GetIt.I<NavigationContract>();
return MaterialPage<void>(
key: state.pageKey,
child: RequestRecoveryScreen(navigationContract: navigationContract),
);
}
}

View File

@@ -0,0 +1,126 @@
import 'package: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:sf_localizations/sf_localizations.dart';
import '../state/recover_password_view_model.dart';
class RequestRecoveryScreen extends ConsumerWidget {
final NavigationContract navigationContract;
const RequestRecoveryScreen({super.key, required this.navigationContract});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themePortProvider);
final viewModel = ref.read(recoverPasswordViewModelProvider.notifier);
final viewState = ref.watch(recoverPasswordViewModelProvider);
return Scaffold(
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
body: Container(
margin: EdgeInsets.all(30/*SizeUtils.getByScreen(small: 30, xl: 20)*/),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Recuperar contaseña",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 26/*SizeUtils.getByScreen(small: 30, xl: 26)*/),
),
SizedBox(height: 24,/*SizeUtils.getByScreen(small: 24, big: 32),*/),
Text(
"Introduce tu email para enviarte un enlace de recuperación",
textAlign: TextAlign.center,
style: TextStyle(letterSpacing: 0, fontSize: 16/*SizeUtils.getByScreen(small: 28, xl: 16)*/),
),
SizedBox(height: 56,/*SizeUtils.getByScreen(small: 56, big: 48),*/),
CustomTextField(
label: "Correo electrónico",
hint: "Correo electrónico",
controller: viewModel.emailController,
),
SizedBox(height: 40/*SizeUtils.getByScreen(small: 40, xl: 28)*/),
Align(
alignment: Alignment.bottomLeft,
child: Text(
"Teléfono móvil",
style: TextStyle(fontSize: 14, letterSpacing: 0),
),
),
SizedBox(height: 8/*SizeUtils.getByScreen(small: 8, xl: 4)*/),
Row(
children: [
CountryPrefixPicker(
headerText: context.translate(I18n.selectYourCountry),
initialCountryCode: viewState.dialCode,
onChanged: (country) {
viewModel.updateDialCode(
country.dialCode ?? viewState.dialCode,
);
},
width: 80,
),
SizedBox(width: 10/*SizeUtils.getByScreen(small: 10, xl: 6)*/),
Expanded(
child: CustomTextField(
hint: "Teléfono",
numeric: true,
controller: viewModel.phoneNumberController,
),
),
],
),
SizedBox(height: 40/*SizeUtils.getByScreen(small: 40, xl: 28)*/),
if (viewState.errorMessage.isNotEmpty)
...[Text(
viewState.errorMessage,
textAlign: TextAlign.center,
style: const TextStyle(
color: Color.fromRGBO(239, 17, 17, 1),
fontSize: 12,
),
),
SizedBox(height: 40/*SizeUtils.getByScreen(small: 40, xl: 28)*/),
],
Row(
children: [
Expanded(
child: SecondaryButton(
onPressed: () => {Navigator.pop(context)},
text: "Volver",
size: 16/*SizeUtils.getByScreen(small: 16, xl: 14)*/,
),
),
SizedBox(width: 20/*SizeUtils.getByScreen(small: 20, xl: 10)*/),
Expanded(
child: PrimaryButton(
onPressed: () async {
await viewModel.requestRecovery();
final updatedState = ref.read(recoverPasswordViewModelProvider);
if (updatedState.recoveryRequested) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => SentScreen(navigationContract: navigationContract),
),
);
}
},
text: "Enviar",
size: 16/*SizeUtils.getByScreen(small: 16, xl: 14)*/,
color: theme.getColorFor(ThemeCode.buttonSecondary),
),
),
],
),
],
),
),
),
);
}
}

View File

@@ -1,114 +0,0 @@
import 'package:auth/src/features/recover_password/presentation/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';
class RestorePasswordScreen extends ConsumerWidget {
final NavigationContract navigationContract;
const RestorePasswordScreen({super.key, required this.navigationContract});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themePortProvider);
return Scaffold(
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
body: Container(
margin: EdgeInsets.all(30),
child: Center(
child: Column(
spacing: 48,
children: [
Spacer(flex: 8),
Column(
spacing: 32,
children: [
Text(
"Recuperar contaseña",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 30),
),
Text(
"Introduce tu email para enviarte un enlace de recuperación",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18, letterSpacing: 0),
),
],
),
Column(
spacing: 40,
children: [
CustomTextField(
label: "Correo electrónico",
hint: "Correo electrónico",
),
Column(
spacing: 8,
children: [
Align(
alignment: Alignment.bottomLeft,
child: Text(
"Teléfono móvil",
style: TextStyle(fontSize: 14, letterSpacing: 0),
),
),
Row(
spacing: 10,
children: [
CustomDropdown(
value: 0,
items: [
Icon(Icons.outlined_flag),
Icon(Icons.outlined_flag),
Icon(Icons.outlined_flag),
],
onChanged: (value) => {},
width: 80,
),
Expanded(
child: CustomTextField(
hint: "Teléfono",
numeric: true,
),
),
],
),
],
),
Row(
spacing: 20,
children: [
Expanded(
child: SecondaryButton(
onPressed: () => {Navigator.pop(context)},
text: "Volver",
),
),
Expanded(
child: PrimaryButton(
onPressed: () => {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => SentScreen(format: "email"),
),
),
},
text: "Enviar",
size: 16,
color: theme.getColorFor(ThemeCode.buttonSecondary),
),
),
],
),
],
),
Spacer(flex: 10),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,18 @@
import 'package:auth/src/features/recover_password/presentation/sent/sent_screen.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:get_it/get_it.dart';
import 'package:navigation/navigation.dart';
class SentBuilder {
const SentBuilder();
Page<void> buildPage(BuildContext context, GoRouterState state) {
final NavigationContract navigationContract = GetIt.I<NavigationContract>();
return MaterialPage<void>(
key: state.pageKey,
child: SentScreen(navigationContract: navigationContract),
);
}
}

View File

@@ -0,0 +1,109 @@
import 'package:auth/src/features/recover_password/presentation/new_password/new_password_screen.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:navigation/navigation.dart';
import '../state/recover_password_view_model.dart';
class SentScreen extends ConsumerWidget {
final NavigationContract navigationContract;
const SentScreen({super.key, required this.navigationContract});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themePortProvider);
final viewModel = ref.read(recoverPasswordViewModelProvider.notifier);
final viewState = ref.watch(recoverPasswordViewModelProvider);
return Scaffold(
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
body: Container(
margin: EdgeInsets.all(24),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Recuperar contraseña",
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 30/*SizeUtils.getByScreen(small: 30, xl: 26)*/,
letterSpacing: 0,
),
),
SizedBox(height: 48/*SizeUtils.getByScreen(small: 48, xl: 40)*/),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.check,
color: theme.getColorFor(ThemeCode.buttonPrimary),
),
SizedBox(width: 6/*SizeUtils.getByScreen(small: 10, xl: 6)*/),
Text(
viewState.recoveryFormat == "email"
? "Correo enviado correctamente"
: "SMS enviado correctamente",
style: TextStyle(fontSize: 18/*SizeUtils.getByScreen(small: 18, xl: 15)*/, fontWeight: FontWeight.bold),
),
],
),
SizedBox(height: 48/*SizeUtils.getByScreen(small: 48, xl: 40)*/),
Text(
viewState.recoveryFormat == "email"
? "Revisa tu email y haz clic en el enlace para crear una nueva contraseña."
: "Revisa tu móvil y sigue las instrucciones para crear una nueva contraseña.",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18/*SizeUtils.getByScreen(small: 18, xl: 15)*/, letterSpacing: 0),
),
SizedBox(height: 16),
Text(
viewState.recoveryFormat == "email"
? "Si no recibes el correo en unos minutos, revisa tu carpeta de spam o pulsa \"Reenviar correo\"."
: "Si no recibes el SMS en unos minutos, asegúrate de tener cobertura o pulsa \"Reenviar SMS \".",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14/*SizeUtils.getByScreen(small: 14, xl: 12)*/, letterSpacing: 0),
),
SizedBox(height: 48/*SizeUtils.getByScreen(small: 48, xl: 40)*/),
Row(
children: [
Expanded(
child: SecondaryButton(
onPressed: () {
if ( viewState.recoveryFormat == "email") {
viewModel.requestEmail();
} else {
viewModel.requestSms();
}
},
text: viewState.recoveryFormat == "email"
? "Reenviar correo"
: "Reenviar SMS",
size: 16/*SizeUtils.getByScreen(small: 16, xl: 14)*/,
),
),
SizedBox(width: 10),
Expanded(
child: PrimaryButton(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => NewPasswordScreen(navigationContract: navigationContract)),
),
text: "Continuar",
color: theme.getColorFor(ThemeCode.buttonSecondary),
size: 16/*SizeUtils.getByScreen(small: 16, xl: 14)*/,
),
),
],
),
],
),
),
),
);
}
}

View File

@@ -1,99 +0,0 @@
import 'package:auth/src/features/recover_password/presentation/new_password_screen.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class SentScreen extends ConsumerWidget {
final String format;
const SentScreen({super.key, required this.format});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themePortProvider);
return Scaffold(
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
body: Container(
margin: EdgeInsets.all(24),
child: Center(
child: Column(
spacing: 48,
children: [
Spacer(flex: 8),
Text(
"Recuperar contraseña",
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 30,
letterSpacing: 0,
),
),
Row(
spacing: 10,
children: [
Spacer(),
Icon(
Icons.check,
color: theme.getColorFor(ThemeCode.buttonPrimary),
),
Text(
format == "email"
? "Correo enviado correctamente"
: "SMS enviado correctamente",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
Spacer(),
],
),
Column(
spacing: 16,
children: [
Text(
format == "email"
? "Revisa tu email y haz clic en el enlace para crear una nueva contraseña."
: "Revisa tu móvil y sigue las instrucciones para crear una nueva contraseña.",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18, letterSpacing: 0),
),
Text(
format == "email"
? "Si no recibes el correo en unos minutos, revisa tu carpeta de spam o pulsa \"Reenviar correo\"."
: "Si no recibes el SMS en unos minutos, asegúrate de tener cobertura o pulsa \"Reenviar SMS \".",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14, letterSpacing: 0),
),
],
),
Row(
spacing: 10,
children: [
Expanded(
child: SecondaryButton(
onPressed: () => {},
text: format == "email"
? "Reenviar correo"
: "Reenviar SMS",
),
),
Expanded(
child: PrimaryButton(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => NewPasswordScreen()),
),
text: "Continuar",
color: theme.getColorFor(ThemeCode.buttonSecondary),
),
),
],
),
Spacer(flex: 10),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,275 @@
import 'package:auth/src/features/recover_password/domain/use_cases/recover_password_use_case.dart';
import 'package:auth/src/features/recover_password/presentation/state/recover_password_view_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/recover_password_provider.dart';
final recoverPasswordViewModelProvider =
NotifierProvider.autoDispose<RecoverPasswordViewModel, RecoverPasswordViewState>(
RecoverPasswordViewModel.new,
);
class RecoverPasswordViewModel extends Notifier<RecoverPasswordViewState> {
late final RecoverPasswordUseCase _recoverPasswordUseCase;
late final TextEditingController phoneNumberController;
late final TextEditingController emailController;
late final TextEditingController passwordController;
late final TextEditingController repeatedPasswordController;
late final TextEditingController newPhoneNumberController;
@override
RecoverPasswordViewState build() {
_recoverPasswordUseCase = ref.read(recoverPasswordUseCaseProvider);
phoneNumberController = TextEditingController();
phoneNumberController.addListener(_onPhoneNumberChanged);
emailController = TextEditingController();
emailController.addListener(_onEmailChanged);
passwordController = TextEditingController();
passwordController.addListener(_onPasswordChanged);
repeatedPasswordController = TextEditingController();
repeatedPasswordController.addListener(_onRepeatedPasswordChanged);
newPhoneNumberController = TextEditingController();
newPhoneNumberController.addListener(_onNewPhoneNumberChanged);
ref.onDispose(disposeControllers);
return const RecoverPasswordViewState();
}
void _onPhoneNumberChanged() {
final String raw = phoneNumberController.text;
state = state.copyWith(
phoneNumber: raw,
errorMessage: '',
recoveryRequested: false,
);
}
void _onNewPhoneNumberChanged() {
final String raw = newPhoneNumberController.text;
state = state.copyWith(
newPhoneNumber: raw,
errorMessage: '',
recoveryRequested: false,
);
}
void _onEmailChanged() {
final String raw = emailController.text;
state = state.copyWith(
email: raw,
errorMessage: '',
recoveryRequested: false,
);
}
void _onPasswordChanged() {
final String raw = passwordController.text;
final bool equalPasswords = raw == repeatedPasswordController.text;
final bool minCheck = raw.length >= 8;
final bool capitalCheck = RegExp(r'[A-Z]').hasMatch(raw);
final bool numberCheck = RegExp(r'[0-9]').hasMatch(raw);
final bool specialCheck = RegExp(r'[^A-Za-z0-9]').hasMatch(raw);
final Map<String, bool> security = {
'min': minCheck,
'capital': capitalCheck,
'number': numberCheck,
'special': specialCheck,
};
state = state.copyWith(
password: raw,
equalPasswords: equalPasswords,
securityChecks: security,
);
}
void _onRepeatedPasswordChanged() {
final String raw = repeatedPasswordController.text;
final bool equalPasswords = raw == passwordController.text;
state = state.copyWith(
repeatedPassword: raw,
equalPasswords: equalPasswords,
);
}
void updateDialCode(String dialCode) {
state = state.copyWith(
dialCode: dialCode,
errorMessage: '',
);
}
void updateNewDialCode(String dialCode) {
state = state.copyWith(
newDialCode: dialCode,
errorMessage: '',
);
}
void togglePasswordVisible(){
state = state.copyWith(
passwordVisible: !state.passwordVisible,
);
}
Future<void> requestRecovery() async {
final trimmedNumber = state.phoneNumber.trim();
final email = state.email.trim();
state = state.copyWith(
isLoading: true,
errorMessage: '',
recoveryRequested: false,
);
if (email.isNotEmpty) {
await requestEmail();
}else if (trimmedNumber.isNotEmpty) {
await requestSms();
} else {
state = state.copyWith(
isLoading: false,
errorMessage: 'errorMessageContactIsEmpty',
);
return;
}
}
Future<void> requestEmail() async {
final email = state.email.trim();
try {
final String token = await _recoverPasswordUseCase.requestEmail(email: email);
if (!ref.mounted) return;
state = state.copyWith(
isLoading: false,
errorMessage: '',
recoveryRequested: true,
recoveryFormat: 'email',
);
} catch (e) {
if (!ref.mounted) return;
state = state.copyWith(
isLoading: false,
errorMessage: e.toString(),
recoveryRequested: false,
passwordChanged: false,
);
}
}
Future<void> requestSms() async {
final trimmedNumber = state.phoneNumber.trim();
final fullPhone = '${state.dialCode}$trimmedNumber';
try {
await _recoverPasswordUseCase.requestSms(phone: fullPhone);
if (!ref.mounted) return;
state = state.copyWith(
isLoading: false,
errorMessage: '',
recoveryRequested: true,
recoveryFormat: 'sms'
);
} catch (e) {
if (!ref.mounted) return;
state = state.copyWith(
isLoading: false,
errorMessage: e.toString(),
recoveryRequested: false,
passwordChanged: false,
);
}
}
Future<void> recoverPassword() async {
final String fullPhone = state.newDialCode + state.newPhoneNumber;
final String password = state.password;
if (!state.equalPasswords) {
state = state.copyWith(
errorMessage: 'errorMessageUnequalPasswords',
passwordChanged: false,
);
return;
}
if (!state.securityChecks['min']!) {
state = state.copyWith(
errorMessage: 'errorMessagePasswordTooShort',
passwordChanged: false,
);
return;
}
if (!state.securityChecks['capital']!) {
state = state.copyWith(
errorMessage: 'errorMessagePasswordNoCapitals',
passwordChanged: false,
);
return;
}
if (!state.securityChecks['number']!) {
state = state.copyWith(
errorMessage: 'errorMessagePasswordNoNumbers',
passwordChanged: false,
);
return;
}
if (!state.securityChecks['special']!) {
state = state.copyWith(
errorMessage: 'errorMessagePasswordNoSpecialChars',
passwordChanged: false,
);
return;
}
state = state.copyWith(
isLoading: true,
passwordChanged: false,
);
try {
/*await _recoverPasswordUseCase.recoverPassword(
newPassword: password, token: "");*/
state = state.copyWith(
isLoading: false,
passwordChanged: true,
);
} catch (error) {
state = state.copyWith(
errorMessage: error.toString(),
isLoading: false,
passwordChanged: false,
);
}
}
void disposeControllers() {
phoneNumberController.removeListener(_onPhoneNumberChanged);
phoneNumberController.dispose();
emailController.removeListener(_onPhoneNumberChanged);
emailController.dispose();
passwordController.removeListener(_onPasswordChanged);
passwordController.dispose();
repeatedPasswordController.removeListener(_onRepeatedPasswordChanged);
repeatedPasswordController.dispose();
newPhoneNumberController.removeListener(_onNewPhoneNumberChanged);
newPhoneNumberController.dispose();
}
}

View File

@@ -0,0 +1,29 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'recover_password_view_state.freezed.dart';
@freezed
abstract class RecoverPasswordViewState with _$RecoverPasswordViewState {
const factory RecoverPasswordViewState({
@Default('') String phoneNumber,
@Default('+34') String dialCode,
@Default('') String email,
@Default('') String errorMessage,
@Default('') String recoveryFormat,
@Default(false) bool isLoading,
@Default(false) bool recoveryRequested,
@Default(false) bool passwordChanged,
@Default('') String password,
@Default('') String repeatedPassword,
@Default(false) bool passwordVisible,
@Default(true) bool equalPasswords,
@Default('+34') String newDialCode,
@Default('') String newPhoneNumber,
@Default({
'min': false,
'capital': false,
'number': false,
'special': false,
}) Map<String, bool> securityChecks,
}) = _RecoverPasswordViewState;
}

View File

@@ -0,0 +1,319 @@
// 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 'recover_password_view_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$RecoverPasswordViewState {
String get phoneNumber; String get dialCode; String get email; String get errorMessage; String get recoveryFormat; bool get isLoading; bool get recoveryRequested; bool get passwordChanged; String get password; String get repeatedPassword; bool get passwordVisible; bool get equalPasswords; String get newDialCode; String get newPhoneNumber; Map<String, bool> get securityChecks;
/// Create a copy of RecoverPasswordViewState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$RecoverPasswordViewStateCopyWith<RecoverPasswordViewState> get copyWith => _$RecoverPasswordViewStateCopyWithImpl<RecoverPasswordViewState>(this as RecoverPasswordViewState, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is RecoverPasswordViewState&&(identical(other.phoneNumber, phoneNumber) || other.phoneNumber == phoneNumber)&&(identical(other.dialCode, dialCode) || other.dialCode == dialCode)&&(identical(other.email, email) || other.email == email)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.recoveryFormat, recoveryFormat) || other.recoveryFormat == recoveryFormat)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.recoveryRequested, recoveryRequested) || other.recoveryRequested == recoveryRequested)&&(identical(other.passwordChanged, passwordChanged) || other.passwordChanged == passwordChanged)&&(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)&&(identical(other.newDialCode, newDialCode) || other.newDialCode == newDialCode)&&(identical(other.newPhoneNumber, newPhoneNumber) || other.newPhoneNumber == newPhoneNumber)&&const DeepCollectionEquality().equals(other.securityChecks, securityChecks));
}
@override
int get hashCode => Object.hash(runtimeType,phoneNumber,dialCode,email,errorMessage,recoveryFormat,isLoading,recoveryRequested,passwordChanged,password,repeatedPassword,passwordVisible,equalPasswords,newDialCode,newPhoneNumber,const DeepCollectionEquality().hash(securityChecks));
@override
String toString() {
return 'RecoverPasswordViewState(phoneNumber: $phoneNumber, dialCode: $dialCode, email: $email, errorMessage: $errorMessage, recoveryFormat: $recoveryFormat, isLoading: $isLoading, recoveryRequested: $recoveryRequested, passwordChanged: $passwordChanged, password: $password, repeatedPassword: $repeatedPassword, passwordVisible: $passwordVisible, equalPasswords: $equalPasswords, newDialCode: $newDialCode, newPhoneNumber: $newPhoneNumber, securityChecks: $securityChecks)';
}
}
/// @nodoc
abstract mixin class $RecoverPasswordViewStateCopyWith<$Res> {
factory $RecoverPasswordViewStateCopyWith(RecoverPasswordViewState value, $Res Function(RecoverPasswordViewState) _then) = _$RecoverPasswordViewStateCopyWithImpl;
@useResult
$Res call({
String phoneNumber, String dialCode, String email, String errorMessage, String recoveryFormat, bool isLoading, bool recoveryRequested, bool passwordChanged, String password, String repeatedPassword, bool passwordVisible, bool equalPasswords, String newDialCode, String newPhoneNumber, Map<String, bool> securityChecks
});
}
/// @nodoc
class _$RecoverPasswordViewStateCopyWithImpl<$Res>
implements $RecoverPasswordViewStateCopyWith<$Res> {
_$RecoverPasswordViewStateCopyWithImpl(this._self, this._then);
final RecoverPasswordViewState _self;
final $Res Function(RecoverPasswordViewState) _then;
/// Create a copy of RecoverPasswordViewState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? phoneNumber = null,Object? dialCode = null,Object? email = null,Object? errorMessage = null,Object? recoveryFormat = null,Object? isLoading = null,Object? recoveryRequested = null,Object? passwordChanged = null,Object? password = null,Object? repeatedPassword = null,Object? passwordVisible = null,Object? equalPasswords = null,Object? newDialCode = null,Object? newPhoneNumber = null,Object? securityChecks = null,}) {
return _then(_self.copyWith(
phoneNumber: null == phoneNumber ? _self.phoneNumber : phoneNumber // ignore: cast_nullable_to_non_nullable
as String,dialCode: null == dialCode ? _self.dialCode : dialCode // ignore: cast_nullable_to_non_nullable
as String,email: null == email ? _self.email : email // ignore: cast_nullable_to_non_nullable
as String,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,recoveryFormat: null == recoveryFormat ? _self.recoveryFormat : recoveryFormat // ignore: cast_nullable_to_non_nullable
as String,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,recoveryRequested: null == recoveryRequested ? _self.recoveryRequested : recoveryRequested // ignore: cast_nullable_to_non_nullable
as bool,passwordChanged: null == passwordChanged ? _self.passwordChanged : passwordChanged // ignore: cast_nullable_to_non_nullable
as bool,password: null == password ? _self.password : password // ignore: cast_nullable_to_non_nullable
as String,repeatedPassword: null == repeatedPassword ? _self.repeatedPassword : repeatedPassword // ignore: cast_nullable_to_non_nullable
as String,passwordVisible: null == passwordVisible ? _self.passwordVisible : passwordVisible // ignore: cast_nullable_to_non_nullable
as bool,equalPasswords: null == equalPasswords ? _self.equalPasswords : equalPasswords // ignore: cast_nullable_to_non_nullable
as bool,newDialCode: null == newDialCode ? _self.newDialCode : newDialCode // ignore: cast_nullable_to_non_nullable
as String,newPhoneNumber: null == newPhoneNumber ? _self.newPhoneNumber : newPhoneNumber // ignore: cast_nullable_to_non_nullable
as String,securityChecks: null == securityChecks ? _self.securityChecks : securityChecks // ignore: cast_nullable_to_non_nullable
as Map<String, bool>,
));
}
}
/// Adds pattern-matching-related methods to [RecoverPasswordViewState].
extension RecoverPasswordViewStatePatterns on RecoverPasswordViewState {
/// 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( _RecoverPasswordViewState value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _RecoverPasswordViewState() 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( _RecoverPasswordViewState value) $default,){
final _that = this;
switch (_that) {
case _RecoverPasswordViewState():
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( _RecoverPasswordViewState value)? $default,){
final _that = this;
switch (_that) {
case _RecoverPasswordViewState() 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( String phoneNumber, String dialCode, String email, String errorMessage, String recoveryFormat, bool isLoading, bool recoveryRequested, bool passwordChanged, String password, String repeatedPassword, bool passwordVisible, bool equalPasswords, String newDialCode, String newPhoneNumber, Map<String, bool> securityChecks)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _RecoverPasswordViewState() when $default != null:
return $default(_that.phoneNumber,_that.dialCode,_that.email,_that.errorMessage,_that.recoveryFormat,_that.isLoading,_that.recoveryRequested,_that.passwordChanged,_that.password,_that.repeatedPassword,_that.passwordVisible,_that.equalPasswords,_that.newDialCode,_that.newPhoneNumber,_that.securityChecks);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( String phoneNumber, String dialCode, String email, String errorMessage, String recoveryFormat, bool isLoading, bool recoveryRequested, bool passwordChanged, String password, String repeatedPassword, bool passwordVisible, bool equalPasswords, String newDialCode, String newPhoneNumber, Map<String, bool> securityChecks) $default,) {final _that = this;
switch (_that) {
case _RecoverPasswordViewState():
return $default(_that.phoneNumber,_that.dialCode,_that.email,_that.errorMessage,_that.recoveryFormat,_that.isLoading,_that.recoveryRequested,_that.passwordChanged,_that.password,_that.repeatedPassword,_that.passwordVisible,_that.equalPasswords,_that.newDialCode,_that.newPhoneNumber,_that.securityChecks);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( String phoneNumber, String dialCode, String email, String errorMessage, String recoveryFormat, bool isLoading, bool recoveryRequested, bool passwordChanged, String password, String repeatedPassword, bool passwordVisible, bool equalPasswords, String newDialCode, String newPhoneNumber, Map<String, bool> securityChecks)? $default,) {final _that = this;
switch (_that) {
case _RecoverPasswordViewState() when $default != null:
return $default(_that.phoneNumber,_that.dialCode,_that.email,_that.errorMessage,_that.recoveryFormat,_that.isLoading,_that.recoveryRequested,_that.passwordChanged,_that.password,_that.repeatedPassword,_that.passwordVisible,_that.equalPasswords,_that.newDialCode,_that.newPhoneNumber,_that.securityChecks);case _:
return null;
}
}
}
/// @nodoc
class _RecoverPasswordViewState implements RecoverPasswordViewState {
const _RecoverPasswordViewState({this.phoneNumber = '', this.dialCode = '+34', this.email = '', this.errorMessage = '', this.recoveryFormat = '', this.isLoading = false, this.recoveryRequested = false, this.passwordChanged = false, this.password = '', this.repeatedPassword = '', this.passwordVisible = false, this.equalPasswords = true, this.newDialCode = '+34', this.newPhoneNumber = '', final Map<String, bool> securityChecks = const {'min' : false, 'capital' : false, 'number' : false, 'special' : false}}): _securityChecks = securityChecks;
@override@JsonKey() final String phoneNumber;
@override@JsonKey() final String dialCode;
@override@JsonKey() final String email;
@override@JsonKey() final String errorMessage;
@override@JsonKey() final String recoveryFormat;
@override@JsonKey() final bool isLoading;
@override@JsonKey() final bool recoveryRequested;
@override@JsonKey() final bool passwordChanged;
@override@JsonKey() final String password;
@override@JsonKey() final String repeatedPassword;
@override@JsonKey() final bool passwordVisible;
@override@JsonKey() final bool equalPasswords;
@override@JsonKey() final String newDialCode;
@override@JsonKey() final String newPhoneNumber;
final Map<String, bool> _securityChecks;
@override@JsonKey() Map<String, bool> get securityChecks {
if (_securityChecks is EqualUnmodifiableMapView) return _securityChecks;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_securityChecks);
}
/// Create a copy of RecoverPasswordViewState
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$RecoverPasswordViewStateCopyWith<_RecoverPasswordViewState> get copyWith => __$RecoverPasswordViewStateCopyWithImpl<_RecoverPasswordViewState>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _RecoverPasswordViewState&&(identical(other.phoneNumber, phoneNumber) || other.phoneNumber == phoneNumber)&&(identical(other.dialCode, dialCode) || other.dialCode == dialCode)&&(identical(other.email, email) || other.email == email)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.recoveryFormat, recoveryFormat) || other.recoveryFormat == recoveryFormat)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.recoveryRequested, recoveryRequested) || other.recoveryRequested == recoveryRequested)&&(identical(other.passwordChanged, passwordChanged) || other.passwordChanged == passwordChanged)&&(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)&&(identical(other.newDialCode, newDialCode) || other.newDialCode == newDialCode)&&(identical(other.newPhoneNumber, newPhoneNumber) || other.newPhoneNumber == newPhoneNumber)&&const DeepCollectionEquality().equals(other._securityChecks, _securityChecks));
}
@override
int get hashCode => Object.hash(runtimeType,phoneNumber,dialCode,email,errorMessage,recoveryFormat,isLoading,recoveryRequested,passwordChanged,password,repeatedPassword,passwordVisible,equalPasswords,newDialCode,newPhoneNumber,const DeepCollectionEquality().hash(_securityChecks));
@override
String toString() {
return 'RecoverPasswordViewState(phoneNumber: $phoneNumber, dialCode: $dialCode, email: $email, errorMessage: $errorMessage, recoveryFormat: $recoveryFormat, isLoading: $isLoading, recoveryRequested: $recoveryRequested, passwordChanged: $passwordChanged, password: $password, repeatedPassword: $repeatedPassword, passwordVisible: $passwordVisible, equalPasswords: $equalPasswords, newDialCode: $newDialCode, newPhoneNumber: $newPhoneNumber, securityChecks: $securityChecks)';
}
}
/// @nodoc
abstract mixin class _$RecoverPasswordViewStateCopyWith<$Res> implements $RecoverPasswordViewStateCopyWith<$Res> {
factory _$RecoverPasswordViewStateCopyWith(_RecoverPasswordViewState value, $Res Function(_RecoverPasswordViewState) _then) = __$RecoverPasswordViewStateCopyWithImpl;
@override @useResult
$Res call({
String phoneNumber, String dialCode, String email, String errorMessage, String recoveryFormat, bool isLoading, bool recoveryRequested, bool passwordChanged, String password, String repeatedPassword, bool passwordVisible, bool equalPasswords, String newDialCode, String newPhoneNumber, Map<String, bool> securityChecks
});
}
/// @nodoc
class __$RecoverPasswordViewStateCopyWithImpl<$Res>
implements _$RecoverPasswordViewStateCopyWith<$Res> {
__$RecoverPasswordViewStateCopyWithImpl(this._self, this._then);
final _RecoverPasswordViewState _self;
final $Res Function(_RecoverPasswordViewState) _then;
/// Create a copy of RecoverPasswordViewState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? phoneNumber = null,Object? dialCode = null,Object? email = null,Object? errorMessage = null,Object? recoveryFormat = null,Object? isLoading = null,Object? recoveryRequested = null,Object? passwordChanged = null,Object? password = null,Object? repeatedPassword = null,Object? passwordVisible = null,Object? equalPasswords = null,Object? newDialCode = null,Object? newPhoneNumber = null,Object? securityChecks = null,}) {
return _then(_RecoverPasswordViewState(
phoneNumber: null == phoneNumber ? _self.phoneNumber : phoneNumber // ignore: cast_nullable_to_non_nullable
as String,dialCode: null == dialCode ? _self.dialCode : dialCode // ignore: cast_nullable_to_non_nullable
as String,email: null == email ? _self.email : email // ignore: cast_nullable_to_non_nullable
as String,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,recoveryFormat: null == recoveryFormat ? _self.recoveryFormat : recoveryFormat // ignore: cast_nullable_to_non_nullable
as String,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,recoveryRequested: null == recoveryRequested ? _self.recoveryRequested : recoveryRequested // ignore: cast_nullable_to_non_nullable
as bool,passwordChanged: null == passwordChanged ? _self.passwordChanged : passwordChanged // ignore: cast_nullable_to_non_nullable
as bool,password: null == password ? _self.password : password // ignore: cast_nullable_to_non_nullable
as String,repeatedPassword: null == repeatedPassword ? _self.repeatedPassword : repeatedPassword // ignore: cast_nullable_to_non_nullable
as String,passwordVisible: null == passwordVisible ? _self.passwordVisible : passwordVisible // ignore: cast_nullable_to_non_nullable
as bool,equalPasswords: null == equalPasswords ? _self.equalPasswords : equalPasswords // ignore: cast_nullable_to_non_nullable
as bool,newDialCode: null == newDialCode ? _self.newDialCode : newDialCode // ignore: cast_nullable_to_non_nullable
as String,newPhoneNumber: null == newPhoneNumber ? _self.newPhoneNumber : newPhoneNumber // ignore: cast_nullable_to_non_nullable
as String,securityChecks: null == securityChecks ? _self._securityChecks : securityChecks // ignore: cast_nullable_to_non_nullable
as Map<String, bool>,
));
}
}
// dart format on

View File

@@ -1,4 +1,4 @@
import 'package:auth/src/features/recover_password/presentation/restore_password_screen.dart';
import 'package:auth/src/features/recover_password/presentation/request_recovery/request_recovery_screen.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:get_it/get_it.dart';
@@ -12,7 +12,7 @@ class RecoverPasswordBuilder {
return MaterialPage<void>(
key: state.pageKey,
child: RestorePasswordScreen(navigationContract: navigationContract),
child: RequestRecoveryScreen(navigationContract: navigationContract),
);
}
}

View File

@@ -1,90 +1,88 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class CustomTextField extends StatefulWidget {
class CustomTextField extends StatelessWidget {
final bool? showPassword;
final VoidCallback? onVisibilityChanged;
final bool numeric;
final String hint;
final String label;
final double labelSize;
final int? lines;
final ValueChanged<String>? onChanged;
final int? length;
final TextEditingController? controller;
final String? initialValue;
final Color color;
const CustomTextField({
super.key,
this.showPassword,
this.onVisibilityChanged,
this.numeric = false,
this.hint = '',
this.label = '',
this.labelSize = 14,
this.lines,
this.length,
this.onChanged,
this.controller,
this.initialValue,
this.color = const Color(0xFF4B4B4B),
});
@override
State<CustomTextField> createState() => CustomTextFieldState();
}
class CustomTextFieldState extends State<CustomTextField> {
late bool _showPassword;
@override
void initState() {
super.initState();
_showPassword = widget.showPassword ?? true;
}
@override
Widget build(BuildContext context) {
return Column(
spacing: 8,
children: [
if (widget.label.isNotEmpty)
if (label.isNotEmpty)
Align(
alignment: Alignment.bottomLeft,
child: Text(
widget.label,
style: const TextStyle(fontSize: 14, letterSpacing: 0),
label,
style: TextStyle(fontSize: labelSize, letterSpacing: 0),
),
),
TextFormField(
controller: widget.controller,
keyboardType: widget.numeric
controller: controller,
keyboardType: numeric
? TextInputType.number
: TextInputType.text,
obscureText: !_showPassword,
enableSuggestions: _showPassword,
autocorrect: !_showPassword,
obscureText: !(showPassword ?? true),
enableSuggestions: (showPassword ?? true),
autocorrect: !(showPassword ?? true),
style: const TextStyle(color: Color(0xFF4B4B4B)),
inputFormatters: widget.numeric
inputFormatters: numeric
? <TextInputFormatter>[FilteringTextInputFormatter.digitsOnly]
: const <TextInputFormatter>[],
decoration: InputDecoration(
counterText: "",
hintText: widget.hint,
border: const OutlineInputBorder(
hintText: hint,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
borderSide: BorderSide(color: Color(0xFF4B4B4B)),
borderSide: BorderSide(color: color),
gapPadding: 16,
),
suffixIcon: widget.showPassword != null
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
borderSide: BorderSide(color: color),
gapPadding: 16,
),
suffixIcon: showPassword != null
? IconButton(
icon: Icon(
_showPassword ? Icons.visibility_off : Icons.visibility,
),
onPressed: () {
setState(() {
_showPassword = !_showPassword;
});
},
)
icon: Icon(
showPassword! ? Icons.visibility_off_outlined : Icons.visibility_outlined,
),
onPressed: onVisibilityChanged,
)
: null,
),
minLines: widget.lines ?? 1,
maxLines: widget.lines ?? 1,
maxLength: widget.length,
onChanged: widget.onChanged,
initialValue: initialValue,
minLines: lines ?? 1,
maxLines: lines ?? 1,
maxLength: length,
onChanged: onChanged,
),
],
);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB