guru_sdk/guru_app/lib/financial/iap/iap_manager.dart

1467 lines
56 KiB
Dart
Raw Normal View History

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/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<Map<ProductId, ProductDetails>> _productDetailsSubject =
BehaviorSubject.seeded({});
final BehaviorSubject<AssetsStore<Asset>> _iapStoreSubject =
BehaviorSubject.seeded(AssetsStore<Asset>.inactive());
final Map<ProductId, IapRequest> iapRequestMap =
HashMap<ProductId, IapRequest>();
Stream<Map<ProductId, ProductDetails>> get observableProductDetails =>
_productDetailsSubject.stream;
Stream<AssetsStore<Asset>> get observableAssetStore =>
_iapStoreSubject.stream;
Map<ProductId, ProductDetails> get loadedProductDetails =>
_productDetailsSubject.value;
AssetsStore<Asset> get purchasedStore => _iapStoreSubject.value;
final BehaviorSubject<bool> availableSubject = BehaviorSubject.seeded(false);
Stream<bool> 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<String, dynamic> _iapRevenueToValue(Map<String, dynamic> 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<List<PurchaseDetails>> purchaseUpdated =
_inAppPurchase.purchaseStream;
subscription = purchaseUpdated.listen(
(List<PurchaseDetails> 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 reloadOrders() async {
final transactions = await GuruDB.instance.selectOrders(
method: TransactionMethod.iap,
attrs: [
TransactionAttributes.asset,
TransactionAttributes.subscriptions
]);
final newAssetStore = AssetsStore<Asset>();
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.addEx(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<bool> 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 = <ProductId, PurchaseDetails>{};
// 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;
}
}
}
Future processRestoredSubscription(
List<PurchaseDetails> subscriptionPurchased) async {
List<PurchaseDetails> purchasedDetails = subscriptionPurchased;
if (Platform.isIOS) {
purchasedDetails = buildLatestPurchasedPlanForIos(subscriptionPurchased);
}
final newPurchasedStore = purchasedStore.clone();
final expiredSkus = <String>{};
// 由于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;
}
purchased.transactionDate;
bool validPurchase = false;
if (Platform.isAndroid) {
validPurchase = true;
} else if (Platform.isIOS) {
final appleProduct = productDetails as AppStoreProductDetails;
final period = appleProduct.skProduct.subscriptionPeriod;
if (period != null) {
final numberOfUnits = period.numberOfUnits;
final unit = period.unit;
final int validInterval = getIOSPeriodInterval(numberOfUnits, unit);
final transactionTs =
int.tryParse(purchased.transactionDate ?? "") ?? 0;
final now = DateTimeUtils.currentTimeInMillis();
validPurchase = transactionTs + validInterval < now;
Log.d(
"productID: ${purchased.productID}) purchaseID: ${purchased.purchaseID}[$numberOfUnits][$unit] $transactionTs + $validInterval < $now ($validPurchase)",
tag: PropertyTags.iap);
}
}
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) {
Log.i("expired orders:${expiredSkus.length}}");
try {
await GuruDB.instance.deleteOrdersBySkus(expiredSkus);
} catch (error, stacktrace) {
Log.w("Failed to upsert order: $error $stacktrace",
tag: PropertyTags.iap);
}
}
_iapStoreSubject.addEx(newPurchasedStore);
Log.d(
"[RestoredSubscription] update purchasedStore ${newPurchasedStore.data.length}");
}
List<PurchaseDetails> buildLatestPurchasedPlanForIos(
List<PurchaseDetails> purchaseDetails) {
if (purchaseDetails.isEmpty) {
return [];
}
final rawTransactionIds = purchaseDetails
.map((details) => (details as AppStorePurchaseDetails)
.skPaymentTransaction
.originalTransaction
?.transactionIdentifier)
.where((element) => element != null)
.cast<String>()
.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> purchaseDetails) {
if (purchaseDetails.isEmpty) {
return;
}
final rawTransactionIds = purchaseDetails
.map((details) => (details as AppStorePurchaseDetails)
.skPaymentTransaction
.originalTransaction
?.transactionIdentifier)
.where((element) => element != null)
.cast<String>()
.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<PurchaseDetails> purchaseDetailsList) async {
final List<Tuple2<ProductId, PurchaseDetails>> restoredIapPurchases = [];
final List<Tuple2<ProductId, PurchaseDetails>> pendingCompletePurchase = [];
final List<PurchaseDetails> 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) {
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<Tuple2<ProductId, PurchaseDetails>> restoredIapPurchases) async {
final newPurchased = purchasedStore.clone();
final currentLoadedProductDetails = loadedProductDetails;
final upsertOrders = <OrderEntity>[];
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<OrderEntity> 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);
if (result.usdPrice > 0) {
logRevenue(
result.usdPrice, order.productId ?? order.subscriptionId);
}
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.price = details.rawPrice.toString();
ordersReport.currency = details.currencyCode;
ordersReport.orderUserInfo =
OrderUserInfo(GuruSettings.instance.bestLevel.get().toString());
ordersReport.userIdentification = GuruAnalytics.instance.userIdentification;
Log.d("orderReport:$ordersReport", tag: "Iap");
try {
final result = await GuruApi.instance.reportOrders(ordersReport);
if ((result.usdPrice > 0) ||
(result.usdPrice == 0 && result.isTestOrder)) {
logRevenue(result.usdPrice, purchaseDetails.productID);
Log.i("reportOrders success! $result");
return;
}
Log.i("ignoreInvalidResult $result", tag: "Iap");
} catch (error, stacktrace) {
Log.i("reportOrders error!", error: error, stackTrace: stacktrace);
}
AppProperty.getInstance().saveFailedIapOrders(ordersReport);
}
Future logRevenue(double usdPrice, String? sku) async {
if (sku == null || sku.isEmpty) {
return;
}
final platform = Platform.isIOS ? "appstore" : "google_play";
GuruAnalytics.instance.logAdRevenue(usdPrice, platform, "USD");
final productId =
GuruApp.instance.findProductId(sku: sku) ?? ProductId.invalid;
GuruAnalytics.instance.logPurchase(usdPrice,
currency: 'USD', contentId: sku, adPlatform: platform);
if (productId.isSubscription) {
GuruAnalytics.instance.logEvent(
"sub_purchase",
{
"platform": platform,
"currency": "USD",
"revenue": usdPrice,
"product_id": sku,
},
options: iapRevenueAppEventOptions);
} else {
GuruAnalytics.instance.logEvent(
"iap_purchase",
{
"platform": platform,
"currency": "USD",
"revenue": usdPrice,
"product_id": sku,
},
options: iapRevenueAppEventOptions);
}
GuruAnalytics.instance.logGuruEvent("dev_iap_action",
{"item_category": "reported", "item_name": sku, "result": "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<bool> _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<OrderEntity?> _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<Manifest> createPurchaseManifest(TransactionIntent intent) {
return ManifestManager.instance.createManifest(intent);
}
Future<ProductDetails?> 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<IapProduct> _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<ProductStore<IapProduct>> buildProducts(
Set<TransactionIntent> intents) async {
ProductStore<IapProduct> 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<bool> 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<bool>();
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<InAppPurchaseAndroidPlatformAddition>();
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<IapAsset>();
_iapStoreSubject.addEx(newPurchased);
}
Future manualConsumePurchase(PurchaseDetails purchase) async {
if (Platform.isAndroid) {
final InAppPurchaseAndroidPlatformAddition androidAddition =
_inAppPurchase
.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
await androidAddition.consumePurchase(purchase);
_inAppPurchase.completePurchase(purchase);
await GuruDB.instance.deletePendingOrderBySku(sku: purchase.productID);
}
}
Future manualConsumeAllPurchases(
List<Tuple2<ProductId, PurchaseDetails>> 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<Tuple2<ProductId, PurchaseDetails>> 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<List<IapProduct>> refreshAllProducts() async {
// final allProductIds = ProductIds.allIapProductIds;
//
// return await refreshProducts(allProductIds);
// }
Map<String, ProductId> _filterProductSkus(
{required Set<ProductId> ids,
required Set<int> attrs,
Set<ProductId>? validIds}) {
final List<MapEntry<String, ProductId>> entries = ids
.where((productId) =>
(validIds?.contains(productId) != false) &&
attrs.contains(productId.attr))
.map((productId) => MapEntry(productId.sku, productId))
.toList();
return Map.fromEntries(entries);
}
Future<ProductDetailsResponse> _queryProducts(Set<String> 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<ProductId, ProductDetails> 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<ProductId, ProductDetails> extractProducts(ProductDetails details) {
final productId = GuruApp.instance.findProductId(sku: details.id);
final Map<ProductId, ProductDetails> 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;
}
}