diff --git a/apps/mobile_app/lib/navigation/app_router.dart b/apps/mobile_app/lib/navigation/app_router.dart index e7ee4782..b8850e17 100644 --- a/apps/mobile_app/lib/navigation/app_router.dart +++ b/apps/mobile_app/lib/navigation/app_router.dart @@ -46,7 +46,7 @@ void configureAppRouter() { GoRoute( path: AppRoutes.recoverPassword, name: 'recover_password', - pageBuilder: RecoverPasswordBuilder().buildPage, + pageBuilder: RequestRecoveryBuilder().buildPage, ), GoRoute( path: AppRoutes.deviceSignup, diff --git a/modules/auth/lib/auth.dart b/modules/auth/lib/auth.dart index 2991c63d..86d23c73 100644 --- a/modules/auth/lib/auth.dart +++ b/modules/auth/lib/auth.dart @@ -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'; diff --git a/modules/auth/lib/src/core/data/datasource/auth_remote_datasource.dart b/modules/auth/lib/src/core/data/datasource/auth_remote_datasource.dart index 573fcef8..c3253f3d 100644 --- a/modules/auth/lib/src/core/data/datasource/auth_remote_datasource.dart +++ b/modules/auth/lib/src/core/data/datasource/auth_remote_datasource.dart @@ -2,4 +2,8 @@ abstract class AuthRemoteDatasource { Future requestPhoneCode({required String phone}); Future verifyPhoneCode({required String phone, required String code}); + + Future requestPasswordReset({String? phone, String? email}); + + Future recoverPassword({required newPassword, required token}); } diff --git a/modules/auth/lib/src/core/data/datasource/auth_remote_datasource_impl.dart b/modules/auth/lib/src/core/data/datasource/auth_remote_datasource_impl.dart index 1053f587..6cffc665 100644 --- a/modules/auth/lib/src/core/data/datasource/auth_remote_datasource_impl.dart +++ b/modules/auth/lib/src/core/data/datasource/auth_remote_datasource_impl.dart @@ -53,4 +53,46 @@ class AuthRemoteDatasourceImpl implements AuthRemoteDatasource { return Exception(message); } + + @override + Future requestPasswordReset({ + String? phone, + String? email + }) async { + try { + if (phone == null && email == null) { + throw FormatException("No phone or email address given"); + } + + late final Map 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>( + '/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 recoverPassword({required newPassword, required token}) async { + try { + await _repository.put( + '/auth/recovery-password', + body: {'newPassword': newPassword, 'token': token}, + ); + } on DioException catch (error) { + throw _mapDioError(error, defaultMessage: 'Error to request password recovery'); + } + } } diff --git a/modules/auth/lib/src/core/data/repositories/auth_repository_impl.dart b/modules/auth/lib/src/core/data/repositories/auth_repository_impl.dart index 127c6070..88ae3a61 100644 --- a/modules/auth/lib/src/core/data/repositories/auth_repository_impl.dart +++ b/modules/auth/lib/src/core/data/repositories/auth_repository_impl.dart @@ -15,4 +15,14 @@ class AuthRepositoryImpl implements AuthRepository { Future verifyPhoneCode({required String phone, required String code}) { return _remote.verifyPhoneCode(phone: phone, code: code); } + + @override + Future requestPasswordReset({String? phone, String? email}) { + return _remote.requestPasswordReset(phone: phone, email: email); + } + + @override + Future recoverPassword({required String newPassword, required String token}) { + return _remote.recoverPassword(newPassword: newPassword, token: token); + } } diff --git a/modules/auth/lib/src/core/domain/repositories/auth_repository.dart b/modules/auth/lib/src/core/domain/repositories/auth_repository.dart index 4584b9db..f6338e9a 100644 --- a/modules/auth/lib/src/core/domain/repositories/auth_repository.dart +++ b/modules/auth/lib/src/core/domain/repositories/auth_repository.dart @@ -2,4 +2,8 @@ abstract class AuthRepository { Future requestPhoneCode({required String phone}); Future verifyPhoneCode({required String phone, required String code}); + + Future requestPasswordReset({String phone, String email}); + + Future recoverPassword({required String newPassword, required String token}); } diff --git a/modules/auth/lib/src/features/recover_password/domain/use_cases/recover_password_use_case.dart b/modules/auth/lib/src/features/recover_password/domain/use_cases/recover_password_use_case.dart new file mode 100644 index 00000000..76228be2 --- /dev/null +++ b/modules/auth/lib/src/features/recover_password/domain/use_cases/recover_password_use_case.dart @@ -0,0 +1,7 @@ +abstract class RecoverPasswordUseCase { + Future requestEmail({required String email}); + + Future requestSms({required String phone}); + + Future recoverPassword({required String newPassword, required String token}); +} \ No newline at end of file diff --git a/modules/auth/lib/src/features/recover_password/domain/use_cases/recover_password_use_case_impl.dart b/modules/auth/lib/src/features/recover_password/domain/use_cases/recover_password_use_case_impl.dart new file mode 100644 index 00000000..6699e8f8 --- /dev/null +++ b/modules/auth/lib/src/features/recover_password/domain/use_cases/recover_password_use_case_impl.dart @@ -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 requestEmail({required String email}) async { + return await _repository.requestPasswordReset(email: email); + } + + @override + Future requestSms({required String phone}) async { + return await _repository.requestPasswordReset(phone: phone); + } + + @override + Future recoverPassword({required String newPassword, required String token}) async { + await _repository.recoverPassword(newPassword: newPassword, token: token); + } +} diff --git a/modules/auth/lib/src/features/recover_password/presentation/new_password/new_password_builder.dart b/modules/auth/lib/src/features/recover_password/presentation/new_password/new_password_builder.dart new file mode 100644 index 00000000..4fe9f6b2 --- /dev/null +++ b/modules/auth/lib/src/features/recover_password/presentation/new_password/new_password_builder.dart @@ -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 buildPage(BuildContext context, GoRouterState state) { + final NavigationContract navigationContract = GetIt.I(); + + return MaterialPage( + key: state.pageKey, + child: NewPasswordScreen(navigationContract: navigationContract), + ); + } +} \ No newline at end of file diff --git a/modules/auth/lib/src/features/recover_password/presentation/new_password/new_password_screen.dart b/modules/auth/lib/src/features/recover_password/presentation/new_password/new_password_screen.dart new file mode 100644 index 00000000..74e35d38 --- /dev/null +++ b/modules/auth/lib/src/features/recover_password/presentation/new_password/new_password_screen.dart @@ -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 createState() => NewPasswordScreenState(); +} + +class NewPasswordScreenState extends ConsumerState { + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + final NavigationContract navigationContract = GetIt.I(); + 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) + ), + ], + ), + ), + ), + )); + } +} diff --git a/modules/auth/lib/src/features/recover_password/presentation/new_password_screen.dart b/modules/auth/lib/src/features/recover_password/presentation/new_password_screen.dart deleted file mode 100644 index 866fe532..00000000 --- a/modules/auth/lib/src/features/recover_password/presentation/new_password_screen.dart +++ /dev/null @@ -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 createState() => NewPasswordScreenState(); -} - -class NewPasswordScreenState extends ConsumerState { - bool passwordVisible = false; - bool equalPasswords = false; - String password = ''; - - Map 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(); - 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 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), - }; - } -} diff --git a/modules/auth/lib/src/features/recover_password/presentation/providers/recover_password_provider.dart b/modules/auth/lib/src/features/recover_password/presentation/providers/recover_password_provider.dart new file mode 100644 index 00000000..d23165b1 --- /dev/null +++ b/modules/auth/lib/src/features/recover_password/presentation/providers/recover_password_provider.dart @@ -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((ref) { + final authRepository = ref.read(authRepositoryProvider); + return RecoverPasswordUseCaseImpl(authRepository); +}); diff --git a/modules/auth/lib/src/features/recover_password/presentation/request_recovery/request_recovery_builder.dart b/modules/auth/lib/src/features/recover_password/presentation/request_recovery/request_recovery_builder.dart new file mode 100644 index 00000000..11e2fa57 --- /dev/null +++ b/modules/auth/lib/src/features/recover_password/presentation/request_recovery/request_recovery_builder.dart @@ -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 buildPage(BuildContext context, GoRouterState state) { + final NavigationContract navigationContract = GetIt.I(); + + return MaterialPage( + key: state.pageKey, + child: RequestRecoveryScreen(navigationContract: navigationContract), + ); + } +} diff --git a/modules/auth/lib/src/features/recover_password/presentation/request_recovery/request_recovery_screen.dart b/modules/auth/lib/src/features/recover_password/presentation/request_recovery/request_recovery_screen.dart new file mode 100644 index 00000000..27f5547d --- /dev/null +++ b/modules/auth/lib/src/features/recover_password/presentation/request_recovery/request_recovery_screen.dart @@ -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), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/modules/auth/lib/src/features/recover_password/presentation/restore_password_screen.dart b/modules/auth/lib/src/features/recover_password/presentation/restore_password_screen.dart deleted file mode 100644 index 7068801f..00000000 --- a/modules/auth/lib/src/features/recover_password/presentation/restore_password_screen.dart +++ /dev/null @@ -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), - ], - ), - ), - ), - ); - } -} diff --git a/modules/auth/lib/src/features/recover_password/presentation/sent/sent_builder.dart b/modules/auth/lib/src/features/recover_password/presentation/sent/sent_builder.dart new file mode 100644 index 00000000..c2ed8e60 --- /dev/null +++ b/modules/auth/lib/src/features/recover_password/presentation/sent/sent_builder.dart @@ -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 buildPage(BuildContext context, GoRouterState state) { + final NavigationContract navigationContract = GetIt.I(); + + return MaterialPage( + key: state.pageKey, + child: SentScreen(navigationContract: navigationContract), + ); + } +} diff --git a/modules/auth/lib/src/features/recover_password/presentation/sent/sent_screen.dart b/modules/auth/lib/src/features/recover_password/presentation/sent/sent_screen.dart new file mode 100644 index 00000000..0ab57d92 --- /dev/null +++ b/modules/auth/lib/src/features/recover_password/presentation/sent/sent_screen.dart @@ -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)*/, + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/modules/auth/lib/src/features/recover_password/presentation/sent_screen.dart b/modules/auth/lib/src/features/recover_password/presentation/sent_screen.dart deleted file mode 100644 index f386c359..00000000 --- a/modules/auth/lib/src/features/recover_password/presentation/sent_screen.dart +++ /dev/null @@ -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), - ], - ), - ), - ), - ); - } -} diff --git a/modules/auth/lib/src/features/recover_password/presentation/state/recover_password_view_model.dart b/modules/auth/lib/src/features/recover_password/presentation/state/recover_password_view_model.dart new file mode 100644 index 00000000..ca96fbb5 --- /dev/null +++ b/modules/auth/lib/src/features/recover_password/presentation/state/recover_password_view_model.dart @@ -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.new, +); + +class RecoverPasswordViewModel extends Notifier { + 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 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 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 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 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 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(); + } +} \ No newline at end of file diff --git a/modules/auth/lib/src/features/recover_password/presentation/state/recover_password_view_state.dart b/modules/auth/lib/src/features/recover_password/presentation/state/recover_password_view_state.dart new file mode 100644 index 00000000..766e67d7 --- /dev/null +++ b/modules/auth/lib/src/features/recover_password/presentation/state/recover_password_view_state.dart @@ -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 securityChecks, + }) = _RecoverPasswordViewState; +} \ No newline at end of file diff --git a/modules/auth/lib/src/features/recover_password/presentation/state/recover_password_view_state.freezed.dart b/modules/auth/lib/src/features/recover_password/presentation/state/recover_password_view_state.freezed.dart new file mode 100644 index 00000000..950aeda5 --- /dev/null +++ b/modules/auth/lib/src/features/recover_password/presentation/state/recover_password_view_state.freezed.dart @@ -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 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 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 get copyWith => _$RecoverPasswordViewStateCopyWithImpl(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 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, + )); +} + +} + + +/// 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 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 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? 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 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 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 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 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? 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 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 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 _securityChecks; +@override@JsonKey() Map 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 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, + )); +} + + +} + +// dart format on diff --git a/modules/auth/lib/src/features/recover_password/recover_password_builder.dart b/modules/auth/lib/src/features/recover_password/recover_password_builder.dart index 930f53ba..1749d1f3 100644 --- a/modules/auth/lib/src/features/recover_password/recover_password_builder.dart +++ b/modules/auth/lib/src/features/recover_password/recover_password_builder.dart @@ -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( key: state.pageKey, - child: RestorePasswordScreen(navigationContract: navigationContract), + child: RequestRecoveryScreen(navigationContract: navigationContract), ); } } diff --git a/packages/design_system/lib/src/inputs/textfields.dart b/packages/design_system/lib/src/inputs/textfields.dart index a665ff8d..53ab0400 100644 --- a/packages/design_system/lib/src/inputs/textfields.dart +++ b/packages/design_system/lib/src/inputs/textfields.dart @@ -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? 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 createState() => CustomTextFieldState(); -} - -class CustomTextFieldState extends State { - 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 ? [FilteringTextInputFormatter.digitsOnly] : const [], 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, ), ], ); diff --git a/packages/design_system/test/goldens/textfield.png b/packages/design_system/test/goldens/textfield.png index 3021a373..dc4db806 100644 Binary files a/packages/design_system/test/goldens/textfield.png and b/packages/design_system/test/goldens/textfield.png differ