diff --git a/.gitignore b/.gitignore
index 06e89a5e..afeb23f8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,4 +17,6 @@
android/build/
ios/XCBuildData/
env/.env
-.env
\ No newline at end of file
+.env
+android/key.properties
+app/upload-keystore.jks
\ No newline at end of file
diff --git a/android/.kotlin/sessions/kotlin-compiler-10039299475187077818.salive b/android/.kotlin/sessions/kotlin-compiler-10039299475187077818.salive
new file mode 100644
index 00000000..e69de29b
diff --git a/android/app/agconnect-services.json b/android/app/agconnect-services.json
new file mode 100644
index 00000000..8ca899db
--- /dev/null
+++ b/android/app/agconnect-services.json
@@ -0,0 +1 @@
+{"agcgw":{"url":"connect-drru.hispace.dbankcloud.ru","backurl":"connect-drru.hispace.dbankcloud.cn","websocketurl":"connect-ws-drru.hispace.dbankcloud.ru","websocketbackurl":"connect-ws-drru.hispace.dbankcloud.cn"},"agcgw_all":{"SG":"connect-dra.dbankcloud.cn","SG_back":"connect-dra.hispace.hicloud.com","CN":"connect-drcn.dbankcloud.cn","CN_back":"connect-drcn.hispace.hicloud.com","RU":"connect-drru.hispace.dbankcloud.ru","RU_back":"connect-drru.hispace.dbankcloud.cn","DE":"connect-dre.dbankcloud.cn","DE_back":"connect-dre.hispace.hicloud.com"},"websocketgw_all":{"SG":"connect-ws-dra.hispace.dbankcloud.cn","SG_back":"connect-ws-dra.hispace.dbankcloud.com","CN":"connect-ws-drcn.hispace.dbankcloud.cn","CN_back":"connect-ws-drcn.hispace.dbankcloud.com","RU":"connect-ws-drru.hispace.dbankcloud.ru","RU_back":"connect-ws-drru.hispace.dbankcloud.cn","DE":"connect-ws-dre.hispace.dbankcloud.cn","DE_back":"connect-ws-dre.hispace.dbankcloud.com"},"client":{"cp_id":"20007000036693451","product_id":"461323198429446894","client_id":"1595824543740483264","client_secret":"2A1727B0D31AA9BE1F5D9E8A5CDA1A67B9EF084AC9FB68D6C633A3CCB08ECDEE","project_id":"461323198429446894","app_id":"113238569","api_key":"DQEDAJNRITp9M3uc1rPHyT8PzLc7bnVcKJAeavZZk0TQNMx9ifvBebr+JOmZe9tEvjnLwv7c1hAHXmPGR/gWG86iABDrCVotq+NGnw==","package_name":"com.eatvioapp.app"},"oauth_client":{"client_id":"113238569","client_type":1},"app_info":{"app_id":"113238569","package_name":"com.eatvioapp.app"},"service":{"analytics":{"collector_url":"datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com","collector_url_cn":"datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn","collector_url_de":"datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn","collector_url_ru":"datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com","collector_url_sg":"datacollector-dra.dt.hicloud.com,datacollector-dra.dt.dbankcloud.cn","resource_id":"p1","channel_id":""},"ml":{"mlservice_url":"ml-api-drru.ai.dbankcloud.ru,ml-api-drru.ai.dbankcloud.cn"},"cloudstorage":{"storage_url":"https://agc-storage-drcn.platform.dbankcloud.cn","storage_url_ru":"https://agc-storage-drru.cloud.huawei.ru","storage_url_sg":"https://ops-dra.agcstorage.link","storage_url_de":"https://ops-dre.agcstorage.link","storage_url_cn":"https://agc-storage-drcn.platform.dbankcloud.cn","storage_url_ru_back":"https://agc-storage-drru.cloud.huawei.ru","storage_url_sg_back":"https://agc-storage-dra.cloud.huawei.asia","storage_url_de_back":"https://agc-storage-dre.cloud.huawei.eu","storage_url_cn_back":"https://agc-storage-drcn.cloud.huawei.com.cn"},"search":{"url":"https://search-drru.cloud.huawei.ru"},"edukit":{"edu_url":"edukit.cloud.huawei.com.cn","dh_url":"edukit.cloud.huawei.com.cn"}},"region":"RU","configuration_version":"3.0","appInfos":[{"package_name":"com.eatvioapp.app","client":{"app_id":"113238569"},"app_info":{"package_name":"com.eatvioapp.app","app_id":"113238569"},"oauth_client":{"client_type":1,"client_id":"113238569"}}]}
\ No newline at end of file
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 9ffa87c4..8fc22d6d 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -5,6 +5,7 @@ plugins {
// END: FlutterFire Configuration
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
+ id 'com.huawei.agconnect'
}
def keystoreProperties = new Properties()
@@ -13,6 +14,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 +36,10 @@ android {
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
+ manifestPlaceholders = [
+ YANDEX_CLIENT_ID: yandexClientId,
+ applicationName: "io.flutter.app.FlutterApplication"
+ ]
}
signingConfigs {
@@ -47,6 +54,9 @@ android {
buildTypes {
release {
signingConfig signingConfigs.release
+ minifyEnabled true
+ shrinkResources true
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
signingConfig signingConfigs.debug
@@ -57,6 +67,15 @@ android {
}
}
+dependencies {
+ implementation 'com.huawei.agconnect:agconnect-core:1.9.1.301'
+ implementation 'com.huawei.hms:push:6.12.0.300'
+ // Huawei Analytics dependency to satisfy com.huawei.hianalytics.* references during R8
+ implementation 'com.huawei.hms:hianalytics:6.12.0.300'
+ // BouncyCastle provider required by Huawei secure utils used transitively by HMS
+ implementation 'org.bouncycastle:bcprov-jdk15to18:1.78.1'
+}
+
flutter {
source = "../.."
}
diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro
new file mode 100644
index 00000000..b657fa16
--- /dev/null
+++ b/android/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+-keep class com.huawei.** { *; }
+-dontwarn com.huawei.**
+
+-keep class org.bouncycastle.** { *; }
+-dontwarn org.bouncycastle.**
+
+-keep class io.flutter.** { *; }
+-dontwarn io.flutter.**
+
+-keep class kotlinx.coroutines.** { *; }
+-dontwarn kotlinx.coroutines.**
+
+-keep class **.R$* { *; }
+-keepclassmembers class ** { @androidx.annotation.Keep *; }
+-keep @androidx.annotation.Keep class * { *; }
+
+-keep public class * extends android.app.Application
+-keep public class * extends android.content.ContentProvider
+
+-keep class * implements android.os.Parcelable { *; }
+-keepclassmembers enum * { *; }
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 0d239126..a99f3deb 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -11,6 +11,16 @@
android:usesCleartextTraffic="true"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
+
+
+
+
+
+
+ when (call.method) {
+ "getHuaweiToken" -> {
+ CoroutineScope(Dispatchers.IO).launch {
+ try {
+ // Try to read appId from agconnect-services.json
+ var appId: String? = try {
+ AGConnectServicesConfig.fromContext(applicationContext)
+ .getString("client/app_id")
+ } catch (_: Exception) {
+ null
+ }
+
+ // Fallback to manifest meta-data if needed
+ if (appId.isNullOrBlank()) {
+ try {
+ val pm: PackageManager = applicationContext.packageManager
+ val ai: ApplicationInfo = pm.getApplicationInfo(applicationContext.packageName, PackageManager.GET_META_DATA)
+ val raw = ai.metaData?.getString("com.huawei.hms.client.appid")
+ if (!raw.isNullOrBlank()) {
+ appId = raw.removePrefix("appid=")
+ }
+ } catch (_: Exception) {
+ // ignore and handle below
+ }
+ }
+
+ if (appId.isNullOrBlank()) {
+ runOnUiThread {
+ result.error("HMS_TOKEN_ERROR", "Missing Huawei appId. Ensure agconnect-services.json or manifest meta-data is configured.", null)
+ }
+ return@launch
+ }
+ val token = HmsInstanceId.getInstance(applicationContext)
+ .getToken(appId, "HCM")
+ runOnUiThread {
+ result.success(token)
+ }
+ } catch (e: Exception) {
+ runOnUiThread {
+ result.error("HMS_TOKEN_ERROR", e.message, null)
+ }
+ }
+ }
+ }
+ else -> result.notImplemented()
+ }
+ }
+ }
}
diff --git a/android/build.gradle b/android/build.gradle
index 9db63328..af06c34c 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -1,8 +1,21 @@
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ maven {url 'https://developer.huawei.com/repo/'}
+ gradlePluginPortal()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:8.12.0'
+ classpath 'com.huawei.agconnect:agcp:1.9.1.301'
+ }
+}
+
allprojects {
repositories {
google()
mavenCentral()
- maven { url 'https://developer.huawei.com/repo/' }
+ maven {url 'https://developer.huawei.com/repo/'}
}
}
@@ -14,6 +27,32 @@ subprojects {
project.evaluationDependsOn(":app")
}
+// Temporary safeguard: set missing namespaces for Android library modules based on their manifest package
+subprojects { Project subproject ->
+ subproject.pluginManager.withPlugin('com.android.library') {
+ def androidExt = subproject.extensions.findByName('android')
+ if (androidExt != null) {
+ def hasNamespaceProp = androidExt.hasProperty('namespace')
+ def currentNamespace = hasNamespaceProp ? androidExt.namespace : null
+ if (hasNamespaceProp && !currentNamespace) {
+ def manifestFile = file("${subproject.projectDir}/src/main/AndroidManifest.xml")
+ if (manifestFile.exists()) {
+ try {
+ def manifest = new groovy.xml.XmlSlurper().parse(manifestFile)
+ def manifestPackage = manifest.@package?.text()
+ if (manifestPackage) {
+ androidExt.namespace = manifestPackage
+ subproject.logger.lifecycle("[namespace-fix] Set namespace for ${subproject.path} to '${manifestPackage}' from AndroidManifest.xml")
+ }
+ } catch (Throwable t) {
+ subproject.logger.warn("[namespace-fix] Failed to read manifest for ${subproject.path}: ${t.message}")
+ }
+ }
+ }
+ }
+ }
+}
+
tasks.register("clean", Delete) {
delete rootProject.buildDir
}
diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html
index 4921e6e4..ce07f9e3 100644
--- a/android/build/reports/problems/problems-report.html
+++ b/android/build/reports/problems/problems-report.html
@@ -650,12 +650,12 @@ code + .copy-button {
diff --git a/android/settings.gradle b/android/settings.gradle
index e173b2c9..76846d0d 100644
--- a/android/settings.gradle
+++ b/android/settings.gradle
@@ -12,6 +12,7 @@ pluginManagement {
repositories {
google()
mavenCentral()
+ maven { url 'https://developer.huawei.com/repo/' }
gradlePluginPortal()
}
}
diff --git a/lib/core/firebase_api.dart b/lib/core/firebase_api.dart
index cd0c4d12..9d8e9cfc 100644
--- a/lib/core/firebase_api.dart
+++ b/lib/core/firebase_api.dart
@@ -1,28 +1,27 @@
import 'dart:developer';
import 'dart:io';
+import 'package:eatvio_app/core/utils/toasts.dart';
+import 'package:flutter/services.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
-import 'package:huawei_push/huawei_push.dart';
+import 'package:flutter/services.dart' show MethodChannel, PlatformException;
class FirebaseApi {
final _firebaseMessaging = FirebaseMessaging.instance;
Future getToken() async {
- await _firebaseMessaging.requestPermission();
+ String? token;
- String? token = await _firebaseMessaging.getToken();
-
- if (token == null && Platform.isAndroid) {
- final isHuawei = await _isHuaweiDevice();
- if (isHuawei) {
- token = await _getHuaweiPushToken();
- }
+ if (await _isHuaweiDevice()) {
+ token = await _getHuaweiPushTokenNative();
+ } else {
+ await _firebaseMessaging.requestPermission();
+ token = await _firebaseMessaging.getToken();
}
-
return token;
}
@@ -46,10 +45,11 @@ class FirebaseApi {
if (await TokenStorage.haveTokenInStorage()) {
try {
final deviceInfo = DeviceInfoPlugin();
+ final isHuawei = Platform.isAndroid ? await _isHuaweiDevice() : false;
await sl().post(
'/api/profile/notification/token/register',
data: {
- 'platform': Platform.isIOS ? 'ios' : 'android',
+ 'platform': Platform.isIOS ? 'ios' : (isHuawei ? 'huawei' : 'android'),
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
'token': await getToken(),
},
@@ -63,33 +63,27 @@ class FirebaseApi {
Future _isHuaweiDevice() async {
if (!Platform.isAndroid) return false;
try {
- final android = await DeviceInfoPlugin().androidInfo;
- final brand = android.brand.toLowerCase();
- final manufacturer = android.manufacturer.toLowerCase();
- return brand.contains('huawei') || manufacturer.contains('huawei') || brand.contains('honor') || manufacturer.contains('honor');
+ final info = await DeviceInfoPlugin().androidInfo;
+ final brand = info.brand.toLowerCase();
+ final manufacturer = info.manufacturer.toLowerCase();
+ return brand.contains('huawei') || brand.contains('honor') || manufacturer.contains('huawei') || manufacturer.contains('honor');
} catch (_) {
return false;
}
}
- Future _getHuaweiPushToken() async {
+ Future _getHuaweiPushTokenNative() async {
try {
- await Push.turnOnPush();
-
- String? resolvedToken;
- final sub = Push.getTokenStream.listen((t) => resolvedToken ??= t, onError: (_) {});
-
- Push.getToken('');
-
- final started = DateTime.now();
- while (resolvedToken == null && DateTime.now().difference(started).inSeconds < 5) {
- await Future.delayed(const Duration(milliseconds: 100));
- }
-
- await sub.cancel();
- return resolvedToken;
+ const channel = MethodChannel('com.eatvioapp/hms_push');
+ final String? token = await channel.invokeMethod('getHuaweiToken');
+ return token;
+ } on PlatformException catch (e, st) {
+ showErrorToast('$e $st');
+ log(e.toString(), stackTrace: st);
+ return null;
} catch (e, st) {
- log('Huawei Push token error: $e', stackTrace: st);
+ showErrorToast('$e $st');
+ log(e.toString(), stackTrace: st);
return null;
}
}
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