import 'dart:async'; import 'dart:developer'; import 'dart:io'; 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:mnemo_cards/features/analytics/analytics.dart'; import 'package:mnemo_cards/managers/repository/repository.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import 'purchase_handler.dart'; class GooglePurchaseHandler implements PurchaseHandler { final InAppPurchase _inAppPurchase = InAppPurchase.instance; final Repository _repository; StreamSubscription? _subscription; GooglePurchaseHandler(this._repository); Future initPurchase({ required String id, required MnemoCardsProductType productType, required String paymentId, String? source, }) async { if (productType == MnemoCardsProductType.unknown) { log('unknown product type'); return; } if (!await isAvailable) { log('Google play payment not available'); return; } final productId = paymentId.split('/').first; final subscriptionId = paymentId.split('/').last; final products = await _getStoreProduct(productId); if (products.isEmpty) { Analytics.buyError( null, paymentSystem, paymentId: paymentId, id: id, message: 'No products', ); return; } switch (productType) { case MnemoCardsProductType.pack: final product = products.first; await _buyInStore(product); return; case MnemoCardsProductType.subscription: // choosing the right subscription plan final subs = products.first.productDetails.subscriptionOfferDetails; final index = subs?.indexWhere((s) => s.basePlanId == subscriptionId); if (index == null) { Analytics.buyError( null, paymentSystem, paymentId: paymentId, id: id, message: 'Unknown subscription id', ); return; } await _buyInStore(products[index]); return; case MnemoCardsProductType.unknown: Analytics.buyError( null, paymentSystem, paymentId: paymentId, id: id, message: 'Unknown product type', ); return; } } Future> _getStoreProduct( String googlePlayId) async { final productDetailResponse = await _inAppPurchase.queryProductDetails({ googlePlayId, }); if (productDetailResponse.error != null || productDetailResponse.productDetails.isEmpty) { Analytics.buyError( null, paymentSystem, paymentId: googlePlayId, data: { 'msg': 'cant get product details ${productDetailResponse.error}', 'not_found': productDetailResponse.notFoundIDs.join(','), }, ); return []; } return productDetailResponse.productDetails.cast(); } Future _buyInStore(ProductDetails product) async { final PurchaseParam purchaseParam = PurchaseParam(productDetails: product); product as GooglePlayProductDetails; if (product.productDetails.productType == ProductType.inapp) { final result = await InAppPurchase.instance.buyConsumable( purchaseParam: purchaseParam, ); return result; } else { final result = InAppPurchase.instance.buyNonConsumable( purchaseParam: purchaseParam, ); return result; } } Future completePurchase(PurchaseDetails purchaseDetails) async { await InAppPurchase.instance.completePurchase(purchaseDetails); } Future _onPurchased(PurchaseDetails purchaseDetails) async { try { final result = await checkPurchase( product: MnemoCardsProduct( id: purchaseDetails.productID, type: MnemoCardsProductType.unknown, ), paymentToken: purchaseDetails.verificationData.serverVerificationData, ); if (result) { Analytics.buyComplete( null, paymentSystem, message: purchaseDetails.productID, ); log('Purchased'); } else { Analytics.buyError( null, PaymentSystem.yookassa, message: 'payment not verified ${purchaseDetails.productID}', ); } } catch (e, s) { log(e.toString(), stackTrace: s); } } Future get isAvailable => _inAppPurchase.isAvailable(); @override PaymentSystem get paymentSystem => PaymentSystem.google; @override Future checkPurchase({ required MnemoCardsProduct product, required String paymentToken, }) async { final result = await _repository.checkPayment( product: product, key: paymentToken, paymentSystem: paymentSystem, ); return result; } Future init() async { if (Platform.isAndroid) { _subscription = _inAppPurchase.purchaseStream.listen( (List events) { Future.forEach( events, (PurchaseDetails purchaseDetails) async { if (purchaseDetails.pendingCompletePurchase || purchaseDetails.status == PurchaseStatus.purchased) { Analytics.buyEvent( null, paymentSystem, message: 'onPurchased', data: { 'status': purchaseDetails.status.name, 'product': purchaseDetails.productID, 'date': purchaseDetails.transactionDate, }, ); _onPurchased(purchaseDetails); } }, ); }, ); } } Future dispose() async { await _subscription?.cancel(); _subscription = null; } }