WT1.1.5 In-app-purchase
This commit is contained in:
@@ -8,6 +8,7 @@ import 'package:badges/badges.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:sentry/sentry.dart';
|
||||
|
||||
class DateRate {
|
||||
static String daily = "daily";
|
||||
@@ -130,4 +131,13 @@ mixin Common {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> sendMessage(String message) async {
|
||||
// Sends a full Sentry event payload to show the different parts of the UI.
|
||||
await Sentry.captureMessage(
|
||||
message,
|
||||
level: SentryLevel.info,
|
||||
template: 'Message: %s, customerId: ' + Cache().userLoggedIn.customerId.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+167
-98
@@ -1,28 +1,29 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
/*
|
||||
import 'package:aitrainer_app/model/cache.dart';
|
||||
import 'package:aitrainer_app/model/product.dart';
|
||||
import 'package:aitrainer_app/service/logging.dart';
|
||||
import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
|
||||
class PlatformPurchaseApi with Logging {
|
||||
static final PlatformPurchaseApi _singleton = PlatformPurchaseApi._internal();
|
||||
|
||||
StreamSubscription _purchaseUpdatedSubscription;
|
||||
StreamSubscription _purchaseErrorSubscription;
|
||||
StreamSubscription _conectionSubscription;
|
||||
String _platformVersion = 'Unknown';
|
||||
List<IAPItem> _items = [];
|
||||
List getIAPItems() => _items;
|
||||
List<PurchasedItem> _purchases = [];
|
||||
bool _kAutoConsume = true;
|
||||
final String _kConsumableId = 'consumable';
|
||||
final InAppPurchaseConnection _connection = InAppPurchaseConnection.instance;
|
||||
bool _isAvailable = false;
|
||||
List<ProductDetails> _products = [];
|
||||
List<PurchaseDetails> _purchases = [];
|
||||
List<String> _notFoundIds = [];
|
||||
List<String> _consumables = [];
|
||||
bool _purchasePending = false;
|
||||
String _queryProductError;
|
||||
|
||||
StreamSubscription<List<PurchaseDetails>> _subscription;
|
||||
|
||||
final List<String> _productList = List();
|
||||
|
||||
List getProductList() => _productList;
|
||||
|
||||
|
||||
|
||||
factory PlatformPurchaseApi() {
|
||||
return _singleton;
|
||||
}
|
||||
@@ -30,55 +31,152 @@ class PlatformPurchaseApi with Logging {
|
||||
PlatformPurchaseApi._internal();
|
||||
|
||||
Future<void> close() async {
|
||||
if (_conectionSubscription != null) {
|
||||
_conectionSubscription.cancel();
|
||||
_conectionSubscription = null;
|
||||
_subscription.cancel();
|
||||
}
|
||||
|
||||
Future<void> initStoreInfo() async {
|
||||
final bool isAvailable = await _connection.isAvailable();
|
||||
if (!isAvailable) {
|
||||
_isAvailable = isAvailable;
|
||||
_products = [];
|
||||
_purchases = [];
|
||||
_notFoundIds = [];
|
||||
_consumables = [];
|
||||
_purchasePending = false;
|
||||
log("Payment processor not available");
|
||||
return;
|
||||
}
|
||||
await FlutterInappPurchase.instance.endConnection;
|
||||
|
||||
getSubscriptions();
|
||||
ProductDetailsResponse productDetailResponse = await _connection.queryProductDetails(_productList.toSet());
|
||||
log("ProductDetailsResponse " + productDetailResponse.toString());
|
||||
if (productDetailResponse.error != null) {
|
||||
_queryProductError = productDetailResponse.error.message;
|
||||
_isAvailable = isAvailable;
|
||||
_products = productDetailResponse.productDetails;
|
||||
_purchases = [];
|
||||
_notFoundIds = productDetailResponse.notFoundIDs;
|
||||
_consumables = [];
|
||||
_purchasePending = false;
|
||||
log("Payment processor not available");
|
||||
return;
|
||||
}
|
||||
|
||||
if (productDetailResponse.productDetails.isEmpty) {
|
||||
_queryProductError = null;
|
||||
_isAvailable = isAvailable;
|
||||
_products = productDetailResponse.productDetails;
|
||||
_purchases = [];
|
||||
_notFoundIds = productDetailResponse.notFoundIDs;
|
||||
_consumables = [];
|
||||
_purchasePending = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_products = productDetailResponse.productDetails;
|
||||
_products.forEach((element) {
|
||||
ProductDetails product = element as ProductDetails;
|
||||
log("Product " + product.id + " " + product.price + " " + product.skuDetail.toString());
|
||||
});
|
||||
|
||||
final QueryPurchaseDetailsResponse purchaseResponse = await _connection.queryPastPurchases();
|
||||
if (purchaseResponse.error != null) {
|
||||
// handle query past purchase error..
|
||||
}
|
||||
|
||||
final List<PurchaseDetails> verifiedPurchases = [];
|
||||
for (PurchaseDetails purchase in purchaseResponse.pastPurchases) {
|
||||
if (await _verifyPurchase(purchase)) {
|
||||
verifiedPurchases.add(purchase);
|
||||
}
|
||||
}
|
||||
|
||||
//TODO List<String> consumables = await ConsumableStore.load();
|
||||
_isAvailable = isAvailable;
|
||||
_products = productDetailResponse.productDetails;
|
||||
_purchases = verifiedPurchases;
|
||||
_notFoundIds = productDetailResponse.notFoundIDs;
|
||||
//_consumables = consumables;
|
||||
_purchasePending = false;
|
||||
}
|
||||
|
||||
// Platform messages are asynchronous, so we initialize in an async method.
|
||||
Future<void> initPurchasePlatformState() async {
|
||||
Future<void> initPurchasePlatform() async {
|
||||
if (_productList.length > 0) {
|
||||
return;
|
||||
}
|
||||
log(' --- InappPurchase connection: $_connection');
|
||||
log(" --- Init PurchasePlatform");
|
||||
String platformVersion;
|
||||
// Platform messages may fail, so we use a try/catch PlatformException.
|
||||
try {
|
||||
platformVersion = await FlutterInappPurchase.instance.platformVersion;
|
||||
} on Exception {
|
||||
platformVersion = 'Failed to get platform version for InAppPurchase.';
|
||||
log("PlatformVersion" + platformVersion);
|
||||
await this.initStoreInfo();
|
||||
|
||||
Stream purchaseUpdated = InAppPurchaseConnection.instance.purchaseUpdatedStream;
|
||||
_subscription = purchaseUpdated.listen((purchaseDetailsList) {
|
||||
_listenToPurchaseUpdated(purchaseDetailsList);
|
||||
}, onDone: () {
|
||||
_subscription.cancel();
|
||||
}, onError: (error) {
|
||||
// handle error here.
|
||||
log("Error listen purchase updated " + error.toString());
|
||||
});
|
||||
}
|
||||
|
||||
void deliverProduct(PurchaseDetails purchaseDetails) async {
|
||||
log("DeliverProduct");
|
||||
// IMPORTANT!! Always verify a purchase purchase details before delivering the product.
|
||||
if (purchaseDetails.productID == _kConsumableId) {
|
||||
//TODO
|
||||
//await ConsumableStore.save(purchaseDetails.purchaseID);
|
||||
//List<String> consumables = await ConsumableStore.load();
|
||||
|
||||
_purchasePending = false;
|
||||
//_consumables = consumables;
|
||||
} else {
|
||||
_purchases.add(purchaseDetails);
|
||||
_purchasePending = false;
|
||||
}
|
||||
}
|
||||
|
||||
// prepare
|
||||
var result = await FlutterInappPurchase.instance.initConnection;
|
||||
log(' FlutterInappPurchase init result: $result');
|
||||
Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) {
|
||||
log("verifyPurchase");
|
||||
// IMPORTANT!! Always verify a purchase before delivering the product.
|
||||
// For the purpose of an example, we directly return true.
|
||||
log("Verifying Purchase" + purchaseDetails.toString());
|
||||
return Future<bool>.value(true);
|
||||
}
|
||||
|
||||
_platformVersion = platformVersion;
|
||||
void _handleInvalidPurchase(PurchaseDetails purchaseDetails) {
|
||||
// handle invalid purchase here if _verifyPurchase` failed.
|
||||
log("Invalid Purchase" + purchaseDetails.toString());
|
||||
}
|
||||
|
||||
// refresh items for android
|
||||
try {
|
||||
String msg = await FlutterInappPurchase.instance.consumeAllItems;
|
||||
log('consumeAllItems: $msg');
|
||||
} catch (err) {
|
||||
log('consumeAllItems error: $err');
|
||||
}
|
||||
|
||||
_conectionSubscription = FlutterInappPurchase.connectionUpdated.listen((connected) {
|
||||
log('connected: $connected');
|
||||
void _listenToPurchaseUpdated(List<PurchaseDetails> purchaseDetailsList) {
|
||||
purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async {
|
||||
if (purchaseDetails.status == PurchaseStatus.pending) {
|
||||
//showPendingUI();
|
||||
log("Purchase pending");
|
||||
} else {
|
||||
if (purchaseDetails.status == PurchaseStatus.error) {
|
||||
//handleError(purchaseDetails.error);
|
||||
} else if (purchaseDetails.status == PurchaseStatus.purchased) {
|
||||
bool valid = await _verifyPurchase(purchaseDetails);
|
||||
if (valid) {
|
||||
deliverProduct(purchaseDetails);
|
||||
} else {
|
||||
_handleInvalidPurchase(purchaseDetails);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (Platform.isAndroid) {
|
||||
if (!_kAutoConsume && purchaseDetails.productID == _kConsumableId) {
|
||||
await InAppPurchaseConnection.instance.consumePurchase(purchaseDetails);
|
||||
}
|
||||
}
|
||||
if (purchaseDetails.pendingCompletePurchase) {
|
||||
await InAppPurchaseConnection.instance.completePurchase(purchaseDetails);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
_purchaseUpdatedSubscription = FlutterInappPurchase.purchaseUpdated.listen((productItem) {
|
||||
log('purchase-updated: $productItem');
|
||||
});
|
||||
|
||||
_purchaseErrorSubscription = FlutterInappPurchase.purchaseError.listen((purchaseError) {
|
||||
log('purchase-error: $purchaseError');
|
||||
});
|
||||
|
||||
getSubscriptions();
|
||||
}
|
||||
|
||||
void getSubscriptions() {
|
||||
@@ -98,7 +196,6 @@ class PlatformPurchaseApi with Logging {
|
||||
_productList.forEach((element) {
|
||||
print(element);
|
||||
});
|
||||
_getSubscription();
|
||||
}
|
||||
|
||||
void purchase(Product product) {
|
||||
@@ -106,57 +203,29 @@ class PlatformPurchaseApi with Logging {
|
||||
throw Exception("No product to purchase");
|
||||
}
|
||||
String productId = Platform.isAndroid ? product.productIdAndroid : product.productIdIos;
|
||||
IAPItem selected;
|
||||
for (var item in _items) {
|
||||
if (item.productId == productId) {
|
||||
selected = item;
|
||||
log("Item to purchase: " + item.toString());
|
||||
ProductDetails productDetails;
|
||||
_products.forEach((element) {
|
||||
if (element.id == productId) {
|
||||
productDetails = element;
|
||||
log("product to purchase: " +
|
||||
productDetails.id +
|
||||
" title " +
|
||||
productDetails.title +
|
||||
" price " +
|
||||
productDetails.price +
|
||||
" period " +
|
||||
productDetails.skuDetail.subscriptionPeriod);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (selected != null) {
|
||||
requestPurchase(selected);
|
||||
} else {
|
||||
throw Exception("product " + productId + " not defined in the PlatformStore");
|
||||
}
|
||||
}
|
||||
|
||||
void requestPurchase(IAPItem item) {
|
||||
//FlutterInappPurchase.instance.requestPurchase(item.productId);
|
||||
FlutterInappPurchase.instance.requestPurchase(item.productId);
|
||||
}
|
||||
|
||||
Future _getSubscription() async {
|
||||
List<IAPItem> items = await FlutterInappPurchase.instance.getSubscriptions(_productList);
|
||||
for (var item in items) {
|
||||
log('${item.toString()}');
|
||||
this._items.add(item);
|
||||
}
|
||||
|
||||
this._items = items;
|
||||
this._purchases = [];
|
||||
}
|
||||
|
||||
Future _getPurchases() async {
|
||||
List<PurchasedItem> items = await FlutterInappPurchase.instance.getAvailablePurchases();
|
||||
for (var item in items) {
|
||||
log('${item.toString()}');
|
||||
this._purchases.add(item);
|
||||
}
|
||||
|
||||
this._items = [];
|
||||
this._purchases = items;
|
||||
}
|
||||
|
||||
Future _getPurchaseHistory() async {
|
||||
List<PurchasedItem> items = await FlutterInappPurchase.instance.getPurchaseHistory();
|
||||
for (var item in items) {
|
||||
log('${item.toString()}');
|
||||
this._purchases.add(item);
|
||||
}
|
||||
|
||||
this._items = [];
|
||||
this._purchases = items;
|
||||
final PurchaseParam purchaseParam = PurchaseParam(productDetails: productDetails);
|
||||
/* if (_isConsumable(productDetails)) {
|
||||
InAppPurchaseConnection.instance.buyConsumable(purchaseParam: purchaseParam);
|
||||
} else { */
|
||||
InAppPurchaseConnection.instance.buyNonConsumable(purchaseParam: purchaseParam);
|
||||
//}
|
||||
// From here the purchase flow will be handled by the underlying storefront.
|
||||
// Updates will be delivered to the `InAppPurchaseConnection.instance.purchaseUpdatedStream`.
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:aitrainer_app/model/cache.dart';
|
||||
import 'package:aitrainer_app/service/logging.dart';
|
||||
import 'package:aitrainer_app/util/common.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:purchases_flutter/purchases_flutter.dart';
|
||||
import 'package:aitrainer_app/model/product.dart' as wtproduct;
|
||||
|
||||
class RevenueCatPurchases with Logging {
|
||||
static final RevenueCatPurchases _singleton = RevenueCatPurchases._internal();
|
||||
Offering _offering;
|
||||
String appUserId;
|
||||
|
||||
factory RevenueCatPurchases() {
|
||||
return _singleton;
|
||||
}
|
||||
|
||||
RevenueCatPurchases._internal();
|
||||
|
||||
Offering get offering => _offering;
|
||||
|
||||
Future<void> initPlatform() async {
|
||||
if (Cache().userLoggedIn != null) {
|
||||
await Purchases.setDebugLogsEnabled(true);
|
||||
await Purchases.setup("yLxGFWaeRtThLImqznvBnUEKjgArSsZE", appUserId: Cache().userLoggedIn.customerId.toString());
|
||||
appUserId = await Purchases.appUserID;
|
||||
log("AppUserId: " + appUserId);
|
||||
await this.restore();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> restore() async {
|
||||
if (appUserId != null) {
|
||||
try {
|
||||
PurchaserInfo purchaserInfo = await Purchases.restoreTransactions();
|
||||
if (purchaserInfo != null &&
|
||||
purchaserInfo.entitlements.all["wt_subscription"] != null &&
|
||||
purchaserInfo.entitlements.all["wt_subscription"].isActive) {
|
||||
Cache().hasPurchased = true;
|
||||
log(" ******************************************** ");
|
||||
log(" Purchase active ! ");
|
||||
} else {
|
||||
log(" ** No subscription active");
|
||||
}
|
||||
} on PlatformException catch (e) {
|
||||
log("Purchaserinfo not reachable " + e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<Offering> getOfferings() async {
|
||||
if (appUserId == null) {
|
||||
await initPlatform();
|
||||
}
|
||||
log(" .. acessing offerings...");
|
||||
try {
|
||||
Offerings offerings = await Purchases.getOfferings();
|
||||
if (offerings.current != null && offerings.current.availablePackages.isNotEmpty) {
|
||||
// Display packages for sale
|
||||
_offering = offerings.current;
|
||||
} else {
|
||||
log("No current offerings");
|
||||
Common.sendMessage("No Current offerings");
|
||||
}
|
||||
} on PlatformException catch (e) {
|
||||
// optional error handling
|
||||
log("!!!! Getting offerings error " + e.toString());
|
||||
}
|
||||
return _offering;
|
||||
}
|
||||
|
||||
Future<void> makePurchase(wtproduct.Product product) async {
|
||||
try {
|
||||
if (_offering == null) {
|
||||
_offering = await getOfferings();
|
||||
}
|
||||
if (_offering != null) {
|
||||
String productId = Platform.isAndroid ? product.productIdAndroid : product.productIdIos;
|
||||
Package selectedPackage;
|
||||
log("Nr of packages: " + _offering.availablePackages.length.toString() + " ProductId: " + productId);
|
||||
for (var package in _offering.availablePackages) {
|
||||
log("package to check " + package.product.identifier.toString());
|
||||
if (package.product.identifier == productId) {
|
||||
selectedPackage = package;
|
||||
log("**** Selected package to purchase" + package.product.identifier);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (selectedPackage != null) {
|
||||
PurchaserInfo purchaserInfo = await Purchases.purchasePackage(selectedPackage);
|
||||
if (purchaserInfo.entitlements.all["wt_subscription"].isActive) {
|
||||
Cache().hasPurchased = true;
|
||||
}
|
||||
} else {
|
||||
log("!!!! No Selected package to purchase");
|
||||
Common.sendMessage("No Selected package to purchase");
|
||||
throw Exception("Purchase was not successful");
|
||||
}
|
||||
} else {
|
||||
log("!!!! No active offering");
|
||||
}
|
||||
} on PlatformException catch (e) {
|
||||
var errorCode = PurchasesErrorHelper.getErrorCode(e);
|
||||
if (errorCode == PurchasesErrorCode.invalidReceiptError) {
|
||||
log("iOS Sandbox invalid receipt");
|
||||
Cache().hasPurchased = true;
|
||||
return;
|
||||
}
|
||||
log(e.toString());
|
||||
if (errorCode == PurchasesErrorCode.purchaseCancelledError) {
|
||||
Common.sendMessage("Purchase was cancelled");
|
||||
throw Exception("Purchase was cancelled");
|
||||
} else {
|
||||
Common.sendMessage("Purchase was not successful");
|
||||
throw Exception("Purchase was not successful");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import 'package:aitrainer_app/service/api.dart';
|
||||
import 'package:aitrainer_app/service/logging.dart';
|
||||
import 'package:aitrainer_app/service/product_service.dart';
|
||||
import 'package:aitrainer_app/service/property_service.dart';
|
||||
import 'package:aitrainer_app/util/purchases.dart';
|
||||
import 'package:devicelocale/devicelocale.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
@@ -28,6 +29,7 @@ class Session with Logging {
|
||||
Cache().setServerAddress(_sharedPreferences);
|
||||
Cache().getHardware(_sharedPreferences);
|
||||
await _fetchToken(_sharedPreferences);
|
||||
await RevenueCatPurchases().initPlatform();
|
||||
//initDeviceLocale();
|
||||
|
||||
// Create the initialization Future outside of `build`:
|
||||
|
||||
Reference in New Issue
Block a user