I take code from offical in_app_purchases documentation, purchases work correctly.
I have function that show paid content, and i need to run it after purchase done correctly, but i don't know where i need to put it, because i steel don't inderstand purchases 100% correctly.
this is my code
final InAppPurchase _inAppPurchase = InAppPurchase.instance;
final String _productID = '1d7ea644f690ffa';
bool _available = true;
List<ProductDetails> _products = [];
List<PurchaseDetails> _purchases = [];
StreamSubscription<List<PurchaseDetails>>? _subscription;
#override
void initState() {
final Stream<List<PurchaseDetails>> purchaseUpdated = _inAppPurchase.purchaseStream;
_subscription = purchaseUpdated.listen((purchaseDetailsList) {
setState(() {
_purchases.addAll(purchaseDetailsList);
_listenToPurchaseUpdated(purchaseDetailsList);
});
}, onDone: () {
_subscription!.cancel();
}, onError: (error) {
_subscription!.cancel();
});
_initialize();
super.initState();
}
#override
void dispose() {
_subscription!.cancel();
super.dispose();
}
void _initialize() async {
_available = await _inAppPurchase.isAvailable();
List<ProductDetails> products = await _getProducts(
productIds: Set<String>.from(
[_productID],
),
);
setState(() {
_products = products;
});
}
void _listenToPurchaseUpdated(List<PurchaseDetails> purchaseDetailsList) {
purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async {
switch (purchaseDetails.status) {
case PurchaseStatus.pending:
// _showPendingUI();
break;
case PurchaseStatus.purchased:
break;
case PurchaseStatus.restored:
// bool valid = await _verifyPurchase(purchaseDetails);
// if (!valid) {
// _handleInvalidPurchase(purchaseDetails);
// }
break;
case PurchaseStatus.error:
print(purchaseDetails.error!);
// _handleError(purchaseDetails.error!);
break;
default:
break;
}
if (purchaseDetails.pendingCompletePurchase) {
await _inAppPurchase.completePurchase(purchaseDetails);
CheckListModel.addPaidData(purchaseDetails.productID);
}
});
}
Future<List<ProductDetails>> _getProducts({required Set<String> productIds}) async {
ProductDetailsResponse response = await _inAppPurchase.queryProductDetails(productIds);
return response.productDetails;
}
void _subscribe({required ProductDetails product}) {
final PurchaseParam purchaseParam = PurchaseParam(productDetails: product);
_inAppPurchase.buyNonConsumable(
purchaseParam: purchaseParam,
);
}
_subscribe function start when user click on special button in ui
function that show paid contenct name is
CheckListModel.addPaidData(purchaseDetails.productID);
when function start it create file paid.paid in getApplicationDocumentsDirectory, if it doesn't exist, and add in it productID . That what must happen
Where i need to place this function?
Related
This was working already but I updated package to latest version:
https://pub.dev/packages/in_app_purchase
I can make the payment and check what I bought but after refresh it doesn't restore this. If I try to buy the same version again it says that I already own it:
Future<void> initStore() async {
final bool isAvailableTemp = await inAppPurchase.isAvailable();
if (!isAvailable) {
isAvailable = isAvailableTemp;
products = [];
purchases = [];
notFoundIds = [];
purchasePending = false;
loading = false;
return;
}
if (Platform.isIOS) {
final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition =
inAppPurchase
.getPlatformAddition<InAppPurchaseStoreKitPlatformAddition>();
await iosPlatformAddition.setDelegate(PaymentQueueDelegate());
}
final ProductDetailsResponse productDetailResponse =
await inAppPurchase.queryProductDetails(_androidProductIds.toSet());
if (productDetailResponse.error != null) {
queryProductError = productDetailResponse.error!.message;
isAvailable = isAvailable;
products = productDetailResponse.productDetails;
purchases = <PurchaseDetails>[];
notFoundIds = productDetailResponse.notFoundIDs;
purchasePending = false;
loading = false;
return;
}
}
Future<List<ProductDetails>> getProducts() async {
if (products.isEmpty) {
final ProductDetailsResponse products =
await inAppPurchase.queryProductDetails(
Platform.isAndroid
? _androidProductIds.toSet()
: _iOSProductIds.toSet(),
);
if (products.productDetails.isNotEmpty) {
// double check the order of the products
if (Helpers.containsInStringIgnoreCase(
products.productDetails[0].title, "plus")) {
this.products.addAll(products.productDetails);
} else {
// pro comes first
this.products.add(products.productDetails[1]);
this.products.add(products.productDetails[0]);
}
}
}
return products;
}
Future<List<PurchaseDetails>> getPastPurchases(BuildContext context) async {
if (purchases.isEmpty) {
final Stream<List<PurchaseDetails>> purchaseUpdated =
inAppPurchase.purchaseStream;
_subscription = purchaseUpdated.listen((purchaseDetailsList) {
if (purchaseDetailsList.isEmpty) {
detectProVideo(context);
} else {
// this.purchases.addAll(purchaseDetailsList);
listenToPurchaseUpdated(context, purchaseDetailsList);
}
_subscription.cancel();
}, onDone: () {
_subscription.cancel();
}, onError: (error) {
// handle error here.
_subscription.cancel();
});
await inAppPurchase.restorePurchases();
if (Platform.isIOS) {
Map<String, PurchaseDetails> purchases =
Map.fromEntries(this.purchases.map((PurchaseDetails purchase) {
if (purchase.pendingCompletePurchase) {
inAppPurchase.completePurchase(purchase);
}
return MapEntry<String, PurchaseDetails>(
purchase.productID, purchase);
}));
if (purchases.isEmpty) {
Provider.of<AdState>(context, listen: false).toggleAds(context, true);
} else {
Provider.of<AdState>(context, listen: false)
.toggleAds(context, false);
}
}
}
return purchases;
}
So this is always true:
if (purchaseDetailsList.isEmpty) {
Testing locally on an Android emulator
I'm trying to refactor my code a little bit, and I want to refactor payment logic using BLoC.
The problem is that I'm still getting an error that says:
emit was called after an event handler completed normally.
I only need information from state if the transaction is Pending, Complete or there is an error, so there is only true or false to show the CircularIndicator.
There is my try to implement Payment using BLoC.
_onStarted - starts subscription and is called once on initState
_buyProduct - is called when user hits the button
class PaymentBloc extends Bloc<PaymentEvent, PaymentState> {
static const bool _started = false;
final InAppPurchase _inAppPurchase = InAppPurchase.instance;
Set<String> _kProductIds = {"mini_burger_4_99"};
List<ProductDetails> _products = [];
PaymentBloc() : super(PaymentInitial(_started)) {
on<PaymentStarted>(_onStarted);
on<PaymentCompletion>(_buyProduct);
}
StreamSubscription<List<PurchaseDetails>>? _subscription;
void _onStarted(PaymentStarted event, Emitter<PaymentState> emit) {
final Stream<List<PurchaseDetails>> purchaseUpdated =
_inAppPurchase.purchaseStream;
_subscription = purchaseUpdated.listen((purchaseDetailsList) {
purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.pending) {
emit(PaymentPending());
} else {
if (purchaseDetails.status == PurchaseStatus.error) {
emit(PaymentError());
} else if (purchaseDetails.pendingCompletePurchase) {
await _inAppPurchase.completePurchase(purchaseDetails);
emit(PaymentComplete());
}
}
});
}, onDone: () {
_subscription?.cancel();
}, onError: (error) {
// handle error.
});
initStoreInfo();
}
Future<void> _buyProduct(
PaymentCompletion event, Emitter<PaymentState> emit) async {
var paymentWrapper = SKPaymentQueueWrapper();
var transactions = await paymentWrapper.transactions();
transactions.forEach((transaction) async {
await paymentWrapper.finishTransaction(transaction);
});
final PurchaseParam purchaseParam = PurchaseParam(
productDetails:
_products.firstWhere((product) => product.id == event.id));
await _inAppPurchase.buyConsumable(purchaseParam: purchaseParam);
}
#override
Future<void> close() {
_subscription?.cancel();
return super.close();
}
Future<void> initStoreInfo() async {
final bool isAvailable = await _inAppPurchase.isAvailable();
if (!isAvailable) {
_products = [];
return;
}
if (Platform.isIOS) {
var iosPlatformAddition = _inAppPurchase
.getPlatformAddition<InAppPurchaseIosPlatformAddition>();
await iosPlatformAddition.setDelegate(ExamplePaymentQueueDelegate());
}
ProductDetailsResponse productDetailResponse =
await _inAppPurchase.queryProductDetails(_kProductIds);
if (productDetailResponse.error != null) {
_products = productDetailResponse.productDetails;
return;
}
if (productDetailResponse.productDetails.isEmpty) {
_products = productDetailResponse.productDetails;
return;
}
_products = productDetailResponse.productDetails;
}
}
Thanks for any kind of help.
Matt
I'm trying to implement a renewable subscription in flutter using the flutter_in_app_purchases plugin. When I click on the screen that this is declared in, it goes through the initState() function and then gets to the initPlatformState() and goes through that successfully, but when it gets to the getProducts() function, it's returning an empty item list for the List items = FlutterInappPurchase.instance.getSubscriptions([productID]); call. I've added the monthly subscription in both the App Store Connect and Google Play Store and completed the tax forms. Any help would be appreciated.
List<IAPItem> _items = [];
static const String productID = 'monthly_subscription';
#override
void initState() {
super.initState();
print("IN INIT STATE");
initPlatformState();
}
Future<void> initPlatformState() async {
print("In init platform state");
// prepare
final bool available = await InAppPurchaseConnection.instance.isAvailable();
print(available);
var close = await FlutterInappPurchase.instance.endConnection;
var result = await FlutterInappPurchase.instance.initConnection;
print('result: $result');
// If the widget was removed from the tree while the asynchronous platform
// message was in flight, we want to discard the reply rather than calling
// setState to update our non-existent appearance.
if (!mounted) {
print('In not mounded');
return;
}
// refresh items for android
/*try {
String msg = await FlutterInappPurchase.instance.consumeAllItems;
print('consumeAllItems: $msg');
} catch(e){
print(e.toString());
}*/
await _getProduct();
}
Future<Null> _getProduct() async {
print("In get products");
try {
List<IAPItem> items = await FlutterInappPurchase.instance.getSubscriptions([productID]);
print("Items is: $items");
for (var item in items) {
print('${item.toString()}');
this._items.add(item);
}
setState(() {
this._items = items;
});
} catch(e) {
print(e.toString());
}
}
Here you have a working example from app in production. Disclaimer: I'm not using it anymore but the last time I did it worked fine:
class _InAppState extends State<InApp> {
StreamSubscription _purchaseUpdatedSubscription;
StreamSubscription _purchaseErrorSubscription;
StreamSubscription _conectionSubscription;
final List<String> _productLists = Platform.isAndroid
? [
'subs_premium', 'subs_user'
]
: ['subs_premium', 'subs_boss', 'subscripcion_user'];
String _platformVersion = 'Unknown';
List<IAPItem> _items = [];
List<IAPItem> _subscripions = [];
List<PurchasedItem> _purchases = [];
#override
void initState() {
super.initState();
initPlatformState();
}
#override
void dispose() {
super.dispose();
if (_conectionSubscription != null) {
_conectionSubscription.cancel();
_conectionSubscription = null;
}
}
// Platform messages are asynchronous, so we initialize in an async method.
Future<void> initPlatformState() async {
String platformVersion;
// Platform messages may fail, so we use a try/catch PlatformException.
try {
platformVersion = await FlutterInappPurchase.instance.platformVersion;
} on PlatformException {
platformVersion = 'Failed to get platform version.';
}
// prepare
var result = await FlutterInappPurchase.instance.initConnection;
print('result: $result');
// If the widget was removed from the tree while the asynchronous platform
// message was in flight, we want to discard the reply rather than calling
// setState to update our non-existent appearance.
if (!mounted) return;
setState(() {
_platformVersion = platformVersion;
});
// refresh items for android
try {
String msg = await FlutterInappPurchase.instance.consumeAllItems;
print('consumeAllItems: $msg');
} catch (err) {
print('consumeAllItems error: $err');
}
_conectionSubscription = FlutterInappPurchase.connectionUpdated.listen((connected) {
print('connected: $connected');
});
_purchaseUpdatedSubscription = FlutterInappPurchase.purchaseUpdated.listen((productItem) {
print('purchase-updated: $productItem');
});
_purchaseErrorSubscription = FlutterInappPurchase.purchaseError.listen((purchaseError) {
print('purchase-error: $purchaseError');
});
final List<String> _SKUS = widget.premium ? ['subs_boss']
: ['subs_user'] ;
_getSubscriptions(_SKUS);
}
void _requestPurchase(IAPItem item) {
FlutterInappPurchase.instance.requestPurchase(item.productId);
}
Future _getProduct() async {
print('TEST 1 HERE ${_productLists.length}, ${_productLists.first.toString()}');
List<IAPItem> items = await FlutterInappPurchase.instance.getProducts(_productLists);
print('TEST 2 HERE ${items.length}');
for (var item in items) {
print('${item.toString()}');
this._items.add(item);
}
setState(() {
this._items = items;
this._purchases = [];
});
}
Future _getPurchases() async {
List<PurchasedItem> items =
await FlutterInappPurchase.instance.getAvailablePurchases();
for (var item in items) {
print('${item.toString()}');
this._purchases.add(item);
}
setState(() {
this._items = [];
this._purchases = items;
});
}
Future _getSubscriptions(_SKUS) async {
List<IAPItem> items =
await FlutterInappPurchase.instance.getSubscriptions(_SKUS);
for (var item in items) {
print('${item.toString()}');
this._subscripions.add(item);
}
setState(() {
this._items = [];
this._subscripions = items;
});
}
Future _getPurchaseHistory() async {
List<PurchasedItem> items = await FlutterInappPurchase.instance.getPurchaseHistory();
for (var item in items) {
print('${item.toString()}');
this._purchases.add(item);
}
setState(() {
this._items = [];
this._purchases = items;
});
}
I want to set up Non-Consumable purchase in my app. I found a lot of tutorials about Consumable purchases, but unfortunately I haven't found any articles about Non-Consumable.
I tried using in_app_purchase package, but it didn't work.
final String testIdAdvanced = 'advanced_training';
InAppPurchaseConnection _iap = InAppPurchaseConnection.instance;
bool _available = true;
List<ProductDetails> advancedProducts = [];
List<ProductDetails> advancedPurchases = [];
StreamSubscription _subscription;
void _initialize() async {
_available = await _iap.isAvailable();
if (_available) {
await _getProducts();
await _getPastPurchases();
}
}
Future<void> _getProducts() async {
Set<String> ids = Set.from([testIdAdvanced, 'test_a']);
ProductDetailsResponse response = await _iap.queryProductDetails(ids);
setState(() {
advancedProducts = response.productDetails;
});
}
// Gets previous purchases
Future<void> _getPastPurchases() async {
QueryPurchaseDetailsResponse response =
await _iap.queryPastPurchases();
for (PurchaseDetails purchase in response.pastPurchases) {
if (Platform.isIOS) {
InAppPurchaseConnection.instance.completePurchase(purchase);
}
}
setState(() {
advancedPurchases = response.pastPurchases.cast<ProductDetails>();
});
}
void _buyProduct(ProductDetails prod) {
final PurchaseParam purchaseParam = PurchaseParam(productDetails: prod);
_iap.buyNonConsumable(purchaseParam: purchaseParam);
}
I am able to buy a subscription on Android, and the subscription shows up in queryPastPurchases(), but the _listenToPurchaseUpdated() method is never triggered after buying the product. I know that this was a bug in previous releases of flutter in_app_purchase, but it was said to be fixed in 0.2.1. However it doesn't seem to work for me... Is there something wrong with my code?
/// The In App Purchase plugin
InAppPurchaseConnection _iap = InAppPurchaseConnection.instance
/// Updates to purchases
StreamSubscription<List<PurchaseDetails>> _subscription;
#override
initState() {
final Stream purchaseUpdates = InAppPurchaseConnection.instance.purchaseUpdatedStream;
_subscription = purchaseUpdates.listen((purchases) {
_purchases.addAll(purchases);
_listenToPurchaseUpdated(_purchases);
});
super.initState();
}
/// Get all products available for sale
Future<void> _getProducts() async {
Set<String> ids = Set.from(['subscription_product']);
ProductDetailsResponse response = await _iap.queryProductDetails(ids);
setState(() {
_products = response.productDetails;
});
if(response.error != null && response.error.message != null){
setState(() {
_iapStoreProblem = response.error.message;
});
}
}
void _buyProduct(ProductDetails prod) {
final PurchaseParam purchaseParam = PurchaseParam(productDetails: prod);
_iap.buyNonConsumable(purchaseParam: purchaseParam);
}
I found the solution.
Turns out I had to use
StreamSubscription _subscription;
instead of
StreamSubscription<List<PurchaseDetails>> _subscription;
and then:
_subscription = _iap.purchaseUpdatedStream.listen((data) => setState(() {
_purchases.addAll(data);
_listenToPurchaseUpdated(data);
}));
void _listenToPurchaseUpdated(List<PurchaseDetails> purchaseDetailsList) {
purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async {
...
});
}
Also, a small note: PurchaseDetails.status on Android is Null.