Skip to content

[Android: In-App-Subscription] Get all plans from a Subscription and buy them. #150348

Open

Description

@devsideal

Steps to reproduce

As im implementing In App Subscription in my app with Googles New subscription model thats desiged like:

Subscriptoin1
>> Base plan1
>> Base plan2

I'm trying to extract these base plan details and buy them.

Is that supported by package in_app_purchase or not?

I did not found any documentation about these implementation.

I already checked this issue but did not found exact answer.

Expected results

Want to get base plan details from subscription as a ProductDetails object.

Actual results

Getting SubscriptionOfferDetailsWrapper instead of ProductDetails

Code sample

I'm trying to get something like this:

Code sample
     ProductDetailsResponse response = await _iap?.queryProductDetails({"<subscription_id>"});
     ProductDetails productDetails = response.productDetails.first;
        if (productDetails is GooglePlayProductDetails) {
              List<SubscriptionOfferDetailsWrapper>? offerWrapperList = productDetails.productDetails.subscriptionOfferDetails;
        }
     
     // How to get ProductDetails object from above subscription base plan?   
     ProductDetails productDetails =  ?   
        
     final PurchaseParam purchaseParam = PurchaseParam(productDetails: productDetails);
    _iap?.buyNonConsumable(purchaseParam: purchaseParam); 

Screenshots or Video

Screenshots / Video demonstration

[Upload media here]

Logs

Logs
[Paste your logs here]

Flutter Doctor output

Doctor output
Flutter 3.19.6 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 54e66469a9 (9 weeks ago) • 2024-04-17 13:08:03 -0700
Engine • revision c4cd48e186
Tools • Dart 3.3.4 • DevTools 2.31.1

