forked from eatvio/eatvio_app
Merge pull request 'main' (#677) from main into test-flight
Reviewed-on: eatvio/eatvio_app#677
This commit is contained in:
@@ -164,8 +164,12 @@ class ProfileRepository {
|
||||
return (jsonDecode(await profileService.getMyPhone())['phone']);
|
||||
}
|
||||
|
||||
Future<void> sendEmailCode() async {
|
||||
return await profileService.sendEmailCode();
|
||||
Future<void> sendEmailCode({String? newEmail}) async {
|
||||
if (newEmail != null) {
|
||||
return await profileService.sendEmailCode(data: {'new_email': newEmail});
|
||||
} else {
|
||||
return await profileService.sendEmailCode();
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> tryVerifyEmailByCode(String code) async {
|
||||
|
||||
@@ -16,9 +16,7 @@ abstract class ProfileService {
|
||||
factory ProfileService(Dio dio, {String baseUrl}) = _ProfileService;
|
||||
|
||||
@GET('/api/profile/show/{slug}')
|
||||
Future<ProfileModel> getProfileBySlug({
|
||||
@Path() required String slug,
|
||||
});
|
||||
Future<ProfileModel> getProfileBySlug({@Path() required String slug});
|
||||
|
||||
@GET('/api/profile/index')
|
||||
Future<ProfileModel> getMyProfile();
|
||||
@@ -27,9 +25,7 @@ abstract class ProfileService {
|
||||
Future<ProfileDetailsModel> getMyProfileDetails();
|
||||
|
||||
@GET('/api/profile/detailz/{slug}')
|
||||
Future<ProfileDetailsModel> getProfileDetailsBySlug({
|
||||
@Path() required String slug,
|
||||
});
|
||||
Future<ProfileDetailsModel> getProfileDetailsBySlug({@Path() required String slug});
|
||||
|
||||
@POST('/api/profile/addCircumference')
|
||||
Future<void> addCircumference({
|
||||
@@ -39,64 +35,38 @@ abstract class ProfileService {
|
||||
});
|
||||
|
||||
@POST('/api/profile/addWeight')
|
||||
Future<void> addWeight({
|
||||
@Part() required double weight,
|
||||
});
|
||||
Future<void> addWeight({@Part() required double weight});
|
||||
|
||||
@GET('/api/blogPosts/search_by_author/{slug}')
|
||||
Future<List<FeedBlogPostModel>> getBlogPostByAuthorSlug({
|
||||
@Path() required String slug,
|
||||
});
|
||||
Future<List<FeedBlogPostModel>> getBlogPostByAuthorSlug({@Path() required String slug});
|
||||
|
||||
@GET('/api/profile/measure/weight/{period}')
|
||||
Future<List<WeightModel>> getWeight({
|
||||
@Path() required String period,
|
||||
});
|
||||
Future<List<WeightModel>> getWeight({@Path() required String period});
|
||||
|
||||
@GET('/api/profile/measure/weight/{period}/{slug}')
|
||||
Future<List<WeightModel>> getWeightBySlug({
|
||||
@Path() required String period,
|
||||
@Path() required String slug,
|
||||
});
|
||||
Future<List<WeightModel>> getWeightBySlug({@Path() required String period, @Path() required String slug});
|
||||
|
||||
@GET('/api/profile/measure/circumferences/{period}')
|
||||
Future<List<CircumferenceModel>> getCircumferences({
|
||||
@Path() required String period,
|
||||
});
|
||||
Future<List<CircumferenceModel>> getCircumferences({@Path() required String period});
|
||||
|
||||
@GET('/api/profile/measure/circumferences/{period}/{slug}')
|
||||
Future<List<CircumferenceModel>> getCircumferencesBySlug({
|
||||
@Path() required String period,
|
||||
@Path() required String slug,
|
||||
});
|
||||
Future<List<CircumferenceModel>> getCircumferencesBySlug({@Path() required String period, @Path() required String slug});
|
||||
|
||||
@GET('/api/profile/{slug}/subscribers')
|
||||
Future<SubPaginationModel> getSubscribers({
|
||||
@Path() required String slug,
|
||||
@Query('filters[name][\$startsWith]') String? query,
|
||||
});
|
||||
Future<SubPaginationModel> getSubscribers({@Path() required String slug, @Query('filters[name][\$startsWith]') String? query});
|
||||
|
||||
@GET('/api/profile/{slug}/subscriptions')
|
||||
Future<SubPaginationModel> getSubscriptions({
|
||||
@Path() required String slug,
|
||||
@Query('filters[name][\$startsWith]') String? query,
|
||||
});
|
||||
Future<SubPaginationModel> getSubscriptions({@Path() required String slug, @Query('filters[name][\$startsWith]') String? query});
|
||||
|
||||
@POST('/api/profile/subscribe/{slug}')
|
||||
Future<void> subscribe({
|
||||
@Path() required String slug,
|
||||
});
|
||||
Future<void> subscribe({@Path() required String slug});
|
||||
|
||||
@POST('/api/profile/unsubscribe/{slug}')
|
||||
Future<void> unsubscribe({
|
||||
@Path() required String slug,
|
||||
});
|
||||
Future<void> unsubscribe({@Path() required String slug});
|
||||
|
||||
@POST('/api/profile/photo/upload')
|
||||
@MultiPart()
|
||||
Future<void> uploadPhoto({
|
||||
@Part(name: 'photo') required File photo,
|
||||
});
|
||||
Future<void> uploadPhoto({@Part(name: 'photo') required File photo});
|
||||
|
||||
@DELETE('/api/profile/photo/delete')
|
||||
Future<void> deletePhoto();
|
||||
@@ -105,20 +75,16 @@ abstract class ProfileService {
|
||||
Future<ChosenDietsPaginationModel> getChosenDiets();
|
||||
|
||||
@POST('/api/profile/make_pass_key')
|
||||
Future<String> getPassKey({
|
||||
@Body() required String json,
|
||||
});
|
||||
Future<String> getPassKey({@Body() required String json});
|
||||
|
||||
@GET('/api/profile/my_phone')
|
||||
Future<String> getMyPhone();
|
||||
|
||||
@POST('/api/profile/send_email_code')
|
||||
Future<void> sendEmailCode();
|
||||
Future<void> sendEmailCode({@Body() Map<String, dynamic> data = const {}});
|
||||
|
||||
@POST('/api/profile/verificate_email_code')
|
||||
Future<void> tryVerifyEmailByCode({
|
||||
@Body() required String code,
|
||||
});
|
||||
Future<void> tryVerifyEmailByCode({@Body() required String code});
|
||||
|
||||
@DELETE('/api/profile/delete_my_account')
|
||||
Future<void> deleteAccount();
|
||||
|
||||
@@ -32,4 +32,16 @@ class VerifyEmailCubit extends Cubit<VerifyEmailState> {
|
||||
emit(const VerifyEmailState.verifyError());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<VerifyEmailScreen> {
|
||||
late Timer _timer;
|
||||
int _secondsRemaining = 60;
|
||||
bool _canSendAgain = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -75,55 +77,82 @@ class _VerifyEmailScreenState extends State<VerifyEmailScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<VerifyEmailCubit>(
|
||||
create: (_) => sl<VerifyEmailCubit>()..sendEmailCode(),
|
||||
child: BlocConsumer<VerifyEmailCubit, VerifyEmailState>(
|
||||
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<VerifyEmailCubit>().sendEmailCode();
|
||||
_startTimer();
|
||||
},
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
return BlocConsumer<VerifyEmailCubit, VerifyEmailState>(
|
||||
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<VerifyEmailCubit>().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<VerifyEmailCubit>();
|
||||
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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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<ChangeEmailDialog> createState() => _ChangeEmailDialogState();
|
||||
}
|
||||
|
||||
class _ChangeEmailDialogState extends State<ChangeEmailDialog> {
|
||||
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<void> _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<VerifyEmailCubit, VerifyEmailState>(
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user