import 'dart:async'; import 'dart:collection'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; import 'package:flutter/rendering.dart'; import 'package:guru_analytics_flutter/events_constants.dart'; import 'package:guru_app/account/account_data_store.dart'; import 'package:guru_app/api/data/orders/orders_model.dart'; import 'package:guru_app/api/guru_api.dart'; import 'package:guru_app/database/guru_db.dart'; import 'package:guru_app/financial/asset/assets_model.dart'; import 'package:guru_app/financial/asset/assets_store.dart'; import 'package:guru_app/financial/data/db/order_database.dart'; import 'package:guru_app/financial/iap/iap_model.dart'; import 'package:guru_app/financial/manifest/manifest.dart'; import 'package:guru_app/financial/manifest/manifest_manager.dart'; import 'package:guru_app/financial/product/product_model.dart'; import 'package:guru_app/financial/product/product_store.dart'; import 'package:guru_app/firebase/firebase.dart'; import 'package:guru_app/guru_app.dart'; import 'package:guru_app/property/app_property.dart'; import 'package:guru_app/property/property_keys.dart'; import 'package:guru_app/property/settings/guru_settings.dart'; import 'package:guru_utils/datetime/datetime_utils.dart'; import 'package:guru_utils/extensions/extensions.dart'; import 'package:guru_utils/math/math_utils.dart'; import 'package:guru_utils/tuple/tuple.dart'; import 'package:in_app_purchase/in_app_purchase.dart'; import 'package:in_app_purchase_android/billing_client_wrappers.dart'; import 'package:in_app_purchase_android/in_app_purchase_android.dart'; import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart'; import 'package:in_app_purchase_storekit/store_kit_wrappers.dart'; /// Created by Haoyi on 2022/6/10 /// enum IapCause { success, error, canceled } class IapManager { static final IapManager _instance = IapManager._(); static IapManager get instance => _instance; static final ProductDetailsResponse _emptyResponse = ProductDetailsResponse(productDetails: [], notFoundIDs: [], error: null); final BehaviorSubject> _productDetailsSubject = BehaviorSubject.seeded({}); final BehaviorSubject> _iapStoreSubject = BehaviorSubject.seeded(AssetsStore.inactive()); final Map iapRequestMap = HashMap(); Stream> get observableProductDetails => _productDetailsSubject.stream; Stream> get observableAssetStore => _iapStoreSubject.stream; Map get loadedProductDetails => _productDetailsSubject.value; AssetsStore get purchasedStore => _iapStoreSubject.value; final BehaviorSubject availableSubject = BehaviorSubject.seeded(false); Stream get observableAvailable => availableSubject.stream; bool get iapAvailable => availableSubject.value; final InAppPurchase _inAppPurchase; StreamSubscription? subscription; Timer? restorePointsTimer; IapManager._() : _inAppPurchase = InAppPurchase.instance; IapCause latestIapCause = IapCause.success; bool _restorePurchase = false; final iapRevenueAppEventOptions = AppEventOptions( capabilities: const AppEventCapabilities(AppEventCapabilities.firebase | AppEventCapabilities.guru), firebaseParamsConvertor: _iapRevenueToValue, guruParamsConvertor: _iapRevenueToValue); static Map _iapRevenueToValue(Map params) { final result = Map.of(params); final revenue = result.remove("revenue"); if (revenue != null) { result["value"] = revenue; } return result; } void init() async { final iapCount = await AppProperty.getInstance().getIapCount(); if (iapCount > 0) { GuruAnalytics.instance.setUserProperty("purchase_count", iapCount.toString()); GuruAnalytics.instance.setUserProperty("is_iap_user", "true"); } else { GuruAnalytics.instance.setUserProperty("is_iap_user", "false"); } try { await reloadOrders(); } catch (error, stacktrace) { Log.w("reloadOrders error! $error", stackTrace: stacktrace, syncCrashlytics: true, syncFirebase: true); } if (subscription == null) { final Stream> purchaseUpdated = _inAppPurchase.purchaseStream; subscription = purchaseUpdated.listen( (List purchaseDetailsList) { _listenToPurchaseUpdated(purchaseDetailsList); }, onDone: () {}, onError: (Object error) { // handle error here. Log.e("iap error:$error"); }); Log.i("iap service initialize completed"); } Log.i("iap service initialized"); _checkAndLoad(); try { await AccountDataStore.instance.observableSaasUser .firstWhere((saasUser) => saasUser?.isValid == true); Future.delayed(const Duration(seconds: 5), () { reportFailedOrders(); }); } catch (error, stacktrace) { Log.w("wait account error! $error", stackTrace: stacktrace); } finally {} } Future switchSession() async { final iapCount = await AppProperty.getInstance().getIapCount(); if (iapCount > 0) { GuruAnalytics.instance.setUserProperty("purchase_count", iapCount.toString()); GuruAnalytics.instance.setUserProperty("is_iap_user", "true"); } else { GuruAnalytics.instance.setUserProperty("is_iap_user", "false"); } try { await reloadOrders(); } catch (error, stacktrace) { Log.w("reloadOrders error! $error", stackTrace: stacktrace, syncCrashlytics: true, syncFirebase: true); } _checkAndLoad(); } Future reloadOrders() async { final transactions = await GuruDB.instance.selectOrders( method: TransactionMethod.iap, attrs: [TransactionAttributes.asset, TransactionAttributes.subscriptions]); final newAssetStore = AssetsStore(); Log.d("reloadOrders ${transactions.length}"); for (var transaction in transactions) { final productId = transaction.productId; Log.d(" ==> reloadOrder:${transaction.sku} $productId"); newAssetStore.addAsset(Asset(productId, transaction)); } _iapStoreSubject.addEx(newAssetStore); } void _checkAndLoad() async { var available = false; var retry = 0; Log.i("_checkAndLoad"); do { final seconds = min(MathUtils.fibonacci(retry, offset: 2), 900); await Future.delayed(Duration(seconds: seconds)); available = await _inAppPurchase.isAvailable().catchError((error, stacktrace) { Log.w("isAvailable error:$error", stackTrace: stacktrace); return false; }); Log.d("_checkAndLoad:$retry available:$available"); retry++; } while (!available); availableSubject.addIfChanged(true); try { await refreshProducts(); if (GuruApp.instance.appSpec.deployment.autoRestoreIap || GuruApp.instance.appSpec.productProfile.hasSubs()) { await restorePurchases(); } } catch (error, stacktrace) { Log.w("restorePurchases error:$error", stackTrace: stacktrace, syncCrashlytics: true, syncFirebase: true); } } Future isAvailable() async { return await _inAppPurchase.isAvailable(); } void _processIapError() async { latestIapCause = IapCause.error; for (var iapRequest in iapRequestMap.values) { iapRequest.response(false); final iapErrorMsg = "_processIapError:${iapRequest.productId}"; Log.w(iapErrorMsg, error: PurchaseError(iapErrorMsg), syncFirebase: true, syncCrashlytics: true); try { await GuruDB.instance.upsertOrder(order: iapRequest.order.error(iapErrorMsg)); } catch (error, stacktrace) { Log.w("_processIapError upsert error! $error", syncFirebase: true); } } iapRequestMap.clear(); } void _processIapCancel() async { latestIapCause = IapCause.canceled; Log.d("_processIapCancel"); for (var iapRequest in iapRequestMap.values) { final order = iapRequest.order; iapRequest.response(false); try { await GuruDB.instance.deleteOrder(order: order); } catch (error, stacktrace) { Log.w("_processIapCancel deleteOrder error! $error", syncFirebase: true); } } iapRequestMap.clear(); // final iapErrorMsg = "_processIapCancel:$productId"; // Log.w(iapErrorMsg, // error: PurchaseError(iapErrorMsg), syncFirebase: true, syncCrashlytics: true); } // void _listenToPurchased() async { // InAppPurchase.instance.purchaseStream.listen((purchaseDetailsList) { // if (purchaseDetailsList.isEmpty) { // return; // } // final subscriptionDetails = {}; // for (var details in purchaseDetailsList) { // Log.d(" details:${details.productID} ${details.status}"); // final productId = // GuruApp.instance.findProductId(sku: details.productID) ?? ProductId.invalid; // if (productId.isSubscription) { // subscriptionDetails[productId] = details; // } // } // if (Platform.isIOS) { // checkSubscriptionForIos(subscriptionDetails); // } // }); // } String dumpProductAndPurchased(ProductDetails details, PurchaseDetails purchaseDetails) { final StringBuffer sb = StringBuffer(); if (Platform.isAndroid) { try { GooglePlayPurchaseDetails googlePlayDetails = purchaseDetails as GooglePlayPurchaseDetails; GooglePlayProductDetails googlePlayProduct = details as GooglePlayProductDetails; Log.d( "Android Product/Purchase ${buildGooglePlayDetailsString(googlePlayProduct, googlePlayDetails)}"); } catch (error, stacktrace) {} } else if (Platform.isIOS) { AppStorePurchaseDetails appleDetails = purchaseDetails as AppStorePurchaseDetails; AppStoreProductDetails appleProduct = details as AppStoreProductDetails; sb.writeln("#### purchase ####"); sb.writeln("productID: ${appleDetails.productID}"); sb.writeln("purchaseID: ${appleDetails.purchaseID}"); sb.writeln("transactionDate: ${appleDetails.transactionDate}"); sb.writeln("verificationData: ${appleDetails.verificationData}"); sb.writeln("status: ${appleDetails.status}"); sb.writeln("skPaymentTransaction:"); sb.writeln( " =>originalTransaction:${appleDetails.skPaymentTransaction.originalTransaction}"); sb.writeln(" =>${appleDetails.skPaymentTransaction.originalTransaction?.payment}"); sb.writeln(" =>${appleDetails.skPaymentTransaction.transactionState}:"); sb.writeln(" =>${appleDetails.skPaymentTransaction.transactionIdentifier}:"); sb.writeln("\n#### product ####"); sb.writeln("currencyCode: ${appleProduct.currencyCode}"); sb.writeln("rawPrice: ${appleProduct.rawPrice}"); sb.writeln("currencyCode: ${appleProduct.currencyCode}"); sb.writeln("currencyCode skProduct"); sb.writeln(" =>localizedTitle: ${appleProduct.skProduct.localizedTitle}"); sb.writeln(" =>localizedDescription: ${appleProduct.skProduct.localizedDescription}"); sb.writeln(" =>priceLocale: ${appleProduct.skProduct.priceLocale}"); sb.writeln(" =>productIdentifier: ${appleProduct.skProduct.productIdentifier}"); sb.writeln( " =>subscriptionGroupIdentifier: ${appleProduct.skProduct.subscriptionGroupIdentifier}"); sb.writeln(" =>appleProduct.skProduct.priceLocale"); sb.writeln(" ->{appleProduct.skProduct.priceLocale}"); Log.d("IOS Product/Purchase ${sb.toString()}"); } return sb.toString(); } static final monthRenewalDurations = [ 3 * DateTimeUtils.minuteInMillis, 5 * DateTimeUtils.minuteInMillis, 15 * DateTimeUtils.minuteInMillis, 30 * DateTimeUtils.minuteInMillis, DateTimeUtils.hourInMillis ]; static final weekRenewalDurations = [ 3 * DateTimeUtils.minuteInMillis, 3 * DateTimeUtils.minuteInMillis, 5 * DateTimeUtils.minuteInMillis, 10 * DateTimeUtils.minuteInMillis, 15 * DateTimeUtils.minuteInMillis ]; int getIOSPeriodInterval(int numberOfUnits, SKSubscriptionPeriodUnit unit) { if (GuruSettings.instance.debugMode.get()) { final renewalSpeed = GuruApp.instance.appSpec.deployment.iosSandboxSubsRenewalSpeed.clamp(1, 5); switch (unit) { case SKSubscriptionPeriodUnit.day: return numberOfUnits * weekRenewalDurations[renewalSpeed - 1] ~/ 7; case SKSubscriptionPeriodUnit.week: return numberOfUnits * weekRenewalDurations[renewalSpeed - 1]; case SKSubscriptionPeriodUnit.month: return numberOfUnits * monthRenewalDurations[renewalSpeed - 1]; case SKSubscriptionPeriodUnit.year: return numberOfUnits * monthRenewalDurations[renewalSpeed - 1] * 12; } } else { switch (unit) { case SKSubscriptionPeriodUnit.day: return numberOfUnits * DateTimeUtils.dayInMillis; case SKSubscriptionPeriodUnit.week: return numberOfUnits * DateTimeUtils.weekInMillis; case SKSubscriptionPeriodUnit.month: return numberOfUnits * DateTimeUtils.dayInMillis * 31; case SKSubscriptionPeriodUnit.year: return numberOfUnits * DateTimeUtils.dayInMillis * 366; } } } bool checkSubscriptionPeriod(PurchaseDetails purchaseDetails, ProductDetails productDetails) { bool validOrder = false; if (Platform.isAndroid) { validOrder = true; } else if (Platform.isIOS) { final appleProduct = productDetails as AppStoreProductDetails; SKProductSubscriptionPeriodWrapper? period = appleProduct.skProduct.subscriptionPeriod; final appleDetails = purchaseDetails as AppStorePurchaseDetails; final paymentDiscount = appleDetails.skPaymentTransaction.payment.paymentDiscount; if (paymentDiscount != null) { Log.i("paymentDiscount: ${paymentDiscount.identifier} ${paymentDiscount.timestamp}"); for (var discount in appleProduct.skProduct.discounts) { final discountSubPeriod = discount.subscriptionPeriod; Log.i( "check discount(${paymentDiscount.identifier}) product [ ${discount.identifier} ${discountSubPeriod.unit} ${discountSubPeriod.numberOfUnits} ]"); if (discount.identifier == paymentDiscount.identifier) { period = discount.subscriptionPeriod; break; } } } Log.i("checkSubscriptionPeriod: ${appleProduct.skProduct.productIdentifier} ${period?.unit} ${period?.numberOfUnits} ${appleDetails.transactionDate}"); if (period != null) { final numberOfUnits = period.numberOfUnits; final unit = period.unit; final int validInterval = getIOSPeriodInterval(numberOfUnits, unit); final transactionTs = int.tryParse(purchaseDetails.transactionDate ?? "") ?? 0; final now = DateTimeUtils.currentTimeInMillis(); final gracePeriod = GuruApp.instance.remoteDeployment.subscriptionGraceDays ?? GuruApp.instance.appSpec.deployment.subscriptionGraceDays; /// 过期时间 = 订单的最后一次刷新时间 + 订阅周期(优惠周期) + 宽限期 final expiredTs = transactionTs + validInterval + gracePeriod; /// 如果当前的时间小于过期时间,那么这个订单是有效的 validOrder = now < expiredTs; Log.d( "productID: ${purchaseDetails.productID}) purchaseID: ${purchaseDetails.purchaseID}[$numberOfUnits][$unit] [$now < $transactionTs + $validInterval]($validOrder) ", tag: PropertyTags.iap); } } return validOrder; } Future processRestoredSubscription(List subscriptionPurchased) async { List purchasedDetails = subscriptionPurchased; if (Platform.isIOS) { purchasedDetails = buildLatestPurchasedPlanForIos(subscriptionPurchased); } final newPurchasedStore = purchasedStore.clone(); final expiredSkus = {}; // 由于Android的订阅项目在失效后,这里将不会返回,因此需要判断这里的newPurchasedStore是否存在对应的purchased // 如果存在将会在后面进行处理,如果不存在。这里将会从purchasedStore中删除 if (Platform.isAndroid) { final purchasedSkus = purchasedDetails.map((e) => e.productID).toSet(); newPurchasedStore.removeWhere((productId, asset) { final expired = productId.isSubscription && !purchasedSkus.contains(productId.sku); Log.i("remove expired subscription[$productId] expired:$expired"); if (expired) { expiredSkus.add(asset.productId.sku); } return expired; }); } for (var purchased in purchasedDetails) { final productId = GuruApp.instance.findProductId(sku: purchased.productID); if (productId == null) { Log.w("productId is null! ${purchased.productID}"); continue; } final productDetails = loadedProductDetails[productId]; if (productDetails == null) { Log.w("product is null! ${purchased.productID}"); continue; } final bool validPurchase = checkSubscriptionPeriod(purchased, productDetails); if (validPurchase) { Log.d( "[Restored Subscription] productID: ${purchased.productID}) purchaseID: ${purchased.purchaseID}", tag: PropertyTags.iap); final asset = newPurchasedStore.getAsset(productId); late OrderEntity newOrder; if (asset == null) { final product = await _createProduct(productId.createIntent(scene: "restore"), productDetails); newOrder = product.createOrder().success(); } else { newOrder = asset.order.success(); } try { await GuruDB.instance.replaceOrderBySku(order: newOrder); } catch (error, stacktrace) { Log.w("Failed to upsert order: $error $stacktrace", tag: PropertyTags.iap); } final newAsset = Asset(productId, newOrder); newPurchasedStore.addAsset(newAsset); } else { expiredSkus.add(productId.sku); Log.d( "Subscription is expired ${purchased.productID}) ${purchased.purchaseID} ${purchased.transactionDate}"); // 这里暂不清newPurchasedStore,下次重进后该订阅信息会失效 } } if (expiredSkus.isNotEmpty) { final graceCount = await AppProperty.getInstance().increaseGraceCount(); Log.i("expired orders:${expiredSkus.length}} grace count: $graceCount"); if (graceCount > GuruApp.instance.appSpec.deployment.subscriptionRestoreGraceCount) { try { await GuruDB.instance.deleteOrdersBySkus(expiredSkus); } catch (error, stacktrace) { Log.w("Failed to upsert order: $error $stacktrace", tag: PropertyTags.iap); } await AppProperty.getInstance().resetGraceCount(); GuruAnalytics.instance.logGuruEvent('dev_iap_audit', { "item_category": "expired", "item_name": "sub", "platform": Platform.isAndroid ? "Android" : "iOS", "value": graceCount }); } } else { await AppProperty.getInstance().resetGraceCount(); } _iapStoreSubject.addEx(newPurchasedStore); Log.d("[RestoredSubscription] update purchasedStore ${newPurchasedStore.data.length}"); } List buildLatestPurchasedPlanForIos(List purchaseDetails) { if (purchaseDetails.isEmpty) { return []; } final rawTransactionIds = purchaseDetails .map((details) => (details as AppStorePurchaseDetails) .skPaymentTransaction .originalTransaction ?.transactionIdentifier) .where((element) => element != null) .cast() .toSet(); Log.d("rawTransactionIds:$rawTransactionIds"); final sortedPurchaseDetails = purchaseDetails.toList(); sortedPurchaseDetails.sort((a, b) => (int.tryParse(b.transactionDate ?? '') ?? 0) .compareTo(int.tryParse(a.transactionDate ?? '') ?? 0)); sortedPurchaseDetails.retainWhere((details) { var detail = details as AppStorePurchaseDetails; Log.d( "checkSubscriptionForIos ${detail.skPaymentTransaction.originalTransaction?.transactionIdentifier} ${detail.transactionDate} ${detail.skPaymentTransaction.transactionTimeStamp}"); return rawTransactionIds .remove(detail.skPaymentTransaction.originalTransaction?.transactionIdentifier); }); return sortedPurchaseDetails; } void checkSubscriptionForIos(List purchaseDetails) { if (purchaseDetails.isEmpty) { return; } final rawTransactionIds = purchaseDetails .map((details) => (details as AppStorePurchaseDetails) .skPaymentTransaction .originalTransaction ?.transactionIdentifier) .where((element) => element != null) .cast() .toSet(); Log.d("rawTransactionIds:$rawTransactionIds"); final sortedPurchaseDetails = purchaseDetails.toList(); sortedPurchaseDetails.sort((a, b) => (int.tryParse(b.transactionDate ?? '') ?? 0) .compareTo(int.tryParse(a.transactionDate ?? '') ?? 0)); sortedPurchaseDetails.retainWhere((details) { var detail = details as AppStorePurchaseDetails; Log.d( "checkSubscriptionForIos ${detail.skPaymentTransaction.originalTransaction?.transactionIdentifier} ${detail.transactionDate} ${detail.skPaymentTransaction.transactionTimeStamp}"); return rawTransactionIds .remove(detail.skPaymentTransaction.originalTransaction?.transactionIdentifier); }); for (var details in sortedPurchaseDetails) { Log.d( "checkSubscriptionForIos ${details.productID} ${details.status} ${details.transactionDate}"); final productId = GuruApp.instance.findProductId(sku: details.productID); final productDetails = loadedProductDetails[productId]; if (productDetails != null) { dumpProductAndPurchased(productDetails, details); } } } void _listenToPurchaseUpdated(List purchaseDetailsList) async { final List> restoredIapPurchases = []; final List> pendingCompletePurchase = []; final List subscriptionPurchases = []; bool existsRestored = false; bool needRestore = false; Log.d("_listenToPurchaseUpdated ${purchaseDetailsList.length}"); if (purchaseDetailsList.isEmpty) { if (_restorePurchase) { try { await processRestoredSubscription(subscriptionPurchases); } catch (error, stacktrace) { Log.w( "purchaseDetailsList is EMPTY! processRestoredSubscription error! $error $stacktrace", syncFirebase: true, syncCrashlytics: true); } _restorePurchase = false; } return; } for (var details in purchaseDetailsList) { final productId = GuruApp.instance.findProductId(sku: details.productID) ?? ProductId.invalid; Log.d( "[details]: $productId [${details.productID}] ${details.purchaseID} ${details.status} ${details.pendingCompletePurchase}"); GuruAnalytics.instance.logGuruEvent('dev_iap_update', { "sku": details.productID, "orderId": details.purchaseID, "status": "${details.status.index}" }); switch (details.status) { case PurchaseStatus.purchased: if (details.productID == "") { if (GuruApp.instance.productProfile.pointsIapIds.isNotEmpty) { Log.w( "details.productID is empty And Exists PointsIap! ${details.purchaseID}! need restore"); needRestore = true; } else { Log.w("details.productID is empty ${details.purchaseID}! ignore!!"); } continue; } final productDetails = loadedProductDetails[productId]; if (productDetails != null) { /// 如果是 IOS的 purchased订单,并且是订阅的订单,他又没在当前请求的列表中,证明他是一个恢复的订单 if (Platform.isIOS && productId.isSubscription && !iapRequestMap.containsKey(productId)) { subscriptionPurchases.add(details); existsRestored = true; } else { await _completePurchase(productId, productDetails, details); } } Log.d("completePurchase ${details.productID} ${details.purchaseID}"); break; case PurchaseStatus.restored: _restorePurchase = false; existsRestored = true; if (productId.isAsset) { restoredIapPurchases.add(Tuple2(productId, details)); Log.d("restore possessive iap:$productId"); } else if (productId.isSubscription) { Log.w("restore subscription product!", syncFirebase: true); subscriptionPurchases.add(details); } // 如果是未完成的商品或是恢复出了消耗品,都需要手动完成 if (Platform.isAndroid) { final originPurchaseState = (details as GooglePlayPurchaseDetails).billingClientPurchase.purchaseState; Log.d( "restore android ${details.pendingCompletePurchase} $productId $originPurchaseState"); if (originPurchaseState == PurchaseStateWrapper.purchased) { if (productId.isConsumable || (details.pendingCompletePurchase && productId.isAsset)) { Log.w("restore consumable product!", syncFirebase: true); pendingCompletePurchase.add(Tuple2(productId, details)); } } } else { if (details.pendingCompletePurchase) { Log.d("restore ios pendingCompletePurchase: $productId"); await _inAppPurchase.completePurchase(details); } } break; case PurchaseStatus.error: _processIapError(); if (details.pendingCompletePurchase) { await _inAppPurchase.completePurchase(details); } break; case PurchaseStatus.canceled: _processIapCancel(); if (details.pendingCompletePurchase) { await _inAppPurchase.completePurchase(details); } break; default: break; } Log.d( "_listenToPurchaseUpdated2:$productId [${details.productID}] ${details.purchaseID} ${details.status} ${details.pendingCompletePurchase}"); } if (existsRestored) { if (pendingCompletePurchase.isNotEmpty) { await completeAllPurchases(pendingCompletePurchase); Log.d("manual complete/consume all purchases!", syncFirebase: true, syncCrashlytics: true); } if (restoredIapPurchases.isNotEmpty) { try { await processRestoredPurchases(restoredIapPurchases); } catch (error, stacktrace) { Log.w("processRestoredPurchases error! $error $stacktrace", syncFirebase: true, syncCrashlytics: true); } } try { await processRestoredSubscription(subscriptionPurchases); } catch (error, stacktrace) { Log.w("processRestoredSubscription error! $error $stacktrace", syncFirebase: true, syncCrashlytics: true); } } if (needRestore) { restorePointsTimer?.cancel(); restorePointsTimer = Timer(const Duration(seconds: 1), () { restorePurchases(); }); } } Future processRestoredPurchases( List> restoredIapPurchases) async { final newPurchased = purchasedStore.clone(); final currentLoadedProductDetails = loadedProductDetails; final upsertOrders = []; for (var iapPurchased in restoredIapPurchases) { final productId = iapPurchased.item1; final asset = newPurchased.getAsset(iapPurchased.item1); final productDetails = currentLoadedProductDetails[productId]; final order = asset?.order; // 证明是已经购买过的 if (order != null) { // 如果没有购买成功,那么就重新创建一个 if (!order.isSuccess) { final newOrder = order.success(); upsertOrders.add(newOrder); } } else if (productDetails != null) { final product = await _createProduct(productId.createIntent(scene: "restore"), productDetails); final newOrder = product.createOrder().success(); upsertOrders.add(newOrder); } } if (upsertOrders.isNotEmpty) { final List updatedOrder = []; try { await GuruDB.instance.upsertOrders(upsertOrders); updatedOrder.addAll(upsertOrders); } catch (error, stacktrace) { Log.w("upsertOrders error:$error $stacktrace", syncCrashlytics: true, syncFirebase: true); for (var order in upsertOrders) { try { await GuruDB.instance.upsertOrder(order: order); updatedOrder.add(order); } catch (error1, stacktrace1) { Log.w("upsertOrder(${order.sku}) error:$error1 $stacktrace1", syncFirebase: true); } } } final assets = updatedOrder.map((order) => Asset(order.productId, order)).toList(); newPurchased.addAllAssets(assets); } _iapStoreSubject.addEx(newPurchased); Log.d("[RestoredPurchases] update purchasedStore ${upsertOrders.length}"); } Future reportFailedOrders() async { final failedIapOrders = await AppProperty.getInstance().loadAllFailedIapOrders(); failedIapOrders.forEach((key, value) async { try { final order = OrdersReport.fromJson(json.decode(value)); final result = await GuruApi.instance.reportOrders(order); // 这里不管返回什么值,都认为是成功的, 只有崩溃才会返回失败 await logRevenue(order, result); AppProperty.getInstance().removeReportSuccessOrder(key); } catch (error, stacktrace) {} }); Log.i("reportFailedOrders success!"); } String buildGooglePlayDetailsString( GooglePlayProductDetails googlePlayProduct, GooglePlayPurchaseDetails googlePlayDetails) { final StringBuffer sb = StringBuffer(); sb.writeln("#### purchase ####"); sb.writeln("productID: ${googlePlayDetails.productID}"); sb.writeln("purchaseID: ${googlePlayDetails.purchaseID}"); sb.writeln("transactionDate: ${googlePlayDetails.transactionDate}"); sb.writeln("status: ${googlePlayDetails.status}"); sb.writeln("verificationData:"); sb.writeln( " => localVerificationData: ${googlePlayDetails.verificationData.localVerificationData}"); sb.writeln( " => serverVerificationData: ${googlePlayDetails.verificationData.localVerificationData}"); sb.writeln(" => source: ${googlePlayDetails.verificationData.source}"); sb.writeln("\n#### product ####"); sb.writeln("price: ${googlePlayProduct.price}"); sb.writeln("rawPrice: ${googlePlayProduct.rawPrice}"); sb.writeln("currencyCode: ${googlePlayProduct.currencyCode}"); sb.writeln("currencySymbol: ${googlePlayProduct.currencySymbol}"); sb.writeln("productDetails:"); final productDetails = googlePlayProduct.productDetails; sb.writeln(" => description: ${productDetails.name}"); sb.writeln(" => freeTrialPeriod: ${productDetails.title}"); sb.writeln(" => description: ${productDetails.description}"); sb.writeln(" => freeTrialPeriod: ${productDetails.productType}"); final oneTimeDetails = productDetails.oneTimePurchaseOfferDetails; if (oneTimeDetails != null) { sb.writeln(" => oneTimeDetails:"); sb.writeln(" - formattedPrice: ${oneTimeDetails.formattedPrice}"); sb.writeln(" - priceAmountMicros: ${oneTimeDetails.priceAmountMicros}"); sb.writeln(" - priceCurrencyCode: ${oneTimeDetails.priceCurrencyCode}"); } final subscriptionOfferDetails = productDetails.subscriptionOfferDetails; if (subscriptionOfferDetails != null && subscriptionOfferDetails.isNotEmpty) { for (var offer in subscriptionOfferDetails) { sb.writeln(" => sub offer: ${offer.offerId}"); sb.writeln(" - basePlanId: ${offer.basePlanId}"); sb.writeln(" - offerTag: ${offer.offerTags}"); sb.writeln(" - offerIdToken: ${offer.offerIdToken}"); final pricingPhases = offer.pricingPhases; for (var idx = 0; idx < pricingPhases.length; ++idx) { final phase = pricingPhases[idx]; sb.writeln(" - pricingPhase[$idx]:"); sb.writeln(" * billingCycleCount: ${phase.billingCycleCount}"); sb.writeln(" * billingPeriod: ${phase.billingPeriod}"); sb.writeln(" * formattedPrice: ${phase.formattedPrice}"); sb.writeln(" * priceAmountMicros: ${phase.priceAmountMicros}"); sb.writeln(" * priceCurrencyCode: ${phase.priceCurrencyCode}"); sb.writeln(" * recurrenceMode: ${phase.recurrenceMode}"); } } } return sb.toString(); } Future reportOrders(ProductId productId, ProductDetails details, PurchaseDetails purchaseDetails, OrderEntity? order) async { final OrdersReport ordersReport = OrdersReport(); if (Platform.isAndroid) { ordersReport.token = purchaseDetails.verificationData.serverVerificationData; ordersReport.packageName = GuruApp.instance.details.packageName; final manifest = order?.manifest; final basePlanId = manifest?.basePlanId; final offerId = manifest?.offerId; if (productId.isSubscription && basePlanId != null && offerId != null) { ordersReport.basePlanId = basePlanId; ordersReport.offerId = offerId; } try { GooglePlayPurchaseDetails googlePlayDetails = purchaseDetails as GooglePlayPurchaseDetails; GooglePlayProductDetails googlePlayProduct = details as GooglePlayProductDetails; Log.d( "Android Product/Purchase ${buildGooglePlayDetailsString(googlePlayProduct, googlePlayDetails)}"); } catch (error, stacktrace) {} } else if (Platform.isIOS) { AppStorePurchaseDetails appleDetails = purchaseDetails as AppStorePurchaseDetails; AppStoreProductDetails appleProduct = details as AppStoreProductDetails; final StringBuffer sb = StringBuffer(); sb.writeln("#### purchase ####"); sb.writeln("productID: ${appleDetails.productID}"); sb.writeln("purchaseID: ${appleDetails.purchaseID}"); sb.writeln("transactionDate: ${appleDetails.transactionDate}"); sb.writeln("verificationData: ${appleDetails.verificationData}"); sb.writeln("status: ${appleDetails.status}"); sb.writeln("skPaymentTransaction:"); sb.writeln( " =>originalTransaction:${appleDetails.skPaymentTransaction.originalTransaction}"); sb.writeln(" =>${appleDetails.skPaymentTransaction.originalTransaction?.payment}"); sb.writeln(" =>${appleDetails.skPaymentTransaction.transactionState}:"); sb.writeln(" =>${appleDetails.skPaymentTransaction.transactionIdentifier}:"); sb.writeln("\n#### product ####"); sb.writeln("currencyCode: ${appleProduct.currencyCode}"); sb.writeln("rawPrice: ${appleProduct.rawPrice}"); sb.writeln("currencyCode: ${appleProduct.currencyCode}"); sb.writeln("currencyCode skProduct"); sb.writeln(" =>localizedTitle: ${appleProduct.skProduct.localizedTitle}"); sb.writeln(" =>localizedDescription: ${appleProduct.skProduct.localizedDescription}"); sb.writeln(" =>priceLocale: ${appleProduct.skProduct.priceLocale}"); sb.writeln(" =>productIdentifier: ${appleProduct.skProduct.productIdentifier}"); sb.writeln( " =>subscriptionGroupIdentifier: ${appleProduct.skProduct.subscriptionGroupIdentifier}"); sb.writeln(" =>appleProduct.skProduct.priceLocale"); sb.writeln(" ->{appleProduct.skProduct.priceLocale}"); ordersReport.bundleId = GuruApp.instance.appSpec.details.bundleId; ordersReport.receipt = purchaseDetails.verificationData.serverVerificationData; ordersReport.sku = appleDetails.productID; ordersReport.countryCode = appleProduct.skProduct.priceLocale.countryCode; Log.d("IOS Product/Purchase ${sb.toString()}"); } if (productId.isSubscription) { ordersReport.orderType = OrderType.subs; ordersReport.subscriptionId = details.id; } else { ordersReport.orderType = OrderType.inapp; ordersReport.productId = details.id; } ordersReport.orderId = purchaseDetails.purchaseID; ordersReport.price = details.rawPrice.toString(); ordersReport.currency = details.currencyCode; ordersReport.orderUserInfo = OrderUserInfo(GuruSettings.instance.bestLevel.get().toString()); ordersReport.userIdentification = GuruAnalytics.instance.userIdentification; final transactionDate = purchaseDetails.transactionDate; if (transactionDate != null && transactionDate.isNotEmpty) { try { final ts = int.tryParse(transactionDate) ?? DateTimeUtils.currentTimeInMillis(); ordersReport.transactionDate = ts; } catch (error, stacktrace) { Log.w("parse transactionDate error! $error", stackTrace: stacktrace); } } Log.d("orderReport:$ordersReport", tag: "Iap"); try { final result = await GuruApi.instance.reportOrders(ordersReport); // 这里不管返回什么值,都认为是成功的 await logRevenue(ordersReport, result); return; } catch (error, stacktrace) { Log.i("reportOrders error!", error: error, stackTrace: stacktrace); } AppProperty.getInstance().saveFailedIapOrders(ordersReport); } Future logRevenue(OrdersReport order, OrdersResponse result) async { final isSubscription = order.orderType == OrderType.subs; final sku = (isSubscription ? order.subscriptionId : order.productId) ?? (order.productId ?? order.subscriptionId ?? order.sku); final usdPrice = result.usdPrice; if (sku == null || sku.isEmpty) { return false; } if (!result.isTestOrder && usdPrice <= 0) { Log.i("ignoreInvalidResult $result", tag: "Iap"); return false; } Log.i("prepare logRevenue! $result $sku"); final platform = Platform.isIOS ? "appstore" : "google_play"; if (GuruApp.instance.appSpec.deployment.purchaseEventTrigger == 2) { GuruAnalytics.instance.logAdRevenue020(usdPrice, platform, "USD", orderId: order.orderId, orderType: isSubscription ? "SUB" : "IAP", productId: sku, transactionDate: order.transactionDate); } else { GuruAnalytics.instance.logAdRevenue(usdPrice, platform, "USD", orderId: order.orderId, orderType: isSubscription ? "SUB" : "IAP", productId: sku, transactionDate: order.transactionDate); } GuruAnalytics.instance .logPurchase(usdPrice, currency: 'USD', contentId: sku, adPlatform: platform); if (isSubscription) { GuruAnalytics.instance.logEvent( "sub_purchase", { "platform": platform, "currency": "USD", "revenue": usdPrice, "product_id": sku, "order_type": "SUB", "order_id": order.orderId, "trans_ts": order.transactionDate }, options: iapRevenueAppEventOptions); } else { GuruAnalytics.instance.logEvent( "iap_purchase", { "platform": platform, "currency": "USD", "revenue": usdPrice, "product_id": sku, "order_type": "IAP", "order_id": order.orderId, "trans_ts": order.transactionDate }, options: iapRevenueAppEventOptions); } GuruAnalytics.instance.logGuruEvent( "dev_iap_action", {"item_category": "reported", "item_name": sku, "result": "true"}); Log.i("reportOrders completed! logRevenue success! $result $sku"); return true; } Future _deliverManifest(ProductId productId, Manifest manifest) async { bool result = false; String cause = ''; try { result = await ManifestManager.instance .deliver(manifest, TransactionMethod.iap) .catchError((error) { Log.w("applyManifest error:$error", syncCrashlytics: true, syncFirebase: true); }); } catch (error, stacktrace) { cause = error.toString(); } GuruAnalytics.instance.logGuruEvent("dev_iap_action", { "item_category": "delivered", "item_name": productId.sku, "mc": manifest.category, "result": result ? "true" : "false", 'cause': cause, }); } Future _completeOrder(OrderEntity order) async { bool result = false; try { final completedOrder = order.success(); result = await GuruDB.instance.completeOrder(order: completedOrder); } catch (error, stacktrace) { Log.w("_completePurchase error.$error", stackTrace: stacktrace, syncFirebase: true, syncCrashlytics: true); } GuruAnalytics.instance.logGuruEvent("dev_iap_action", { "item_category": "complete", "item_name": order.productId.sku, "mc": order.manifest?.category ?? "unknown", "result": result ? "true" : "false" }); final manifest = order.manifest; if (manifest != null) { await _deliverManifest(order.productId, manifest); } if (order.isAsset || order.isSubscription) { final changedPurchasedStore = purchasedStore.clone(); changedPurchasedStore.addAsset(Asset(order.productId, order)); _iapStoreSubject.addEx(changedPurchasedStore); } return true; } Future completePoints( ProductId productId, ProductDetails productDetails, PurchaseDetails details) async { final count = await AppProperty.getInstance().increaseAndGetIapCount(); GuruAnalytics.instance.setUserProperty("purchase_count", count.toString()); try { final cost = productDetails.rawPrice; if (cost > 0) { final double price = cost; if (count == 1) { GuruAnalytics.instance.logEventEx("first_iap", itemName: productId.sku, value: price, parameters: {"currency": productDetails.currencyCode}); GuruAnalytics.instance.setUserProperty("is_iap_user", "true"); } GuruAnalytics.instance.logEventEx(productId.iapEventName, itemName: productId.sku, value: price, parameters: {"currency": productDetails.currencyCode}); } } catch (error, stacktrace) { GuruAnalytics.instance.logException(error, stacktrace: stacktrace); } final intent = productId.createIntent(scene: "outside_points"); final manifest = await ManifestManager.instance.createManifest(intent); await _deliverManifest(productId, manifest); // 这里不需要传 order,因为 points 商品是非订阅商品 await reportOrders(productId, productDetails, details, null); } Future _completePurchase(ProductId definedProductId, ProductDetails originProductDetails, PurchaseDetails details) async { ProductId productId = definedProductId; await _inAppPurchase.completePurchase(details); final count = await AppProperty.getInstance().increaseAndGetIapCount(); GuruAnalytics.instance.setUserProperty("purchase_count", count.toString()); Log.d("_completePurchase $productId ${details.pendingCompletePurchase}", tag: "Iap"); OrderEntity? resultOrder; IapRequest? iapRequest = iapRequestMap.remove(productId); if (iapRequest == null) { final offerProductIds = GuruApp.instance.offerProductIds(productId); for (var offerProductId in offerProductIds) { iapRequest = iapRequestMap.remove(offerProductId); if (iapRequest != null) { productId = offerProductId; break; } } } if (iapRequest != null) { resultOrder = iapRequest.order; final result = await _completeOrder(iapRequest.order); iapRequest.response(result); } else { Log.d("Not found iapRequest for $productId"); final orders = await GuruDB.instance.getPendingOrders(productId); if (orders.isNotEmpty) { orders.sort((a, b) => b.timestamp.compareTo(a.timestamp)); await _completeOrder(orders.first); resultOrder = orders.first; } } final productDetails = iapRequest?.product.offerDetails ?? iapRequest?.product.details ?? loadedProductDetails[productId] ?? originProductDetails; Log.d( "productId:$productId productDetails:${productDetails.rawPrice} originProductDetails:${originProductDetails.rawPrice}"); try { // final item = pendingTransaction.product.item; final cost = productDetails.rawPrice; if (cost > 0) { final double price = cost; if (count == 1) { GuruAnalytics.instance.logEventEx("first_iap", itemName: productId.sku, value: price, parameters: {"currency": productDetails.currencyCode}); GuruAnalytics.instance.setUserProperty("is_iap_user", "true"); } GuruAnalytics.instance.logEventEx(productId.iapEventName, itemName: productId.sku, value: price, parameters: {"currency": productDetails.currencyCode}); } } catch (error, stacktrace) { GuruAnalytics.instance.logException(error, stacktrace: stacktrace); } if (productId.isSubscription) { if (resultOrder != null) { recordSubscription(resultOrder); } } if (resultOrder != null) { reportOrders(productId, productDetails, details, resultOrder); } return resultOrder; } Future recordSubscription(OrderEntity order) async { final sku = order.sku; final manifest = order.manifest; final productId = ProductId.fromSku( sku: sku, attr: TransactionAttributes.subscriptions, basePlan: manifest?.basePlanId, offerId: manifest?.offerId); final group = GuruApp.instance.appSpec.productProfile.group(productId); final appProperty = AppProperty.getInstance(); await appProperty.getAndIncrease(PropertyKeys.subscriptionCount); if (group != null) { await appProperty.getAndIncrease(PropertyKeys.buildGroupSubscriptionCount(group)); } await appProperty.getAndIncrease(PropertyKeys.buildSubscriptionCount(productId)); } Future createPurchaseManifest(TransactionIntent intent) { return ManifestManager.instance.createManifest(intent); } Future checkAndDistributeOfferDetails( ProductId productId, ProductDetails? details, EligibilityCriteria eligibilityCriteria) async { Log.d("checkAndDistributeOfferDetails $productId $eligibilityCriteria"); switch (eligibilityCriteria) { case EligibilityCriteria.newCustomerNeverHadSubscribedThisGroup: final group = GuruApp.instance.appSpec.productProfile.group(productId); if (group != null) { final key = PropertyKeys.buildGroupSubscriptionCount(group); final count = await AppProperty.getInstance().getInt(key, defValue: 0); Log.d(" ==> $key $count"); return count > 0 ? null : details; } Log.d(" ==> not found group($group)! return null"); break; case EligibilityCriteria.newCustomerNeverHadThisSubscription: final key = PropertyKeys.buildSubscriptionCount(productId); final count = await AppProperty.getInstance().getInt(key, defValue: 0); Log.d(" ==> $key $count"); return count > 0 ? null : details; case EligibilityCriteria.newCustomerNeverHadAnySubscription: final count = await AppProperty.getInstance().getInt(PropertyKeys.subscriptionCount, defValue: 0); Log.d(" ==> subscriptionCount $count"); return count > 0 ? null : details; default: return details; } return null; } Future _createProduct(TransactionIntent intent, ProductDetails details) async { final productId = intent.productId; Manifest manifest = await ManifestManager.instance.createManifest(intent); Log.d("createProduct ${productId.sku} ${productId.hasBasePlan}"); ProductDetails baseDetails = details; ProductDetails? offerDetails; if (Platform.isAndroid && productId.isSubscription && productId.hasBasePlan) { final googlePlayProductDetails = details as GooglePlayProductDetails; final productDetails = googlePlayProductDetails.productDetails; final subscriptionOfferDetails = productDetails.subscriptionOfferDetails; final offerProductDetails = GooglePlayProductDetails.fromProductDetails(productDetails); final expectBasePlan = productId.basePlan; final expectOfferId = productId.offerId; Log.d( "expectOfferId:$expectOfferId expectBasePlan:$expectBasePlan offers:${offerProductDetails.length}"); if (expectBasePlan != null && subscriptionOfferDetails != null && subscriptionOfferDetails.length >= offerProductDetails.length) { for (int i = 0; i < subscriptionOfferDetails.length; i++) { final offer = subscriptionOfferDetails[i]; Log.d( "$i expectOfferId:$expectOfferId offerId:${offer.offerId} expectBasePlan:$expectBasePlan basePlanId:${offer.basePlanId}"); if (expectBasePlan != offer.basePlanId) { continue; } if (offer.offerId == null) { baseDetails = offerProductDetails[i]; } else if (expectOfferId != null && expectOfferId == offer.offerId) { offerDetails = offerProductDetails[i]; } } try { offerDetails = await checkAndDistributeOfferDetails( productId, offerDetails, intent.eligibilityCriteria); } catch (error, stacktrace) { Log.w("checkAndDistributeOfferDetails error! $error $stacktrace"); } } } return Product.iap(productId, baseDetails, manifest, offerDetails: offerDetails) as IapProduct; } Future> buildProducts(Set intents) async { ProductStore iapStore = ProductStore(); final _productDetails = loadedProductDetails; for (var intent in intents) { // 这里需要使用原始 ID 进行查找 final productId = intent.productId.originId; final details = _productDetails[productId]; Log.d("buildProducts $productId $details"); if (details == null) { continue; } final product = await _createProduct(intent, details); iapStore.putProduct(product); if (intent.productId.hasOffer && !iapStore.existsProduct(productId)) { final originProduct = await _createProduct(productId.createIntent(scene: intent.scene), details); iapStore.putProduct(originProduct); } } return iapStore; } Future buy(IapProduct product) async { final productId = product.productId.originId; final asset = purchasedStore.getAsset(productId); if (asset != null) { Log.v("IAP buy ${asset.productId} direct success!"); return true; } final pendingProduct = iapRequestMap[productId]; if (pendingProduct != null) { Log.v("_requestPurchases has pending product"); return pendingProduct.completer.future; } final param = PurchaseParam( productDetails: product.offerDetails ?? product.details, applicationUserName: AccountDataStore.instance.user?.uid); late OrderEntity order; try { order = product.createOrder(); await GuruDB.instance.upsertOrder(order: order); } catch (error, stacktrace) { Log.w("addOrder error! $error", stackTrace: stacktrace, syncCrashlytics: true, syncFirebase: true); return false; } bool result = false; if (product.isConsumable()) { result = await _inAppPurchase.buyConsumable(purchaseParam: param); } else { result = await _inAppPurchase.buyNonConsumable(purchaseParam: param); } if (!result) { Log.d("_requestPurchases error! ${product.productId} ${product.details.price}", syncFirebase: true); GuruAnalytics.instance.logGuruEvent("dev_iap_action", { "item_category": "request", "item_name": order.productId.sku, "mc": order.manifest?.category ?? "unknown", "result": "false", "cause": "buy error" }); await GuruDB.instance.deleteOrder(order: order); return false; } else { GuruAnalytics.instance.logGuruEvent("dev_iap_action", { "item_category": "request", "item_name": order.productId.sku, "mc": order.manifest?.category ?? "unknown", "result": "true" }); } final completer = Completer(); final iapRequest = IapRequest(product, order, completer); iapRequestMap[productId] = iapRequest; return await completer.future; } Future clearAssetRecord() async { if (!Platform.isAndroid) { return; } final InAppPurchaseAndroidPlatformAddition androidAddition = _inAppPurchase.getPlatformAddition(); final response = await androidAddition.queryPastPurchases(); for (var purchase in response.pastPurchases) { androidAddition.consumePurchase(purchase); Log.w("[clearAssetRecord] purchase clear:${purchase.productID} ${purchase.verificationData}"); _inAppPurchase.completePurchase(purchase); } await GuruDB.instance.clearOrders(method: TransactionMethod.iap); final newPurchased = AssetsStore(); _iapStoreSubject.addEx(newPurchased); } Future manualConsumePurchase(PurchaseDetails purchase) async { if (Platform.isAndroid) { final InAppPurchaseAndroidPlatformAddition androidAddition = _inAppPurchase.getPlatformAddition(); await androidAddition.consumePurchase(purchase); _inAppPurchase.completePurchase(purchase); await GuruDB.instance.deletePendingOrderBySku(sku: purchase.productID); } } Future manualConsumeAllPurchases(List> tuples) async { for (var tuple in tuples) { try { final productId = tuple.item1; final purchase = tuple.item2; await manualConsumePurchase(purchase); } catch (error, stacktrace) { Log.w("consumePurchase error! $error", stackTrace: stacktrace, syncFirebase: true); } } } Future completeAllPurchases(List> tuples) async { for (var tuple in tuples) { try { final productId = tuple.item1; final details = tuple.item2; final productDetails = loadedProductDetails[productId]; if (productDetails != null) { if (details.pendingCompletePurchase) { GuruAnalytics.instance.logGuruEvent("dev_iap_action", { "item_category": "pending_complete", "item_name": productId.sku, "result": "true", }); final order = await _completePurchase(productId, productDetails, details); } else { GuruAnalytics.instance.logGuruEvent("dev_iap_action", { "item_category": "pending_consume", "item_name": productId.sku, "result": "true", }); await manualConsumePurchase(details); if (productId.isPoints) { try { await completePoints(productId, productDetails, details); } catch (error, stacktrace) { Log.w("completePoints error! $error", stackTrace: stacktrace, syncFirebase: true); } } } } } catch (error, stacktrace) { Log.w("consumePurchase error! $error", stackTrace: stacktrace, syncFirebase: true); } } } // // Future> refreshAllProducts() async { // final allProductIds = ProductIds.allIapProductIds; // // return await refreshProducts(allProductIds); // } Map _filterProductSkus( {required Set ids, required Set attrs, Set? validIds}) { final List> entries = ids .where((productId) => (validIds?.contains(productId) != false) && attrs.contains(productId.attr)) .map((productId) => MapEntry(productId.sku, productId)) .toList(); return Map.fromEntries(entries); } Future _queryProducts(Set skus) async { try { return await _inAppPurchase.queryProductDetails(skus); } catch (error, stacktrace) { Log.i("_getProducts error:$error $stacktrace"); } return _emptyResponse; } Future restorePurchases() async { Log.d("restorePurchases!"); if (!iapAvailable) { Log.w("ignore restorePurchases! iap service not available!", tag: "IAP"); return; } if (!_restorePurchase) { _restorePurchase = Platform.isAndroid; // 只有Android需要进行处理 return await _inAppPurchase.restorePurchases(); } } Future refreshProducts() async { if (!iapAvailable) { Log.w("ignore refreshProducts! iap service not available!", tag: "IAP"); return; } final validIds = GuruApp.instance.productProfile.oneOffChargeIapIds.toSet() ..removeAll(loadedProductDetails.keys.toSet()); final queryOneOffChargeSkuMap = _filterProductSkus( ids: GuruApp.instance.productProfile.oneOffChargeIapIds, attrs: TransactionAttributes.oneOffChargeAttributes, validIds: validIds); Log.i("refreshProduct $queryOneOffChargeSkuMap", tag: "IAP"); final Map detailsMap = {}; if (queryOneOffChargeSkuMap.isEmpty) { Log.i("refreshProducts ignore! already loaded!", tag: "IAP"); return; } final queryProductIds = queryOneOffChargeSkuMap.keys.toSet(); queryProductIds.addAll(GuruApp.instance.productProfile.subscriptionsIapIds.map((e) => e.sku)); Log.d("refresh product:", tag: "IAP"); for (var productId in queryProductIds) { Log.d(" => $productId", tag: "IAP"); } final response = await _queryProducts(queryProductIds).catchError((error, stacktrace) { Log.e("getProducts($queryOneOffChargeSkuMap}) error: $error $stacktrace", tag: "IAP"); return _emptyResponse; }); Log.i("refreshProduct COMPLETED:", tag: "IAP"); for (var details in response.productDetails) { Log.i(" => ${details.id}", tag: "IAP"); } Log.i("refreshProduct notFoundId:", tag: "IAP"); for (var id in response.notFoundIDs) { Log.i(" => $id", tag: "IAP"); } for (var details in response.productDetails) { detailsMap.addAll(extractProducts(details)); } GuruAnalytics.instance .logGuruEvent("dev_iap_action", {"item_category": "load", "result": "true"}); final newProductDetails = Map.of(loadedProductDetails); newProductDetails.addAll(detailsMap); _productDetailsSubject.addEx(newProductDetails); } Map extractProducts(ProductDetails details) { final productId = GuruApp.instance.findProductId(sku: details.id); final Map detailsMap = {}; if (productId == null) { return detailsMap; } detailsMap[productId] = details; final ids = GuruApp.instance.offerProductIds(productId); if (ids.isNotEmpty) { final googlePlayProductDetails = details as GooglePlayProductDetails; final productDetails = googlePlayProductDetails.productDetails; final subscriptionOfferDetails = productDetails.subscriptionOfferDetails; final offerProductDetails = GooglePlayProductDetails.fromProductDetails(productDetails); for (var id in ids) { final expectBasePlan = id.basePlan; final expectOfferId = id.offerId; if (expectBasePlan != null && subscriptionOfferDetails != null && subscriptionOfferDetails.length == offerProductDetails.length) { for (int i = 0; i < subscriptionOfferDetails.length; i++) { final offer = subscriptionOfferDetails[i]; Log.d( "$i expectOfferId:$expectOfferId offerId:${offer.offerId} expectBasePlan:$expectBasePlan basePlanId:${offer.basePlanId}"); if (expectBasePlan != offer.basePlanId || expectOfferId != offer.offerId) { continue; } detailsMap[id] = offerProductDetails[i]; } } } } return detailsMap; } }