forked from eatvio/eatvio_app
290 lines
10 KiB
Dart
290 lines
10 KiB
Dart
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:eatvio_app/generated/l10n.dart';
|
|
import 'package:go_router/go_router.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<String?> signInWithVk() async {
|
|
final context = AppRouter.navigatorKey.currentContext;
|
|
if (context == null) return null;
|
|
final result = await signInWithVkAndFetchProfile(context: context);
|
|
return result?.accessToken;
|
|
}
|
|
|
|
static Future<AuthResultEntity?> 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, clientId: clientIdStr);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
static Future<AuthResultEntity?> _parseVKAuthResult({
|
|
required Map<String, String> redirectParams,
|
|
required VkIDController controller,
|
|
required String codeVerifier,
|
|
required String clientId,
|
|
}) 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(),
|
|
clientId: clientId,
|
|
);
|
|
}
|
|
|
|
static Future<AuthResultEntity?> signInWithOkAndFetchProfile() async {
|
|
final context = AppRouter.navigatorKey.currentContext;
|
|
if (context == null) return null;
|
|
return await _signInWithVkIdProvider(context: context, provider: VkProvider.okRu);
|
|
}
|
|
|
|
static Future<AuthResultEntity?> signInWithMailRuAndFetchProfile() async {
|
|
final context = AppRouter.navigatorKey.currentContext;
|
|
if (context == null) return null;
|
|
return await _signInWithVkIdProvider(context: context, provider: VkProvider.mailRu);
|
|
}
|
|
|
|
static Future<AuthResultEntity?> 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,
|
|
'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<String, dynamic> 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,
|
|
clientId: clientId,
|
|
);
|
|
} 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<AuthResultEntity?> _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, scopes: [VkScope.phone, VkScope.email]);
|
|
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, clientId: clientIdStr);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
static Future<Map<String, String>?> _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 = <String, String>{}..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;
|
|
}
|
|
}
|
|
}
|
|
if (GoRouter.of(context).canPop()) {
|
|
GoRouter.of(context).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<Map<String, String>>(
|
|
context: context,
|
|
barrierDismissible: true,
|
|
builder: (dialogContext) {
|
|
final s = S.of(dialogContext);
|
|
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: MediaQuery.of(context).size.height * 0.7,
|
|
child: Scaffold(
|
|
appBar: AppBar(
|
|
leading: IconButton(
|
|
icon: const Icon(Icons.arrow_back),
|
|
onPressed: () {
|
|
Navigator.of(dialogContext).pop();
|
|
},
|
|
),
|
|
title: Text(s.authorization),
|
|
),
|
|
body: WebViewWidget(controller: controller),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|