mnemo_cards/lib/features/purchase/in_app_purchase.dart
2024-08-07 00:33:11 +03:00

316 lines
9.2 KiB
Dart

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:in_app_purchase_android/in_app_purchase_android.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<List<PurchaseDetails>> storeSubscription =
InAppPurchase.instance.purchaseStream;
InAppPurchase get instance => _inAppPurchase;
Future<bool> buySubscription(SubscriptionPlanDto dto) async {
if (dto.paymentSystem == PaymentSystem.yookassa) {
return false;
}
final subscriptionProducts = await _getSubscriptionStoreProduct(dto);
ProductDetails? product;
if (Platform.isAndroid && !RUSTORE_BUILD) {
// final googlePlayProducts = subscriptionProducts.whereType<GooglePlayProductDetails>();
product = subscriptionProducts.firstWhereOrNull(
(p) => p.rawPrice == double.parse(dto.price),
);
} else {
product = subscriptionProducts.first;
}
if (product == null) {
Analytics.buySubscriptionError(
dto,
androidPaymentSystem,
message: 'cant find plan with price ${dto.price}',
data: {
'plans': subscriptionProducts.join(';'),
},
);
return false;
}
final result = await _buySubscriptionInStore(product);
return result;
}
Future<bool> 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<bool> 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 {
launchUrlString(url);
return NavigationDecision.prevent;
}
},
),
);
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<List<ProductDetails>> _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) RUSTORE_BUILD ? dto.rustoreId : 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<List<ProductDetails>> _getSubscriptionStoreProduct(
SubscriptionPlanDto dto,
) async {
final bool isAvailable = await _inAppPurchase.isAvailable();
if (!isAvailable) {
Analytics.buySubscriptionError(
dto,
androidPaymentSystem,
message: 'in app purchase not available',
);
return [];
}
final ids = {
if (Platform.isAndroid)
if (RUSTORE_BUILD && dto.paymentSystem == PaymentSystem.rustore)
dto.paymentId,
if (!RUSTORE_BUILD && dto.paymentSystem == PaymentSystem.google)
dto.googlePlaySubscriptionId,
if (dto.paymentSystem == PaymentSystem.yookassa) dto.paymentId,
if (Platform.isIOS && dto.paymentSystem == PaymentSystem.apple)
dto.paymentId,
}.whereNotNull().toSet();
if (ids.isEmpty) {
Analytics.buySubscriptionError(
dto,
androidPaymentSystem,
message: 'product id not set',
);
return [];
}
final productDetailResponse = await _inAppPurchase.queryProductDetails(ids);
if (productDetailResponse.error != null ||
productDetailResponse.productDetails.isEmpty) {
Analytics.buySubscriptionError(
dto,
androidPaymentSystem,
data: {
'msg': 'cant get product details ${productDetailResponse.error}',
'not_found': productDetailResponse.notFoundIDs.join(','),
},
);
return [];
}
return productDetailResponse.productDetails;
}
Future<bool> _buyItemInStore(ProductDetails product) async {
final PurchaseParam purchaseParam = PurchaseParam(productDetails: product);
return InAppPurchase.instance.buyConsumable(purchaseParam: purchaseParam);
}
Future<bool> _buySubscriptionInStore(ProductDetails product) async {
final PurchaseParam purchaseParam = PurchaseParam(productDetails: product);
return InAppPurchase.instance
.buyNonConsumable(purchaseParam: purchaseParam);
}
Future<void> completePurchase(PurchaseDetails purchaseDetails) async {
await InAppPurchase.instance.completePurchase(purchaseDetails);
}
}
class PurchaseDetailsStreamSubscription {
final InAppPurchaseService inAppPurchaseService = getIt.get();
Future<void> 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<List<PurchaseDetails>>? _streamSubscription;
PurchaseDetailsStreamSubscription();
Future<void> init() async {
_streamSubscription = inAppPurchaseService.storeSubscription.listen(
(List<PurchaseDetails> events) {
Future.forEach(
events,
(PurchaseDetails purchaseDetails) async {
if (purchaseDetails.pendingCompletePurchase ||
purchaseDetails.status == PurchaseStatus.purchased) {
onPurchased(purchaseDetails);
}
},
);
},
);
}
void close() {
_streamSubscription?.cancel();
}
}