import 'dart:async'; import 'dart:convert'; import 'dart:developer'; import 'dart:io'; import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart'; import 'package:eatvio_app/features/auth/models/yandex_profile_model.dart'; import 'package:flutter/material.dart'; import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/env_config.dart'; import 'package:http/http.dart' as http; import 'package:vk_id/vk_id.dart'; import 'package:webview_flutter/webview_flutter.dart'; class SocialAuthFacade { static Future signInWithVk() async { final context = AppRouter.navigatorKey.currentContext; if (context == null) return null; final result = await signInWithVkAndFetchProfile(context: context); return result?.accessToken; } static Future signInWithVkAndFetchProfile({required BuildContext context}) async { final clientIdStr = Platform.isIOS ? EnvConfig.vkClientIdIos.trim() : EnvConfig.vkClientIdAndroid.trim(); if (clientIdStr.isEmpty) { return null; } try { final controller = VkIDController(clID: int.parse(clientIdStr)); final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: VkProvider.vkID, scopes: [VkScope.email, VkScope.phone]); final authUrl = authUri.value.toString(); final codeVerifier = authUri.key; final redirectScheme = 'vk$clientIdStr://'; final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUrl, redirectSchemePrefix: redirectScheme); if (redirectParams == null) { return null; } return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier); } catch (e) { return null; } } static Future _parseVKAuthResult({ required Map redirectParams, required VkIDController controller, required String codeVerifier, }) async { final code = redirectParams['code']; final deviceId = redirectParams['device_id']; final state = redirectParams['state']; if (code == null || deviceId == null || state == null) { return null; } final authRes = await controller.retrieveOAuthToken(authorizationCode: code, deviceId: deviceId, codeVerifier: codeVerifier, state: state); final auth = authRes.result; if (auth == null) { var msg = "Retrieve OAuth token error"; final err = authRes.error?.vkErr; if (err != null) { msg += ": ${err.error} - ${err.description}"; } log(msg); return null; } final profileRes = await controller.getProfileInfo(); final profile = profileRes.result; if (profile == null) { var msg = "Profile info load error"; final err = profileRes.error?.vkErr; if (err != null) { msg += ": ${err.error} - ${err.description}"; } log(msg); return null; } return AuthResultEntity( accessToken: auth.accessToken, refreshToken: auth.refreshToken, userId: profile.userId.toString(), firstName: profile.firstName, lastName: profile.lastName, email: profile.email, phone: profile.phone, gender: GenderEnum.fromVkGender(profile.parsedSex), birthDate: profile.bDayDt?.toString(), ); } static Future signInWithOkAndFetchProfile() async { final context = AppRouter.navigatorKey.currentContext; if (context == null) return null; return await _signInWithVkIdProvider(context: context, provider: VkProvider.okRu); } static Future signInWithMailRuAndFetchProfile() async { final context = AppRouter.navigatorKey.currentContext; if (context == null) return null; return await _signInWithVkIdProvider(context: context, provider: VkProvider.mailRu); } static Future signInWithYandex() async { final clientId = EnvConfig.yandexClientId.trim(); final redirectUri = EnvConfig.yandexRedirectUri.trim(); if (clientId.isEmpty || redirectUri.isEmpty) { return null; } try { final context = AppRouter.navigatorKey.currentContext; if (context == null) return null; final authorizeUrl = Uri.https('oauth.yandex.ru', '/authorize', { 'response_type': 'code', 'client_id': clientId, 'redirect_uri': redirectUri, 'scope': 'login:info login:email', 'force_confirm': 'true', }).toString(); final redirectSchemePrefix = redirectUri; final params = await _presentOAuthWebView(context: context, initialUrl: authorizeUrl, redirectSchemePrefix: redirectSchemePrefix); if (params == null) return null; final code = params['code']; if (code == null || code.isEmpty) { return null; } final tokens = await _getTokensFomYandex(code: code, clientId: clientId, clientSecret: EnvConfig.yandexClientSecret); if (tokens == null) { return null; } final response = await http.get(Uri.parse('https://login.yandex.ru/info?format=json'), headers: {'Authorization': 'OAuth ${tokens.accessToken}'}); final Map json = jsonDecode(response.body); final yandexProfileData = YandexProfileModel.fromJson(json); return AuthResultEntity( accessToken: tokens.accessToken, refreshToken: tokens.refreshToken, userId: yandexProfileData.id, firstName: yandexProfileData.firstName, lastName: yandexProfileData.lastName, email: yandexProfileData.defaultEmail, phone: yandexProfileData.defaultPhone.number, gender: GenderEnum.fromString(yandexProfileData.sex), birthDate: yandexProfileData.birthday, ); } catch (e) { log(e.toString()); } return null; } static Future<({String accessToken, String refreshToken})?> _getTokensFomYandex({ required String code, required String clientId, required String clientSecret, }) async { final response = await http.post( Uri.parse('https://oauth.yandex.ru/token'), headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: {'grant_type': 'authorization_code', 'code': code, 'client_id': clientId, 'client_secret': clientSecret}, ); if (response.statusCode != 200) { log('Yandex token exchange failed: ${response.body}'); return null; } final tokens = jsonDecode(response.body); final accessToken = tokens['access_token'] as String; final refreshToken = tokens['refresh_token'] as String; return (accessToken: accessToken, refreshToken: refreshToken); } static Future _signInWithVkIdProvider({required BuildContext context, required VkProvider provider}) async { final clientIdStr = Platform.isIOS ? EnvConfig.vkClientIdIos.trim() : EnvConfig.vkClientIdAndroid.trim(); if (clientIdStr.isEmpty) { return null; } try { final controller = VkIDController(clID: int.parse(clientIdStr)); final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider); final codeVerifier = authUri.key; final authUrl = authUri.value.toString(); final redirectScheme = 'vk$clientIdStr://'; final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUrl, redirectSchemePrefix: redirectScheme); if (redirectParams == null) return null; final code = redirectParams['code']; final deviceId = redirectParams['device_id']; if (code == null || deviceId == null) return null; return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier); } catch (e) { 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); if (uri.fragment.isNotEmpty) { final fragPairs = uri.fragment.split('&'); for (final pair in fragPairs) { final eq = pair.indexOf('='); if (eq > 0 && eq < pair.length - 1) { final key = Uri.decodeComponent(pair.substring(0, eq)); final value = Uri.decodeComponent(pair.substring(eq + 1)); params[key] = value; } } } Navigator.of(context, rootNavigator: true).pop(params); return NavigationDecision.prevent; } return NavigationDecision.navigate; }, onPageStarted: (url) {}, onPageFinished: (url) {}, onWebResourceError: (error) {}, ), ); try { await controller.loadRequest(Uri.parse(initialUrl)); } catch (e) { return null; } if (!context.mounted) return null; return showDialog>( context: context, barrierDismissible: false, builder: (_) { const borderRadius = BorderRadius.all(Radius.circular(16)); return Dialog( insetPadding: const EdgeInsets.all(16), shape: const RoundedRectangleBorder(borderRadius: borderRadius), elevation: 0, child: ClipRRect( borderRadius: borderRadius, child: SizedBox(height: 560, child: WebViewWidget(controller: controller)), ), ); }, ); } }