diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 3d0ffb19..a07e12ba 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -7,6 +7,7 @@ import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/core/websockets/ws_manager.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart'; +import 'package:eatvio_app/features/auth/presentation/pages/forget_password_screen.dart'; import 'package:eatvio_app/features/blog_post/presentation/pages/blog_post_screen.dart'; import 'package:eatvio_app/features/blog_post_create/presentation/pages/blog_post_create_screen.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; @@ -71,6 +72,7 @@ import 'package:go_router/go_router.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:version/version.dart'; +import 'dart:developer'; class AppRouter { static GlobalKey navigatorKey = GlobalKey(); @@ -89,6 +91,11 @@ class AppRouter { name: 'registrationScreen', builder: (context, state) => const RegistrationScreen(), ), + GoRoute( + path: 'forget-password-screen', + name: 'forgetPasswordScreen', + builder: (context, state) => const ForgetPasswordScreen(), + ), ], ), ShellRoute( @@ -117,10 +124,10 @@ class AppRouter { redirect: (context, state) async { try { await sl().initialize(); - } catch (e) { - rethrow; + } catch (e, st) { + log(e.toString(), stackTrace: st); } - return '/feed-screen'; + return null; }, ), GoRoute( diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 9395a49f..d912f826 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -5,9 +5,12 @@ import 'package:eatvio_app/core/firebase_api.dart'; import 'package:eatvio_app/core/interceptors/token_interceptor.dart'; import 'package:eatvio_app/core/websockets/ws_manager.dart'; import 'package:eatvio_app/core/websockets/ws_service.dart'; -import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart'; import 'package:eatvio_app/features/auth/repositories/auth_repository.dart'; +import 'package:eatvio_app/features/auth/repositories/reset_password_repository.dart'; import 'package:eatvio_app/features/auth/services/auth_service.dart'; +import 'package:eatvio_app/features/auth/services/reset_password_service.dart'; import 'package:eatvio_app/features/cancel_plan_bottom_sheet/cubit/cancel_plan_cubit.dart'; import 'package:eatvio_app/features/cancel_plan_bottom_sheet/repository/cancel_plan_repository.dart'; import 'package:eatvio_app/features/cancel_plan_bottom_sheet/service/cancel_plan_service.dart'; @@ -158,6 +161,7 @@ Future init() async { sl.registerFactory(() => ConsultationsArchiveCubit(consultationsRepository: sl())); sl.registerFactory(() => ConsultantFinancesCubit(consultationsRepository: sl())); sl.registerFactory(() => UserPurchasesCubit(consultationsRepository: sl())); + sl.registerFactory(() => ResetPasswordCubit(resetPasswordRepository: sl())); // Repository sl.registerLazySingleton(() => ChatRepository(chatService: sl())); @@ -186,6 +190,7 @@ Future init() async { sl.registerLazySingleton(() => CreatePlanRepository(createPlanService: sl())); sl.registerLazySingleton(() => NotificationsRepository(notificationsService: sl())); sl.registerFactory(() => MyRecipesRepository(myRecipesService: sl())); + sl.registerFactory(() => ResetPasswordRepository(resetPasswordService: sl())); // Services sl.registerLazySingleton(() => ChatService(sl())); @@ -216,6 +221,7 @@ Future init() async { sl.registerLazySingleton(() => NotificationsService(sl())); sl.registerLazySingleton(() => MyRecipesService(sl())); sl.registerLazySingleton(() => FirebaseApi()); + sl.registerLazySingleton(() => ResetPasswordService(sl())); // External sl.registerLazySingleton( diff --git a/lib/features/auth/presentation/bloc/auth_cubit.dart b/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart similarity index 97% rename from lib/features/auth/presentation/bloc/auth_cubit.dart rename to lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart index 3c5e30af..20686cce 100644 --- a/lib/features/auth/presentation/bloc/auth_cubit.dart +++ b/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart @@ -2,7 +2,7 @@ import 'dart:developer'; import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; -import 'package:eatvio_app/features/auth/presentation/bloc/auth_state.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state.dart'; import 'package:eatvio_app/features/auth/repositories/auth_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/lib/features/auth/presentation/bloc/auth_state.dart b/lib/features/auth/presentation/bloc/auth_cubit/auth_state.dart similarity index 100% rename from lib/features/auth/presentation/bloc/auth_state.dart rename to lib/features/auth/presentation/bloc/auth_cubit/auth_state.dart diff --git a/lib/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart b/lib/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart new file mode 100644 index 00000000..37fd0d5a --- /dev/null +++ b/lib/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart @@ -0,0 +1,35 @@ +import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_state.dart'; +import 'package:eatvio_app/features/auth/repositories/reset_password_repository.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class ResetPasswordCubit extends Cubit { + final ResetPasswordRepository resetPasswordRepository; + + ResetPasswordCubit({required this.resetPasswordRepository}) : super(const ResetPasswordState.empty()); + + trySendEmailCode(String email) async { + emit(const ResetPasswordState.loadingSendEmailCode()); + try { + if (await resetPasswordRepository.sendEmailCodeForPasswordChange(email)) { + emit(const ResetPasswordState.codeSendSuccess()); + } else { + emit(const ResetPasswordState.codeSendError()); + } + } catch (e) { + emit(const ResetPasswordState.codeSendError()); + } + } + + tryResetPassword(String email, String code, String password, String passwordConfirmation) async { + emit(const ResetPasswordState.loadingResetPassword()); + try { + if (await resetPasswordRepository.confirmPasswordChange(code, password, passwordConfirmation, email)) { + emit(const ResetPasswordState.resetPasswordSuccess()); + } else { + emit(const ResetPasswordState.resetPasswordError()); + } + } catch (e) { + emit(const ResetPasswordState.resetPasswordError()); + } + } +} diff --git a/lib/features/auth/presentation/bloc/reset_password_cubit/reset_password_state.dart b/lib/features/auth/presentation/bloc/reset_password_cubit/reset_password_state.dart new file mode 100644 index 00000000..a936ccaa --- /dev/null +++ b/lib/features/auth/presentation/bloc/reset_password_cubit/reset_password_state.dart @@ -0,0 +1,22 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'reset_password_state.freezed.dart'; + +@freezed +abstract class ResetPasswordState with _$ResetPasswordState { + const factory ResetPasswordState() = _ResetPasswordState; + + const factory ResetPasswordState.empty() = ResetPasswordStateEmpty; + + const factory ResetPasswordState.loadingSendEmailCode() = ResetPasswordStateLoadingSendEmailCode; + + const factory ResetPasswordState.codeSendSuccess() = ResetPasswordStateCodeSendSuccess; + + const factory ResetPasswordState.codeSendError() = ResetPasswordStateCodeSendError; + + const factory ResetPasswordState.loadingResetPassword() = ResetPasswordStateLoadingResetPassword; + + const factory ResetPasswordState.resetPasswordError() = ResetPasswordStateResetPasswordError; + + const factory ResetPasswordState.resetPasswordSuccess() = ResetPasswordStateResetPassowrdSuccess; +} diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 179e0974..123eac0f 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -6,8 +6,8 @@ import 'package:eatvio_app/core/utils/image_constant.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/websockets/ws_manager.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; -import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit.dart'; -import 'package:eatvio_app/features/auth/presentation/bloc/auth_state.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart'; @@ -135,6 +135,20 @@ class _AuthScreenState extends State { ), ), const SizedBox(height: 16), + Center( + child: GestureDetector( + onTap: () => GoRouter.of(context).goNamed('forgetPasswordScreen'), + child: Text( + 'Забыли пароль?', + style: TextStyle( + color: appTheme.blueA400, + decorationColor: appTheme.blueA400, + decoration: TextDecoration.underline, + ), + ), + ), + ), + const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/lib/features/auth/presentation/pages/forget_password_screen.dart b/lib/features/auth/presentation/pages/forget_password_screen.dart new file mode 100644 index 00000000..78313a15 --- /dev/null +++ b/lib/features/auth/presentation/pages/forget_password_screen.dart @@ -0,0 +1,165 @@ +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/utils/toasts.dart'; +import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_state.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +class ForgetPasswordScreen extends StatefulWidget { + const ForgetPasswordScreen({super.key}); + + @override + State createState() => _ForgetPasswordScreenState(); +} + +class _ForgetPasswordScreenState extends State { + final emailController = TextEditingController(); + final codeController = TextEditingController(); + final passwordController = TextEditingController(); + final passwordConfirmController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (BuildContext context) => sl(), + child: BlocConsumer( + builder: (BuildContext context, ResetPasswordState state) { + return Scaffold( + appBar: AppBar( + title: const Text('Смена пароля'), + centerTitle: false, + ), + body: state is ResetPasswordStateLoadingResetPassword || state is ResetPasswordStateLoadingSendEmailCode + ? const Center( + child: CircularProgressIndicator(), + ) + : Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (state is ResetPasswordStateCodeSendError || state is ResetPasswordStateEmpty) ...[ + const Text( + 'Забыли пароль? Укажите свой email,\nи мы пришлём код для сброса.', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 24), + const Text( + 'E-mail', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 4), + CustomTextFormField( + controller: emailController, + hintText: 'Введите e-mail', + ), + ], + if (state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError) ...[ + const Text( + 'Код', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 4), + CustomTextFormField( + controller: codeController, + hintText: 'Введите код', + ), + const SizedBox(height: 12), + const Text( + 'Пароль', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 4), + CustomTextFormField( + controller: passwordController, + hintText: 'Введите новый пароль', + ), + const SizedBox(height: 12), + const Text( + 'Подтвердите пароль', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 4), + CustomTextFormField( + controller: passwordConfirmController, + hintText: 'Введите новый пароль', + ), + ], + const Spacer(), + InkWell( + onTap: () { + if (state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError) { + context.read().tryResetPassword( + emailController.text, + codeController.text, + passwordController.text, + passwordConfirmController.text, + ); + } else { + context.read().trySendEmailCode(emailController.text); + } + }, + borderRadius: BorderRadius.circular(20), + child: Ink( + width: MediaQuery.of(context).size.width - 32, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: appTheme.blueA400, + ), + child: Center( + child: Text( + state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError + ? 'Сменить пароль' + : 'Сбросить пароль', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ), + ), + ), + ], + ), + ), + ); + }, + listener: (BuildContext context, ResetPasswordState state) { + if (state is ResetPasswordStateCodeSendSuccess) { + showSuccessToast('Код на почту успешно отправлен'); + } + if (state is ResetPasswordStateCodeSendError) { + showErrorToast('Ошибка отправки кода. Проверьте правильность ввёдной почты'); + } + if (state is ResetPasswordStateResetPasswordError) { + showErrorToast('Ошибка смены пароля. Проверьте правильность ввёднного кода и совпадение пароля'); + } + if (state is ResetPasswordStateResetPassowrdSuccess) { + GoRouter.of(context).pop(); + showSuccessToast('Пароль успешно сменён. Войдите в аккаунт'); + } + }, + ), + ); + } +} diff --git a/lib/features/auth/repositories/reset_password_repository.dart b/lib/features/auth/repositories/reset_password_repository.dart new file mode 100644 index 00000000..e0b1d69f --- /dev/null +++ b/lib/features/auth/repositories/reset_password_repository.dart @@ -0,0 +1,53 @@ +import 'package:eatvio_app/features/auth/services/reset_password_service.dart'; + +class ResetPasswordRepository { + final ResetPasswordService resetPasswordService; + + ResetPasswordRepository({ + required this.resetPasswordService, + }); + + Future sendEmailCodeForPasswordChange(String email) async { + try { + final response = await resetPasswordService.sendEmailCodeForPasswordChange( + data: { + 'email': email, + }, + ); + int statusCode = response.response.statusCode ?? 0; + if (statusCode >= 200 && statusCode < 300) { + return true; + } else { + return false; + } + } catch (e) { + rethrow; + } + } + + Future confirmPasswordChange( + String code, + String password, + String passwordConfirmation, + String email, + ) async { + try { + final response = await resetPasswordService.confirmPasswordChange( + data: { + 'code': code, + 'password': password, + 'password_confirmation': passwordConfirmation, + 'email': email, + }, + ); + int statusCode = response.response.statusCode ?? 0; + if (statusCode >= 200 && statusCode < 300) { + return true; + } else { + return false; + } + } catch (e) { + rethrow; + } + } +} diff --git a/lib/features/auth/services/reset_password_service.dart b/lib/features/auth/services/reset_password_service.dart new file mode 100644 index 00000000..9a7a101d --- /dev/null +++ b/lib/features/auth/services/reset_password_service.dart @@ -0,0 +1,19 @@ +import 'package:dio/dio.dart'; +import 'package:retrofit/retrofit.dart'; + +part 'reset_password_service.g.dart'; + +@RestApi() +abstract class ResetPasswordService { + factory ResetPasswordService(Dio dio, {String baseUrl}) = _ResetPasswordService; + + @POST('/api/send_password_reset_code') + Future sendEmailCodeForPasswordChange({ + @Body() required Map data, + }); + + @POST('/api/confirm_password_reset_code') + Future confirmPasswordChange({ + @Body() required Map data, + }); +} diff --git a/lib/features/registration/presentation/pages/webview_registration_screen.dart b/lib/features/registration/presentation/pages/webview_registration_screen.dart index f1dc435f..abb6537d 100644 --- a/lib/features/registration/presentation/pages/webview_registration_screen.dart +++ b/lib/features/registration/presentation/pages/webview_registration_screen.dart @@ -3,7 +3,7 @@ import 'dart:developer'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/locator_service.dart'; -import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart'; import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; diff --git a/lib/features/settings/repositories/service_repository.dart b/lib/features/settings/repositories/service_repository.dart index e1e71f09..eb462de3 100644 --- a/lib/features/settings/repositories/service_repository.dart +++ b/lib/features/settings/repositories/service_repository.dart @@ -38,36 +38,4 @@ class SettingsRepository { rethrow; } } - - Future sendEmailCodeForPasswordChange(String email) async { - try { - return await settingsService.sendEmailCodeForPasswordChange( - data: { - 'email': email, - }, - ); - } catch (e) { - rethrow; - } - } - - Future confirmPasswordChange( - String code, - String password, - String passwordConfirmation, - String email, - ) async { - try { - return await settingsService.confirmPasswordChange( - data: { - 'code': code, - 'password': password, - 'password_confirmation': passwordConfirmation, - 'email': email, - }, - ); - } catch (e) { - rethrow; - } - } } diff --git a/lib/features/settings/services/settings_service.dart b/lib/features/settings/services/settings_service.dart index e4cdb6b7..d17b5842 100644 --- a/lib/features/settings/services/settings_service.dart +++ b/lib/features/settings/services/settings_service.dart @@ -21,14 +21,4 @@ abstract class SettingsService { Future updateProfileDetails({ @Body() required Map data, }); - - @POST('/api/send_password_reset_code') - Future sendEmailCodeForPasswordChange({ - @Body() required Map data, - }); - - @POST('/api/confirm_password_reset_code') - Future confirmPasswordChange({ - @Body() required Map data, - }); } diff --git a/pubspec.yaml b/pubspec.yaml index 6fe00c91..d39798fa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,7 +26,7 @@ dependencies: flutter_form_builder: ^10.0.1 image_picker: ^1.1.2 flutter_typeahead: ^5.2.0 - go_router: ^15.1.2 + go_router: ^15.2.3 keyboard_dismisser: ^3.0.0 retrofit: ^4.4.2 get_it: ^8.0.3