forked from eatvio/eatvio_app
234 lines
8.6 KiB
Dart
234 lines
8.6 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_login_yandex/flutter_login_yandex.dart';
|
|
import 'package:eatvio_app/core/app_router.dart';
|
|
import 'package:eatvio_app/core/env_config.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<String?> signInWithVk({required BuildContext context}) async {
|
|
final result = await signInWithVkAndFetchProfile(context: context);
|
|
return result?.accessToken;
|
|
}
|
|
|
|
static Future<VkAuthResult?> signInWithVkAndFetchProfile({required BuildContext context}) async {
|
|
final clientIdStr = EnvConfig.vkClientId.trim();
|
|
print('[VK] VK_CLIENT_ID from EnvConfig: $clientIdStr');
|
|
if (clientIdStr.isEmpty) {
|
|
print('[VK] VK_CLIENT_ID is not set. Configure it in .env');
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
print('[VK] Creating VkIDController with clientId: $clientIdStr');
|
|
dynamic controller = VkIDController(clID: int.parse(clientIdStr));
|
|
final authUri = controller.generateAuthorizeLinkWithCodeVerifier();
|
|
print('[VK] Generated auth URI: $authUri');
|
|
|
|
// Extract URL from MapEntry
|
|
String authUrl;
|
|
if (authUri is MapEntry) {
|
|
authUrl = authUri.value.toString();
|
|
} else {
|
|
authUrl = authUri.toString();
|
|
}
|
|
print('[VK] Extracted auth URL: $authUrl');
|
|
|
|
final redirectScheme = 'vk$clientIdStr://';
|
|
print('[VK] Redirect scheme: $redirectScheme');
|
|
|
|
final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUrl, redirectSchemePrefix: redirectScheme);
|
|
print('[VK] WebView returned params: $redirectParams');
|
|
if (redirectParams == null) {
|
|
print('[VK] No redirect params received');
|
|
return null;
|
|
}
|
|
|
|
final code = redirectParams['code'];
|
|
final deviceId = redirectParams['device_id'];
|
|
print('[VK] Code: $code, DeviceId: $deviceId');
|
|
if (code == null || deviceId == null) {
|
|
print('[VK] Missing code or deviceId');
|
|
return null;
|
|
}
|
|
|
|
// Instead of exchanging the code on-device (the sdk method is not available),
|
|
// return the authorization code so the backend can exchange it securely.
|
|
print('[VK] Returning authorization code to backend for exchange');
|
|
return VkAuthResult(accessToken: code);
|
|
} catch (e) {
|
|
print('[VK] Error in signInWithVkAndFetchProfile: $e');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
static Future<String?> signInWithOk() async {
|
|
final context = AppRouter.navigatorKey.currentContext;
|
|
if (context == null) return null;
|
|
return _signInWithVkIdProvider(context: context, provider: 'ok');
|
|
}
|
|
|
|
static Future<String?> signInWithMailRu() async {
|
|
final context = AppRouter.navigatorKey.currentContext;
|
|
if (context == null) return null;
|
|
return _signInWithVkIdProvider(context: context, provider: 'mail');
|
|
}
|
|
|
|
static Future<String?> signInWithYandex() async {
|
|
final clientId = EnvConfig.yandexClientId.trim();
|
|
final redirectUri = EnvConfig.yandexRedirectUri.trim();
|
|
|
|
print('[Yandex] Client ID: $clientId');
|
|
print('[Yandex] Redirect URI: $redirectUri');
|
|
|
|
if (clientId.isEmpty || redirectUri.isEmpty) {
|
|
print('[Yandex] Missing YANDEX_CLIENT_ID or YANDEX_REDIRECT_URI in .env');
|
|
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<String?> _signInWithVkIdProvider({required BuildContext context, required String provider}) async {
|
|
final clientIdStr = EnvConfig.vkClientId.trim();
|
|
if (clientIdStr.isEmpty) {
|
|
return null;
|
|
}
|
|
try {
|
|
print('[VK] Provider flow start: $provider, clientId: $clientIdStr');
|
|
dynamic controller = VkIDController(clID: int.parse(clientIdStr));
|
|
|
|
// Try different approaches for provider parameter
|
|
dynamic authUri;
|
|
try {
|
|
// First try with provider as string
|
|
authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider);
|
|
print('[VK] [$provider] Generated auth URI with string provider: $authUri');
|
|
} catch (e) {
|
|
print('[VK] [$provider] Failed with string provider, trying null: $e');
|
|
try {
|
|
// Try with null provider
|
|
authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: null);
|
|
print('[VK] [$provider] Generated auth URI with null provider: $authUri');
|
|
} catch (e2) {
|
|
print('[VK] [$provider] Failed with null provider, trying without provider: $e2');
|
|
// Try without provider parameter
|
|
authUri = controller.generateAuthorizeLinkWithCodeVerifier();
|
|
print('[VK] [$provider] Generated auth URI without provider: $authUri');
|
|
}
|
|
}
|
|
|
|
// Extract URL from MapEntry
|
|
String authUrl;
|
|
if (authUri is MapEntry) {
|
|
authUrl = authUri.value.toString();
|
|
} else {
|
|
authUrl = authUri.toString();
|
|
}
|
|
print('[VK] [$provider] Extracted auth URL: $authUrl');
|
|
|
|
final redirectScheme = 'vk$clientIdStr://';
|
|
print('[VK] [$provider] Redirect scheme: $redirectScheme');
|
|
final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUrl, redirectSchemePrefix: redirectScheme);
|
|
print('[VK] [$provider] WebView returned params: $redirectParams');
|
|
if (redirectParams == null) return null;
|
|
final code = redirectParams['code'];
|
|
final deviceId = redirectParams['device_id'];
|
|
print('[VK] [$provider] Code: $code, DeviceId: $deviceId');
|
|
if (code == null || deviceId == null) return null;
|
|
// Return the authorization code to be exchanged by the backend for provider flow
|
|
return code;
|
|
} catch (e) {
|
|
print('[VK] [$provider] Error in provider flow: $e');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Token extraction is no longer needed because we return the authorization code
|
|
// to the backend for exchange.
|
|
|
|
static Future<Map<String, String>?> _presentOAuthWebView({
|
|
required BuildContext context,
|
|
required String initialUrl,
|
|
required String redirectSchemePrefix,
|
|
}) async {
|
|
print('[VK] WebView: Loading URL: $initialUrl');
|
|
print('[VK] WebView: Redirect scheme: $redirectSchemePrefix');
|
|
|
|
final controller = WebViewController()
|
|
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
|
..setNavigationDelegate(
|
|
NavigationDelegate(
|
|
onNavigationRequest: (request) {
|
|
final url = request.url;
|
|
print('[VK] WebView: Navigation to: $url');
|
|
if (url.startsWith(redirectSchemePrefix)) {
|
|
print('[VK] WebView: Redirect detected, parsing params');
|
|
final uri = Uri.parse(url);
|
|
final params = <String, String>{}..addAll(uri.queryParameters);
|
|
print('[VK] WebView: Parsed params: $params');
|
|
Navigator.of(context, rootNavigator: true).pop(params);
|
|
return NavigationDecision.prevent;
|
|
}
|
|
return NavigationDecision.navigate;
|
|
},
|
|
onPageStarted: (url) {
|
|
print('[VK] WebView: Page started loading: $url');
|
|
},
|
|
onPageFinished: (url) {
|
|
print('[VK] WebView: Page finished loading: $url');
|
|
},
|
|
onWebResourceError: (error) {
|
|
print('[VK] WebView: Error: ${error.description}');
|
|
},
|
|
),
|
|
);
|
|
|
|
try {
|
|
await controller.loadRequest(Uri.parse(initialUrl));
|
|
} catch (e) {
|
|
print('[VK] WebView: Failed to load URL: $e');
|
|
return null;
|
|
}
|
|
|
|
return showDialog<Map<String, String>>(
|
|
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)),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|