From 5354c3f517c21f3af679074f8d9e28b7bfa67e66 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Tue, 14 Oct 2025 12:16:20 +0300 Subject: [PATCH 1/2] feat(auth): Issue-691 --- lib/core/app_router.dart | 1 + .../auth/models/oauth_login_request.dart | 15 ++++ .../bloc/oauth_cubit/oauth_cubit.dart | 19 ++++ .../auth/presentation/pages/auth_screen.dart | 90 +++++++++++++++++-- .../pages/register_via_social_screen.dart | 24 +++-- .../auth/repositories/auth_repository.dart | 42 +++++---- lib/features/auth/services/auth_service.dart | 16 ++-- 7 files changed, 168 insertions(+), 39 deletions(-) create mode 100644 lib/features/auth/models/oauth_login_request.dart diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 26355055..5ce04746 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -53,6 +53,7 @@ class AppRouter { } static void navigateToRegisterViaSocial({required AuthResultEntity authResult, required String provider}) { + print('AppRouter: Navigating to register via social screen for provider: $provider'); appRouter.goNamed('registerViaSocialScreen', extra: {'authResult': authResult, 'provider': provider}); } } diff --git a/lib/features/auth/models/oauth_login_request.dart b/lib/features/auth/models/oauth_login_request.dart new file mode 100644 index 00000000..43da454b --- /dev/null +++ b/lib/features/auth/models/oauth_login_request.dart @@ -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 json) => _$OAuthLoginRequestFromJson(json); +} diff --git a/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart index 8d73b195..7d45b4b1 100644 --- a/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart +++ b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart @@ -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 { OAuthCubit({required this.authRepository}) : super(const OAuthState()); + Future 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 getOAuthToken({ required String provider, required String accessToken, @@ -22,6 +40,7 @@ class OAuthCubit extends Cubit { required int height, }) async { try { + emit(const OAuthState.loading()); final token = await authRepository.getOAuthToken( provider: provider, accessToken: accessToken, diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 13713922..fbdbd809 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -14,6 +14,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'; @@ -391,16 +395,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'); } } @@ -413,7 +430,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()); } @@ -428,7 +449,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()); } @@ -436,14 +461,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 _tryOAuthLogin(BuildContext context, AuthResultEntity authResult, String provider) async { + log('Starting OAuth login for provider: $provider'); + + try { + // Создаем OAuth cubit + final oauthCubit = CubitRegistrations.sl(); + 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 _reconnectAfterAuth() async { final context = AppRouter.navigatorKey.currentContext; if (context == null) return; diff --git a/lib/features/auth/presentation/pages/register_via_social_screen.dart b/lib/features/auth/presentation/pages/register_via_social_screen.dart index 22fa5d18..a0175726 100644 --- a/lib/features/auth/presentation/pages/register_via_social_screen.dart +++ b/lib/features/auth/presentation/pages/register_via_social_screen.dart @@ -30,8 +30,8 @@ class RegisterViaSocialScreen extends StatefulWidget { } class _RegisterViaSocialScreenState extends State { - 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(); @@ -99,10 +99,16 @@ class _RegisterViaSocialScreenState extends State { 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; @@ -265,7 +271,10 @@ class _RegisterViaSocialScreenState extends State { controller: phoneController, keyboardType: TextInputType.number, inputFormatters: [maskFormatter], - onChanged: (newValue) {}, + onChanged: (newValue) { + // Дополнительная обработка изменений номера телефона + // Это поможет маске корректно работать при редактировании + }, ), const SizedBox(height: 12), Row( @@ -502,6 +511,9 @@ class _RegisterViaSocialScreenState extends State { return; } + // Очищаем номер телефона от маски для отправки + final cleanPhone = phoneController.text.replaceAll(RegExp(r'[^\d+]'), ''); + // Вызываем OAuth регистрацию context.read().getOAuthToken( provider: widget.provider ?? 'social', @@ -511,7 +523,7 @@ class _RegisterViaSocialScreenState extends State { firstName: firstNameController.text, lastName: lastNameController.text, email: emailController.text, - phone: phoneController.text, + phone: cleanPhone, dateOfBirth: dateOfBirthController.text, gender: isMaleSelected ? 'М' : 'Ж', weight: weight, diff --git a/lib/features/auth/repositories/auth_repository.dart b/lib/features/auth/repositories/auth_repository.dart index bddebce1..1f66c039 100644 --- a/lib/features/auth/repositories/auth_repository.dart +++ b/lib/features/auth/repositories/auth_repository.dart @@ -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 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 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; } diff --git a/lib/features/auth/services/auth_service.dart b/lib/features/auth/services/auth_service.dart index 3d8d43e2..02f8722a 100644 --- a/lib/features/auth/services/auth_service.dart +++ b/lib/features/auth/services/auth_service.dart @@ -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 getTokenByCredentials({ - @Body() required CredentialsModel credentials, - }); + Future getTokenByCredentials({@Body() required CredentialsModel credentials}); @POST('/api/social/login') - Future getTokenBySocial({ - @Body() required SocialExchangeRequest body, - }); + Future getTokenBySocial({@Body() required SocialExchangeRequest body}); @POST('/api/oauth/callback') - Future> getOAuthToken({ - @Body() required OAuthModel body, - }); + Future> getOAuthToken({@Body() required OAuthModel body}); + + @POST('/api/oauth/login') + Future getOAuthLogin({@Body() required OAuthLoginRequest body}); } From f573bb28f806b29b2991e21e969296a1153e5104 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Tue, 14 Oct 2025 19:25:26 +0300 Subject: [PATCH 2/2] feat(oauth): Issue-691 --- lib/core/app_router.dart | 3 +- .../pages/register_via_social_screen.dart | 54 +------------------ lib/features/auth/sdk/social_auth_facade.dart | 1 - lib/generated/l10n.dart | 7 ++- 4 files changed, 6 insertions(+), 59 deletions(-) diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 5ce04746..d17019d2 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -53,7 +53,6 @@ class AppRouter { } static void navigateToRegisterViaSocial({required AuthResultEntity authResult, required String provider}) { - print('AppRouter: Navigating to register via social screen for provider: $provider'); - appRouter.goNamed('registerViaSocialScreen', extra: {'authResult': authResult, 'provider': provider}); + appRouter.pushNamed('registerViaSocialScreen', extra: {'authResult': authResult, 'provider': provider}); } } diff --git a/lib/features/auth/presentation/pages/register_via_social_screen.dart b/lib/features/auth/presentation/pages/register_via_social_screen.dart index b93f82d7..c803f687 100644 --- a/lib/features/auth/presentation/pages/register_via_social_screen.dart +++ b/lib/features/auth/presentation/pages/register_via_social_screen.dart @@ -79,17 +79,6 @@ class _RegisterViaSocialScreenState extends State { 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!; } @@ -153,44 +142,6 @@ class _RegisterViaSocialScreenState extends State { 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(); @@ -208,15 +159,14 @@ class _RegisterViaSocialScreenState extends State { return BlocProvider( create: (context) => CubitRegistrations.sl(), child: Scaffold( - appBar: AppBar(toolbarHeight: 0), + appBar: AppBar(), body: BlocBuilder( 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( 'Регистрация', diff --git a/lib/features/auth/sdk/social_auth_facade.dart b/lib/features/auth/sdk/social_auth_facade.dart index 4cab0962..140da37b 100644 --- a/lib/features/auth/sdk/social_auth_facade.dart +++ b/lib/features/auth/sdk/social_auth_facade.dart @@ -119,7 +119,6 @@ class SocialAuthFacade { 'response_type': 'code', 'client_id': clientId, 'redirect_uri': redirectUri, - 'scope': 'login:info login:email', 'force_confirm': 'true', }).toString(); diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 1055e664..0ccdeac3 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -28,10 +28,9 @@ class S { static const AppLocalizationDelegate delegate = AppLocalizationDelegate(); static Future 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;