feat(verify_email): Issue-645

This commit is contained in:
mso7nueh
2025-08-27 11:33:06 +03:00
parent 0f1dbe16f1
commit d8324c1635
6 changed files with 293 additions and 106 deletions

View File

@@ -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());
}
}
}

View File

@@ -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;
}

View File

@@ -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)),
],
),
),
),
);
},
);
},
),

View File

@@ -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,
),
),
],
),
],
),
),
),
),
);
}
}