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

1479 lines
60 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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<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 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<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.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<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;
}
}
}
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<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;
}
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<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) {
/// 如果是 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<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);
// 这里不管返回什么值,都认为是成功的, 只有崩溃才会返回失败
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<bool> 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<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;
}
}