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.
-
Add the dependency Add
in_app_purchase: ^3.2.0to yourpubspec.yaml. We won't use it to buy things, only to query the Google Play API directly. -
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;
}
}
- 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!
- 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!
