Repository navigation
[in_app_purchase]InAppPurchase.instance.purchaseStream.listen increase call in every product consume. #98910
Description
UPDATE : InAppPurchase.instance.purchaseStream.listen() onDone method is not calling after succesfull consume.
If you do not call _subscription.cancel() after succesfull consume , it increase number of calls with same purchaseDetailsList object one by one...
Example:
Product B consumed and money taken from user
.listen((purchaseDetailsList) {
...
}
called one time.
Product B consumed again and money taken from user or process cancelled by user
.listen((purchaseDetailsList) {
...
}
called two time.
the loop goes this way
"final Stream<List<PurchaseDetails>> purchaseUpdated = InAppPurchase.instance.purchaseStream;
_subscription = purchaseUpdated.listen((purchaseDetailsList) {
_listenToPurchaseUpdated(purchaseDetailsList);
}, onDone: () {
_subscription.cancel();
}, onError: (error) {
debugPrint("_initializeIAP Error : $error");
});"
hello, we have an app with comsumable products for android and ios. When a user starts to receive consumables in the app, if they start receiving the same consumables again without closing the app, the listen method starts making extra calls one by one. That's why we load multiple credits for a payment on the backend side. I am sharing the whole service below.
it always call listen method with the last purchased consumable
package version : 2.0.1
code
// ignore_for_file: prefer_final_fields, depend_on_referenced_packages, directives_ordering, null_closures, unnecessary_null_comparison
import 'dart:async';
import 'dart:io';
import 'package:falsepeti/constants/api_urls.dart';
import 'package:falsepeti/models/credit.dart';
import 'package:falsepeti/providers/api_mode_provider.dart';
import 'package:falsepeti/services/shared_prefs_service.dart';
import 'package:http/http.dart' as http;
import 'package:falsepeti/providers/credits_provider.dart';
import 'package:falsepeti/providers/user_provider.dart';
import 'package:flutter/material.dart';
import 'package:in_app_purchase_android/in_app_purchase_android.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:provider/provider.dart';
class PurchaseModel {
final dynamic pckg;
final ProductDetails productDetail;
PurchaseModel({this.pckg, required this.productDetail});
}
class PurchaseService {
bool pendingPurchases = false;
late BuildContext context;
late InAppPurchase _connection;
late StreamSubscription<List<PurchaseDetails>> _subscription;
late List<ProductDetails> _playProducts = [];
late List<Credit> _apiPackages = [];
late List<PurchaseModel> _products = [];
int completed = 0;
void dispose() {
_subscription.cancel();
}
PurchaseService({required this.context});
void init() {
_connection = InAppPurchase.instance;
_initializeIAP();
_proccessPastPurchases();
debugPrint("Puschase Service initialized!");
}
Future<void> _initializeIAP() async {
final Stream<List<PurchaseDetails>> purchaseUpdated = InAppPurchase.instance.purchaseStream;
_subscription = purchaseUpdated.listen((purchaseDetailsList) {
_listenToPurchaseUpdated(purchaseDetailsList);
}, onDone: () {
_subscription.cancel();
}, onError: (error) {
debugPrint("_initializeIAP Error : $error");
});
}
//BIND PLAY PRODUCTS AND API PACKAGES
Future<void> _mapProducts() async {
_playProducts.sort((pda, pdb) {
final pa = _apiPackages.firstWhere((package) => package.sku == pda.id, orElse: null);
final pb = _apiPackages.firstWhere((package) => package.sku == pdb.id, orElse: null);
if (pa == null || pb == null) {
return 0;
}
// ignore: cast_nullable_to_non_nullable
return (pa.orderNumber as int).compareTo(pb.orderNumber as int);
});
_products.clear();
final UserProvider userProvider = Provider.of<UserProvider>(context, listen: false);
for (final ProductDetails pd in _playProducts) {
final pckg = _apiPackages.firstWhere((package) => package.sku == pd.id);
if (pckg == null) continue;
if (userProvider.user.usedSpecialPackageForMobile == 1) {
if (pd.id == "special_for_mobile") continue;
if (pd.id == "ios_special_for_mobile") continue;
}
_products.add(PurchaseModel(pckg: pckg, productDetail: pd));
}
}
//FETCH API PACKAGES
Future<void> _getApiProducts() async {
final CreditsProvider creditProvider = Provider.of<CreditsProvider>(context, listen: false);
_apiPackages.clear();
for (final package in creditProvider.credits) {
_apiPackages.add(package);
}
}
Future<List<PurchaseModel>> getProducts() async {
await _getApiProducts();
await getPlayProducts();
_mapProducts();
return _products;
}
//FETCH PLAY PRODUCTS
Future<void> getPlayProducts() async {
final bool isAvailable = await _connection.isAvailable();
if (!isAvailable) return;
_playProducts.clear();
final ProductDetailsResponse pdr = await _connection.queryProductDetails(_apiPackages.map<String>((credit) => credit.sku ?? "").toSet());
_playProducts.addAll(pdr.productDetails);
}
Future<bool> buyProduct({required PurchaseModel purchaseModel}) async {
return _connection.buyConsumable(
purchaseParam: PurchaseParam(
productDetails: purchaseModel.productDetail,
),
);
}
Future<void> _proccessPastPurchases() async {
debugPrint("===== Purchase Service _proccessPastPurchases ===========");
await _connection.restorePurchases();
final Stream<List<PurchaseDetails>> purchaseUpdated = InAppPurchase.instance.purchaseStream;
purchaseUpdated.listen((purchaseDetailsList) {
_processPurchaseDetailsList(purchaseDetailsList, past: true);
});
}
void _listenToPurchaseUpdated(List<PurchaseDetails> purchaseDetailsList) {
_processPurchaseDetailsList(purchaseDetailsList);
}
Future<void> _processPurchaseDetailsList(List<PurchaseDetails> purchaseDetailsList, {bool past = false}) async {
// ignore: unused_local_variable
PurchaseDetails pending;
if (purchaseDetailsList.isEmpty) {
debugPrint("_processPurchaseDetailsList() Purchase List Empty ");
} else {
pending = purchaseDetailsList.first;
for (final PurchaseDetails purchaseDetails in purchaseDetailsList) {
if (purchaseDetails.status == PurchaseStatus.pending) {
pending = purchaseDetails;
}
await _processPurchase(purchaseDetails);
}
}
}
Future<void> _processPurchase(PurchaseDetails purchaseDetails) async {
switch (purchaseDetails.status) {
case PurchaseStatus.purchased:
await _processPurchasedPurchase(purchaseDetails);
break;
case PurchaseStatus.pending:
await _processPendingPurchase(purchaseDetails);
break;
case PurchaseStatus.restored:
await _processPendingPurchase(purchaseDetails);
break;
case PurchaseStatus.error:
await _processErrorPurchase(purchaseDetails);
break;
case PurchaseStatus.canceled:
await _processErrorPurchase(purchaseDetails);
break;
}
}
Future<void> _processPendingPurchase(PurchaseDetails purchaseDetails) async {
if (Platform.isAndroid) {
// Acquire the InAppPurchaseAndroidPlatformAddition instance.
final InAppPurchaseAndroidPlatformAddition androidAddition = _connection.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
// Consume an Android purchase.
await ApiModeProvider().getCurrentApiMode();
final String _url = ApiUrls.playStorePending;
final String _token = await SharedPrefsService().getToken() ?? "";
final Map<String, dynamic> playData = {
"product_id": purchaseDetails.productID,
"purchase_token": purchaseDetails.verificationData.serverVerificationData,
"order_id": purchaseDetails.purchaseID,
"important": "1" // this is for apiclient if there is no token it will use oldtoken
};
await http.post(Uri.parse(_url),
body: playData,
headers: {"Authorization": "Bearer $_token", "Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded"}).catchError((e) {
debugPrint("_processPendingPurchase error : $e");
});
await androidAddition.consumePurchase(purchaseDetails).catchError((e) {
debugPrint("_processPendingPurchase comsumePurchase Error : $e");
});
}
if (Platform.isIOS) {}
return;
}
Future<void> _processPurchasedPurchase(PurchaseDetails purchaseDetails) async {
if (Platform.isAndroid) {
final InAppPurchaseAndroidPlatformAddition androidAddition = _connection.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
// Consume an Android purchase.
await ApiModeProvider().getCurrentApiMode();
final String _url = ApiUrls.playStoreInPayment;
final String _token = await SharedPrefsService().getToken() ?? "";
final Map<String, dynamic> playPurchasedData = {
"product_id": purchaseDetails.productID,
"purchase_token": purchaseDetails.verificationData.serverVerificationData,
"important": "1" // this is for apiclient if there is no token it will use oldtoken
};
debugPrint("Android backend load request");
await http.post(Uri.parse(_url),
body: playPurchasedData,
headers: {"Authorization": "Bearer $_token", "Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded"}).catchError((e) {
debugPrint("_processPurchasedPurchase error : $e");
});
await androidAddition.consumePurchase(purchaseDetails).catchError((e) {
debugPrint("_processPurchasedPurchase comsumePurchase Error : $e");
}).then((value) => {completed = 1});
}
if (Platform.isIOS) {
await ApiModeProvider().getCurrentApiMode();
final String _url = ApiUrls.appStoreInPayment;
final String _token = await SharedPrefsService().getToken() ?? "";
final Map<String, dynamic> appStorePurchasedData = {
"product_id": purchaseDetails.productID,
"purchase_token": purchaseDetails.verificationData.serverVerificationData,
"important": "1" // this is for apiclient if there is no token it will use oldtoken
};
await http.post(Uri.parse(_url),
body: appStorePurchasedData,
headers: {"Authorization": "Bearer $_token", "Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded"}).catchError((e) {
debugPrint("_processPurchasedPurchase error : $e");
});
await _connection.completePurchase(purchaseDetails).catchError((e) {
debugPrint("_processPurchasedPurchase completePurchase Error : $e");
}).then((value) => {completed = 1});
}
}
Future<void> _processErrorPurchase(PurchaseDetails purchaseDetails) async {
if (Platform.isAndroid) {
completed = 2;
}
if (Platform.isIOS) {
// in in error status will be complete too.
await _connection.completePurchase(purchaseDetails);
completed = 2;
}
}
}
Details
```Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel stable, 2.8.1, on Microsoft Windows [Version 10.0.19044.1526], locale tr-TR)
[√] Android toolchain - develop for Android devices (Android SDK version 33.0.0-rc1)
[√] Chrome - develop for the web
[√] Android Studio (version 2021.1)
[!] Android Studio (version 4.1)
X Unable to determine bundled Java version.
[√] VS Code (version 1.64.2)
[√] Connected device (3 available)
</details>
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsNeeds Vetting
Activity
darshankawar commentedon Feb 23, 2022
@utkudenis
In order to properly address and analyze this behavior, please provide a self-contained but minimal reproducible code sample without any third party plugins that we can directly use and run to verify. Current code you provided seems to have third party plugin code and is too large to go through.
Also provide the stacktrace or console log which shows
increase call in every product consume..utkudenis commentedon Feb 23, 2022
Hi @darshankawar
Main problem is :
Even though I consume a product after buying it, it makes too many calls by increasing it without decreasing the for variable in itself.
the thirdy party packages only http(for packend request) and provider(for statemanegement). They are must packages :)
Although I have examined the codes in the service many times, I have shared the whole service here because I could not find the deficiency that would cause this problem.
we discovered the issue by adding debugPrints to Future _processPurchasedPurchase(PurchaseDetails purchaseDetails) function
utkudenis commentedon Feb 24, 2022
By the way, even if the user does not pay and opens and closes the structure, "listen" constantly increases the number of calls. I opened and closed the play store payment structure opened in google play 3 times and it called once, twice in the second, and three times in the third.
utkudenis commentedon Feb 24, 2022
I updated version to 3.0.0 but still continue:
That is the refactored version of the service
Note : The "onDone" function in "listen" does not work at all, even if the operation is completed or canceled :(
Note 2 : I'm calling dispose() function according to complete variable on the widget side
code
darshankawar commentedon Feb 28, 2022
@utkudenis
Thanks for further details and for your patience. Looking at above code snippet you shared, Can you do something like below and see if you still get same behavior ?
StreamSubscription _subscription;instead oflate StreamSubscription<List<PurchaseDetails>> _subscription;and then
utkudenis commentedon Mar 3, 2022
@darshankawar I updated version to 3.0.0 and changed some parts of the service. Now, loop is stopped.
I am closing stream after every consumable purchase when it is succesfull or not.
But in normal condition your package should stop it :)
your onDone method is not working although purchase completed or cancelled...
Now, I have a different problem.
If there is a crash or connection problem in purchase I can easily get purchased but not consumed product from Android,
However, iOS does not send purchased but not consumed product when I call
"await _connection.restorePurchases();" method.
In order to solve it ı find a code snippet which is
but I cannot get "purchaseDetails.verificationData.serverVerificationData" from transaction.
How can ı get serverVerificationData or how to get purchased but not consumed productdetaillist in iOS ?
darshankawar commentedon Mar 3, 2022
@utkudenis this seems specific to iOS, so I suggest you to open a new issue with all details and we'll address it.
For your original issue description, what exactly is the current status now ?
utkudenis commentedon Mar 3, 2022
@darshankawar hello,
current issue solved with manuel _subscription.cancel(); but your onDone method not working after purchase completed :(
7 remaining items
utkudenis commentedon Mar 11, 2022
Hi @darshankawar,
Package also increase call time in every init :(
init method code sample :
" void init() {
_connection = InAppPurchase.instance;
_initializeIAP();
debugPrint("Puschase Service initialized!");
}
Future _initializeIAP() async {
final Stream<List> purchaseUpdated = InAppPurchase.instance.purchaseStream;
_subscription = purchaseUpdated.listen((purchaseDetailsList) {
_listenToPurchaseUpdated(purchaseDetailsList);
}, onDone: () {
_subscription.cancel();
}, onError: (error) {
debugPrint("_initializeIAP Error : $error");
});"
shenjingfs commentedon Apr 27, 2023
Hi @utkudenis,
You may have misunderstood purchaseStream: Even if you purchase multiple times, you only need to subscribe once in the App lifecycle. After subscribing to purchaseStream once, it will deliver all purchase events. Multiple subscriptions at the same time will result in receiving the same event multiple times. For example, in the
_initializeIAPand_proccessPastPurchasesmethods, they are subscribed twice.Regarding the problem of
onDone method is not calling,onDoneis not called at the end of the purchase process, but is called when the stream is closed, and the comment of purchaseStream says:This stream will never close as long as the app is active., which meansonDonewill not be called during the normal App lifecycle. PurchaseDetails whose status is not pending in _listenToPurchaseUpdated means the end of the purchase process.I suggest only subscribing purchaseStream once in the
initStatedofrootWidget(example: _MyAppState), and there is no need to subscribe again later.If
PurchaseServiceis not a Singleton, but will be created multiple times (such as each time you enter the purchase page), then you need to manually unsubscribe at the right time (when you exit the purchase page)qiezihwy commentedon Jul 15, 2025
also stack in there ,thank you for your explaination. @shenjingfs