Flutter: purchases stream is empty and attempted purchase throws 'pending transaction for the same product identifier'
Asked Answered
S

3

10

I'm using the Flutter in_app_purchase plugin, v0.3.3+1.

While testing on iOS, I began a purchase but cancelled mid-way through. After that, whenever I try the purchase again, I get an exception with this message:

There is a pending transaction for the same product identifier

I have a listener setup on the purchase stream (code below) to complete the purchases. But the stream is not emitting any events.

_purchaseListener = InAppPurchaseConnection.instance.purchaseUpdatedStream.listen((purchases) {
  purchases.forEach((purchase) async {
    if (purchase.status == PurchaseStatus.purchased) //...
    if (purchase.pendingCompletePurchase) {
      //Complete purchase (retrying as Google Play refunds after 3 days if this does not succeed)
      retry<BillingResultWrapper>(() async {
        final completion = await InAppPurchaseConnection.instance.completePurchase(purchase);
        const errors = {BillingResponse.error, BillingResponse.serviceUnavailable};
        if (errors.contains(completion.responseCode)) throw Exception();
        return completion;
      });
    }
  });
});
Shenyang answered 10/5, 2020 at 12:48 Comment(0)
R
8

You must clean the pending transaction first.

To do that, first import

import 'package:in_app_purchase_storekit/store_kit_wrappers.dart';

and then call

final paymentWrapper = SKPaymentQueueWrapper();
final transactions = await paymentWrapper.transactions();
transactions.forEach((transaction) async {
    await paymentWrapper.finishTransaction(transaction);
});
Ropable answered 9/12, 2020 at 15:25 Comment(9)
How can i acces SKPaymentQueueWrapper() . I have got an error The function 'SKPaymentQueueWrapper' isn't defined.Carriole
I fixed -> import 'package:in_app_purchase_ios/store_kit_wrappers.dart';Carriole
Where is the best place to use this? I've tried calling this at the start of my listener, but I am stuck in a loop where I always have 99 Restore transactions returned (on iOS). But my device has a canceled subscription. Lost on how to fix this. To note, this is in the Sandbox Environment.Tamatamable
Still relevant fix...Saponin
Unfortunately store_kit_wrappers can't be imported anymore; any ideas? The IAP lib for flutter doesn't support transaction cleaning ..Foliation
For anyone still looking this is the library reference pub.dev/packages/in_app_purchase_storekitDouma
import 'package:in_app_purchase_storekit/store_kit_wrappers.dart';Reciprocation
Using this, I get Unhandled Exception: PlatformException(storekit_finish_transaction_exception, NSInvalidArgumentException, Cannot finish a purchasing transaction, null)Linville
executing await paymentWrapper.finishTransaction(transaction); does not guarantee of transaction being finished, as the await returns as soon as the finishing the transaction has been added to the queue. Currently I have not found a way to wait for the full queue to be completed.Rodge
S
0

The stream was not emitting any events in debug mode, so the purchase was never coming through and being completed.

Testing in release mode, it works perfectly.

Shenyang answered 10/5, 2020 at 12:48 Comment(2)
how did you manage to test that in release mode with the sandbox user?Oliver
Perhaps more to the point, how did you manage to clear the queue once you figured out what was going wrong?Linis
T
0

No need any other library, just use of in_app_purchase: ^3.1.11.

The best approach is to start a timer and ready to check the pendingCompletePurchase status every 1 second, once it is true, the transaction is really really really completed.

Below are my code:

  late List<ProductDetails> purchaseProducts = [];
  late List<PurchaseDetails> purchasedList = [];
  Timer? timer;

  @override
  void initState() { 
    super.initState(); 
    timer = Timer.periodic(const Duration(seconds: 1), (Timer t) {
      if (purchasedList.isNotEmpty) {
        for (int a = 0; a < purchasedList.length; a++) {
          if (purchasedList[a].pendingCompletePurchase) {
            completedPurchase(purchasedList[a]);
            purchasedList.removeAt(a);
          }
        }
      }
    });
  }

......

  void _listenToPurchaseUpdated(List<PurchaseDetails> purchaseDetailsList) async { 
    purchasedList = purchaseDetailsList;
    for (var detail in purchaseDetailsList) {
      if (detail.status == PurchaseStatus.pending) { 
        setState(() { showIndicator = true; });
      } else {
        if (detail.status == PurchaseStatus.purchased || detail.status == PurchaseStatus.restored) { 
          showMsg(detail.status == PurchaseStatus.restored ? "Restored Successfully" : "Purchase Successfully");
          Future.delayed(const Duration(seconds: 3), () async {
            setState(() { showIndicator = false; });
            if (detail.pendingCompletePurchase) { 
              completedPurchase(detail);
              // Successfully payment 
            } else {
              purchasedList.add(detail);
            }
          });  
        } else if (detail.status == PurchaseStatus.canceled) {
          Future.delayed(const Duration(seconds: 3), () async {
            setState(() { showIndicator = false; });
          });
          showMsg("Purchase Cancalled"); 
        } else if (detail.status == PurchaseStatus.error) {
          setState(() { showIndicator = false; });
          var s = detail.error?.message ?? "Unexpected error when the purchase occurred";
          showMsg("code: ${detail.error?.code} \nmessage: $s");
        }
      }
    }
  }

```
Tensive answered 12/1, 2024 at 11:26 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.