diff --git a/example/android/build.gradle b/example/android/build.gradle index 90dce397..1cc04d89 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.6.10' + ext.kotlin_version = '1.8.21' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:7.1.2' + classpath 'com.android.tools.build:gradle:7.3.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/example/lib/main_huawei_app_gallery.dart b/example/lib/main_huawei_app_gallery.dart new file mode 100644 index 00000000..1274accf --- /dev/null +++ b/example/lib/main_huawei_app_gallery.dart @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2019-2024 Larry Aasen. All rights reserved. + */ + +import 'package:flutter/material.dart'; +import 'package:huawei_hmsavailability/huawei_hmsavailability.dart'; +import 'package:upgrader/upgrader.dart'; + +//Add Your App id , clientId , clientSecret to Your Env file +String appId = "your_app_id"; +String clientId = "your_client_id"; +String clientSecret = "your_client_secret"; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + HmsApiAvailability client = HmsApiAvailability(); + +// 0: HMS Core (APK) is available. +// 1: No HMS Core (APK) is found on device. +// 2: HMS Core (APK) installed is out of date. +// 3: HMS Core (APK) installed on the device is unavailable. +// 9: HMS Core (APK) installed on the device is not the official version. +// 21: The device is too old to support HMS Core (APK). + int status = await client.isHMSAvailable(); + + // Clear any saved settings for testing purposes (REMOVE this line in production). + await Upgrader.clearSavedSettings(); // REMOVE this for release builds + + // Start the app with the appropriate store controller based on Google Play availability. + runApp(MyApp( + isHuawei: status == 0, //change this as your need + )); +} + +class MyApp extends StatelessWidget { + final bool isHuawei; + const MyApp({ + super.key, + required this.isHuawei, + }); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Upgrader Huawei Example', + home: UpgradeAlert( + upgrader: Upgrader( + storeController: UpgraderStoreController( + onAndroid: () { + // If Google Play Services are unavailable, use Huawei AppGallery. + if (isHuawei) { + return UpgraderHuaweiStore( + appId: appId, + clientId: clientId, + clientSecret: clientSecret, + ); + } + // Otherwise, default to Google Play Store. + return UpgraderPlayStore(); + }, + ), + ), + child: Scaffold( + appBar: AppBar( + title: const Text('Upgrader Huawei Example'), + ), + body: const Center( + child: Text('Checking for updates...'), + ), + ), + ), + ); + } +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 859848fe..25be5dbc 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -15,6 +15,7 @@ dependencies: go_router: ^7.1.1 http: ^1.2.2 + huawei_hmsavailability: ^6.12.0+300 path: ^1.9.0 upgrader: diff --git a/lib/model/huawei_app_info/app_info_response.dart b/lib/model/huawei_app_info/app_info_response.dart new file mode 100644 index 00000000..a3207564 --- /dev/null +++ b/lib/model/huawei_app_info/app_info_response.dart @@ -0,0 +1,257 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'app_info_response.g.dart'; + +@JsonSerializable() +class AppInfoResponse { + @JsonKey(name: "ret") + Ret? ret; + @JsonKey(name: "appInfo") + AppInfo? appInfo; + @JsonKey(name: "auditInfo") + AuditInfo? auditInfo; + @JsonKey(name: "languages") + List? languages; + + AppInfoResponse({ + this.ret, + this.appInfo, + this.auditInfo, + this.languages, + }); + + factory AppInfoResponse.fromJson(Map json) => + _$AppInfoResponseFromJson(json); + + Map toJson() => _$AppInfoResponseToJson(this); +} + +@JsonSerializable() +class AppInfo { + @JsonKey(name: "releaseState") + int? releaseState; + @JsonKey(name: "defaultLang") + String? defaultLang; + @JsonKey(name: "parentType") + int? parentType; + @JsonKey(name: "childType") + int? childType; + @JsonKey(name: "grandChildType") + int? grandChildType; + @JsonKey(name: "privacyPolicy") + String? privacyPolicy; + @JsonKey(name: "appAdapters") + String? appAdapters; + @JsonKey(name: "isFree") + int? isFree; + @JsonKey(name: "price") + String? price; + @JsonKey(name: "priceDetail") + String? priceDetail; + @JsonKey(name: "publishCountry") + String? publishCountry; + @JsonKey(name: "contentRate") + String? contentRate; + @JsonKey(name: "hispaceAutoDown") + int? hispaceAutoDown; + @JsonKey(name: "appTariffType") + String? appTariffType; + @JsonKey(name: "developerNameCn") + String? developerNameCn; + @JsonKey(name: "developerNameEn") + String? developerNameEn; + @JsonKey(name: "developerAddr") + String? developerAddr; + @JsonKey(name: "developerEmail") + String? developerEmail; + @JsonKey(name: "developerPhone") + String? developerPhone; + @JsonKey(name: "developerWebsite") + String? developerWebsite; + @JsonKey(name: "certificateURLs") + String? certificateUrLs; + @JsonKey(name: "publicationURLs") + String? publicationUrLs; + @JsonKey(name: "cultureRecordURLs") + String? cultureRecordUrLs; + @JsonKey(name: "updateTime") + DateTime? updateTime; + @JsonKey(name: "versionNumber") + String? versionNumber; + @JsonKey(name: "familyShareTag") + int? familyShareTag; + @JsonKey(name: "deviceTypes") + List? deviceTypes; + + AppInfo({ + this.releaseState, + this.defaultLang, + this.parentType, + this.childType, + this.grandChildType, + this.privacyPolicy, + this.appAdapters, + this.isFree, + this.price, + this.priceDetail, + this.publishCountry, + this.contentRate, + this.hispaceAutoDown, + this.appTariffType, + this.developerNameCn, + this.developerNameEn, + this.developerAddr, + this.developerEmail, + this.developerPhone, + this.developerWebsite, + this.certificateUrLs, + this.publicationUrLs, + this.cultureRecordUrLs, + this.updateTime, + this.versionNumber, + this.familyShareTag, + this.deviceTypes, + }); + + factory AppInfo.fromJson(Map json) => + _$AppInfoFromJson(json); + + Map toJson() => _$AppInfoToJson(this); +} + +@JsonSerializable() +class DeviceType { + @JsonKey(name: "deviceType") + int? deviceType; + @JsonKey(name: "appAdapters") + String? appAdapters; + + DeviceType({ + this.deviceType, + this.appAdapters, + }); + + factory DeviceType.fromJson(Map json) => + _$DeviceTypeFromJson(json); + + Map toJson() => _$DeviceTypeToJson(this); +} + +@JsonSerializable() +class AuditInfo { + @JsonKey(name: "auditOpinion") + String? auditOpinion; + + AuditInfo({ + this.auditOpinion, + }); + + factory AuditInfo.fromJson(Map json) => + _$AuditInfoFromJson(json); + + Map toJson() => _$AuditInfoToJson(this); +} + +@JsonSerializable() +class Language { + @JsonKey(name: "lang") + String? lang; + @JsonKey(name: "appName") + String? appName; + @JsonKey(name: "appDesc") + String? appDesc; + @JsonKey(name: "briefInfo") + String? briefInfo; + @JsonKey(name: "newFeatures") + String? newFeatures; + @JsonKey(name: "icon") + String? icon; + @JsonKey(name: "showType") + int? showType; + @JsonKey(name: "videoShowType") + int? videoShowType; + @JsonKey(name: "introPic") + String? introPic; + @JsonKey(name: "deviceMaterials") + List? deviceMaterials; + @JsonKey(name: "rcmdPic") + String? rcmdPic; + + Language({ + this.lang, + this.appName, + this.appDesc, + this.briefInfo, + this.newFeatures, + this.icon, + this.showType, + this.videoShowType, + this.introPic, + this.deviceMaterials, + this.rcmdPic, + }); + + factory Language.fromJson(Map json) => + _$LanguageFromJson(json); + + Map toJson() => _$LanguageToJson(this); +} + +@JsonSerializable() +class DeviceMaterial { + @JsonKey(name: "deviceType") + int? deviceType; + @JsonKey(name: "appIcon") + String? appIcon; + @JsonKey(name: "screenShots") + List? screenShots; + @JsonKey(name: "showType") + int? showType; + @JsonKey(name: "vrCoverLayeredImage") + List? vrCoverLayeredImage; + @JsonKey(name: "vrRecomGraphic4to3") + List? vrRecomGraphic4To3; + @JsonKey(name: "vrRecomGraphic1to1") + List? vrRecomGraphic1To1; + @JsonKey(name: "promoGraphics") + List? promoGraphics; + @JsonKey(name: "videoShowType") + int? videoShowType; + @JsonKey(name: "rcmdPics") + String? rcmdPics; + + DeviceMaterial({ + this.deviceType, + this.appIcon, + this.screenShots, + this.showType, + this.vrCoverLayeredImage, + this.vrRecomGraphic4To3, + this.vrRecomGraphic1To1, + this.promoGraphics, + this.videoShowType, + this.rcmdPics, + }); + + factory DeviceMaterial.fromJson(Map json) => + _$DeviceMaterialFromJson(json); + + Map toJson() => _$DeviceMaterialToJson(this); +} + +@JsonSerializable() +class Ret { + @JsonKey(name: "code") + int? code; + @JsonKey(name: "msg") + String? msg; + + Ret({ + this.code, + this.msg, + }); + + factory Ret.fromJson(Map json) => _$RetFromJson(json); + + Map toJson() => _$RetToJson(this); +} diff --git a/lib/model/huawei_app_info/app_info_response.g.dart b/lib/model/huawei_app_info/app_info_response.g.dart new file mode 100644 index 00000000..5ba090d9 --- /dev/null +++ b/lib/model/huawei_app_info/app_info_response.g.dart @@ -0,0 +1,184 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'app_info_response.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AppInfoResponse _$AppInfoResponseFromJson(Map json) => + AppInfoResponse( + ret: json['ret'] == null + ? null + : Ret.fromJson(json['ret'] as Map), + appInfo: json['appInfo'] == null + ? null + : AppInfo.fromJson(json['appInfo'] as Map), + auditInfo: json['auditInfo'] == null + ? null + : AuditInfo.fromJson(json['auditInfo'] as Map), + languages: (json['languages'] as List?) + ?.map((e) => Language.fromJson(e as Map)) + .toList(), + ); + +Map _$AppInfoResponseToJson(AppInfoResponse instance) => + { + 'ret': instance.ret, + 'appInfo': instance.appInfo, + 'auditInfo': instance.auditInfo, + 'languages': instance.languages, + }; + +AppInfo _$AppInfoFromJson(Map json) => AppInfo( + releaseState: (json['releaseState'] as num?)?.toInt(), + defaultLang: json['defaultLang'] as String?, + parentType: (json['parentType'] as num?)?.toInt(), + childType: (json['childType'] as num?)?.toInt(), + grandChildType: (json['grandChildType'] as num?)?.toInt(), + privacyPolicy: json['privacyPolicy'] as String?, + appAdapters: json['appAdapters'] as String?, + isFree: (json['isFree'] as num?)?.toInt(), + price: json['price'] as String?, + priceDetail: json['priceDetail'] as String?, + publishCountry: json['publishCountry'] as String?, + contentRate: json['contentRate'] as String?, + hispaceAutoDown: (json['hispaceAutoDown'] as num?)?.toInt(), + appTariffType: json['appTariffType'] as String?, + developerNameCn: json['developerNameCn'] as String?, + developerNameEn: json['developerNameEn'] as String?, + developerAddr: json['developerAddr'] as String?, + developerEmail: json['developerEmail'] as String?, + developerPhone: json['developerPhone'] as String?, + developerWebsite: json['developerWebsite'] as String?, + certificateUrLs: json['certificateURLs'] as String?, + publicationUrLs: json['publicationURLs'] as String?, + cultureRecordUrLs: json['cultureRecordURLs'] as String?, + updateTime: json['updateTime'] == null + ? null + : DateTime.parse(json['updateTime'] as String), + versionNumber: json['versionNumber'] as String?, + familyShareTag: (json['familyShareTag'] as num?)?.toInt(), + deviceTypes: (json['deviceTypes'] as List?) + ?.map((e) => DeviceType.fromJson(e as Map)) + .toList(), + ); + +Map _$AppInfoToJson(AppInfo instance) => { + 'releaseState': instance.releaseState, + 'defaultLang': instance.defaultLang, + 'parentType': instance.parentType, + 'childType': instance.childType, + 'grandChildType': instance.grandChildType, + 'privacyPolicy': instance.privacyPolicy, + 'appAdapters': instance.appAdapters, + 'isFree': instance.isFree, + 'price': instance.price, + 'priceDetail': instance.priceDetail, + 'publishCountry': instance.publishCountry, + 'contentRate': instance.contentRate, + 'hispaceAutoDown': instance.hispaceAutoDown, + 'appTariffType': instance.appTariffType, + 'developerNameCn': instance.developerNameCn, + 'developerNameEn': instance.developerNameEn, + 'developerAddr': instance.developerAddr, + 'developerEmail': instance.developerEmail, + 'developerPhone': instance.developerPhone, + 'developerWebsite': instance.developerWebsite, + 'certificateURLs': instance.certificateUrLs, + 'publicationURLs': instance.publicationUrLs, + 'cultureRecordURLs': instance.cultureRecordUrLs, + 'updateTime': instance.updateTime?.toIso8601String(), + 'versionNumber': instance.versionNumber, + 'familyShareTag': instance.familyShareTag, + 'deviceTypes': instance.deviceTypes, + }; + +DeviceType _$DeviceTypeFromJson(Map json) => DeviceType( + deviceType: (json['deviceType'] as num?)?.toInt(), + appAdapters: json['appAdapters'] as String?, + ); + +Map _$DeviceTypeToJson(DeviceType instance) => + { + 'deviceType': instance.deviceType, + 'appAdapters': instance.appAdapters, + }; + +AuditInfo _$AuditInfoFromJson(Map json) => AuditInfo( + auditOpinion: json['auditOpinion'] as String?, + ); + +Map _$AuditInfoToJson(AuditInfo instance) => { + 'auditOpinion': instance.auditOpinion, + }; + +Language _$LanguageFromJson(Map json) => Language( + lang: json['lang'] as String?, + appName: json['appName'] as String?, + appDesc: json['appDesc'] as String?, + briefInfo: json['briefInfo'] as String?, + newFeatures: json['newFeatures'] as String?, + icon: json['icon'] as String?, + showType: (json['showType'] as num?)?.toInt(), + videoShowType: (json['videoShowType'] as num?)?.toInt(), + introPic: json['introPic'] as String?, + deviceMaterials: (json['deviceMaterials'] as List?) + ?.map((e) => DeviceMaterial.fromJson(e as Map)) + .toList(), + rcmdPic: json['rcmdPic'] as String?, + ); + +Map _$LanguageToJson(Language instance) => { + 'lang': instance.lang, + 'appName': instance.appName, + 'appDesc': instance.appDesc, + 'briefInfo': instance.briefInfo, + 'newFeatures': instance.newFeatures, + 'icon': instance.icon, + 'showType': instance.showType, + 'videoShowType': instance.videoShowType, + 'introPic': instance.introPic, + 'deviceMaterials': instance.deviceMaterials, + 'rcmdPic': instance.rcmdPic, + }; + +DeviceMaterial _$DeviceMaterialFromJson(Map json) => + DeviceMaterial( + deviceType: (json['deviceType'] as num?)?.toInt(), + appIcon: json['appIcon'] as String?, + screenShots: (json['screenShots'] as List?) + ?.map((e) => e as String?) + .toList(), + showType: (json['showType'] as num?)?.toInt(), + vrCoverLayeredImage: json['vrCoverLayeredImage'] as List?, + vrRecomGraphic4To3: json['vrRecomGraphic4to3'] as List?, + vrRecomGraphic1To1: json['vrRecomGraphic1to1'] as List?, + promoGraphics: json['promoGraphics'] as List?, + videoShowType: (json['videoShowType'] as num?)?.toInt(), + rcmdPics: json['rcmdPics'] as String?, + ); + +Map _$DeviceMaterialToJson(DeviceMaterial instance) => + { + 'deviceType': instance.deviceType, + 'appIcon': instance.appIcon, + 'screenShots': instance.screenShots, + 'showType': instance.showType, + 'vrCoverLayeredImage': instance.vrCoverLayeredImage, + 'vrRecomGraphic4to3': instance.vrRecomGraphic4To3, + 'vrRecomGraphic1to1': instance.vrRecomGraphic1To1, + 'promoGraphics': instance.promoGraphics, + 'videoShowType': instance.videoShowType, + 'rcmdPics': instance.rcmdPics, + }; + +Ret _$RetFromJson(Map json) => Ret( + code: (json['code'] as num?)?.toInt(), + msg: json['msg'] as String?, + ); + +Map _$RetToJson(Ret instance) => { + 'code': instance.code, + 'msg': instance.msg, + }; diff --git a/lib/src/huawei_store_search_api.dart b/lib/src/huawei_store_search_api.dart new file mode 100644 index 00000000..88467afd --- /dev/null +++ b/lib/src/huawei_store_search_api.dart @@ -0,0 +1,195 @@ +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:version/version.dart'; +import 'dart:convert'; +import '../model/huawei_app_info/app_info_response.dart'; + +class HuaweiStoreSearchAPI { + HuaweiStoreSearchAPI({http.Client? client, this.clientHeaders}) + : client = client ?? http.Client(); + + /// Huawei AppGallery API base URL + final String huaweiStorePrefixURL = 'appgallery.cloud.huawei.com'; + + /// Key used to store the access token in shared preferences + final String accessTokenKey = 'AccessToken'; + + /// HTTP client for making requests (can be mocked for testing) + final http.Client client; + + /// Headers used by the HTTP client for requests + final Map? clientHeaders; + + /// Enable or disable debug logging + bool debugLogging = false; + + /// Retrieves app information from Huawei AppGallery by appId + Future lookupById({ + bool useCacheBuster = true, + required String appId, + required String clientId, + required String clientSecret, + }) async { + // Fetch access token from cache or refresh if needed + String? accessToken = await _getAccessToken(clientId, clientSecret); + if (accessToken == null) return null; + + // Fetch app details using the valid access token + return await _getAppDetails(clientId, accessToken, appId, clientSecret); + } + + /// Retrieves access token from shared preferences or refreshes it if not available + Future _getAccessToken(String clientId, String clientSecret) async { + final SharedPreferences sharedPreferences = + await SharedPreferences.getInstance(); + String? accessToken = sharedPreferences.getString(accessTokenKey); + + // Refresh access token if not found in cache + if (accessToken == null) { + accessToken = await _refreshAccessToken(clientId, clientSecret); + if (accessToken != null) { + await sharedPreferences.setString(accessTokenKey, accessToken); + } + } + return accessToken; + } + + /// Fetches a new access token from Huawei API + Future _refreshAccessToken( + String clientId, String clientSecret) async { + return await getToken(clientId, clientSecret); + } + + /// Fetches app details by appId with retry logic for token refresh + Future _getAppDetails( + String clientId, + String token, + String appId, + String clientSecret, { + bool hasRetried = false, // Prevent infinite retries + }) async { + final Uri uri = Uri.parse( + 'https://connect-api.cloud.huawei.com/api/publish/v2/app-info?appId=$appId'); + + try { + final response = await client.get( + uri, + headers: { + 'Authorization': 'Bearer $token', + 'client_id': clientId, + }, + ); + + if (response.statusCode == 200) { + // Parse and return app info from response + final jsonResponse = jsonDecode(response.body); + if (debugLogging) { + print('App info result: ${jsonResponse['ret']}'); + print('App version: ${jsonResponse['appInfo']["versionNumber"]}'); + } + return AppInfoResponse.fromJson(jsonResponse); + } else if (response.statusCode == 401 && !hasRetried) { + // Token expired, attempt refresh and retry once + final newToken = await _refreshAccessToken(clientId, clientSecret); + if (newToken != null) { + return _getAppDetails(clientId, newToken, appId, clientSecret, + hasRetried: true); + } + } else { + // Log any non-401 error response + print('Error: ${response.statusCode} - ${response.body}'); + } + } catch (e) { + // Catch and log exceptions during HTTP request + print('Exception occurred: $e'); + } + return null; + } + + /// Requests a new access token from Huawei OAuth API + Future getToken(String clientId, String clientSecret) async { + String? token; + try { + final uri = Uri.parse( + 'https://connect-api-dre.cloud.huawei.com/api/oauth2/v1/token'); + + final body = jsonEncode({ + 'client_id': clientId, + 'client_secret': clientSecret, + 'grant_type': 'client_credentials', + }); + + final response = await client.post( + uri, + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: body, + ); + + if (response.statusCode == 200) { + // Extract token from response + final jsonResponse = json.decode(response.body); + token = jsonResponse['access_token']; + } else { + // Log any error in token fetching + print('Error: ${response.statusCode} - ${response.body}'); + } + } catch (e) { + // Catch and log exceptions during token request + print('Exception occurred: $e'); + } + return token; + } + + /// Constructs a URL to view app details in Huawei AppGallery by appId + String? lookupURLById(String id, + {String? country = 'US', + String? language = 'en', + bool useCacheBuster = true}) { + assert(id.isNotEmpty); + if (id.isEmpty) return null; + + final url = Uri.https(huaweiStorePrefixURL, 'ag/n/app/C$id').toString(); + return url; + } +} + +extension HuaweiStoreResults on HuaweiStoreSearchAPI { + static final RegExp releaseNotesSpan = RegExp(r'>(.*?)'); + + /// Return the minimum app version taken from a tag in the description field from the store response. + /// The [tagRegExpSource] is used to represent the format of a tag using a regular expression. + /// The format in the description by default is like this: `[Minimum supported app version: 1.2.3]`, which + /// returns the version `1.2.3`. If there is no match, it returns null. + Version? minAppVersion(String? desc, + {String tagRegExpSource = + r'\[\Minimum supported app version\:[\s]*(?[^\s]+)[\s]*\]'}) { + Version? version; + try { + if (desc != null) { + final regExp = RegExp(tagRegExpSource, caseSensitive: false); + final match = regExp.firstMatch(desc); + final mav = match?.namedGroup('version'); + + if (mav != null) { + try { + // Parse and validate the version string + version = Version.parse(mav); + } on Exception catch (e) { + if (debugLogging) { + print( + 'HuaweiStoreResults.minAppVersion: mav=$mav, tag=$tagRegExpSource, error=$e'); + } + } + } + } + } on Exception catch (e) { + if (debugLogging) { + print('HuaweiStoreResults.minAppVersion: $e'); + } + } + return version; + } +} diff --git a/lib/src/upgrade_store_controller.dart b/lib/src/upgrade_store_controller.dart index 3216deb2..1184dea1 100644 --- a/lib/src/upgrade_store_controller.dart +++ b/lib/src/upgrade_store_controller.dart @@ -4,7 +4,9 @@ import 'dart:async'; import 'package:version/version.dart'; +import '../model/huawei_app_info/app_info_response.dart'; import 'appcast.dart'; +import 'huawei_store_search_api.dart'; import 'itunes_search_api.dart'; import 'play_store_search_api.dart'; import 'upgrade_os.dart'; @@ -149,6 +151,120 @@ class UpgraderPlayStore extends UpgraderStore { } } +class UpgraderHuaweiStore extends UpgraderStore { + /// Required parameters for the Huawei Store API: appId, clientId, clientSecret. + final String appId; + final String clientId; + final String clientSecret; + + /// Constructor initializes the necessary parameters for API requests. + UpgraderHuaweiStore({ + required this.appId, + required this.clientId, + required this.clientSecret, + }); + + /// Fetches the version info from Huawei AppGallery, processes the data, and returns + /// [UpgraderVersionInfo] with version, release notes, minimum supported version, etc. + @override + Future getVersionInfo({ + required UpgraderState state, + required Version installedVersion, + required String? country, + required String? language, + }) async { + // Return early if packageInfo is not available + if (state.packageInfo == null) return UpgraderVersionInfo(); + + // Initialize Huawei Store API client + final huaweiStore = HuaweiStoreSearchAPI( + client: state.client, + clientHeaders: state.clientHeaders, + ); + huaweiStore.debugLogging = state.debugLogging; + + // Initialize variables to hold version information, release notes, etc. + String? huaweiStoreListingURL; + Version? huaweiStoreVersion; + bool? isCriticalUpdate; + Version? minAppVersion; + String? releaseNotes; + + // Fetch app information from Huawei Store by appId + final AppInfoResponse? appInfoResponse = await huaweiStore.lookupById( + appId: appId, + clientId: clientId, + clientSecret: clientSecret, + ); + + // If a valid response is received, process the version details + if (appInfoResponse != null && appInfoResponse.ret?.code == 0) { + // Parse the app version + final version = appInfoResponse.appInfo?.versionNumber; + if (version != null) { + try { + huaweiStoreVersion = Version.parse(version); + } catch (e) { + if (state.debugLogging) { + print( + 'upgrader: UpgraderHuaweiStore.appStoreVersion "$version" exception: $e'); + } + } + } + + // Build the AppGallery listing URL + huaweiStoreListingURL ??= huaweiStore.lookupURLById( + appId, + language: language, + country: country, + ); + + // Extract release notes from the first available language + releaseNotes ??= appInfoResponse.languages?[0].newFeatures; + + // Simulate minimum supported app version if not available in app info + String appDesc = "${appInfoResponse.languages?[0].appDesc ?? ''} " + "[Minimum supported app version: 3.0.1]"; + + // Extract and validate the minimum supported app version + final mav = huaweiStore.minAppVersion(appDesc); + if (mav != null) { + try { + minAppVersion = Version.parse(mav.toString()); + + if (state.debugLogging) { + print( + 'upgrader: UpgraderHuaweiStore.minAppVersion: $minAppVersion'); + } + } catch (e) { + if (state.debugLogging) { + print('upgrader: UpgraderHuaweiStore.minAppVersion exception: $e'); + } + } + } + } else { + // Log an error if the app info response is null or unsuccessful + print('Upgrader Huawei Store response: null or failed'); + } + + // Construct version information to be returned + final versionInfo = UpgraderVersionInfo( + installedVersion: installedVersion, + appStoreListingURL: huaweiStoreListingURL, + appStoreVersion: huaweiStoreVersion, + isCriticalUpdate: isCriticalUpdate, + minAppVersion: minAppVersion, + releaseNotes: releaseNotes, + ); + + if (state.debugLogging) { + print('upgrader: UpgraderHuaweiStore: version info: $versionInfo'); + } + + return versionInfo; + } +} + class UpgraderAppcastStore extends UpgraderStore { UpgraderAppcastStore({ required this.appcastURL, diff --git a/pubspec.yaml b/pubspec.yaml index f2d087e3..832aade8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,13 +30,14 @@ dependencies: shared_preferences: '>=2.1.1 <3.0.0' # From Flutter Team: A Flutter plugin for launching a URL in the mobile platform. - url_launcher: ^6.1.11 + url_launcher: ^6.3.0 # A dart library for comparing and incrementing version numbers with Semantic Versioning. version: ^3.0.2 # A dart library for parsing, traversing, querying, transforming and building XML documents. xml: ^6.3.0 + json_annotation: ^4.9.0 dev_dependencies: flutter_test: @@ -47,4 +48,8 @@ dev_dependencies: flutter_lints: ^4.0.0 go_router: ^14.2.7 + build_runner: ^2.4.12 + json_serializable: ^6.8.0 + + flutter: