forked from eatvio/eatvio_app
feat(auth): implement OAuth verification request and confirmation flow with error handling and UI updates
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user