Activity

  1. changed the title [-][Android: In-App-Subscription] Get all plans form a Subscription and buy them.[/-] [+][Android: In-App-Subscription] Get all plans from a Subscription and buy them.[/+] on Jun 17, 2024
  2. darshankawar commented on Jun 17, 2024

    @darshankawar
    Member

    @devsideal
    Check https://stackoverflow.com/a/78514249 and see if it helps in your case or not.

  3. added
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Jun 17, 2024
  4. devsideal commented on Jun 17, 2024

    @devsideal
    Author

    @darshankawar Thanks fo the reply, Yes it helps to know the library is migrated to BillingLibrary V5 and now the queryProductDetails(-) will return both base plans and their offers for subscriptions, that i also tried to retrieve as list of SubscriptionOfferDetailsWrapper But its still unknow how to identify the base plans and go for buy them?

  5. removed
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Jun 17, 2024
  6. darshankawar commented on Jun 18, 2024

    @darshankawar
    Member

    @devsideal Check #127844 for SubscriptionOfferDetailsWrapper and to see if it helps further in your case or not.

  7. added
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Jun 18, 2024
  8. devsideal commented on Jun 18, 2024

    @devsideal
    Author

    @darshankawar It doesn't helped, Still not found what i'm looking for,
    Basically i just want to migrate these two steps as per the new Subscription Model:

    1. Loading products for sale refrence

    const Set<String> _kIds = <String>{'product1', 'product2'};
    final ProductDetailsResponse response =
        await InAppPurchase.instance.queryProductDetails(_kIds);
    if (response.notFoundIDs.isNotEmpty) {
      // Handle the error.
    }
    List<ProductDetails> products = response.productDetails; 
    

    2. Making a purchase refrence

    final ProductDetails productDetails = ... // Saved earlier from queryProductDetails().
    final PurchaseParam purchaseParam = PurchaseParam(productDetails: productDetails);
    if (_isConsumable(productDetails)) {
      InAppPurchase.instance.buyConsumable(purchaseParam: purchaseParam);
    } else {
      InAppPurchase.instance.buyNonConsumable(purchaseParam: purchaseParam);
    }
    
  9. removed
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Jun 18, 2024
  10. darshankawar commented on Jun 18, 2024

    @darshankawar
    Member

    Thanks for the update. Keeping the issue open for team's tracking.

  11. added
    platform-androidAndroid applications specifically
    packageflutter/packages repository. See also p: labels.
    team-androidOwned by Android platform team
    on Jun 18, 2024
  12. 17 remaining items

  13. michael-joseph-payne commented on Feb 27, 2025

    @michael-joseph-payne

    I appreciate all the help from the folks in this thread. In the end, I did figure this out. The library will handle it - but I fail to find where any of this is documented, or even how it could be figured out by the average developer...

    The clue for me is in google_play_products.dart, where there is a static method:

      /// Generates a list of [GooglePlayProductDetails] based on an Android
      /// [ProductDetailsWrapper] object.
      ///
      /// If [productDetails] is of type [ProductType.inapp], a single
      /// [GooglePlayProductDetails] will be constructed.
      /// If [productDetails] is of type [ProductType.subs], a list is returned
      /// where every element corresponds to a base plan or its offer in
      /// [productDetails.subscriptionOfferDetails].
      static List<GooglePlayProductDetails> fromProductDetails(
        ProductDetailsWrapper productDetails,
      ) {
        if (productDetails.productType == ProductType.inapp) {
          return <GooglePlayProductDetails>[
            GooglePlayProductDetails._fromOneTimePurchaseProductDetails(
                productDetails),
          ];
        } else {
          final List<GooglePlayProductDetails> productDetailList =
              <GooglePlayProductDetails>[];
          for (int subscriptionIndex = 0;
              subscriptionIndex < productDetails.subscriptionOfferDetails!.length;
              subscriptionIndex++) {
            productDetailList.add(GooglePlayProductDetails._fromSubscription(
              productDetails,
              subscriptionIndex,
            ));
          }
    
          return productDetailList;
        }
      }
    

    Emulating this, I was able to iterate through the offers and create a ProductDetails struct that could be used with buyNonConsumable:

    import 'package:in_app_purchase/in_app_purchase.dart';
    import 'package:in_app_purchase_android/billing_client_wrappers.dart';
    import 'package:in_app_purchase_android/in_app_purchase_android.dart';
    
    final playProductDetails = matchingProductDetails as GooglePlayProductDetails;
    final subscription = playProductDetails.productDetails.productType == ProductType.subs;
    
    if (subscription) {
      final subscriptionOfferDetails = playProductDetails.productDetails.subscriptionOfferDetails;
    
      if (subscriptionOfferDetails != null) {
        for (var index = 0; index < subscriptionOfferDetails.length; index++) {
          if (subscriptionOfferDetails[index].basePlanId == product.basePlanID) {
            final productDetailsWrapper = ProductDetailsWrapper(
              description: playProductDetails.productDetails.description,
              name: playProductDetails.productDetails.name,
              productId: playProductDetails.productDetails.productId,
              productType: ProductType.subs,
              subscriptionOfferDetails: [subscriptionOfferDetails[index]],
              title: playProductDetails.productDetails.title,
            );
            final productDetails = GooglePlayProductDetails.fromProductDetails(productDetailsWrapper);
            
            if (productDetails.isNotEmpty) {
              final purchaseParam = PurchaseParam(
                productDetails: productDetails.first as ProductDetails,
                applicationUserName: authState.userID,
              );
    
              return _inAppPurchasesAPI.buyProduct(purchaseParam, gwid);
            }
          }
        }
      }
    
      throw Exception('can not find the base plan ${product.basePlanID} to purchase');
    } else {
      final purchaseParams = PurchaseParam(
        productDetails: matchingProductDetails,
        applicationUserName: authState.appUser?.userID,
      );
    
      return _inAppPurchasesAPI.buyProduct(purchaseParams, gwid, uid: uid);
    }
    

    This allowed me to select between the two base plans I had set up in Play.

    I feel Google could have done a way better job here and throughout this forum. We couldn't even get a response on whether the library supports these kind of things or not, and I've failed to find examples anywhere in the library or documentation. When bringing up issues like this, the Flutter team rarely follows their own code of conduct. Sending users to StackOverflow instead of Google documentation is case in point. What a mess.

    All I've really done here is work around the fact that one can not set GooglePlayProductDetails.subscriptionIndex nor use any other base plan than the first element in the list GooglePlayProductDetails.productDetails.subscriptionOfferDetails. I did this by creating an instance of GooglePlayProductDetails that only had one element in GooglePlayProductDetails.productDetails.subscriptionOfferDetails.

    Why can't we select a base plan directly using the API? If we can, where is this documented?

  14. DusBekas commented on Feb 28, 2025

    @DusBekas

    I appreciate all the help from the folks in this thread. In the end, I did figure this out. The library will handle it - but I fail to find where any of this is documented, or even how it could be figured out by the average developer...

    The clue for me is in google_play_products.dart, where there is a static method:

      /// Generates a list of [GooglePlayProductDetails] based on an Android
      /// [ProductDetailsWrapper] object.
      ///
      /// If [productDetails] is of type [ProductType.inapp], a single
      /// [GooglePlayProductDetails] will be constructed.
      /// If [productDetails] is of type [ProductType.subs], a list is returned
      /// where every element corresponds to a base plan or its offer in
      /// [productDetails.subscriptionOfferDetails].
      static List<GooglePlayProductDetails> fromProductDetails(
        ProductDetailsWrapper productDetails,
      ) {
        if (productDetails.productType == ProductType.inapp) {
          return <GooglePlayProductDetails>[
            GooglePlayProductDetails._fromOneTimePurchaseProductDetails(
                productDetails),
          ];
        } else {
          final List<GooglePlayProductDetails> productDetailList =
              <GooglePlayProductDetails>[];
          for (int subscriptionIndex = 0;
              subscriptionIndex < productDetails.subscriptionOfferDetails!.length;
              subscriptionIndex++) {
            productDetailList.add(GooglePlayProductDetails._fromSubscription(
              productDetails,
              subscriptionIndex,
            ));
          }
    
          return productDetailList;
        }
      }
    

    Emulating this, I was able to iterate through the offers and create a ProductDetails struct that could be used with buyNonConsumable:

    import 'package:in_app_purchase/in_app_purchase.dart';
    import 'package:in_app_purchase_android/billing_client_wrappers.dart';
    import 'package:in_app_purchase_android/in_app_purchase_android.dart';
    
    final playProductDetails = matchingProductDetails as GooglePlayProductDetails;
    final subscription = playProductDetails.productDetails.productType == ProductType.subs;
    
    if (subscription) {
      final subscriptionOfferDetails = playProductDetails.productDetails.subscriptionOfferDetails;
    
      if (subscriptionOfferDetails != null) {
        for (var index = 0; index < subscriptionOfferDetails.length; index++) {
          if (subscriptionOfferDetails[index].basePlanId == product.basePlanID) {
            final productDetailsWrapper = ProductDetailsWrapper(
              description: playProductDetails.productDetails.description,
              name: playProductDetails.productDetails.name,
              productId: playProductDetails.productDetails.productId,
              productType: ProductType.subs,
              subscriptionOfferDetails: [subscriptionOfferDetails[index]],
              title: playProductDetails.productDetails.title,
            );
            final productDetails = GooglePlayProductDetails.fromProductDetails(productDetailsWrapper);
            
            if (productDetails.isNotEmpty) {
              final purchaseParam = PurchaseParam(
                productDetails: productDetails.first as ProductDetails,
                applicationUserName: authState.userID,
              );
    
              return _inAppPurchasesAPI.buyProduct(purchaseParam, gwid);
            }
          }
        }
      }
    
      throw Exception('can not find the base plan ${product.basePlanID} to purchase');
    } else {
      final purchaseParams = PurchaseParam(
        productDetails: matchingProductDetails,
        applicationUserName: authState.appUser?.userID,
      );
    
      return _inAppPurchasesAPI.buyProduct(purchaseParams, gwid, uid: uid);
    }
    

    This allowed me to select between the two base plans I had set up in Play.

    I feel Google could have done a way better job here and throughout this forum. We couldn't even get a response on whether the library supports these kind of things or not, and I've failed to find examples anywhere in the library or documentation. When bringing up issues like this, the Flutter team rarely follows their own code of conduct. Sending users to StackOverflow instead of Google documentation is case in point. What a mess.

    All I've really done here is work around the fact that one can not set GooglePlayProductDetails.subscriptionIndex nor use any other base plan than the first element in the list GooglePlayProductDetails.productDetails.subscriptionOfferDetails. I did this by creating an instance of GooglePlayProductDetails that only had one element in GooglePlayProductDetails.productDetails.subscriptionOfferDetails.

    Why can't we select a base plan directly using the API? If we can, where is this documented?

    Does it work? Have you published your app to the store?

    I have a use case that a plan is able to have some offers. Also it has a free trial too, for a new subscriber. Maybe you have a similar use case too for you app

  15. michael-joseph-payne commented on Feb 28, 2025

    @michael-joseph-payne

    The example I used above worked for selecting between multiple base plans mapped to the same subscription.

    I have not tried selecting between multiple offers mapped to the same base plan yet, so I'm not sure if the same approach can be used for that case.

    We haven't started taking production payments yet, but test purchases are certainly working.

  16. alihassan143 commented on Feb 28, 2025

    @alihassan143
    extension PurchaseModifer on ProductDetails {
      ProductDetails? get perctangeOffer {
        final GooglePlayProductDetails googlePlayProductDetails =
            this as GooglePlayProductDetails;
        final int? basePlanIndex = googlePlayProductDetails.subscriptionIndex;
    
        final List<SubscriptionOfferDetailsWrapper>? subscriptionOfferDetails =
            googlePlayProductDetails.productDetails.subscriptionOfferDetails;
    
        if (basePlanIndex != null && subscriptionOfferDetails != null) {
          final SubscriptionOfferDetailsWrapper offerDetailsWrapper =
              subscriptionOfferDetails[basePlanIndex];
          if (offerDetailsWrapper.offerId != null &&
              offerDetailsWrapper.offerId == StringConstants.offerPlanId) {
            return this;
          }
        }
        return null;
      }
    }
    

    I created this extension that is working fine, you need to create a map condition on different base plan offers

  17. agufagit commented on May 17, 2025

    @agufagit

    For purchaseDetails, how to distinguish which plan is bought? I casted to GooglePlayPurchaseDetails, but there is no plan id (OfferDetails), only product id.

  18. igopi77 commented on May 27, 2025

    @igopi77

    You can handle multiple subscription base plans or offers for the same product ID by iterating through all GooglePlayProductDetails objects returned in the query. Each object represents a unique plan or offer configuration under your subscription product.

    Sample Solution:

    final response = await InAppPurchase.instance.queryProductDetails({'your_product_id'});
    
    // The response may contain multiple GooglePlayProductDetails objects
    // for the same productId, each corresponding to a different base plan or offer.
    for (final product in response.productDetails) {
      if (product is GooglePlayProductDetails) {
        // Each object here represents a unique base plan/offer for the same product ID.
        print('Product ID: ${product.id}');
        print('Title: ${product.title}');
        print('Price: ${product.price}');
        print('Offer Token: ${product.offerToken}');
        // ...display or process as needed
      }
    }
    
    // Present all options to your user so they can select their preferred plan.
    
    

    Tip:
    When displaying subscription options in your app, make sure to list all GooglePlayProductDetails objects for the product so the user can choose between available plans or offers (e.g., monthly, yearly, free trial, introductory discount, etc.).

    summary

    The plugin is working as intended: simply loop through all GooglePlayProductDetails results to get and present all available base plans and offers for your product.

  19. agufagit commented on May 28, 2025

    @agufagit

    That's for displaying products, but we handle purchases by subscribing to purchase stream, where we get purchasedetails, and send to backend for verification. For this step, there is no way to distinguish plan id from purchasedetails

  20. michael-joseph-payne commented on May 28, 2025

    @michael-joseph-payne

    That's for displaying products, but we handle purchases by subscribing to purchase stream, where we get purchasedetails, and send to backend for verification. For this step, there is no way to distinguish plan id from purchasedetails

    Hi @agufagit. The approach above by @alihassan143 seems to be the way to go. It will work with server verification (we do the same). It's a little different than the approach I took with casting it to GoogleProductDetails and iterating through base plans, but not much.

    Here is the extension that @alihassan143 wrote:

    extension PurchaseModifer on ProductDetails {
      ProductDetails? get perctangeOffer {
        final GooglePlayProductDetails googlePlayProductDetails =
            this as GooglePlayProductDetails;
        final int? basePlanIndex = googlePlayProductDetails.subscriptionIndex;
    
        final List<SubscriptionOfferDetailsWrapper>? subscriptionOfferDetails =
            googlePlayProductDetails.productDetails.subscriptionOfferDetails;
    
        if (basePlanIndex != null && subscriptionOfferDetails != null) {
          final SubscriptionOfferDetailsWrapper offerDetailsWrapper =
              subscriptionOfferDetails[basePlanIndex];
          if (offerDetailsWrapper.offerId != null &&
              offerDetailsWrapper.offerId == StringConstants.offerPlanId) {
            return this;
          }
        }
        return null;
      }
    }
    

    I would modify that a bit to do what you're trying to do. See below:

    final playProductDetails = productDetails as GooglePlayProductDetails;
    final offers = playProductDetails.productDetails.subscriptionOfferDetails ?? [];
    
    for (final offer in offers) {
      if (offer.offerId != null && offer.offerId == 'put your offer token here') {
        /// This is your match - call buyProduct with these details
        final productDetailsWrapper = ProductDetailsWrapper(
          description: playProductDetails.productDetails.description,
          name: playProductDetails.productDetails.name,
          productId: playProductDetails.productDetails.productId,
          productType: ProductType.subs,
          subscriptionOfferDetails: [offer],
          title: playProductDetails.productDetails.title,
        );
        final productDetails = GooglePlayProductDetails.fromProductDetails(productDetailsWrapper);
    
        if (productDetails.isNotEmpty) {
          final purchaseParam = PurchaseParam(
            productDetails: productDetails.first as ProductDetails,
            applicationUserName: authState.userID,
          );
    
          return buyProduct(purchaseParam, gwid);
        }
      }
    }
    
  21. igopi77 commented on May 30, 2025

    @igopi77

    @vjamrich did you face any legal issue for using a private API.

  22. FrancoTerranovaJob commented on Mar 12, 2026

    @FrancoTerranovaJob

    Hi, I'm facing a related issue with this. I have two base plan created on google play console but Im getting only one of them. Anyone knows why it occurs ?

    I'm using the 0.4.0+8 version of android in-app-purchase package (as override) and the 3.2.3 of in-app-purchase package

    UPDATE
    I just downgrade android in-app-purchase to 0.3.6+13 and it is working now, but Im getting duplicated subscription

  23. nero-angela commented on Apr 10, 2026

    @nero-angela
    Contributor

    In version 3.2.3 of in_app_purchase, even though multiple pricing plans were created under a single subscription in the Play Store, only one pricing plan is being returned. When implemented directly via the MethodChannel, it works correctly. It appears there may be an issue with in_app_purchase_android.

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.platform-androidAndroid applications specificallyteam-androidOwned by Android platform teamtriaged-androidTriaged by Android platform team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions