feat(auth): implement OAuth verification request and confirmation flow with error handling and UI updates

This commit is contained in:
mso7nueh
2025-12-17 15:41:38 +03:00
parent 91bd8f9d68
commit 2ab3bc1cf0
49 changed files with 601 additions and 39 deletions

View File

@@ -1,3 +1,4 @@
import 'dart:developer';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
@@ -49,6 +50,8 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
bool weightError = false;
bool heightError = false;
bool incorrectEmailFormatError = false;
bool showVerificationButton = false;
String? verificationType; // 'email' or 'phone'
// Состояние для выбора пола
bool isMaleSelected = true;
@@ -221,7 +224,28 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
hasError: emailExistError || incorrectEmailFormatError,
onChanged: (newValue) {},
),
if (emailExistError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Такая почта уже существует.')],
if (emailExistError) ...[
const SizedBox(height: 4),
const ValidationErrorWidget(error: 'Такая почта уже существует.'),
if (showVerificationButton && verificationType == 'email') ...[
const SizedBox(height: 12),
InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () => _handleVerificationRequest(context, 'email'),
child: Ink(
width: MediaQuery.of(context).size.width - 32,
height: 52,
decoration: BoxDecoration(color: appTheme.blueA400, borderRadius: BorderRadius.circular(20)),
child: const Center(
child: Text(
'Да, это мой аккаунт, подтвердить',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16, color: Colors.white),
),
),
),
),
],
],
if (incorrectEmailFormatError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Некорректный формат email')],
const SizedBox(height: 12),
const Text('Номер телефона'),
@@ -235,7 +259,28 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
hasError: phoneExistError,
onChanged: (newValue) {},
),
if (phoneExistError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Такой номер телефона уже существует.')],
if (phoneExistError) ...[
const SizedBox(height: 4),
const ValidationErrorWidget(error: 'Такой номер телефона уже существует.'),
if (showVerificationButton && verificationType == 'phone') ...[
const SizedBox(height: 12),
InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () => _handleVerificationRequest(context, 'phone'),
child: Ink(
width: MediaQuery.of(context).size.width - 32,
height: 52,
decoration: BoxDecoration(color: appTheme.blueA400, borderRadius: BorderRadius.circular(20)),
child: const Center(
child: Text(
'Да, это мой аккаунт, подтвердить',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16, color: Colors.white),
),
),
),
),
],
],
const SizedBox(height: 12),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -501,6 +546,10 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
clientId: clientID,
);
if (errors != null) {
bool hasEmailExist = false;
bool hasPhoneExist = false;
// Сначала определяем, какие ошибки присутствуют
for (final error in errors) {
switch (error) {
case RegistrationValidationErrors.incorrectEmailFormat:
@@ -509,14 +558,10 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
});
break;
case RegistrationValidationErrors.emailExist:
setState(() {
emailExistError = true;
});
hasEmailExist = true;
break;
case RegistrationValidationErrors.phoneExist:
setState(() {
phoneExistError = true;
});
hasPhoneExist = true;
break;
case RegistrationValidationErrors.incorrectDateOfBirthFormat:
setState(() {
@@ -525,8 +570,236 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
break;
}
}
// Устанавливаем verificationType согласно правилам:
// - Если оба (email и phone) существуют -> 'email'
// - Если только email -> 'email'
// - Если только phone -> 'phone'
if (hasEmailExist || hasPhoneExist) {
setState(() {
emailExistError = hasEmailExist;
phoneExistError = hasPhoneExist;
showVerificationButton = true;
if (hasEmailExist && hasPhoneExist) {
verificationType = 'email'; // Если оба существуют, используем email
} else if (hasEmailExist) {
verificationType = 'email'; // Только email
} else {
verificationType = 'phone'; // Только phone
}
});
}
// Если есть ошибки, возвращаемся и не показываем toast об успехе
return;
}
// Регистрация успешна только если нет ошибок
showSuccessToast('Регистрация успешна');
}
Future<void> _handleVerificationRequest(BuildContext context, String type) async {
try {
// Используем service locator для доступа к OAuthCubit
final oauthCubit = CubitRegistrations.sl<OAuthCubit>();
// Всегда передаем и email и phone
final String email = emailController.text;
final String phone = phoneController.text.replaceAll(RegExp(r'[^\d+]'), '');
final response = await oauthCubit.requestOAuthVerification(email: email, phone: phone);
if (response != null && context.mounted) {
// Показываем диалог для ввода кода
final displayText = response.email;
_showVerificationCodeDialog(context, type, displayText);
} else {
if (context.mounted) {
showErrorToast('Не удалось отправить код верификации');
}
}
} catch (e) {
if (context.mounted) {
showErrorToast('Ошибка: ${e.toString()}');
}
}
}
void _showVerificationCodeDialog(BuildContext context, String verificationType, String displayText) {
final List<FocusNode> focusNodes = List.generate(4, (_) => FocusNode());
final List<TextEditingController> controllers = List.generate(4, (_) => TextEditingController());
showDialog(
context: context,
barrierDismissible: false,
builder: (dialogContext) => AlertDialog(
title: const Text('Подтверждение аккаунта'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Код отправлен на $displayText'),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(4, (index) {
return SizedBox(
width: 50,
child: TextField(
controller: controllers[index],
focusNode: focusNodes[index],
textAlign: TextAlign.center,
keyboardType: TextInputType.number,
maxLength: 1,
decoration: const InputDecoration(counterText: '', border: OutlineInputBorder()),
onChanged: (value) {
if (value.isNotEmpty && index < 3) {
focusNodes[index + 1].requestFocus();
}
if (value.isEmpty && index > 0) {
focusNodes[index - 1].requestFocus();
}
// Проверяем, заполнен ли весь код
final code = controllers.map((c) => c.text).join();
if (code.length == 4) {
// Небольшая задержка перед отправкой, чтобы пользователь увидел последний символ
Future.delayed(const Duration(milliseconds: 100), () async {
if (dialogContext.mounted && context.mounted) {
await _handleVerificationConfirm(context, dialogContext, verificationType, code, controllers, focusNodes);
}
});
}
},
),
);
}),
),
],
),
),
actions: [
TextButton(
onPressed: () {
for (var controller in controllers) {
controller.dispose();
}
for (var node in focusNodes) {
node.dispose();
}
Navigator.of(dialogContext).pop();
},
child: const Text('Отмена'),
),
],
),
);
}
Future<void> _handleVerificationConfirm(
BuildContext widgetContext,
BuildContext dialogContext,
String verificationType,
String code,
List<TextEditingController> controllers,
List<FocusNode> focusNodes,
) async {
try {
// Используем service locator для доступа к OAuthCubit, так как контекст из диалога может не иметь доступа к провайдеру
final oauthCubit = CubitRegistrations.sl<OAuthCubit>();
final authResult = widget.authResult;
if (authResult == null) {
showErrorToast('Ошибка: данные авторизации не найдены');
return;
}
// Всегда передаем и email и phone (полный набор данных от провайдера)
// Используем данные из формы, если они есть, иначе из authResult
final String email = emailController.text.isNotEmpty ? emailController.text : (authResult.email ?? '');
final String phone = phoneController.text.isNotEmpty ? phoneController.text.replaceAll(RegExp(r'[^\d+]'), '') : (authResult.phone ?? '');
// Проверяем, что данные есть
if (email.isEmpty && phone.isEmpty) {
log('OAuth Verification Confirm: Both email and phone are empty');
showErrorToast('Ошибка: не указан email или телефон');
return;
}
// Проверяем, что все необходимые данные от провайдера есть
if (authResult.accessToken.isEmpty || authResult.clientId.isEmpty || authResult.userId.isEmpty) {
log('OAuth Verification Confirm: Missing required OAuth data');
showErrorToast('Ошибка: неполные данные авторизации');
return;
}
// Проверяем, что код не пустой
if (code.isEmpty || code.length != 4) {
showErrorToast('Введите полный код');
return;
}
// Получаем данные профиля из формы
final String firstName = firstNameController.text.isNotEmpty ? firstNameController.text : (authResult.firstName ?? '');
final String lastName = lastNameController.text.isNotEmpty ? lastNameController.text : (authResult.lastName ?? '');
final String dateOfBirth = dateOfBirthController.text.isNotEmpty ? dateOfBirthController.text : (authResult.birthDate ?? '');
final String gender = isMaleSelected ? 'male' : 'female';
final int weight = weightController.text.isNotEmpty ? (int.tryParse(weightController.text) ?? 0) : 0;
final int height = heightController.text.isNotEmpty ? (int.tryParse(heightController.text) ?? 0) : 0;
// Проверяем, что все обязательные поля заполнены
if (firstName.isEmpty || lastName.isEmpty || dateOfBirth.isEmpty || weight == 0 || height == 0) {
log('OAuth Verification Confirm: Missing required profile data');
showErrorToast('Ошибка: заполните все обязательные поля');
return;
}
// Логируем перед отправкой запроса
log('OAuth Verification Confirm: provider=${widget.provider}, verificationType=$verificationType, code=$code, email=$email, phone=$phone');
// Отправляем запрос на подтверждение с полным набором данных от провайдера
await oauthCubit.confirmOAuthVerification(
provider: widget.provider ?? 'social',
accessToken: authResult.accessToken,
refreshToken: authResult.refreshToken ?? '',
clientId: authResult.clientId,
id: authResult.userId,
email: email,
phone: phone,
code: code,
verificationType: verificationType,
firstName: firstName,
lastName: lastName,
dateOfBirth: dateOfBirth,
gender: gender,
weight: weight,
height: height,
);
// Закрываем диалог
for (var controller in controllers) {
controller.dispose();
}
for (var node in focusNodes) {
node.dispose();
}
if (dialogContext.mounted) {
Navigator.of(dialogContext).pop();
}
// Перезагружаем веб-сокет и переходим на главный экран
if (widgetContext.mounted) {
_reconnectWebSocket(widgetContext);
}
} catch (e, stackTrace) {
log('OAuth Verification Confirm Error: $e', error: e, stackTrace: stackTrace);
if (widgetContext.mounted) {
showErrorToast('Неверный код. Попробуйте снова.');
}
// Очищаем поля
for (var controller in controllers) {
controller.clear();
}
if (focusNodes.isNotEmpty) {
focusNodes[0].requestFocus();
}
}
}
}