2024-05-22 20:48:44 +00:00
|
|
|
import 'dart:async';
|
|
|
|
|
import 'dart:developer';
|
|
|
|
|
import 'dart:io';
|
|
|
|
|
|
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:in_app_purchase/in_app_purchase.dart';
|
2024-08-06 21:33:11 +00:00
|
|
|
import 'package:in_app_purchase_android/in_app_purchase_android.dart';
|
2024-05-22 20:48:44 +00:00
|
|
|
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
2024-07-25 08:10:15 +00:00
|
|
|
import 'package:url_launcher/url_launcher_string.dart';
|
2024-05-22 20:48:44 +00:00
|
|
|
import 'package:webview_flutter/webview_flutter.dart';
|
|
|
|
|
import '../../di/injector.dart';
|
|
|
|
|
import '../../di/locator.dart';
|
2024-07-18 21:45:04 +00:00
|
|
|
import '../../flags.dart';
|
2024-05-22 20:48:44 +00:00
|
|
|
import '../../main.dart';
|
2024-05-28 21:25:51 +00:00
|
|
|
import '../analytics/analytics.dart';
|
2024-07-18 21:45:04 +00:00
|
|
|
import 'rustore/mnemo_rustore.dart';
|
2024-05-22 20:48:44 +00:00
|
|
|
|
|
|
|
|
enum StoreItemType {
|
|
|
|
|
ads,
|
|
|
|
|
subscription,
|
|
|
|
|
pack,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class InAppPurchaseService {
|
|
|
|
|
final InAppPurchase _inAppPurchase = InAppPurchase.instance;
|
|
|
|
|
final Stream<List<PurchaseDetails>> storeSubscription =
|
|
|
|
|
InAppPurchase.instance.purchaseStream;
|
|
|
|
|
|
|
|
|
|
InAppPurchase get instance => _inAppPurchase;
|
|
|
|
|
|
2024-08-06 21:33:11 +00:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-22 20:48:44 +00:00
|
|
|
Future<bool> buyPack(CardPackBuyDto dto) async {
|
2024-05-28 21:25:51 +00:00
|
|
|
try {
|
2024-07-18 21:45:04 +00:00
|
|
|
if (RUSTORE_BUILD) {
|
2024-08-05 00:12:20 +00:00
|
|
|
// return RustorePurchaseService().buyPack(dto);
|
|
|
|
|
return false;
|
2024-07-18 21:45:04 +00:00
|
|
|
} else {
|
|
|
|
|
final pack = await _getPackStoreProduct(dto);
|
|
|
|
|
final result = await _buyItemInStore(pack.first);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
2024-05-28 21:25:51 +00:00
|
|
|
} catch (e, s) {
|
|
|
|
|
log('err', error: e, stackTrace: s);
|
|
|
|
|
Analytics.buyError(
|
|
|
|
|
dto,
|
2024-07-18 21:45:04 +00:00
|
|
|
androidPaymentSystem,
|
2024-05-28 21:25:51 +00:00
|
|
|
data: {
|
|
|
|
|
'e': e.toString(),
|
|
|
|
|
's': s.toString(),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2024-05-22 20:48:44 +00:00
|
|
|
}
|
|
|
|
|
|
2024-05-28 21:25:51 +00:00
|
|
|
Future<bool> buyPackWithYooMoney(
|
|
|
|
|
CardPackBuyDto dto, BuildContext? context) async {
|
2024-05-22 20:48:44 +00:00
|
|
|
final paymentUrl =
|
|
|
|
|
await locator.repository.createPayment(dto.id, PaymentSystem.yookassa);
|
|
|
|
|
if (paymentUrl == null) {
|
2024-05-28 21:25:51 +00:00
|
|
|
Analytics.buyError(
|
|
|
|
|
dto,
|
|
|
|
|
PaymentSystem.yookassa,
|
|
|
|
|
message: 'payment url is null',
|
|
|
|
|
);
|
2024-05-22 20:48:44 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
final controller = WebViewController()
|
|
|
|
|
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
2024-07-25 08:10:15 +00:00
|
|
|
..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;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
);
|
2024-05-22 20:48:44 +00:00
|
|
|
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) {
|
2024-05-28 21:25:51 +00:00
|
|
|
Analytics.buyComplete(
|
|
|
|
|
dto,
|
|
|
|
|
PaymentSystem.yookassa,
|
|
|
|
|
);
|
2024-05-22 20:48:44 +00:00
|
|
|
locator.packUpdater.updateAvailablePacks();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2024-05-28 21:25:51 +00:00
|
|
|
Analytics.buyError(
|
|
|
|
|
dto,
|
|
|
|
|
PaymentSystem.yookassa,
|
|
|
|
|
message: 'payment not verified',
|
|
|
|
|
);
|
2024-05-22 20:48:44 +00:00
|
|
|
return false;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-18 21:45:04 +00:00
|
|
|
Future<List<ProductDetails>> _getPackStoreProduct(CardPackBuyDto dto) async {
|
2024-05-22 20:48:44 +00:00
|
|
|
final bool isAvailable = await _inAppPurchase.isAvailable();
|
|
|
|
|
if (!isAvailable) {
|
2024-05-28 21:25:51 +00:00
|
|
|
Analytics.buyError(
|
|
|
|
|
dto,
|
2024-07-18 21:45:04 +00:00
|
|
|
androidPaymentSystem,
|
2024-05-28 21:25:51 +00:00
|
|
|
message: 'in app purchase not available',
|
|
|
|
|
);
|
2024-05-22 20:48:44 +00:00
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
final ids = {
|
2024-08-06 21:33:11 +00:00
|
|
|
if (Platform.isAndroid) RUSTORE_BUILD ? dto.rustoreId : dto.googlePlayId,
|
2024-05-22 20:48:44 +00:00
|
|
|
if (Platform.isIOS) dto.appStoreId,
|
|
|
|
|
}.whereNotNull().toSet();
|
|
|
|
|
if (ids.isEmpty) {
|
2024-05-28 21:25:51 +00:00
|
|
|
Analytics.buyError(
|
|
|
|
|
dto,
|
2024-07-18 21:45:04 +00:00
|
|
|
androidPaymentSystem,
|
2024-05-28 21:25:51 +00:00
|
|
|
message: 'product id not set',
|
|
|
|
|
);
|
2024-05-22 20:48:44 +00:00
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final ProductDetailsResponse productDetailResponse =
|
|
|
|
|
await _inAppPurchase.queryProductDetails(ids);
|
|
|
|
|
|
|
|
|
|
if (productDetailResponse.error != null ||
|
|
|
|
|
productDetailResponse.productDetails.isEmpty) {
|
2024-05-28 21:25:51 +00:00
|
|
|
Analytics.buyError(
|
|
|
|
|
dto,
|
2024-07-18 21:45:04 +00:00
|
|
|
androidPaymentSystem,
|
2024-05-28 21:25:51 +00:00
|
|
|
data: {
|
|
|
|
|
'msg': 'cant get product details ${productDetailResponse.error}',
|
|
|
|
|
'not_found': productDetailResponse.notFoundIDs.join(','),
|
|
|
|
|
},
|
|
|
|
|
);
|
2024-05-22 20:48:44 +00:00
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return productDetailResponse.productDetails;
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-06 21:33:11 +00:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-18 21:45:04 +00:00
|
|
|
Future<bool> _buyItemInStore(ProductDetails product) async {
|
2024-08-06 21:33:11 +00:00
|
|
|
final PurchaseParam purchaseParam = PurchaseParam(productDetails: product);
|
|
|
|
|
return InAppPurchase.instance.buyConsumable(purchaseParam: purchaseParam);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<bool> _buySubscriptionInStore(ProductDetails product) async {
|
2024-05-22 20:48:44 +00:00
|
|
|
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 {
|
2024-05-28 21:25:51 +00:00
|
|
|
Analytics.buyEvent(
|
|
|
|
|
null,
|
2024-07-18 21:45:04 +00:00
|
|
|
androidPaymentSystem,
|
2024-05-28 21:25:51 +00:00
|
|
|
message: 'onPurchased',
|
|
|
|
|
data: {
|
|
|
|
|
'status': purchaseDetails.status.name,
|
|
|
|
|
'product': purchaseDetails.productID,
|
|
|
|
|
'date': purchaseDetails.transactionDate,
|
|
|
|
|
},
|
|
|
|
|
);
|
2024-05-22 20:48:44 +00:00
|
|
|
final result = await locator.repository.checkPayment(
|
2024-08-03 22:37:10 +00:00
|
|
|
itemId: purchaseDetails.productID,
|
|
|
|
|
key: purchaseDetails.verificationData.serverVerificationData,
|
|
|
|
|
paymentSystem: androidPaymentSystem,
|
2024-05-22 20:48:44 +00:00
|
|
|
);
|
|
|
|
|
if (result) {
|
2024-05-28 21:25:51 +00:00
|
|
|
Analytics.buyComplete(
|
|
|
|
|
null,
|
2024-07-18 21:45:04 +00:00
|
|
|
androidPaymentSystem,
|
2024-05-28 21:25:51 +00:00
|
|
|
message: purchaseDetails.productID,
|
|
|
|
|
);
|
2024-05-22 20:48:44 +00:00
|
|
|
log('Purchased');
|
|
|
|
|
} else {
|
2024-05-28 21:25:51 +00:00
|
|
|
Analytics.buyError(
|
|
|
|
|
null,
|
|
|
|
|
PaymentSystem.yookassa,
|
|
|
|
|
message: 'payment not verified ${purchaseDetails.productID}',
|
|
|
|
|
);
|
2024-05-22 20:48:44 +00:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|