Skip to content

[in_app_purchase]InAppPurchase.instance.purchaseStream.listen increase call in every product consume. #98910

Open

Description

@utkudenis

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>


Activity

  1. darshankawar commented on Feb 23, 2022

    @darshankawar
    Member

    @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..

  2. added
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Feb 23, 2022
  3. utkudenis commented on Feb 23, 2022

    @utkudenis
    Author

    Hi @darshankawar
    Main problem is :

    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");
       });
     }
    
    
    • Llsten function increase call number in every consume.
    • For example : user buy one consumable and purchaseUpdated.listen called with purchased product one time and this function worked _listenToPurchaseUpdated()
    • Without closing app, when user buy one consumable again purchaseUpdated.listen funtion call 2 times with last product
    • it continue this process like for loop every buying process increase this calling one by one.
      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

  4. removed
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Feb 23, 2022
  5. utkudenis commented on Feb 24, 2022

    @utkudenis
    Author

    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.

  6. utkudenis commented on Feb 24, 2022

    @utkudenis
    Author

    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
    
    // 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';
    
    const PAST_PURCHASE_CHECK_DURATION = Duration(minutes: 60);
    
    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 {
        try {
          await _connection.restorePurchases();
          final Stream<List<PurchaseDetails>> purchaseUpdated = InAppPurchase.instance.purchaseStream;
          purchaseUpdated.listen((purchaseDetailsList) {
            _processPurchaseDetailsList(purchaseDetailsList, past: true);
          });
        } catch (e) {
          debugPrint("Purchase Service _proccessPastPurchases Exception $e");
        }
      }
    
      void _listenToPurchaseUpdated(List<PurchaseDetails> purchaseDetailsList) {
        _processPurchaseDetailsList(purchaseDetailsList);
      }
    
      Future<void> _processPurchaseDetailsList(List<PurchaseDetails> purchaseDetailsList, {bool past = false}) async {
        // ignore: unused_local_variable
        if (purchaseDetailsList.isNotEmpty) {
          for (final PurchaseDetails purchaseDetails in purchaseDetailsList) {
            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) {
          _connection.completePurchase(purchaseDetails);
        }
    
        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("Backend purchase Android 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 (purchaseDetails.pendingCompletePurchase) {
            await _connection.completePurchase(purchaseDetails);
          }
        }
        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");
          });
          debugPrint("Backend purchase iOS request");
          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;
        }
      }
    }
    
    
  7. darshankawar commented on Feb 28, 2022

    @darshankawar
    Member

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

    @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 of late 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 {
          ...
        });
    }
    
  8. added
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Feb 28, 2022
  9. utkudenis commented on Mar 3, 2022

    @utkudenis
    Author

    @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 :)

    "final Stream<List<PurchaseDetails>> purchaseUpdated = InAppPurchase.instance.purchaseStream;
        _subscription = purchaseUpdated.listen((purchaseDetailsList) {
          _listenToPurchaseUpdated(purchaseDetailsList);
        }, onDone: () {
          _subscription.cancel();
        }, onError: (error) {
          debugPrint("_initializeIAP Error : $error");
        });"
    

    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

    "
    // checks ios not consumed purchased consumables
        if (Platform.isIOS) {
          if (checkIosPendings) {
            var paymentWrapper = SKPaymentQueueWrapper();
            var transactions = await paymentWrapper.transactions();
            transactions.forEach((transaction) async {
              await paymentWrapper.finishTransaction(transaction).then((value) async => {
                    if (transaction.transactionState.name == "purchased") {await processIosPendings(transaction.payment.productIdentifier, "")}
                  });
            });
            checkIosPendings = false;
          }
        }"
    

    but I cannot get "purchaseDetails.verificationData.serverVerificationData" from transaction.

    How can ı get serverVerificationData or how to get purchased but not consumed productdetaillist in iOS ?

  10. removed
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Mar 3, 2022
  11. darshankawar commented on Mar 3, 2022

    @darshankawar
    Member

    How can ı get serverVerificationData or how to get purchased but not consumed productdetaillist in iOS ?

    @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 ?

  12. added
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Mar 3, 2022
  13. utkudenis commented on Mar 3, 2022

    @utkudenis
    Author

    @darshankawar hello,
    current issue solved with manuel _subscription.cancel(); but your onDone method not working after purchase completed :(

  14. removed
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Mar 3, 2022
  15. 7 remaining items

  16. added
    P2Important issues not at the top of the work list
    on Mar 10, 2022
  17. utkudenis commented on Mar 11, 2022

    @utkudenis
    Author

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

  18. shenjingfs commented on Apr 27, 2023

    @shenjingfs

    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 _initializeIAP and _proccessPastPurchases methods, they are subscribed twice.

    Regarding the problem of onDone method is not calling, onDone is 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 means onDone will 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 initStated of rootWidget(example: _MyAppState), and there is no need to subscribe again later.
    If PurchaseService is 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)

  19. qiezihwy commented on Jul 15, 2025

    @qiezihwy

    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 _initializeIAP and _proccessPastPurchases methods, they are subscribed twice.

    Regarding the problem of onDone method is not calling, onDone is 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 means onDone will 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 initStated of rootWidget(example: _MyAppState), and there is no need to subscribe again later. If PurchaseService is 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)

    also stack in there ,thank you for your explaination. @shenjingfs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work listp: in_app_purchasePlugin for in-app purchasepackageflutter/packages repository. See also p: labels.team-ecosystemOwned by Ecosystem teamtriaged-ecosystemTriaged by Ecosystem team

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions