From 41f77bd373375eb5e6ab1985a3ae7ade26eb2c6e Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Tue, 16 Sep 2025 09:28:17 +0300 Subject: [PATCH] feat(;auth): social --- .../auth/models/social_exchange_request.dart | 11 ++ .../bloc/auth_cubit/auth_cubit.dart | 34 +++-- .../auth/presentation/pages/auth_screen.dart | 130 ++++++++++++++++++ .../auth/repositories/auth_repository.dart | 14 ++ lib/features/auth/sdk/social_auth_facade.dart | 24 ++++ lib/features/auth/services/auth_service.dart | 6 + 6 files changed, 204 insertions(+), 15 deletions(-) create mode 100644 lib/features/auth/models/social_exchange_request.dart create mode 100644 lib/features/auth/sdk/social_auth_facade.dart diff --git a/lib/features/auth/models/social_exchange_request.dart b/lib/features/auth/models/social_exchange_request.dart new file mode 100644 index 00000000..85bf7330 --- /dev/null +++ b/lib/features/auth/models/social_exchange_request.dart @@ -0,0 +1,11 @@ +class SocialExchangeRequest { + final String provider; + final String accessToken; + + SocialExchangeRequest({required this.provider, required this.accessToken}); + + Map toJson() { + return {'provider': provider, 'access_token': accessToken}; + } +} + diff --git a/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart b/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart index 20686cce..7b58a4b0 100644 --- a/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart +++ b/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart @@ -6,28 +6,17 @@ import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state import 'package:eatvio_app/features/auth/repositories/auth_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; - class AuthCubit extends Cubit { final AuthRepository authRepository; - AuthCubit({ - required this.authRepository, - }) : super(const AuthState()); + AuthCubit({required this.authRepository}) : super(const AuthState()); Future getTokenByCredentials(String email, String password) async { emit(const AuthState.loading()); try { - final token = (await authRepository.getTokenByCredentials( - email, - password, - )) - .token; - emit( - AuthState.loaded( - token: token, - ), - ); - await TokenStorage.saveTokenInStorage(token); + final token = (await authRepository.getTokenByCredentials(email, password)).token; + emit(AuthState.loaded(token: token)); + await TokenStorage.saveTokenInStorage(token); return true; } catch (e) { log(e.toString()); @@ -36,4 +25,19 @@ class AuthCubit extends Cubit { return false; } } + + Future getTokenBySocial({required String provider, required String accessToken}) async { + emit(const AuthState.loading()); + try { + final token = (await authRepository.getTokenBySocial(provider: provider, accessToken: accessToken)).token; + emit(AuthState.loaded(token: token)); + await TokenStorage.saveTokenInStorage(token); + return true; + } catch (e) { + log(e.toString()); + showErrorToast('Не удалось войти через $provider'); + emit(AuthState.error(errorMessage: '$e')); + return false; + } + } } diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 73162bd2..61976300 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -24,6 +24,7 @@ import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:eatvio_app/features/auth/sdk/social_auth_facade.dart'; class AuthScreen extends StatefulWidget { const AuthScreen({super.key}); @@ -165,6 +166,52 @@ class _AuthScreenState extends State { ), ], ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _SocialButton( + onTap: _onVkPressed, + child: SvgPicture.asset( + 'assets/icons/vk.svg', + height: 24, + width: 24, + colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), + ), + backgroundColor: const Color(0xFF2787F5), + tooltip: 'VK ID', + ), + _SocialButton( + onTap: _onOkPressed, + child: SvgPicture.asset( + 'assets/icons/ok.svg', + height: 24, + width: 24, + colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), + ), + backgroundColor: const Color(0xFFF58220), + tooltip: 'Одноклассники', + ), + _SocialButton( + onTap: _onMailRuPressed, + child: const Text( + 'Mail', + style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700), + ), + backgroundColor: const Color(0xFF005FF9), + tooltip: 'Mail.ru', + ), + _SocialButton( + onTap: _onYandexPressed, + child: const Text( + 'Yandex', + style: TextStyle(color: Colors.black, fontWeight: FontWeight.w700), + ), + backgroundColor: const Color(0xFFFFCC00), + tooltip: 'Яндекс ID', + ), + ], + ), const Spacer(), Center( child: RichText( @@ -317,3 +364,86 @@ class _AuthScreenState extends State { ); } } + +class _SocialButton extends StatelessWidget { + const _SocialButton({required this.onTap, required this.child, required this.backgroundColor, required this.tooltip}); + + final VoidCallback onTap; + final Widget child; + final Color backgroundColor; + final String tooltip; + + @override + Widget build(BuildContext context) { + return Tooltip( + message: tooltip, + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: onTap, + child: Ink( + height: 48, + width: (MediaQuery.of(context).size.width - 16 * 2 - 12 * 3) / 4, + decoration: BoxDecoration(color: backgroundColor, borderRadius: BorderRadius.circular(12)), + child: Center(child: child), + ), + ), + ); + } +} + +void _onVkPressed() async { + final accessToken = await SocialAuthFacade.signInWithVk(); + if (accessToken == null) return; + final isAuthenticated = await sl().getTokenBySocial(provider: 'vk', accessToken: accessToken); + if (isAuthenticated) { + if (AppRouter.navigatorKey.currentContext != null) { + // Reuse success flow + // ignore: use_build_context_synchronously + await _reconnectAfterAuth(); + } + } +} + +void _onOkPressed() async { + final accessToken = await SocialAuthFacade.signInWithOk(); + if (accessToken == null) return; + final isAuthenticated = await sl().getTokenBySocial(provider: 'ok', accessToken: accessToken); + if (isAuthenticated) { + await _reconnectAfterAuth(); + } +} + +void _onMailRuPressed() async { + final accessToken = await SocialAuthFacade.signInWithMailRu(); + if (accessToken == null) return; + final isAuthenticated = await sl().getTokenBySocial(provider: 'mailru', accessToken: accessToken); + if (isAuthenticated) { + await _reconnectAfterAuth(); + } +} + +void _onYandexPressed() async { + final accessToken = await SocialAuthFacade.signInWithYandex(); + if (accessToken == null) return; + final isAuthenticated = await sl().getTokenBySocial(provider: 'yandex', accessToken: accessToken); + if (isAuthenticated) { + await _reconnectAfterAuth(); + } +} + +Future _reconnectAfterAuth() async { + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return; + try { + await sl().clear(); + await sl().initialize(); + await sl().getMyProfile(); + await sl().getChatRoomsWithLoading(); + await sl().connectToWebSocket(); + await sl().getNotifications(); + await sl().connectToWebSocket(); + await sl().registerPushToken(); + showSuccessToast('Успешный вход'); + } catch (_) {} + AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false}); +} diff --git a/lib/features/auth/repositories/auth_repository.dart b/lib/features/auth/repositories/auth_repository.dart index d0bfcce5..84df92f6 100644 --- a/lib/features/auth/repositories/auth_repository.dart +++ b/lib/features/auth/repositories/auth_repository.dart @@ -1,6 +1,7 @@ import 'package:eatvio_app/features/auth/models/credentials_model.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'; class AuthRepository { final AuthService authService; @@ -22,4 +23,17 @@ class AuthRepository { rethrow; } } + + Future getTokenBySocial({ + required String provider, + required String accessToken, + }) async { + try { + return await authService.getTokenBySocial( + body: SocialExchangeRequest(provider: provider, accessToken: accessToken), + ); + } catch (e) { + rethrow; + } + } } diff --git a/lib/features/auth/sdk/social_auth_facade.dart b/lib/features/auth/sdk/social_auth_facade.dart new file mode 100644 index 00000000..975f4196 --- /dev/null +++ b/lib/features/auth/sdk/social_auth_facade.dart @@ -0,0 +1,24 @@ +import 'dart:async'; + +class SocialAuthFacade { + static Future signInWithVk() async { + // TODO: Integrate official VK ID SDK and return access token + return null; + } + + static Future signInWithOk() async { + // TODO: Integrate official OK SDK and return access token + return null; + } + + static Future signInWithMailRu() async { + // TODO: Integrate official Mail.ru SDK and return access token + return null; + } + + static Future signInWithYandex() async { + // TODO: Integrate official Yandex ID SDK and return access token + return null; + } +} + diff --git a/lib/features/auth/services/auth_service.dart b/lib/features/auth/services/auth_service.dart index 62256f8b..7dabb65b 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/token_model.dart'; +import 'package:eatvio_app/features/auth/models/social_exchange_request.dart'; import 'package:retrofit/retrofit.dart'; part 'auth_service.g.dart'; @@ -13,4 +14,9 @@ abstract class AuthService { Future getTokenByCredentials({ @Body() required CredentialsModel credentials, }); + + @POST('/api/social/login') + Future getTokenBySocial({ + @Body() required SocialExchangeRequest body, + }); }