2023-12-21 09:14:40 +00:00
|
|
|
|
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';
|
2024-03-07 03:46:50 +00:00
|
|
|
|
import 'package:guru_app/firebase/firebase.dart';
|
2023-12-21 09:14:40 +00:00
|
|
|
|
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());
|
|
|
|
|
|
|
2024-03-07 03:46:50 +00:00
|
|
|
|
final Map<ProductId, IapRequest> iapRequestMap = HashMap<ProductId, IapRequest>();
|
2023-12-21 09:14:40 +00:00
|
|
|
|
|
|
|
|
|
|
Stream<Map<ProductId, ProductDetails>> get observableProductDetails =>
|
|
|
|
|
|
_productDetailsSubject.stream;
|
|
|
|
|
|
|
2024-03-07 03:46:50 +00:00
|
|
|
|
Stream<AssetsStore<Asset>> get observableAssetStore => _iapStoreSubject.stream;
|
2023-12-21 09:14:40 +00:00
|
|
|
|
|
2024-03-07 03:46:50 +00:00
|
|
|
|
Map<ProductId, ProductDetails> get loadedProductDetails => _productDetailsSubject.value;
|
2023-12-21 09:14:40 +00:00
|
|
|
|
|
|
|
|
|
|
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(
|
2024-03-07 03:46:50 +00:00
|
|
|
|
capabilities:
|
|
|
|
|
|
const AppEventCapabilities(AppEventCapabilities.firebase | AppEventCapabilities.guru),
|
2023-12-21 09:14:40 +00:00
|
|
|
|
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) {
|
2024-03-07 03:46:50 +00:00
|
|
|
|
GuruAnalytics.instance.setUserProperty("purchase_count", iapCount.toString());
|
2023-12-21 09:14:40 +00:00
|
|
|
|
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) {
|
2024-03-07 03:46:50 +00:00
|
|
|
|
final Stream<List<PurchaseDetails>> purchaseUpdated = _inAppPurchase.purchaseStream;
|
2023-12-21 09:14:40 +00:00
|
|
|
|
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 {}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-03-07 03:46:50 +00:00
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-12-21 09:14:40 +00:00
|
|
|
|
Future reloadOrders() async {
|
|
|
|
|
|
final transactions = await GuruDB.instance.selectOrders(
|
|
|
|
|
|
method: TransactionMethod.iap,
|
2024-03-07 03:46:50 +00:00
|
|
|
|
attrs: [TransactionAttributes.asset, TransactionAttributes.subscriptions]);
|
2023-12-21 09:14:40 +00:00
|
|
|
|
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));
|
2024-03-07 03:46:50 +00:00
|
|
|
|
available = await _inAppPurchase.isAvailable().catchError((error, stacktrace) {
|
2023-12-21 09:14:40 +00:00
|
|
|
|
Log.w("isAvailable error:$error", stackTrace: stacktrace);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
});
|
|
|
|
|
|
Log.d("_checkAndLoad:$retry available:$available");
|
|
|
|
|
|
retry++;
|
|
|
|
|
|
} while (!available);
|
2024-03-07 03:46:50 +00:00
|
|
|
|
availableSubject.addIfChanged(true);
|
2023-12-21 09:14:40 +00:00
|
|
|
|
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,
|
2024-03-07 03:46:50 +00:00
|
|
|
|
error: PurchaseError(iapErrorMsg), syncFirebase: true, syncCrashlytics: true);
|
2023-12-21 09:14:40 +00:00
|
|
|
|
try {
|
2024-03-07 03:46:50 +00:00
|
|
|
|
await GuruDB.instance.upsertOrder(order: iapRequest.order.error(iapErrorMsg));
|
2023-12-21 09:14:40 +00:00
|
|
|
|
} 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) {
|
2024-03-07 03:46:50 +00:00
|
|
|
|
Log.w("_processIapCancel deleteOrder error! $error", syncFirebase: true);
|
2023-12-21 09:14:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
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);
|
|
|
|
|
|
// }
|
|
|
|
|
|
// });
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
2024-03-07 03:46:50 +00:00
|
|
|
|
String dumpProductAndPurchased(ProductDetails details, PurchaseDetails purchaseDetails) {
|
2023-12-21 09:14:40 +00:00
|
|
|
|
final StringBuffer sb = StringBuffer();
|
|
|
|
|
|
|
|
|
|
|
|
if (Platform.isAndroid) {
|
|
|
|
|
|
try {
|
2024-03-07 03:46:50 +00:00
|
|
|
|
GooglePlayPurchaseDetails googlePlayDetails = purchaseDetails as GooglePlayPurchaseDetails;
|
|
|
|
|
|
GooglePlayProductDetails googlePlayProduct = details as GooglePlayProductDetails;
|
2023-12-21 09:14:40 +00:00
|
|
|
|
Log.d(
|
|
|
|
|
|
"Android Product/Purchase ${buildGooglePlayDetailsString(googlePlayProduct, googlePlayDetails)}");
|
|
|
|
|
|
} catch (error, stacktrace) {}
|
|
|
|
|
|
} else if (Platform.isIOS) {
|
2024-03-07 03:46:50 +00:00
|
|
|
|
AppStorePurchaseDetails appleDetails = purchaseDetails as AppStorePurchaseDetails;
|
2023-12-21 09:14:40 +00:00
|
|
|
|
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}");
|
2024-03-07 03:46:50 +00:00
|
|
|
|
sb.writeln(" =>${appleDetails.skPaymentTransaction.originalTransaction?.payment}");
|
2023-12-21 09:14:40 +00:00
|
|
|
|
sb.writeln(" =>${appleDetails.skPaymentTransaction.transactionState}:");
|
2024-03-07 03:46:50 +00:00
|
|
|
|
sb.writeln(" =>${appleDetails.skPaymentTransaction.transactionIdentifier}:");
|
2023-12-21 09:14:40 +00:00
|
|
|
|
sb.writeln("\n#### product ####");
|
|
|
|
|
|
sb.writeln("currencyCode: ${appleProduct.currencyCode}");
|
|
|
|
|
|
sb.writeln("rawPrice: ${appleProduct.rawPrice}");
|
|
|
|
|
|
sb.writeln("currencyCode: ${appleProduct.currencyCode}");
|
|
|
|
|
|
sb.writeln("currencyCode skProduct");
|
2024-03-07 03:46:50 +00:00
|
|
|
|
sb.writeln(" =>localizedTitle: ${appleProduct.skProduct.localizedTitle}");
|
|
|
|
|
|
sb.writeln(" =>localizedDescription: ${appleProduct.skProduct.localizedDescription}");
|
2023-12-21 09:14:40 +00:00
|
|
|
|
sb.writeln(" =>priceLocale: ${appleProduct.skProduct.priceLocale}");
|
2024-03-07 03:46:50 +00:00
|
|
|
|
sb.writeln(" =>productIdentifier: ${appleProduct.skProduct.productIdentifier}");
|
2023-12-21 09:14:40 +00:00
|
|
|
|
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()) {
|
2024-03-07 03:46:50 +00:00
|
|
|
|
final renewalSpeed =
|
|
|
|
|
|
GuruApp.instance.appSpec.deployment.iosSandboxSubsRenewalSpeed.clamp(1, 5);
|
2023-12-21 09:14:40 +00:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-03-07 03:46:50 +00:00
|
|
|
|
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 {
|
2023-12-21 09:14:40 +00:00
|
|
|
|
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) {
|
2024-03-07 03:46:50 +00:00
|
|
|
|
final expired = productId.isSubscription && !purchasedSkus.contains(productId.sku);
|
2023-12-21 09:14:40 +00:00
|
|
|
|
Log.i("remove expired subscription[$productId] expired:$expired");
|
|
|
|
|
|
if (expired) {
|
|
|
|
|
|
expiredSkus.add(asset.productId.sku);
|
|
|
|
|
|
}
|
|
|
|
|
|
return expired;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (var purchased in purchasedDetails) {
|
2024-03-07 03:46:50 +00:00
|
|
|
|
final productId = GuruApp.instance.findProductId(sku: purchased.productID);
|
2023-12-21 09:14:40 +00:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2024-03-07 03:46:50 +00:00
|
|
|
|
final bool validPurchase = checkSubscriptionPeriod(purchased, productDetails);
|
2023-12-21 09:14:40 +00:00
|
|
|
|
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) {
|
2024-03-07 03:46:50 +00:00
|
|
|
|
final product =
|
|
|
|
|
|
await _createProduct(productId.createIntent(scene: "restore"), productDetails);
|
2023-12-21 09:14:40 +00:00
|
|
|
|
newOrder = product.createOrder().success();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
newOrder = asset.order.success();
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
await GuruDB.instance.replaceOrderBySku(order: newOrder);
|
|
|
|
|
|
} catch (error, stacktrace) {
|
2024-03-07 03:46:50 +00:00
|
|
|
|
Log.w("Failed to upsert order: $error $stacktrace", tag: PropertyTags.iap);
|
2023-12-21 09:14:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
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) {
|
2024-03-07 03:46:50 +00:00
|
|
|
|
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
|
|
|
|
|
|
});
|
2023-12-21 09:14:40 +00:00
|
|
|
|
}
|
2024-03-07 03:46:50 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
await AppProperty.getInstance().resetGraceCount();
|
2023-12-21 09:14:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
_iapStoreSubject.addEx(newPurchasedStore);
|
2024-03-07 03:46:50 +00:00
|
|
|
|
Log.d("[RestoredSubscription] update purchasedStore ${newPurchasedStore.data.length}");
|
2023-12-21 09:14:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-03-07 03:46:50 +00:00
|
|
|
|
List<PurchaseDetails> buildLatestPurchasedPlanForIos(List<PurchaseDetails> purchaseDetails) {
|
2023-12-21 09:14:40 +00:00
|
|
|
|
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();
|
2024-03-07 03:46:50 +00:00
|
|
|
|
sortedPurchaseDetails.sort((a, b) => (int.tryParse(b.transactionDate ?? '') ?? 0)
|
|
|
|
|
|
.compareTo(int.tryParse(a.transactionDate ?? '') ?? 0));
|
2023-12-21 09:14:40 +00:00
|
|
|
|
sortedPurchaseDetails.retainWhere((details) {
|
|
|
|
|
|
var detail = details as AppStorePurchaseDetails;
|
|
|
|
|
|
Log.d(
|
|
|
|
|
|
"checkSubscriptionForIos ${detail.skPaymentTransaction.originalTransaction?.transactionIdentifier} ${detail.transactionDate} ${detail.skPaymentTransaction.transactionTimeStamp}");
|
2024-03-07 03:46:50 +00:00
|
|
|
|
return rawTransactionIds
|
|
|
|
|
|
.remove(detail.skPaymentTransaction.originalTransaction?.transactionIdentifier);
|
2023-12-21 09:14:40 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
2024-03-07 03:46:50 +00:00
|
|
|
|
sortedPurchaseDetails.sort((a, b) => (int.tryParse(b.transactionDate ?? '') ?? 0)
|
|
|
|
|
|
.compareTo(int.tryParse(a.transactionDate ?? '') ?? 0));
|
2023-12-21 09:14:40 +00:00
|
|
|
|
sortedPurchaseDetails.retainWhere((details) {
|
|
|
|
|
|
var detail = details as AppStorePurchaseDetails;
|
|
|
|
|
|
Log.d(
|
|
|
|
|
|
"checkSubscriptionForIos ${detail.skPaymentTransaction.originalTransaction?.transactionIdentifier} ${detail.transactionDate} ${detail.skPaymentTransaction.transactionTimeStamp}");
|
2024-03-07 03:46:50 +00:00
|
|
|
|
return rawTransactionIds
|
|
|
|
|
|
.remove(detail.skPaymentTransaction.originalTransaction?.transactionIdentifier);
|
2023-12-21 09:14:40 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-03-07 03:46:50 +00:00
|
|
|
|
void _listenToPurchaseUpdated(List<PurchaseDetails> purchaseDetailsList) async {
|
2023-12-21 09:14:40 +00:00
|
|
|
|
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) {
|
2024-03-07 03:46:50 +00:00
|
|
|
|
final productId = GuruApp.instance.findProductId(sku: details.productID) ?? ProductId.invalid;
|
2023-12-21 09:14:40 +00:00
|
|
|
|
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) {
|
2024-03-07 03:46:50 +00:00
|
|
|
|
/// 如果是 IOS的 purchased订单,并且是订阅的订单,他又没在当前请求的列表中,证明他是一个恢复的订单
|
|
|
|
|
|
if (Platform.isIOS && productId.isSubscription && !iapRequestMap.containsKey(productId)) {
|
|
|
|
|
|
subscriptionPurchases.add(details);
|
|
|
|
|
|
existsRestored = true;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await _completePurchase(productId, productDetails, details);
|
|
|
|
|
|
}
|
2023-12-21 09:14:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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) {
|
2024-03-07 03:46:50 +00:00
|
|
|
|
final originPurchaseState =
|
|
|
|
|
|
(details as GooglePlayPurchaseDetails).billingClientPurchase.purchaseState;
|
2023-12-21 09:14:40 +00:00
|
|
|
|
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);
|
2024-03-07 03:46:50 +00:00
|
|
|
|
Log.d("manual complete/consume all purchases!", syncFirebase: true, syncCrashlytics: true);
|
2023-12-21 09:14:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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) {
|
2024-03-07 03:46:50 +00:00
|
|
|
|
final product =
|
|
|
|
|
|
await _createProduct(productId.createIntent(scene: "restore"), productDetails);
|
2023-12-21 09:14:40 +00:00
|
|
|
|
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) {
|
2024-03-07 03:46:50 +00:00
|
|
|
|
Log.w("upsertOrders error:$error $stacktrace", syncCrashlytics: true, syncFirebase: true);
|
2023-12-21 09:14:40 +00:00
|
|
|
|
for (var order in upsertOrders) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await GuruDB.instance.upsertOrder(order: order);
|
|
|
|
|
|
updatedOrder.add(order);
|
|
|
|
|
|
} catch (error1, stacktrace1) {
|
2024-03-07 03:46:50 +00:00
|
|
|
|
Log.w("upsertOrder(${order.sku}) error:$error1 $stacktrace1", syncFirebase: true);
|
2023-12-21 09:14:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-03-07 03:46:50 +00:00
|
|
|
|
final assets = updatedOrder.map((order) => Asset(order.productId, order)).toList();
|
2023-12-21 09:14:40 +00:00
|
|
|
|
newPurchased.addAllAssets(assets);
|
|
|
|
|
|
}
|
|
|
|
|
|
_iapStoreSubject.addEx(newPurchased);
|
|
|
|
|
|
Log.d("[RestoredPurchases] update purchasedStore ${upsertOrders.length}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Future reportFailedOrders() async {
|
2024-03-07 03:46:50 +00:00
|
|
|
|
final failedIapOrders = await AppProperty.getInstance().loadAllFailedIapOrders();
|
2023-12-21 09:14:40 +00:00
|
|
|
|
failedIapOrders.forEach((key, value) async {
|
|
|
|
|
|
try {
|
|
|
|
|
|
final order = OrdersReport.fromJson(json.decode(value));
|
|
|
|
|
|
final result = await GuruApi.instance.reportOrders(order);
|
2024-03-07 03:46:50 +00:00
|
|
|
|
// 这里不管返回什么值,都认为是成功的, 只有崩溃才会返回失败
|
|
|
|
|
|
await logRevenue(order, result);
|
2023-12-21 09:14:40 +00:00
|
|
|
|
AppProperty.getInstance().removeReportSuccessOrder(key);
|
|
|
|
|
|
} catch (error, stacktrace) {}
|
|
|
|
|
|
});
|
|
|
|
|
|
Log.i("reportFailedOrders success!");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
String buildGooglePlayDetailsString(
|
2024-03-07 03:46:50 +00:00
|
|
|
|
GooglePlayProductDetails googlePlayProduct, GooglePlayPurchaseDetails googlePlayDetails) {
|
2023-12-21 09:14:40 +00:00
|
|
|
|
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}");
|
2024-03-07 03:46:50 +00:00
|
|
|
|
sb.writeln(" - priceAmountMicros: ${oneTimeDetails.priceAmountMicros}");
|
|
|
|
|
|
sb.writeln(" - priceCurrencyCode: ${oneTimeDetails.priceCurrencyCode}");
|
2023-12-21 09:14:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
final subscriptionOfferDetails = productDetails.subscriptionOfferDetails;
|
2024-03-07 03:46:50 +00:00
|
|
|
|
if (subscriptionOfferDetails != null && subscriptionOfferDetails.isNotEmpty) {
|
2023-12-21 09:14:40 +00:00
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-03-07 03:46:50 +00:00
|
|
|
|
Future reportOrders(ProductId productId, ProductDetails details, PurchaseDetails purchaseDetails,
|
|
|
|
|
|
OrderEntity? order) async {
|
2023-12-21 09:14:40 +00:00
|
|
|
|
final OrdersReport ordersReport = OrdersReport();
|
|
|
|
|
|
|
|
|
|
|
|
if (Platform.isAndroid) {
|
2024-03-07 03:46:50 +00:00
|
|
|
|
ordersReport.token = purchaseDetails.verificationData.serverVerificationData;
|
2023-12-21 09:14:40 +00:00
|
|
|
|
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 {
|
2024-03-07 03:46:50 +00:00
|
|
|
|
GooglePlayPurchaseDetails googlePlayDetails = purchaseDetails as GooglePlayPurchaseDetails;
|
|
|
|
|
|
GooglePlayProductDetails googlePlayProduct = details as GooglePlayProductDetails;
|
2023-12-21 09:14:40 +00:00
|
|
|
|
Log.d(
|
|
|
|
|
|
"Android Product/Purchase ${buildGooglePlayDetailsString(googlePlayProduct, googlePlayDetails)}");
|
|
|
|
|
|
} catch (error, stacktrace) {}
|
|
|
|
|
|
} else if (Platform.isIOS) {
|
2024-03-07 03:46:50 +00:00
|
|
|
|
AppStorePurchaseDetails appleDetails = purchaseDetails as AppStorePurchaseDetails;
|
2023-12-21 09:14:40 +00:00
|
|
|
|
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}");
|
2024-03-07 03:46:50 +00:00
|
|
|
|
sb.writeln(" =>${appleDetails.skPaymentTransaction.originalTransaction?.payment}");
|
2023-12-21 09:14:40 +00:00
|
|
|
|
sb.writeln(" =>${appleDetails.skPaymentTransaction.transactionState}:");
|
2024-03-07 03:46:50 +00:00
|
|
|
|
sb.writeln(" =>${appleDetails.skPaymentTransaction.transactionIdentifier}:");
|
2023-12-21 09:14:40 +00:00
|
|
|
|
sb.writeln("\n#### product ####");
|
|
|
|
|
|
sb.writeln("currencyCode: ${appleProduct.currencyCode}");
|
|
|
|
|
|
sb.writeln("rawPrice: ${appleProduct.rawPrice}");
|
|
|
|
|
|
sb.writeln("currencyCode: ${appleProduct.currencyCode}");
|
|
|
|
|
|
sb.writeln("currencyCode skProduct");
|
2024-03-07 03:46:50 +00:00
|
|
|
|
sb.writeln(" =>localizedTitle: ${appleProduct.skProduct.localizedTitle}");
|
|
|
|
|
|
sb.writeln(" =>localizedDescription: ${appleProduct.skProduct.localizedDescription}");
|
2023-12-21 09:14:40 +00:00
|
|
|
|
sb.writeln(" =>priceLocale: ${appleProduct.skProduct.priceLocale}");
|
2024-03-07 03:46:50 +00:00
|
|
|
|
sb.writeln(" =>productIdentifier: ${appleProduct.skProduct.productIdentifier}");
|
2023-12-21 09:14:40 +00:00
|
|
|
|
sb.writeln(
|
|
|
|
|
|
" =>subscriptionGroupIdentifier: ${appleProduct.skProduct.subscriptionGroupIdentifier}");
|
|
|
|
|
|
sb.writeln(" =>appleProduct.skProduct.priceLocale");
|
|
|
|
|
|
sb.writeln(" ->{appleProduct.skProduct.priceLocale}");
|
|
|
|
|
|
|
|
|
|
|
|
ordersReport.bundleId = GuruApp.instance.appSpec.details.bundleId;
|
2024-03-07 03:46:50 +00:00
|
|
|
|
ordersReport.receipt = purchaseDetails.verificationData.serverVerificationData;
|
2023-12-21 09:14:40 +00:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2024-03-07 03:46:50 +00:00
|
|
|
|
ordersReport.orderId = purchaseDetails.purchaseID;
|
2023-12-21 09:14:40 +00:00
|
|
|
|
ordersReport.price = details.rawPrice.toString();
|
|
|
|
|
|
ordersReport.currency = details.currencyCode;
|
2024-03-07 03:46:50 +00:00
|
|
|
|
ordersReport.orderUserInfo = OrderUserInfo(GuruSettings.instance.bestLevel.get().toString());
|
2023-12-21 09:14:40 +00:00
|
|
|
|
ordersReport.userIdentification = GuruAnalytics.instance.userIdentification;
|
|
|
|
|
|
|
2024-03-07 03:46:50 +00:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-12-21 09:14:40 +00:00
|
|
|
|
Log.d("orderReport:$ordersReport", tag: "Iap");
|
|
|
|
|
|
try {
|
|
|
|
|
|
final result = await GuruApi.instance.reportOrders(ordersReport);
|
2024-03-07 03:46:50 +00:00
|
|
|
|
// 这里不管返回什么值,都认为是成功的
|
|
|
|
|
|
await logRevenue(ordersReport, result);
|
|
|
|
|
|
return;
|
2023-12-21 09:14:40 +00:00
|
|
|
|
} catch (error, stacktrace) {
|
|
|
|
|
|
Log.i("reportOrders error!", error: error, stackTrace: stacktrace);
|
|
|
|
|
|
}
|
|
|
|
|
|
AppProperty.getInstance().saveFailedIapOrders(ordersReport);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-03-07 03:46:50 +00:00
|
|
|
|
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;
|
2023-12-21 09:14:40 +00:00
|
|
|
|
if (sku == null || sku.isEmpty) {
|
2024-03-07 03:46:50 +00:00
|
|
|
|
return false;
|
2023-12-21 09:14:40 +00:00
|
|
|
|
}
|
2024-03-07 03:46:50 +00:00
|
|
|
|
|
|
|
|
|
|
if (!result.isTestOrder && usdPrice <= 0) {
|
|
|
|
|
|
Log.i("ignoreInvalidResult $result", tag: "Iap");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
Log.i("prepare logRevenue! $result $sku");
|
|
|
|
|
|
|
2023-12-21 09:14:40 +00:00
|
|
|
|
final platform = Platform.isIOS ? "appstore" : "google_play";
|
2024-03-07 03:46:50 +00:00
|
|
|
|
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) {
|
2023-12-21 09:14:40 +00:00
|
|
|
|
GuruAnalytics.instance.logEvent(
|
|
|
|
|
|
"sub_purchase",
|
|
|
|
|
|
{
|
|
|
|
|
|
"platform": platform,
|
|
|
|
|
|
"currency": "USD",
|
|
|
|
|
|
"revenue": usdPrice,
|
|
|
|
|
|
"product_id": sku,
|
2024-03-07 03:46:50 +00:00
|
|
|
|
"order_type": "SUB",
|
|
|
|
|
|
"order_id": order.orderId,
|
|
|
|
|
|
"trans_ts": order.transactionDate
|
2023-12-21 09:14:40 +00:00
|
|
|
|
},
|
|
|
|
|
|
options: iapRevenueAppEventOptions);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
GuruAnalytics.instance.logEvent(
|
|
|
|
|
|
"iap_purchase",
|
|
|
|
|
|
{
|
|
|
|
|
|
"platform": platform,
|
|
|
|
|
|
"currency": "USD",
|
|
|
|
|
|
"revenue": usdPrice,
|
|
|
|
|
|
"product_id": sku,
|
2024-03-07 03:46:50 +00:00
|
|
|
|
"order_type": "IAP",
|
|
|
|
|
|
"order_id": order.orderId,
|
|
|
|
|
|
"trans_ts": order.transactionDate
|
2023-12-21 09:14:40 +00:00
|
|
|
|
},
|
|
|
|
|
|
options: iapRevenueAppEventOptions);
|
|
|
|
|
|
}
|
2024-03-07 03:46:50 +00:00
|
|
|
|
GuruAnalytics.instance.logGuruEvent(
|
|
|
|
|
|
"dev_iap_action", {"item_category": "reported", "item_name": sku, "result": "true"});
|
|
|
|
|
|
Log.i("reportOrders completed! logRevenue success! $result $sku");
|
|
|
|
|
|
return true;
|
2023-12-21 09:14:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Future _deliverManifest(ProductId productId, Manifest manifest) async {
|
|
|
|
|
|
bool result = false;
|
|
|
|
|
|
String cause = '';
|
|
|
|
|
|
try {
|
|
|
|
|
|
result = await ManifestManager.instance
|
|
|
|
|
|
.deliver(manifest, TransactionMethod.iap)
|
|
|
|
|
|
.catchError((error) {
|
2024-03-07 03:46:50 +00:00
|
|
|
|
Log.w("applyManifest error:$error", syncCrashlytics: true, syncFirebase: true);
|
2023-12-21 09:14:40 +00:00
|
|
|
|
});
|
|
|
|
|
|
} 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());
|
2024-03-07 03:46:50 +00:00
|
|
|
|
Log.d("_completePurchase $productId ${details.pendingCompletePurchase}", tag: "Iap");
|
2023-12-21 09:14:40 +00:00
|
|
|
|
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) {
|
2024-03-07 03:46:50 +00:00
|
|
|
|
await appProperty.getAndIncrease(PropertyKeys.buildGroupSubscriptionCount(group));
|
2023-12-21 09:14:40 +00:00
|
|
|
|
}
|
2024-03-07 03:46:50 +00:00
|
|
|
|
await appProperty.getAndIncrease(PropertyKeys.buildSubscriptionCount(productId));
|
2023-12-21 09:14:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Future<Manifest> createPurchaseManifest(TransactionIntent intent) {
|
|
|
|
|
|
return ManifestManager.instance.createManifest(intent);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-03-07 03:46:50 +00:00
|
|
|
|
Future<ProductDetails?> checkAndDistributeOfferDetails(
|
|
|
|
|
|
ProductId productId, ProductDetails? details, EligibilityCriteria eligibilityCriteria) async {
|
2023-12-21 09:14:40 +00:00
|
|
|
|
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);
|
2024-03-07 03:46:50 +00:00
|
|
|
|
final count = await AppProperty.getInstance().getInt(key, defValue: 0);
|
2023-12-21 09:14:40 +00:00
|
|
|
|
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:
|
2024-03-07 03:46:50 +00:00
|
|
|
|
final count =
|
|
|
|
|
|
await AppProperty.getInstance().getInt(PropertyKeys.subscriptionCount, defValue: 0);
|
2023-12-21 09:14:40 +00:00
|
|
|
|
Log.d(" ==> subscriptionCount $count");
|
|
|
|
|
|
return count > 0 ? null : details;
|
|
|
|
|
|
default:
|
|
|
|
|
|
return details;
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-03-07 03:46:50 +00:00
|
|
|
|
Future<IapProduct> _createProduct(TransactionIntent intent, ProductDetails details) async {
|
2023-12-21 09:14:40 +00:00
|
|
|
|
final productId = intent.productId;
|
|
|
|
|
|
Manifest manifest = await ManifestManager.instance.createManifest(intent);
|
|
|
|
|
|
Log.d("createProduct ${productId.sku} ${productId.hasBasePlan}");
|
|
|
|
|
|
ProductDetails baseDetails = details;
|
|
|
|
|
|
ProductDetails? offerDetails;
|
2024-03-07 03:46:50 +00:00
|
|
|
|
if (Platform.isAndroid && productId.isSubscription && productId.hasBasePlan) {
|
2023-12-21 09:14:40 +00:00
|
|
|
|
final googlePlayProductDetails = details as GooglePlayProductDetails;
|
|
|
|
|
|
final productDetails = googlePlayProductDetails.productDetails;
|
|
|
|
|
|
final subscriptionOfferDetails = productDetails.subscriptionOfferDetails;
|
2024-03-07 03:46:50 +00:00
|
|
|
|
final offerProductDetails = GooglePlayProductDetails.fromProductDetails(productDetails);
|
2023-12-21 09:14:40 +00:00
|
|
|
|
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");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-03-07 03:46:50 +00:00
|
|
|
|
return Product.iap(productId, baseDetails, manifest, offerDetails: offerDetails) as IapProduct;
|
2023-12-21 09:14:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-03-07 03:46:50 +00:00
|
|
|
|
Future<ProductStore<IapProduct>> buildProducts(Set<TransactionIntent> intents) async {
|
2023-12-21 09:14:40 +00:00
|
|
|
|
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)) {
|
2024-03-07 03:46:50 +00:00
|
|
|
|
final originProduct =
|
|
|
|
|
|
await _createProduct(productId.createIntent(scene: intent.scene), details);
|
2023-12-21 09:14:40 +00:00
|
|
|
|
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) {
|
2024-03-07 03:46:50 +00:00
|
|
|
|
Log.d("_requestPurchases error! ${product.productId} ${product.details.price}",
|
2023-12-21 09:14:40 +00:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2024-03-07 03:46:50 +00:00
|
|
|
|
final InAppPurchaseAndroidPlatformAddition androidAddition =
|
|
|
|
|
|
_inAppPurchase.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
|
2023-12-21 09:14:40 +00:00
|
|
|
|
final response = await androidAddition.queryPastPurchases();
|
|
|
|
|
|
for (var purchase in response.pastPurchases) {
|
|
|
|
|
|
androidAddition.consumePurchase(purchase);
|
2024-03-07 03:46:50 +00:00
|
|
|
|
Log.w("[clearAssetRecord] purchase clear:${purchase.productID} ${purchase.verificationData}");
|
2023-12-21 09:14:40 +00:00
|
|
|
|
_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 =
|
2024-03-07 03:46:50 +00:00
|
|
|
|
_inAppPurchase.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
|
2023-12-21 09:14:40 +00:00
|
|
|
|
await androidAddition.consumePurchase(purchase);
|
|
|
|
|
|
_inAppPurchase.completePurchase(purchase);
|
|
|
|
|
|
await GuruDB.instance.deletePendingOrderBySku(sku: purchase.productID);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-03-07 03:46:50 +00:00
|
|
|
|
Future manualConsumeAllPurchases(List<Tuple2<ProductId, PurchaseDetails>> tuples) async {
|
2023-12-21 09:14:40 +00:00
|
|
|
|
for (var tuple in tuples) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
final productId = tuple.item1;
|
|
|
|
|
|
final purchase = tuple.item2;
|
|
|
|
|
|
await manualConsumePurchase(purchase);
|
|
|
|
|
|
} catch (error, stacktrace) {
|
2024-03-07 03:46:50 +00:00
|
|
|
|
Log.w("consumePurchase error! $error", stackTrace: stacktrace, syncFirebase: true);
|
2023-12-21 09:14:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-03-07 03:46:50 +00:00
|
|
|
|
Future completeAllPurchases(List<Tuple2<ProductId, PurchaseDetails>> tuples) async {
|
2023-12-21 09:14:40 +00:00
|
|
|
|
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",
|
|
|
|
|
|
});
|
2024-03-07 03:46:50 +00:00
|
|
|
|
final order = await _completePurchase(productId, productDetails, details);
|
2023-12-21 09:14:40 +00:00
|
|
|
|
} 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) {
|
2024-03-07 03:46:50 +00:00
|
|
|
|
Log.w("consumePurchase error! $error", stackTrace: stacktrace, syncFirebase: true);
|
2023-12-21 09:14:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//
|
|
|
|
|
|
// Future<List<IapProduct>> refreshAllProducts() async {
|
|
|
|
|
|
// final allProductIds = ProductIds.allIapProductIds;
|
|
|
|
|
|
//
|
|
|
|
|
|
// return await refreshProducts(allProductIds);
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
Map<String, ProductId> _filterProductSkus(
|
2024-03-07 03:46:50 +00:00
|
|
|
|
{required Set<ProductId> ids, required Set<int> attrs, Set<ProductId>? validIds}) {
|
2023-12-21 09:14:40 +00:00
|
|
|
|
final List<MapEntry<String, ProductId>> entries = ids
|
|
|
|
|
|
.where((productId) =>
|
2024-03-07 03:46:50 +00:00
|
|
|
|
(validIds?.contains(productId) != false) && attrs.contains(productId.attr))
|
2023-12-21 09:14:40 +00:00
|
|
|
|
.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();
|
2024-03-07 03:46:50 +00:00
|
|
|
|
queryProductIds.addAll(GuruApp.instance.productProfile.subscriptionsIapIds.map((e) => e.sku));
|
2023-12-21 09:14:40 +00:00
|
|
|
|
Log.d("refresh product:", tag: "IAP");
|
|
|
|
|
|
for (var productId in queryProductIds) {
|
|
|
|
|
|
Log.d(" => $productId", tag: "IAP");
|
|
|
|
|
|
}
|
2024-03-07 03:46:50 +00:00
|
|
|
|
final response = await _queryProducts(queryProductIds).catchError((error, stacktrace) {
|
|
|
|
|
|
Log.e("getProducts($queryOneOffChargeSkuMap}) error: $error $stacktrace", tag: "IAP");
|
2023-12-21 09:14:40 +00:00
|
|
|
|
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));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-03-07 03:46:50 +00:00
|
|
|
|
GuruAnalytics.instance
|
|
|
|
|
|
.logGuruEvent("dev_iap_action", {"item_category": "load", "result": "true"});
|
2023-12-21 09:14:40 +00:00
|
|
|
|
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;
|
2024-03-07 03:46:50 +00:00
|
|
|
|
final offerProductDetails = GooglePlayProductDetails.fromProductDetails(productDetails);
|
2023-12-21 09:14:40 +00:00
|
|
|
|
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}");
|
2024-03-07 03:46:50 +00:00
|
|
|
|
if (expectBasePlan != offer.basePlanId || expectOfferId != offer.offerId) {
|
2023-12-21 09:14:40 +00:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
detailsMap[id] = offerProductDetails[i];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return detailsMap;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|