From fc09fa36b8ae48c78e1d88c7e39240040b797036 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 24 Sep 2025 23:47:11 +0300 Subject: [PATCH] feat(auth): Issue-691 --- android/app/build.gradle | 3 + .../auth/presentation/pages/auth_screen.dart | 94 +++++------ lib/features/auth/sdk/social_auth_facade.dart | 152 ++++++++++++++++-- pubspec.yaml | 2 + 4 files changed, 194 insertions(+), 57 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 9ffa87c4..457960f7 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -13,6 +13,8 @@ if (keystorePropertiesFile.exists()) { keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) } +def yandexClientId = project.hasProperty('YANDEX_CLIENT_ID') ? project.property('YANDEX_CLIENT_ID') : (System.getenv('YANDEX_CLIENT_ID') ?: "") + android { namespace = "com.eatvioapp.app" compileSdk = flutter.compileSdkVersion @@ -33,6 +35,7 @@ android { targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName + manifestPlaceholders = [YANDEX_CLIENT_ID: yandexClientId] } signingConfigs { diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index ea457d6c..f9eeddf5 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -169,51 +169,51 @@ class _AuthScreenState extends State { ], ), // const SizedBox(height: 16), - // Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // _SocialButton( - // onTap: _onVkPressed, - // backgroundColor: const Color(0xFF2787F5), - // tooltip: 'VK ID', - // child: SvgPicture.asset( - // 'assets/icons/vk.svg', - // height: 24, - // width: 24, - // colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), - // ), - // ), - // _SocialButton( - // onTap: _onOkPressed, - // backgroundColor: const Color(0xFFF58220), - // tooltip: 'Одноклассники', - // child: SvgPicture.asset( - // 'assets/icons/ok.svg', - // height: 24, - // width: 24, - // colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), - // ), - // ), - // const _SocialButton( - // onTap: _onMailRuPressed, - // backgroundColor: Color(0xFF005FF9), - // tooltip: 'Mail.ru', - // child: Text( - // 'Mail', - // style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700), - // ), - // ), - // const _SocialButton( - // onTap: _onYandexPressed, - // backgroundColor: Color(0xFFFFCC00), - // tooltip: 'Яндекс ID', - // child: Text( - // 'Yandex', - // style: TextStyle(color: Colors.black, fontWeight: FontWeight.w700), - // ), - // ), - // ], - // ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _SocialButton( + onTap: () => _onVkPressed(context), + backgroundColor: const Color(0xFF2787F5), + tooltip: 'VK ID', + child: SvgPicture.asset( + 'assets/icons/vk.svg', + height: 24, + width: 24, + colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), + ), + ), + _SocialButton( + onTap: _onOkPressed, + backgroundColor: const Color(0xFFF58220), + tooltip: 'Одноклассники', + child: SvgPicture.asset( + 'assets/icons/ok.svg', + height: 24, + width: 24, + colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), + ), + ), + const _SocialButton( + onTap: _onMailRuPressed, + backgroundColor: Color(0xFF005FF9), + tooltip: 'Mail.ru', + child: Text( + 'Mail', + style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700), + ), + ), + const _SocialButton( + onTap: _onYandexPressed, + backgroundColor: Color(0xFFFFCC00), + tooltip: 'Яндекс ID', + child: Text( + 'Yandex', + style: TextStyle(color: Colors.black, fontWeight: FontWeight.w700), + ), + ), + ], + ), const Spacer(), Center( child: RichText( @@ -395,8 +395,8 @@ class _SocialButton extends StatelessWidget { } // ignore: unused_element -void _onVkPressed() async { - final accessToken = await SocialAuthFacade.signInWithVk(); +void _onVkPressed(BuildContext context) async { + final accessToken = await SocialAuthFacade.signInWithVk(context: context); if (accessToken == null) return; final isAuthenticated = await sl().getTokenBySocial(provider: 'vk', accessToken: accessToken); if (isAuthenticated) { diff --git a/lib/features/auth/sdk/social_auth_facade.dart b/lib/features/auth/sdk/social_auth_facade.dart index 62985d5d..e5761498 100644 --- a/lib/features/auth/sdk/social_auth_facade.dart +++ b/lib/features/auth/sdk/social_auth_facade.dart @@ -1,25 +1,157 @@ import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_login_yandex/flutter_login_yandex.dart'; +import 'package:eatvio_app/core/app_router.dart'; +import 'package:vk_id/vk_id.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +class VkAuthResult { + VkAuthResult({required this.accessToken, this.refreshToken, this.userId, this.firstName, this.lastName, this.email, this.phone, this.gender, this.birthDate}); + + final String accessToken; + final String? refreshToken; + final String? userId; + final String? firstName; + final String? lastName; + final String? email; + final String? phone; + final String? gender; + final String? birthDate; +} class SocialAuthFacade { - static Future signInWithVk() async { - // TODO: Integrate official VK ID SDK and return access token - return null; + static Future signInWithVk({required BuildContext context}) async { + final result = await signInWithVkAndFetchProfile(context: context); + return result?.accessToken; + } + + static Future signInWithVkAndFetchProfile({required BuildContext context}) async { + final clientIdStr = dotenv.maybeGet('VK_CLIENT_ID')?.trim() ?? Platform.environment['VK_CLIENT_ID']; + if (clientIdStr == null || clientIdStr.isEmpty) { + return null; + } + + try { + dynamic controller = VkIDController(clID: int.parse(clientIdStr)); + final authUri = controller.generateAuthorizeLinkWithCodeVerifier(); + final redirectScheme = 'vk$clientIdStr://'; + final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUri.toString(), redirectSchemePrefix: redirectScheme); + if (redirectParams == null) return null; + final code = redirectParams['code']; + final deviceId = redirectParams['device_id']; + if (code == null || deviceId == null) return null; + + final tokenResponse = await controller.exchangeAuthorizationCodeForToken(code: code, deviceId: deviceId); + final accessToken = _extractAccessToken(tokenResponse); + if (accessToken == null) return null; + return VkAuthResult(accessToken: accessToken); + } catch (_) { + return null; + } } static Future signInWithOk() async { - // TODO: Integrate official OK SDK and return access token - return null; + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return null; + return _signInWithVkIdProvider(context: context, provider: 'ok'); } static Future signInWithMailRu() async { - // TODO: Integrate official Mail.ru SDK and return access token - return null; + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return null; + return _signInWithVkIdProvider(context: context, provider: 'mail'); } static Future signInWithYandex() async { - // TODO: Integrate official Yandex ID SDK and return access token + final clientId = dotenv.maybeGet('YANDEX_CLIENT_ID')?.trim() ?? Platform.environment['YANDEX_CLIENT_ID']; + final redirectUri = dotenv.maybeGet('YANDEX_REDIRECT_URI')?.trim() ?? Platform.environment['YANDEX_REDIRECT_URI']; + + if (clientId == null || clientId.isEmpty || redirectUri == null || redirectUri.isEmpty) { + return null; + } + try { + final plugin = FlutterLoginYandex(); + final response = await plugin.signIn(); + final token = (response is Map) ? response['token'] as String? : null; + return (token != null && token.isNotEmpty) ? token : null; + } catch (_) { + return null; + } + } + + static Future _signInWithVkIdProvider({required BuildContext context, required String provider}) async { + final clientIdStr = dotenv.maybeGet('VK_CLIENT_ID')?.trim() ?? Platform.environment['VK_CLIENT_ID']; + if (clientIdStr == null || clientIdStr.isEmpty) { + return null; + } + try { + dynamic controller = VkIDController(clID: int.parse(clientIdStr)); + final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider); + final redirectScheme = 'vk$clientIdStr://'; + final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUri.toString(), redirectSchemePrefix: redirectScheme); + if (redirectParams == null) return null; + final code = redirectParams['code']; + final deviceId = redirectParams['device_id']; + if (code == null || deviceId == null) return null; + final tokenResponse = await controller.exchangeAuthorizationCodeForToken(code: code, deviceId: deviceId, provider: provider); + final accessToken = _extractAccessToken(tokenResponse); + return accessToken; + } catch (_) { + return null; + } + } + + static String? _extractAccessToken(dynamic tokenResponse) { + if (tokenResponse == null) return null; + if (tokenResponse is Map) { + return (tokenResponse['access_token'] as String?) ?? (tokenResponse['token'] as String?); + } + try { + final token = (tokenResponse as dynamic).accessToken as String?; + if (token != null && token.isNotEmpty) return token; + } catch (_) {} + try { + final token = (tokenResponse as dynamic).token as String?; + if (token != null && token.isNotEmpty) return token; + } catch (_) {} return null; } + + static Future?> _presentOAuthWebView({ + required BuildContext context, + required String initialUrl, + required String redirectSchemePrefix, + }) async { + final controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate( + NavigationDelegate( + onNavigationRequest: (request) { + final url = request.url; + if (url.startsWith(redirectSchemePrefix)) { + final uri = Uri.parse(url); + final params = {}..addAll(uri.queryParameters); + Navigator.of(context, rootNavigator: true).pop(params); + return NavigationDecision.prevent; + } + return NavigationDecision.navigate; + }, + ), + ) + ..loadRequest(Uri.parse(initialUrl)); + + return showDialog>( + context: context, + barrierDismissible: false, + builder: (_) { + return Dialog( + insetPadding: const EdgeInsets.all(16), + child: SizedBox(height: 560, child: WebViewWidget(controller: controller)), + ); + }, + ); + } } - - diff --git a/pubspec.yaml b/pubspec.yaml index 3f1e036e..a62a13af 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -75,6 +75,8 @@ dependencies: uuid: ^4.5.1 appmetrica_plugin: ^3.3.0 huawei_push: ^6.11.0+300 + vk_id: ^1.0.3 + flutter_login_yandex: ^0.0.5 dev_dependencies: build_runner: ^2.7.0