1

I am using the in_app_purchase package in Flutter. I have the following setup:

Purchase initiation: await InAppPurchase.instance.buyNonConsumable(purchaseParam: purchaseParam,);

Stream listener:

 InAppPurchase.instance.purchaseStream.listen(
 _onPurchaseUpdate,
 onDone: _onStreamDone,
 onError: _onError,
 );

Purchase update handler:

 Future<void> _onPurchaseUpdate(
 List<PurchaseDetails> purchaseDetailsList,
 ) async {
 for (final purchaseDetails in purchaseDetailsList) {
 await _handlePurchase(purchaseDetails);
 }
 }

Individual purchase handler:

 /// Handles a single purchase update.
 Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
 switch (purchaseDetails.status) {
 case PurchaseStatus.purchased:
 // do some work
 case PurchaseStatus.restored:
 // do some work
 case PurchaseStatus.error:
 // do some work
 case PurchaseStatus.canceled:
 // do some work
 case PurchaseStatus.pending:
 // do some work
 }
 }

The problem:

  • I open the app on iOS
  • I launch the purchase flow
  • The iOS payment sheet appears
  • User cancels the payment sheet
  • The purchase stream does not emit anything
  • The loader that I show in my app when I initiated the purchase flow stays stuck forever because I don't know that the purchase was canceled

I researched a lot and I don't seem to be able to find a way to fix it.

How to know if purchase was cancelled?

asked 2 days ago
4
  • 2
    This looks like an issue you need to raise with the plug in developers; They specifically do nothing when the user cancels the purchase process github.com/flutter/packages/blob/… Commented 2 days ago
  • @Paulw11what about in_app_purchase_storekit, it seems to have wrappers around the ios methods, do you know if its possible to know if the user cancelled the purchase from one of the wrapper methods? I tried searching for such method but I am only little familliar with ios and this iap seems overly complicated to me (why not having simple method to call to know the current status instead of a stream?!) Commented 2 days ago
  • 2
    The line in the code that I linked to is where the plugin would need to do something in response to the user cancelling the purchase process. It does nothing Commented 2 days ago
  • 1
    I found a GitHub issue about this: github.com/flutter/flutter/issues/176757. The issue started appearing in v0.4.6 but it is not present in v0.4.5. You can downgrade, if you can, to fix this temporarily. There's also a new GitHub issue opened today on this: github.com/flutter/flutter/issues/178292. Commented yesterday

1 Answer 1

0

TL;DR

As pointed out in a comment, the issue is that the plugin code is doing nothing in case user cancelled the flow.

Below is a workaround that solves MY PROBLEM (loader stuck forever).

High level explanation

After calling buyNonConsumable(), no app code executes - we're just waiting for the stream to emit so we can react and remove the loader.

So we can update the UI state (hide loader) BEFORE the native payment sheet appears, then show a loader again ONLY when the stream actually emits. If the stream never emits (user cancelled), the loader is already hidden, so no stuck loading state.

The Flow

1. User taps "Buy" button
 → _isLoading = true (loader shows)
2. buyProduct() executes code before calling buyNonConsumable()
 → When it finishes async work (e.g., fetching product details): Pre-initiate callback fires
 → _isLoading = false (loader hides)
 → very small time passes without a loader on the screen, and then the payment sheet appears
3. buyNonConsumable() called
 → Native payment/auth sheet appears
 IF USER CANCELS:
 ✓ Stream never emits
 ✓ _isLoading stays false as we set it in the pre-initiate callback (no loader blocking UI)
 IF USER COMPLETES:
 ✓ Stream emits purchase
 ✓ _isLoading = true (we set it when stream emits to show loader again)
 ✓ Backend verification happens
 ✓ _isLoading = false (we hide loader when done)

Implementation

Step 1: Wrap the IAP service with a pre-initiate callback

class IapService {
 final InAppPurchase _iap = InAppPurchase.instance;
 final List<VoidCallback> _preInitiateListeners = [];
 // code to register/unregister listeners...
 Future<void> buyProduct(String productId) async {
 // Get product details, some ASYNC work (loader shown while doing this work)
 final ProductDetailsResponse response = await _iap.queryProductDetails({productId});
 final productDetails = response.productDetails.first;
 // Notify listeners BEFORE calling buyNonConsumable, so they can hide the loader
 for (final listener in _preInitiateListeners) {
 listener();
 }
 // Now call the purchase method - native sheet will appear, loader is already hidden at this point
 final purchaseParam = PurchaseParam(productDetails: productDetails);
 await _iap.buyNonConsumable(purchaseParam: purchaseParam);
 }
}

Step 2: Use the callback to manage UI state

class CheckoutScreen extends StatefulWidget {
 @override
 State<CheckoutScreen> createState() => _CheckoutScreenState();
}
class _CheckoutScreenState extends State<CheckoutScreen> {
 final IapService _iapService = IapService();
 StreamSubscription<List<PurchaseDetails>>? _subscription; // to capture the subscription so we can unregister it later
 bool _isLoading = false;
 @override
 void initState() {
 super.initState();
 // Register the pre-initiate callback
 _iapService.registerOnPreInitiatePurchaseFlow(() {
 // Hide loader BEFORE native sheet appears
 setState(() => _isLoading = false);
 });
 // Listen to purchase stream
 _subscription = InAppPurchase.instance.purchaseStream.listen((purchases) {
 // Show loader again when purchase stream emits, so we can in this time verify the purchase with our backend
 setState(() => _isLoading = true);
 // Process purchase, verify with backend, etc.
 await verifyPurchaseWithBackend(purchases.first);
 // some code to handle BE response ...
 // Hide loader when done
 setState(() => _isLoading = false);
 });
 }
 void _onBuyButtonPressed() {
 // Show initial loader
 setState(() => _isLoading = true);
 // Trigger purchase
 _iapService.buyProduct('your_product_id');
 }
 // build method...
 // Unregister the pre-initiate callback and dispose subscription in dispose...
}
answered yesterday
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.