How to use in_app_purchase using BLoC? - flutter

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

Related

Flutter can't restore purchases

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

Flutter in_app_purchase, show content after purchase

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?

the in_app_purchase is unavaliable when running into inital logic in flutter

Now I am using in_app_purchase to integrete with my app. This is my dependencies using:
in_app_purchase: 1.0.0
I follow the docs of example. When running into page and initial the store info I facing a problem. This is my full code I am using now:
import 'dart:async';
import 'package:cruise/src/models/pay/pay_model.dart';
import 'package:fish_redux/fish_redux.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'action.dart';
import 'state.dart';
Effect<PayState> buildEffect() {
return combineEffects(<Object, Effect<PayState>>{
Lifecycle.initState: _onInit,
});
}
const bool _kAutoConsume = true;
const String _kConsumableId = 'consumable';
const String _kUpgradeId = 'upgrade';
const String _kSilverSubscriptionId = 'subscription_silver';
const String _kGoldSubscriptionId = 'subscription_gold';
final InAppPurchase _inAppPurchase = InAppPurchase.instance;
late StreamSubscription<List<PurchaseDetails>> _subscription;
String? _queryProductError;
const List<String> _kProductIds = <String>[
_kConsumableId,
_kUpgradeId,
_kSilverSubscriptionId,
_kGoldSubscriptionId,
];
Future _onInit(Action action, Context<PayState> ctx) async {
// https://pub.dev/packages/in_app_purchase
// https://joebirch.co/flutter/adding-in-app-purchases-to-flutter-apps/
final Stream<List<PurchaseDetails>> purchaseUpdated =
_inAppPurchase.purchaseStream;
_subscription = purchaseUpdated.listen((purchaseDetailsList) {
_listenToPurchaseUpdated(purchaseDetailsList);
}, onDone: () {
_subscription.cancel();
}, onError: (error) {
// handle error here.
});
initStoreInfo(ctx);
}
void _listenToPurchaseUpdated(List<PurchaseDetails> purchaseDetailsList) {
purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.pending) {
//_showPendingUI();
} else {
if (purchaseDetails.status == PurchaseStatus.error) {
//_handleError(purchaseDetails.error!);
} else if (purchaseDetails.status == PurchaseStatus.purchased ||
purchaseDetails.status == PurchaseStatus.restored) {}
if (purchaseDetails.pendingCompletePurchase) {
await InAppPurchase.instance.completePurchase(purchaseDetails);
}
}
});
}
Future<void> initStoreInfo(Context<PayState> ctx) async {
final bool isAvailable = await _inAppPurchase.isAvailable();
if (!isAvailable) {
PayModel payModel = PayModel(
isAvailable: isAvailable,
products: [],
purchases: [],
notFoundIds: [],
purchasePending: false,
loading: false);
ctx.dispatch(PayActionCreator.onUpdate(payModel));
return;
}
ProductDetailsResponse productDetailResponse = await _inAppPurchase.queryProductDetails(_kProductIds.toSet());
if (productDetailResponse.error != null) {
/*setState(() {
_queryProductError = productDetailResponse.error!.message;
_isAvailable = isAvailable;
_products = productDetailResponse.productDetails;
_purchases = [];
_notFoundIds = productDetailResponse.notFoundIDs;
_consumables = [];
_purchasePending = false;
_loading = false;
});*/
return;
}
if (productDetailResponse.productDetails.isEmpty) {
/*setState(() {
_queryProductError = null;
_isAvailable = isAvailable;
_products = productDetailResponse.productDetails;
_purchases = [];
_notFoundIds = productDetailResponse.notFoundIDs;
_consumables = [];
_purchasePending = false;
_loading = false;
});*/
return;
}
await _inAppPurchase.restorePurchases();
/*List<String> consumables = await ConsumableStore.load();
setState(() {
_isAvailable = isAvailable;
_products = productDetailResponse.productDetails;
_notFoundIds = productDetailResponse.notFoundIDs;
_consumables = consumables;
_purchasePending = false;
_loading = false;
});*/
}
what makes me confusing is that when running into this code _inAppPurchase.isAvailable(), shows the _inAppPurchase is unavaliable(return false). why the _inAppPurchase unavaliable? what situation may cause this problem? what should I do to make it avaliable? this is the full code with project context maybe help to understand what I am facing.
I already do:
add subscribe product in apple connect
add a sandbox user
add in app purchase capablilities in XCode
anywhere could see the log why the inAppPurchase is unavaliable? This is my in purchase config:

Flutter flutter_in_app_purchases subscription FlutterInAppPurchses.instance.getSubscriptions() is not retrieving any items for IAPItem

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;
});
}

Flutter: How to set up Non-Consumable purchase?

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);
}