import 'dart:async'; import 'dart:developer'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:in_app_purchase/in_app_purchase.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:webview_flutter/webview_flutter.dart'; import '../../di/injector.dart'; import '../../di/locator.dart'; import '../../flags.dart'; import '../../main.dart'; import '../analytics/analytics.dart'; import 'rustore/mnemo_rustore.dart'; enum StoreItemType { ads, subscription, pack, } class InAppPurchaseService { final InAppPurchase _inAppPurchase = InAppPurchase.instance; final Stream> storeSubscription = InAppPurchase.instance.purchaseStream; InAppPurchase get instance => _inAppPurchase; Future buyPack(CardPackBuyDto dto) async { try { if (RUSTORE_BUILD) { // return RustorePurchaseService().buyPack(dto); return false; } else { final pack = await _getPackStoreProduct(dto); final result = await _buyItemInStore(pack.first); return result; } } catch (e, s) { log('err', error: e, stackTrace: s); Analytics.buyError( dto, androidPaymentSystem, data: { 'e': e.toString(), 's': s.toString(), }, ); return false; } } Future buyPackWithYooMoney( CardPackBuyDto dto, BuildContext? context) async { final paymentUrl = await locator.repository.createPayment(dto.id, PaymentSystem.yookassa); if (paymentUrl == null) { Analytics.buyError( dto, PaymentSystem.yookassa, message: 'payment url is null', ); return false; } final controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..loadRequest(Uri.parse(paymentUrl)) ..setNavigationDelegate( NavigationDelegate( onNavigationRequest: (request) async { final url = request.url; if (url.startsWith('http')) { return NavigationDecision.navigate; } else { if (await canLaunchUrlString(url)) { launchUrlString(url); return NavigationDecision.prevent; } return NavigationDecision.navigate; } }, ), ); return showModalBottomSheet( context: context ?? scaffoldKey.currentContext!, enableDrag: false, showDragHandle: true, useSafeArea: true, isScrollControlled: true, backgroundColor: Colors.white, builder: (context) { return Container( child: WebViewWidget(controller: controller), padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, ), ); }, ).then((_) async { final success = await locator.repository.checkUserPayment(); if (success) { Analytics.buyComplete( dto, PaymentSystem.yookassa, ); locator.packUpdater.updateAvailablePacks(); return true; } Analytics.buyError( dto, PaymentSystem.yookassa, message: 'payment not verified', ); return false; }); } Future> _getPackStoreProduct(CardPackBuyDto dto) async { final bool isAvailable = await _inAppPurchase.isAvailable(); if (!isAvailable) { Analytics.buyError( dto, androidPaymentSystem, message: 'in app purchase not available', ); return []; } final ids = { if (Platform.isAndroid) dto.googlePlayId, if (Platform.isIOS) dto.appStoreId, }.whereNotNull().toSet(); if (ids.isEmpty) { Analytics.buyError( dto, androidPaymentSystem, message: 'product id not set', ); return []; } final ProductDetailsResponse productDetailResponse = await _inAppPurchase.queryProductDetails(ids); if (productDetailResponse.error != null || productDetailResponse.productDetails.isEmpty) { Analytics.buyError( dto, androidPaymentSystem, data: { 'msg': 'cant get product details ${productDetailResponse.error}', 'not_found': productDetailResponse.notFoundIDs.join(','), }, ); return []; } return productDetailResponse.productDetails; } Future _buyItemInStore(ProductDetails product) async { final PurchaseParam purchaseParam = PurchaseParam(productDetails: product); return InAppPurchase.instance .buyNonConsumable(purchaseParam: purchaseParam); } Future completePurchase(PurchaseDetails purchaseDetails) async { await InAppPurchase.instance.completePurchase(purchaseDetails); } } class PurchaseDetailsStreamSubscription { final InAppPurchaseService inAppPurchaseService = getIt.get(); Future onPurchased(PurchaseDetails purchaseDetails) async { try { Analytics.buyEvent( null, androidPaymentSystem, message: 'onPurchased', data: { 'status': purchaseDetails.status.name, 'product': purchaseDetails.productID, 'date': purchaseDetails.transactionDate, }, ); final result = await locator.repository.checkPayment( itemId: purchaseDetails.productID, key: purchaseDetails.verificationData.serverVerificationData, paymentSystem: androidPaymentSystem, ); if (result) { Analytics.buyComplete( null, androidPaymentSystem, message: purchaseDetails.productID, ); log('Purchased'); } else { Analytics.buyError( null, PaymentSystem.yookassa, message: 'payment not verified ${purchaseDetails.productID}', ); throw Exception('Cant verify purchase'); } } catch (e, s) { log(e.toString(), stackTrace: s); } } StreamSubscription>? _streamSubscription; PurchaseDetailsStreamSubscription(); Future init() async { _streamSubscription = inAppPurchaseService.storeSubscription.listen( (List events) { Future.forEach( events, (PurchaseDetails purchaseDetails) async { if (purchaseDetails.pendingCompletePurchase || purchaseDetails.status == PurchaseStatus.purchased) { onPurchased(purchaseDetails); } }, ); }, ); } void close() { _streamSubscription?.cancel(); } }