diff --git a/lib/features/profile/repositories/profile_repository.dart b/lib/features/profile/repositories/profile_repository.dart index 531cf589..4867d85a 100644 --- a/lib/features/profile/repositories/profile_repository.dart +++ b/lib/features/profile/repositories/profile_repository.dart @@ -164,8 +164,12 @@ class ProfileRepository { return (jsonDecode(await profileService.getMyPhone())['phone']); } - Future sendEmailCode() async { - return await profileService.sendEmailCode(); + Future sendEmailCode({String? newEmail}) async { + if (newEmail != null) { + return await profileService.sendEmailCode(data: {'new_email': newEmail}); + } else { + return await profileService.sendEmailCode(); + } } Future tryVerifyEmailByCode(String code) async { diff --git a/lib/features/profile/services/profile_service.dart b/lib/features/profile/services/profile_service.dart index bddd46f8..b313f27f 100644 --- a/lib/features/profile/services/profile_service.dart +++ b/lib/features/profile/services/profile_service.dart @@ -16,9 +16,7 @@ abstract class ProfileService { factory ProfileService(Dio dio, {String baseUrl}) = _ProfileService; @GET('/api/profile/show/{slug}') - Future getProfileBySlug({ - @Path() required String slug, - }); + Future getProfileBySlug({@Path() required String slug}); @GET('/api/profile/index') Future getMyProfile(); @@ -27,9 +25,7 @@ abstract class ProfileService { Future getMyProfileDetails(); @GET('/api/profile/detailz/{slug}') - Future getProfileDetailsBySlug({ - @Path() required String slug, - }); + Future getProfileDetailsBySlug({@Path() required String slug}); @POST('/api/profile/addCircumference') Future addCircumference({ @@ -39,64 +35,38 @@ abstract class ProfileService { }); @POST('/api/profile/addWeight') - Future addWeight({ - @Part() required double weight, - }); + Future addWeight({@Part() required double weight}); @GET('/api/blogPosts/search_by_author/{slug}') - Future> getBlogPostByAuthorSlug({ - @Path() required String slug, - }); + Future> getBlogPostByAuthorSlug({@Path() required String slug}); @GET('/api/profile/measure/weight/{period}') - Future> getWeight({ - @Path() required String period, - }); + Future> getWeight({@Path() required String period}); @GET('/api/profile/measure/weight/{period}/{slug}') - Future> getWeightBySlug({ - @Path() required String period, - @Path() required String slug, - }); + Future> getWeightBySlug({@Path() required String period, @Path() required String slug}); @GET('/api/profile/measure/circumferences/{period}') - Future> getCircumferences({ - @Path() required String period, - }); + Future> getCircumferences({@Path() required String period}); @GET('/api/profile/measure/circumferences/{period}/{slug}') - Future> getCircumferencesBySlug({ - @Path() required String period, - @Path() required String slug, - }); + Future> getCircumferencesBySlug({@Path() required String period, @Path() required String slug}); @GET('/api/profile/{slug}/subscribers') - Future getSubscribers({ - @Path() required String slug, - @Query('filters[name][\$startsWith]') String? query, - }); + Future getSubscribers({@Path() required String slug, @Query('filters[name][\$startsWith]') String? query}); @GET('/api/profile/{slug}/subscriptions') - Future getSubscriptions({ - @Path() required String slug, - @Query('filters[name][\$startsWith]') String? query, - }); + Future getSubscriptions({@Path() required String slug, @Query('filters[name][\$startsWith]') String? query}); @POST('/api/profile/subscribe/{slug}') - Future subscribe({ - @Path() required String slug, - }); + Future subscribe({@Path() required String slug}); @POST('/api/profile/unsubscribe/{slug}') - Future unsubscribe({ - @Path() required String slug, - }); + Future unsubscribe({@Path() required String slug}); @POST('/api/profile/photo/upload') @MultiPart() - Future uploadPhoto({ - @Part(name: 'photo') required File photo, - }); + Future uploadPhoto({@Part(name: 'photo') required File photo}); @DELETE('/api/profile/photo/delete') Future deletePhoto(); @@ -105,20 +75,16 @@ abstract class ProfileService { Future getChosenDiets(); @POST('/api/profile/make_pass_key') - Future getPassKey({ - @Body() required String json, - }); + Future getPassKey({@Body() required String json}); @GET('/api/profile/my_phone') Future getMyPhone(); @POST('/api/profile/send_email_code') - Future sendEmailCode(); + Future sendEmailCode({@Body() Map data = const {}}); @POST('/api/profile/verificate_email_code') - Future tryVerifyEmailByCode({ - @Body() required String code, - }); + Future tryVerifyEmailByCode({@Body() required String code}); @DELETE('/api/profile/delete_my_account') Future deleteAccount(); diff --git a/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart b/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart index d1f547f8..57554130 100644 --- a/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart +++ b/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart @@ -32,4 +32,16 @@ class VerifyEmailCubit extends Cubit { emit(const VerifyEmailState.verifyError()); } } + + Future changeEmail(String newEmail) async { + try { + emit(const VerifyEmailState.changeEmailLoading()); + await profileRepository.sendEmailCode(newEmail: newEmail); + emit(const VerifyEmailState.changeEmailSuccess()); + // Обновляем email в состоянии после успешной смены + emit(VerifyEmailState.loaded(email: newEmail)); + } catch (e) { + emit(const VerifyEmailState.changeEmailError()); + } + } } diff --git a/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart b/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart index 718b2cce..6cb91a32 100644 --- a/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart +++ b/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart @@ -6,13 +6,9 @@ part 'verify_email_state.freezed.dart'; abstract class VerifyEmailState with _$VerifyEmailState { const factory VerifyEmailState.loading() = VerifyEmailStateLoading; - const factory VerifyEmailState.loaded({ - required String email, - }) = VerifyEmailStateLoaded; + const factory VerifyEmailState.loaded({required String email}) = VerifyEmailStateLoaded; - const factory VerifyEmailState.error({ - required String errorMessage, - }) = VerifyEmailStateError; + const factory VerifyEmailState.error({required String errorMessage}) = VerifyEmailStateError; const factory VerifyEmailState() = _VerifyEmailState; @@ -21,4 +17,10 @@ abstract class VerifyEmailState with _$VerifyEmailState { const factory VerifyEmailState.verifySuccess() = VerifyEmailStateVerifySuccess; const factory VerifyEmailState.verifyError() = VerifyEmailStateVerifyError; + + const factory VerifyEmailState.changeEmailLoading() = VerifyEmailStateChangeEmailLoading; + + const factory VerifyEmailState.changeEmailSuccess() = VerifyEmailStateChangeEmailSuccess; + + const factory VerifyEmailState.changeEmailError() = VerifyEmailStateChangeEmailError; } diff --git a/lib/features/registration/presentation/pages/verify_email_screen.dart b/lib/features/registration/presentation/pages/verify_email_screen.dart index e91b9e9e..8d1ef619 100644 --- a/lib/features/registration/presentation/pages/verify_email_screen.dart +++ b/lib/features/registration/presentation/pages/verify_email_screen.dart @@ -7,6 +7,7 @@ import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart'; import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart'; +import 'package:eatvio_app/features/registration/presentation/widgets/change_email_dialog.dart'; import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -27,6 +28,7 @@ class _VerifyEmailScreenState extends State { late Timer _timer; int _secondsRemaining = 60; bool _canSendAgain = false; + @override void initState() { super.initState(); @@ -75,55 +77,82 @@ class _VerifyEmailScreenState extends State { Widget build(BuildContext context) { return BlocProvider( create: (_) => sl()..sendEmailCode(), - child: BlocConsumer( - listener: (context, state) { - if (state is VerifyEmailStateLoaded) { - setState(() { - email = state.email; - }); - } else if (state is VerifyEmailStateVerifySuccess) { - showSuccessToast('Почта успешно подтверждена'); - goRouter.pushNamed('feedScreen'); - } else if (state is VerifyEmailStateVerifyError) { - showErrorToast("Неверный код. Попробуйте снова."); - for (var controller in _controllers) { - controller.clear(); - } - FocusScope.of(context).requestFocus(_focusNodes[0]); - } - }, - builder: (context, state) { - return Scaffold( - appBar: AppBar(title: const Text("Подтвердите почту")), - body: Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text("Код отправлен на почту\n$email", textAlign: TextAlign.center), - const SizedBox(height: 20), - Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: List.generate(4, (index) => _buildCodeField(context, index))), - const SizedBox(height: 40), - CustomOutlinedButton( - text: 'Запросить код повторно', - height: 52, - isDisabled: !_canSendAgain, - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), - buttonStyle: ButtonStyle( - side: const WidgetStatePropertyAll(BorderSide.none), - backgroundColor: WidgetStatePropertyAll(_canSendAgain ? appTheme.blueA400 : appTheme.gray), - ), - onPressed: () async { - await context.read().sendEmailCode(); - _startTimer(); - }, + child: Builder( + builder: (context) { + return BlocConsumer( + listener: (context, state) { + if (state is VerifyEmailStateLoaded) { + setState(() { + email = state.email; + }); + _startTimer(); + } else if (state is VerifyEmailStateVerifySuccess) { + showSuccessToast('Почта успешно подтверждена'); + goRouter.pushNamed('feedScreen'); + } else if (state is VerifyEmailStateVerifyError) { + showErrorToast("Неверный код. Попробуйте снова."); + for (var controller in _controllers) { + controller.clear(); + } + FocusScope.of(context).requestFocus(_focusNodes[0]); + } else if (state is VerifyEmailStateChangeEmailSuccess) { + showSuccessToast('Код подтверждения отправлен на новый email'); + // Диалог закроется автоматически в ChangeEmailDialog при успешной смене + } else if (state is VerifyEmailStateChangeEmailError) { + showErrorToast('Ошибка при смене email. Попробуйте снова.'); + } + }, + builder: (context, state) { + return Scaffold( + appBar: AppBar(title: const Text("Подтвердите почту")), + body: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text("Код отправлен на почту\n$email", textAlign: TextAlign.center), + const SizedBox(height: 20), + Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: List.generate(4, (index) => _buildCodeField(context, index))), + const SizedBox(height: 40), + CustomOutlinedButton( + text: 'Запросить код повторно', + height: 52, + isDisabled: !_canSendAgain, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: const WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(_canSendAgain ? appTheme.blueA400 : appTheme.gray), + ), + onPressed: () async { + await context.read().sendEmailCode(); + _startTimer(); + }, + ), + const SizedBox(height: 8), + if (!_canSendAgain) + Text("Зпросить код повторно можно через $_secondsRemaining с.", style: TextStyles.textSMedium.copyWith(color: appTheme.gray)), + const SizedBox(height: 16), + CustomOutlinedButton( + text: 'Сменить email', + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.blueA400), + buttonStyle: ButtonStyle( + side: WidgetStatePropertyAll(BorderSide(color: appTheme.blueA400)), + backgroundColor: const WidgetStatePropertyAll(Colors.transparent), + ), + onPressed: () { + final verifyEmailCubit = context.read(); + showDialog( + context: context, + builder: (context) => ChangeEmailDialog(currentEmail: email, verifyEmailCubit: verifyEmailCubit), + ); + }, + ), + ], ), - const SizedBox(height: 8), - if (!_canSendAgain) - Text("Зпросить код повторно можно через $_secondsRemaining с.", style: TextStyles.textSMedium.copyWith(color: appTheme.gray)), - ], - ), - ), + ), + ); + }, ); }, ), diff --git a/lib/features/registration/presentation/widgets/change_email_dialog.dart b/lib/features/registration/presentation/widgets/change_email_dialog.dart new file mode 100644 index 00000000..266e9f04 --- /dev/null +++ b/lib/features/registration/presentation/widgets/change_email_dialog.dart @@ -0,0 +1,174 @@ +import 'package:eatvio_app/core/theme/custom_text_style.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_outlined_button.dart'; +import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart'; +import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class ChangeEmailDialog extends StatefulWidget { + final String currentEmail; + final VerifyEmailCubit verifyEmailCubit; + + const ChangeEmailDialog({super.key, required this.currentEmail, required this.verifyEmailCubit}); + + @override + State createState() => _ChangeEmailDialogState(); +} + +class _ChangeEmailDialogState extends State { + final TextEditingController _emailController = TextEditingController(); + final FocusNode _emailFocusNode = FocusNode(); + bool _isLoading = false; + String? _errorText; + + @override + void initState() { + super.initState(); + _emailController.text = widget.currentEmail; + } + + @override + void dispose() { + _emailController.dispose(); + _emailFocusNode.dispose(); + super.dispose(); + } + + bool _isValidEmail(String email) { + return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(email); + } + + void _validateEmail() { + final email = _emailController.text.trim(); + if (email.isEmpty) { + setState(() { + _errorText = 'Введите email'; + }); + } else if (!_isValidEmail(email)) { + setState(() { + _errorText = 'Введите корректный email'; + }); + } else if (email == widget.currentEmail) { + setState(() { + _errorText = 'Новый email должен отличаться от текущего'; + }); + } else { + setState(() { + _errorText = null; + }); + } + } + + Future _changeEmail() async { + _validateEmail(); + if (_errorText != null) return; + + setState(() { + _isLoading = true; + }); + + try { + await widget.verifyEmailCubit.changeEmail(_emailController.text.trim()); + // Диалог закроется автоматически при успешной смене email через основной экран + } catch (e) { + if (mounted) { + showErrorToast('Ошибка при смене email. Попробуйте снова.'); + setState(() { + _isLoading = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: widget.verifyEmailCubit, + child: BlocListener( + listener: (context, state) { + if (state is VerifyEmailStateChangeEmailSuccess) { + Navigator.of(context).pop(); // Закрываем диалог при успешной смене + } else if (state is VerifyEmailStateChangeEmailError) { + setState(() { + _isLoading = false; + }); + } + }, + child: Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Сменить email', style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)), + const SizedBox(height: 16), + Text('Введите новый email для получения кода подтверждения', style: TextStyles.textSMedium.copyWith(color: appTheme.gray)), + const SizedBox(height: 20), + TextField( + controller: _emailController, + focusNode: _emailFocusNode, + keyboardType: TextInputType.emailAddress, + decoration: InputDecoration( + hintText: 'Введите новый email', + hintStyle: TextStyles.textSMedium.copyWith(color: appTheme.gray), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: appTheme.gray), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: appTheme.blueA400), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: appTheme.red), + ), + errorText: _errorText, + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + ), + onChanged: (_) => _validateEmail(), + onSubmitted: (_) => _changeEmail(), + ), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: CustomOutlinedButton( + text: 'Отмена', + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black), + buttonStyle: ButtonStyle( + side: const WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0), + ), + onPressed: () => Navigator.of(context).pop(), + ), + ), + const SizedBox(width: 12), + Expanded( + child: CustomOutlinedButton( + text: _isLoading ? 'Отправка...' : 'Сменить', + height: 52, + isDisabled: _isLoading, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: const WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(_isLoading ? appTheme.gray : appTheme.blueA400), + ), + onPressed: _isLoading ? null : _changeEmail, + ), + ), + ], + ), + ], + ), + ), + ), + ), + ); + } +}