Skip to main content
Question

RevenueCat not exposing Google Play's OneTimePurchaseOfferDetails

  • March 21, 2026
  • 1 reply
  • 19 views

Forum|alt.badge.img

If anyone else is struggling with RevenueCat not exposing Google Play's OneTimePurchaseOfferDetails (like Launch Offers or region-specific discounts for In-App products on Android), here is a bulletproof workaround we implemented in production.

The Problem: Google Play added oneTimePurchaseOfferDetailsList in Billing Library v6 for in-app products, but purchases_flutter (and the underlying purchases-hybrid-common) currently drops this array when serializing the Native ProductDetails into the Dart 

StoreProduct. So storeProduct.price just returns the un-discounted base list price.

The Workaround: Use the official in_app_purchase package strictly as a "read-only scanner" to fetch the real JSON natively, and keep using RevenueCat for the actual purchase.

  1. Add the dependency Add in_app_purchase: ^3.2.0 to your pubspec.yaml. We won't use it to buy things, only to query the Google Play API directly.

  2. Create a quick Extractor Service This queries the Google Play Billing Library and finds the lowest available offer for a specific one-time product.

import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:in_app_purchase_android/in_app_purchase_android.dart';

class GooglePlayOfferExtractor {
static Future<double?> getLowestInAppOffer(String productId) async {
if (!Platform.isAndroid) return null;

try {
final iap = InAppPurchase.instance;
final response = await iap.queryProductDetails({productId});

if (response.error != null || response.productDetails.isEmpty) {
return null;
}

final details = response.productDetails.first;
if (details is GooglePlayProductDetails) {
final rawDetails = details.productDetails;
final offer = rawDetails.oneTimePurchaseOfferDetails; // Native Play Billing v6 offer wrapper

if (offer != null) {
// You now have access to the exact eligible local offer!
return offer.priceAmountMicros.toDouble() / 1000000.0;
}
}
} catch (e) {
debugPrint('Error extracting native Android offer: $e');
}
return null;
}
}
  1. Hook it up in your UI When you call Purchases.getOfferings(), also call the extractor.
final offerings = await Purchases.getOfferings();
final lifetimePackage = offerings.current?.availablePackages.firstWhere((p) => p.packageType == PackageType.lifetime);

double? realOfferPrice;
if (Platform.isAndroid) {
realOfferPrice = await GooglePlayOfferExtractor.getLowestInAppOffer('your_lifetime_id');
}

// In your UI, if realOfferPrice is not null, display IT instead of package.storeProduct.price!
  1. Buy using RevenueCat When the user taps "Buy", just call Purchases.purchasePackage(lifetimePackage). RevenueCat will hand it over to Google Play, and Google Play will automatically charge the discounted Offer price we just fetched, but RevenueCat gets the purchase state properly synced.

It takes 10 minutes to add and solves the discrepancy flawlessly!

1 reply

Tarek
RevenueCat Staff
Forum|alt.badge.img+4
  • RevenueCat Staff
  • March 25, 2026

Hello there,

 

Tarek from the RevenueCat Support Team here, happy to assist you!

 

Thank you for your help!

 

I checked internally, and it looks like our SDK already properly reads and takes into account oneTimePurchaseOfferDetails, the price surfaced in storeProduct.price comes directly from it.

If you're seeing a discrepancy between the Play Console price and storeProduct.price, the issue might be elsewhere. Feel free to open a ticket with details so we can investigate and do a fix if there is an issue.

 

Best regards,