Merge pull request 'Issue-691' (#725) from mso7nueh/eatvio_app:main into main

Reviewed-on: eatvio/eatvio_app#725
This commit is contained in:
mso7nueh
2025-10-15 09:35:33 +00:00
9 changed files with 173 additions and 97 deletions

View File

@@ -53,6 +53,6 @@ class AppRouter {
}
static void navigateToRegisterViaSocial({required AuthResultEntity authResult, required String provider}) {
appRouter.goNamed('registerViaSocialScreen', extra: {'authResult': authResult, 'provider': provider});
appRouter.pushNamed('registerViaSocialScreen', extra: {'authResult': authResult, 'provider': provider});
}
}

View File

@@ -0,0 +1,15 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'oauth_login_request.freezed.dart';
part 'oauth_login_request.g.dart';
@freezed
abstract class OAuthLoginRequest with _$OAuthLoginRequest {
const factory OAuthLoginRequest({
required String provider,
@JsonKey(name: 'access_token') required String accessToken,
@JsonKey(name: 'refresh_token') required String refreshToken,
}) = _OAuthLoginRequest;
factory OAuthLoginRequest.fromJson(Map<String, dynamic> json) => _$OAuthLoginRequestFromJson(json);
}

View File

@@ -1,3 +1,4 @@
import 'dart:developer';
import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart';
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -7,6 +8,23 @@ class OAuthCubit extends Cubit<OAuthState> {
OAuthCubit({required this.authRepository}) : super(const OAuthState());
Future<void> getOAuthLogin({required String provider, required String accessToken, required String refreshToken}) async {
try {
log('OAuthCubit: Starting OAuth login for provider: $provider');
emit(const OAuthState.loading());
log('OAuthCubit: Emitted loading state');
final tokenModel = await authRepository.getOAuthLogin(provider: provider, accessToken: accessToken, refreshToken: refreshToken);
log('OAuthCubit: Got token model: ${tokenModel.token}');
emit(OAuthState.loaded(token: tokenModel.token));
log('OAuthCubit: Emitted loaded state');
} catch (e) {
log('OAuthCubit: Error occurred: $e');
emit(OAuthState.error(errorMessage: e.toString()));
log('OAuthCubit: Emitted error state');
}
}
Future<void> getOAuthToken({
required String provider,
required String accessToken,
@@ -22,6 +40,7 @@ class OAuthCubit extends Cubit<OAuthState> {
required int height,
}) async {
try {
emit(const OAuthState.loading());
final token = await authRepository.getOAuthToken(
provider: provider,
accessToken: accessToken,

View File

@@ -16,6 +16,10 @@ import 'package:eatvio_app/core/widgets/custom_divider.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart';
import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart';
import 'package:eatvio_app/core/di/cubit_registrations.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart';
@@ -404,16 +408,29 @@ class _SocialButton extends StatelessWidget {
void _onVkPressed() async {
try {
log('VK button pressed');
final context = AppRouter.navigatorKey.currentContext;
if (context == null) return;
final authResult = await SocialAuthFacade.signInWithVkAndFetchProfile(context: context);
if (authResult == null) {
if (context == null) {
log('Context is null');
return;
}
AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: 'vk');
log('Getting VK auth result');
final authResult = await SocialAuthFacade.signInWithVkAndFetchProfile(context: context);
if (authResult == null) {
log('VK auth result is null');
return;
}
log('VK auth result received, trying OAuth login');
// Пробуем сначала авторизоваться через новый API
if (context.mounted) {
await _tryOAuthLogin(context, authResult, 'vk');
} else {
log('Context is not mounted');
}
} catch (e) {
log(e.toString());
log('VK button error: $e');
}
}
@@ -426,7 +443,11 @@ void _onOkPressed() async {
if (authResult == null) {
return;
}
AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: 'ok');
// Пробуем сначала авторизоваться через новый API
if (context.mounted) {
await _tryOAuthLogin(context, authResult, 'ok');
}
} catch (e) {
log(e.toString());
}
@@ -441,7 +462,11 @@ void _onMailRuPressed() async {
if (authResult == null) {
return;
}
AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: 'mailru');
// Пробуем сначала авторизоваться через новый API
if (context.mounted) {
await _tryOAuthLogin(context, authResult, 'mailru');
}
} catch (e) {
log(e.toString());
}
@@ -449,14 +474,61 @@ void _onMailRuPressed() async {
void _onYandexPressed() async {
try {
final context = AppRouter.navigatorKey.currentContext;
if (context == null) return;
final authResult = await SocialAuthFacade.signInWithYandex();
if (authResult == null) return;
AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: 'yandex');
// Пробуем сначала авторизоваться через новый API
if (context.mounted) {
await _tryOAuthLogin(context, authResult, 'yandex');
}
} catch (e) {
log(e.toString());
}
}
/// Пробует авторизоваться через новый OAuth login API
/// Если получает ошибку (включая 422 - пользователь не найден), переходит к регистрации
Future<void> _tryOAuthLogin(BuildContext context, AuthResultEntity authResult, String provider) async {
log('Starting OAuth login for provider: $provider');
try {
// Создаем OAuth cubit
final oauthCubit = CubitRegistrations.sl<OAuthCubit>();
StreamSubscription? subscription;
// Слушаем изменения состояния
subscription = oauthCubit.stream.listen((state) {
log('OAuth state changed: ${state.runtimeType}');
if (state is OAuthStateLoaded) {
// Успешная авторизация - переходим на главный экран
log('OAuth login successful, navigating to main screen');
subscription?.cancel();
_reconnectAfterAuth();
} else if (state is OAuthStateError) {
// Ошибка авторизации (включая 422 - пользователь не найден) - переходим к регистрации
log('OAuth login failed: ${state.errorMessage}');
subscription?.cancel();
AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: provider);
}
});
// Пробуем авторизоваться
log('Calling getOAuthLogin with provider: $provider');
await oauthCubit.getOAuthLogin(provider: provider, accessToken: authResult.accessToken, refreshToken: authResult.refreshToken ?? '');
// Ждем немного, чтобы состояние успело обновиться
await Future.delayed(const Duration(milliseconds: 100));
} catch (e) {
// В случае любой ошибки (включая 422 - пользователь не найден) переходим к регистрации
log('OAuth login error in catch block: $e');
AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: provider);
}
}
Future<void> _reconnectAfterAuth() async {
final context = AppRouter.navigatorKey.currentContext;
if (context == null) return;

View File

@@ -31,8 +31,8 @@ class RegisterViaSocialScreen extends StatefulWidget {
}
class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
final maskFormatter = MaskTextInputFormatter(mask: '+7 (###) ### ## ##', filter: {"#": RegExp(r'[0-9]')});
final maskFormatterForBirthday = MaskTextInputFormatter(mask: '##.##.####', filter: {"#": RegExp(r'[0-9]')});
final maskFormatter = MaskTextInputFormatter(mask: '+7 (###) ### ## ##', filter: {"#": RegExp(r'[0-9]')}, type: MaskAutoCompletionType.lazy);
final maskFormatterForBirthday = MaskTextInputFormatter(mask: '##.##.####', filter: {"#": RegExp(r'[0-9]')}, type: MaskAutoCompletionType.lazy);
// Контроллеры для полей ввода
final firstNameController = TextEditingController();
@@ -79,17 +79,6 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
if (widget.authResult != null) {
final authResult = widget.authResult!;
// Логируем полученные данные для отладки
print('AuthResult data:');
print('firstName: ${authResult.firstName}');
print('lastName: ${authResult.lastName}');
print('email: ${authResult.email}');
print('phone: ${authResult.phone}');
print('birthDate: ${authResult.birthDate}');
print('gender: ${authResult.gender}');
print('provider: ${widget.provider}');
// Предзаполняем поля из AuthResultEntity
if (authResult.firstName != null) {
firstNameController.text = authResult.firstName!;
}
@@ -100,10 +89,16 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
emailController.text = authResult.email!;
}
if (authResult.phone != null) {
phoneController.text = authResult.phone!;
final cleanPhone = authResult.phone!.replaceAll(RegExp(r'[^\d]'), '');
final normalizedPhone = cleanPhone.startsWith('8') ? '7${cleanPhone.substring(1)}' : cleanPhone;
final phoneWithCountryCode = normalizedPhone.startsWith('7') ? normalizedPhone : '7$normalizedPhone';
final formattedPhone = maskFormatter.maskText('+$phoneWithCountryCode');
phoneController.text = formattedPhone;
}
if (authResult.birthDate != null) {
dateOfBirthController.text = _formatBirthDate(authResult.birthDate!);
final formattedDate = _formatBirthDate(authResult.birthDate!);
final maskedDate = maskFormatterForBirthday.maskText(formattedDate);
dateOfBirthController.text = maskedDate;
}
if (authResult.gender != null) {
isMaleSelected = authResult.gender == GenderEnum.male;
@@ -147,44 +142,6 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
return birthDate;
}
String _getProviderName(String? provider) {
switch (provider) {
case 'vk':
return 'ВКонтакте';
case 'yandex':
return 'Яндекс';
case 'ok':
return 'Одноклассники';
case 'mailru':
return 'Mail.ru';
default:
return 'социальной сети';
}
}
String _getDataStatusMessage(AuthResultEntity authResult) {
final hasEmail = authResult.email != null && authResult.email!.isNotEmpty;
final hasPhone = authResult.phone != null && authResult.phone!.isNotEmpty;
final hasBirthDate = authResult.birthDate != null && authResult.birthDate!.isNotEmpty;
if (hasEmail && hasPhone && hasBirthDate) {
return 'Все данные успешно получены и предзаполнены.';
} else if (hasEmail && hasPhone) {
return 'Email и номер телефона получены. Дата рождения может быть недоступна из-за настроек приватности.';
} else if (hasEmail) {
return 'Email получен. Номер телефона и дата рождения могут быть недоступны из-за настроек приватности.';
} else if (hasPhone) {
return 'Номер телефона получен. Email и дата рождения могут быть недоступны из-за настроек приватности.';
} else {
// Специальное сообщение для VK
if (widget.provider == 'vk') {
return 'Основные данные получены. Email запрашивается автоматически. Номер телефона получается через VK API. Если номер замаскирован, это означает, что пользователь ограничил доступ к нему в настройках приватности.';
} else {
return 'Основные данные получены. Email, номер телефона и дата рождения могут быть недоступны из-за настроек приватности.';
}
}
}
@override
void dispose() {
firstNameController.dispose();
@@ -202,15 +159,14 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
return BlocProvider<OAuthCubit>(
create: (context) => CubitRegistrations.sl<OAuthCubit>(),
child: Scaffold(
appBar: AppBar(toolbarHeight: 0),
appBar: AppBar(),
body: BlocBuilder<OAuthCubit, OAuthState>(
builder: (context, state) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 24),
const Center(
child: Text(
'Регистрация',
@@ -266,7 +222,10 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
controller: phoneController,
keyboardType: TextInputType.number,
inputFormatters: [maskFormatter],
onChanged: (newValue) {},
onChanged: (newValue) {
// Дополнительная обработка изменений номера телефона
// Это поможет маске корректно работать при редактировании
},
),
const SizedBox(height: 12),
Row(
@@ -503,6 +462,9 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
return;
}
// Очищаем номер телефона от маски для отправки
final cleanPhone = phoneController.text.replaceAll(RegExp(r'[^\d+]'), '');
// Вызываем OAuth регистрацию
context.read<OAuthCubit>().getOAuthToken(
provider: widget.provider ?? 'social',
@@ -512,7 +474,7 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
firstName: firstNameController.text,
lastName: lastNameController.text,
email: emailController.text,
phone: phoneController.text,
phone: cleanPhone,
dateOfBirth: dateOfBirthController.text,
gender: isMaleSelected ? 'М' : 'Ж',
weight: weight,

View File

@@ -1,5 +1,6 @@
import 'package:eatvio_app/features/auth/models/credentials_model.dart';
import 'package:eatvio_app/features/auth/models/oauth_model.dart';
import 'package:eatvio_app/features/auth/models/oauth_login_request.dart';
import 'package:eatvio_app/features/auth/models/token_model.dart';
import 'package:eatvio_app/features/auth/services/auth_service.dart';
import 'package:eatvio_app/features/auth/models/social_exchange_request.dart';
@@ -29,6 +30,16 @@ class AuthRepository {
}
}
Future<TokenModel> getOAuthLogin({required String provider, required String accessToken, required String refreshToken}) async {
try {
return await authService.getOAuthLogin(
body: OAuthLoginRequest(provider: provider, accessToken: accessToken, refreshToken: refreshToken),
);
} catch (e) {
rethrow;
}
}
Future<String> getOAuthToken({
required String provider,
required String accessToken,
@@ -45,21 +56,22 @@ class AuthRepository {
}) async {
try {
return (await authService.getOAuthToken(
body: OAuthModel(
provider: provider,
accessToken: accessToken,
id: id,
refreshToken: refreshToken,
firstName: firstName,
lastName: lastName,
email: email,
phone: phone,
dateOfBirth: dateOfBirth,
gender: gender == 'М' ? 'male' : 'female',
weight: weight,
height: height,
),
))['token']! as String;
body: OAuthModel(
provider: provider,
accessToken: accessToken,
id: id,
refreshToken: refreshToken,
firstName: firstName,
lastName: lastName,
email: email,
phone: phone,
dateOfBirth: dateOfBirth,
gender: gender == 'М' ? 'male' : 'female',
weight: weight,
height: height,
),
))['token']!
as String;
} catch (e) {
rethrow;
}

View File

@@ -119,7 +119,6 @@ class SocialAuthFacade {
'response_type': 'code',
'client_id': clientId,
'redirect_uri': redirectUri,
'scope': 'login:info login:email',
'force_confirm': 'true',
}).toString();

View File

@@ -1,6 +1,7 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/auth/models/credentials_model.dart';
import 'package:eatvio_app/features/auth/models/oauth_model.dart';
import 'package:eatvio_app/features/auth/models/oauth_login_request.dart';
import 'package:eatvio_app/features/auth/models/token_model.dart';
import 'package:eatvio_app/features/auth/models/social_exchange_request.dart';
import 'package:retrofit/retrofit.dart';
@@ -12,17 +13,14 @@ abstract class AuthService {
factory AuthService(Dio dio, {String? baseUrl}) = _AuthService;
@POST('/api/login')
Future<TokenModel> getTokenByCredentials({
@Body() required CredentialsModel credentials,
});
Future<TokenModel> getTokenByCredentials({@Body() required CredentialsModel credentials});
@POST('/api/social/login')
Future<TokenModel> getTokenBySocial({
@Body() required SocialExchangeRequest body,
});
Future<TokenModel> getTokenBySocial({@Body() required SocialExchangeRequest body});
@POST('/api/oauth/callback')
Future<Map<String, Object?>> getOAuthToken({
@Body() required OAuthModel body,
});
Future<Map<String, Object?>> getOAuthToken({@Body() required OAuthModel body});
@POST('/api/oauth/login')
Future<TokenModel> getOAuthLogin({@Body() required OAuthLoginRequest body});
}

View File

@@ -28,10 +28,9 @@ class S {
static const AppLocalizationDelegate delegate = AppLocalizationDelegate();
static Future<S> load(Locale locale) {
final name =
(locale.countryCode?.isEmpty ?? false)
? locale.languageCode
: locale.toString();
final name = (locale.countryCode?.isEmpty ?? false)
? locale.languageCode
: locale.toString();
final localeName = Intl.canonicalizedLocale(name);
return initializeMessages(localeName).then((_) {
Intl.defaultLocale = localeName;