You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Android: In-App-Subscription] Get all plans from a Subscription and buy them. #150348
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
@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?
@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:
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?
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
For purchaseDetails, how to distinguish which plan is bought? I casted to GooglePlayPurchaseDetails, but there is no plan id (OfferDetails), only product id.
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.
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
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.
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);
}
}
}
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
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.
Activity
[-][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.[/+]darshankawar commentedon Jun 17, 2024
@devsideal
Check https://stackoverflow.com/a/78514249 and see if it helps in your case or not.
devsideal commentedon Jun 17, 2024
@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 ofSubscriptionOfferDetailsWrapperBut its still unknow how to identify the base plans and go for buy them?darshankawar commentedon Jun 18, 2024
@devsideal Check #127844 for
SubscriptionOfferDetailsWrapperand to see if it helps further in your case or not.devsideal commentedon Jun 18, 2024
@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
2. Making a purchase refrence
darshankawar commentedon Jun 18, 2024
Thanks for the update. Keeping the issue open for team's tracking.
17 remaining items
michael-joseph-payne commentedon Feb 27, 2025
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:
Emulating this, I was able to iterate through the offers and create a ProductDetails struct that could be used with buyNonConsumable:
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?
DusBekas commentedon Feb 28, 2025
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
michael-joseph-payne commentedon Feb 28, 2025
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.
alihassan143 commentedon Feb 28, 2025
I created this extension that is working fine, you need to create a map condition on different base plan offers
agufagit commentedon May 17, 2025
For purchaseDetails, how to distinguish which plan is bought? I casted to GooglePlayPurchaseDetails, but there is no plan id (OfferDetails), only product id.
igopi77 commentedon May 27, 2025
You can handle multiple subscription base plans or offers for the same product ID by iterating through all
GooglePlayProductDetailsobjects returned in the query. Each object represents a unique plan or offer configuration under your subscription product.Sample Solution:
Tip:
When displaying subscription options in your app, make sure to list all
GooglePlayProductDetailsobjects for the product so the user can choose between available plans or offers (e.g., monthly, yearly, free trial, introductory discount, etc.).summary
agufagit commentedon May 28, 2025
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
michael-joseph-payne commentedon May 28, 2025
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:
I would modify that a bit to do what you're trying to do. See below:
igopi77 commentedon May 30, 2025
@vjamrich did you face any legal issue for using a private API.
[in_app_purchase_android] GooglePlayPurchaseParam add possibility set…
[in_app_purchase_android] GooglePlayPurchaseParam add possibility set…
FrancoTerranovaJob commentedon Mar 12, 2026
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
nero-angela commentedon Apr 10, 2026
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.
[in_app_purchase_android] GooglePlayPurchaseParam add possibility set…
[in_app_purchase_android] GooglePlayPurchaseParam add possibility set…